Просмотр исходного кода

WIP: transplant typescriptized component

Yuki Takei 4 лет назад
Родитель
Сommit
a556452f26

+ 9 - 27
packages/app/src/components/Navbar/DrawerToggler.tsx

@@ -1,18 +1,13 @@
-import React, { useCallback } from 'react';
-import PropTypes from 'prop-types';
+import React, { FC } from 'react';
+import { useDrawerOpened } from '~/stores/ui';
 
-import { withTranslation } from 'react-i18next';
+type Props = {
+  iconClass?: string,
+}
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-import NavigationContainer from '~/client/services/NavigationContainer';
+const DrawerToggler: FC<Props> = (props: Props) => {
 
-const DrawerToggler = (props) => {
-
-  const { navigationContainer } = props;
-
-  const clickHandler = useCallback(() => {
-    navigationContainer.toggleDrawer();
-  }, [navigationContainer]);
+  const { data: isOpened, mutate } = useDrawerOpened();
 
   const iconClass = props.iconClass || 'icon-menu';
 
@@ -22,7 +17,7 @@ const DrawerToggler = (props) => {
       type="button"
       aria-expanded="false"
       aria-label="Toggle navigation"
-      onClick={clickHandler}
+      onClick={() => mutate(!isOpened)}
     >
       <i className={iconClass}></i>
     </button>
@@ -30,17 +25,4 @@ const DrawerToggler = (props) => {
 
 };
 
-/**
- * Wrapper component for using unstated
- */
-const DrawerTogglerWrapper = withUnstatedContainers(DrawerToggler, [NavigationContainer]);
-
-
-DrawerToggler.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
-
-  iconClass: PropTypes.string,
-};
-
-export default withTranslation()(DrawerTogglerWrapper);
+export default DrawerToggler;

+ 270 - 186
packages/app/src/components/Sidebar.tsx

@@ -1,242 +1,326 @@
-import React from 'react';
-import PropTypes from 'prop-types';
+import React, {
+  useCallback, useEffect, useRef, useState,
+} from 'react';
 
 import {
-  withNavigationUIController,
-  LayoutManager,
-  NavigationProvider,
-  ThemeProvider,
-} from '@atlaskit/navigation-next';
-
-import { withUnstatedContainers } from './UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import NavigationContainer from '~/client/services/NavigationContainer';
+  useCurrentSidebarContents, useDrawerMode, useDrawerOpened, usePreferDrawerModeByUser,
+  useCurrentProductNavWidth, useSidebarCollapsed, useSidebarResizeDisabled,
+} from '~/stores/ui';
 
 import DrawerToggler from './Navbar/DrawerToggler';
 
 import SidebarNav from './Sidebar/SidebarNav';
-import SidebarContents from './Sidebar/SidebarContents';
-import StickyStretchableScroller from './StickyStretchableScroller';
 
-const sidebarDefaultWidth = 320;
+const sidebarMinWidth = 240;
+const sidebarMinimizeWidth = 20;
+
+const GlobalNavigation = () => {
+  const { data: currentContents } = useCurrentSidebarContents();
+  const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed();
+
+  const itemSelectedHandler = useCallback((selectedContents) => {
+
+    // already selected
+    if (currentContents === selectedContents) {
+      // toggle collapsed
+      mutateSidebarCollapsed(!isCollapsed);
+    }
+    // switch and expand
+    else {
+      mutateSidebarCollapsed(false);
+    }
+  }, [currentContents, isCollapsed]);
+
+  return <SidebarNav onItemSelected={itemSelectedHandler} />;
+};
+
+// dummy skelton contents
+const GlobalNavigationSkelton = () => {
+  return (
+    <div className="grw-sidebar-nav">
+      <div className="grw-sidebar-nav-primary-container">
+      </div>
+      <div className="grw-sidebar-nav-secondary-container">
+      </div>
+    </div>
+  );
+};
+
+
+const SidebarContents = () => {
+  const scrollTargetSelector = '#grw-sidebar-contents-scroll-target';
+
+  const calcViewHeight = useCallback(() => {
+    const scrollTargetElem = document.querySelector('#grw-sidebar-contents-scroll-target');
+    return scrollTargetElem != null
+      ? window.innerHeight - scrollTargetElem?.getBoundingClientRect().top
+      : window.innerHeight;
+  }, []);
 
-class Sidebar extends React.Component {
+  return (
+    <>
+      {/* <StickyStretchableScroller
+        scrollTargetSelector={scrollTargetSelector}
+        contentsElemSelector="#grw-sidebar-content-container"
+        stickyElemSelector=".grw-sidebar"
+        calcViewHeightFunc={calcViewHeight}
+      /> */}
+
+      <div id="grw-sidebar-contents-scroll-target">
+        <div id="grw-sidebar-content-container">
+          {/* TODO: set isSharedUser
+          <SidebarContents
+            isSharedUser={this.props.appContainer.isSharedUser}
+          />
+          */}
+          <SidebarContents />
+        </div>
+      </div>
+
+      <DrawerToggler iconClass="icon-arrow-left" />
+    </>
+  );
+};
+
+// dummy skelton contents
+const SidebarSkeltonContents = () => {
+  return (
+    <div>Skelton Contents!!!</div>
+  );
+};
 
-  static propTypes = {
-    appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-    navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
-    navigationUIController: PropTypes.any.isRequired,
-    isDrawerModeOnInit: PropTypes.bool,
-  };
 
-  componentWillMount() {
-    this.hackUIController();
-  }
+type Props = {
+  productNavWidth: number
+}
 
-  componentDidUpdate(prevProps, prevState) {
-    this.toggleDrawerMode(this.isDrawerMode);
-  }
+const Sidebar = (props: Props) => {
+  const { data: isDrawerMode } = useDrawerMode();
+  const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
+  const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth(props.productNavWidth);
+  const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed();
+  const { data: isResizeDisabled, mutate: mutateSidebarResizeDisabled } = useSidebarResizeDisabled();
 
   /**
    * hack and override UIController.storeState
    *
    * Since UIController is an unstated container, setState() in storeState method should be awaited before writing to cache.
    */
-  hackUIController() {
-    const { navigationUIController } = this.props;
-
-    // see: @atlaskit/navigation-next/dist/esm/ui-controller/UIController.js
-    const orgStoreState = navigationUIController.storeState;
-    navigationUIController.storeState = async(state) => {
-      await navigationUIController.setState(state);
-      orgStoreState(state);
-    };
-  }
-
-  /**
-   * return whether drawer mode or not
-   */
-  get isDrawerMode() {
-    let isDrawerMode = this.props.navigationContainer.state.isDrawerMode;
-    if (isDrawerMode == null) {
-      isDrawerMode = this.props.isDrawerModeOnInit;
-    }
-    return isDrawerMode;
-  }
-
-  toggleDrawerMode(bool) {
-    const { navigationUIController } = this.props;
-
-    const isStateModified = navigationUIController.state.isResizeDisabled !== bool;
+  // hackUIController() {
+  //   const { navigationUIController } = this.props;
+
+  //   // see: @atlaskit/navigation-next/dist/esm/ui-controller/UIController.js
+  //   const orgStoreState = navigationUIController.storeState;
+  //   navigationUIController.storeState = async(state) => {
+  //     await navigationUIController.setState(state);
+  //     orgStoreState(state);
+  //   };
+  // }
+
+  const toggleDrawerMode = useCallback((bool) => {
+    const isStateModified = isResizeDisabled !== bool;
     if (!isStateModified) {
       return;
     }
 
     // Drawer <-- Dock
     if (bool) {
-      // cache state
-      this.sidebarCollapsedCached = navigationUIController.state.isCollapsed;
-      this.sidebarWidthCached = navigationUIController.state.productNavWidth;
+      // // cache state
+      // this.sidebarCollapsedCached = navigationUIController.state.isCollapsed;
+      // this.sidebarWidthCached = navigationUIController.state.productNavWidth;
 
-      // clear transition temporary
-      if (this.sidebarCollapsedCached) {
-        this.addCssClassTemporary('grw-sidebar-supress-transitions-to-drawer');
-      }
-
-      navigationUIController.disableResize();
+      // // clear transition temporary
+      // if (this.sidebarCollapsedCached) {
+      //   this.addCssClassTemporary('grw-sidebar-supress-transitions-to-drawer');
+      // }
 
-      // fix width
-      navigationUIController.setState({ productNavWidth: sidebarDefaultWidth });
+      // disable resize
+      mutateSidebarResizeDisabled(true);
     }
     // Drawer --> Dock
     else {
-      // clear transition temporary
-      if (this.sidebarCollapsedCached) {
-        this.addCssClassTemporary('grw-sidebar-supress-transitions-to-dock');
-      }
+      // // clear transition temporary
+      // if (this.sidebarCollapsedCached) {
+      //   this.addCssClassTemporary('grw-sidebar-supress-transitions-to-dock');
+      // }
+
+      // enable resize
+      mutateSidebarResizeDisabled(false);
+
+      // // restore width
+      // if (this.sidebarWidthCached != null) {
+      //   navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
+      // }
+    }
+  }, [isResizeDisabled]);
 
-      navigationUIController.enableResize();
+  // addCssClassTemporary(className) {
+  //   // clear
+  //   this.sidebarElem.classList.add(className);
 
-      // restore width
-      if (this.sidebarWidthCached != null) {
-        navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
-      }
-    }
-  }
+  //   // restore after 300ms
+  //   setTimeout(() => {
+  //     this.sidebarElem.classList.remove(className);
+  //   }, 300);
+  // }
 
-  get sidebarElem() {
-    return document.querySelector('.grw-sidebar');
-  }
+  const backdropClickedHandler = useCallback(() => {
+    mutateDrawerOpened(false);
+  }, [mutateDrawerOpened]);
 
-  addCssClassTemporary(className) {
-    // clear
-    this.sidebarElem.classList.add(className);
+  const [isMounted, setMounted] = useState(false);
 
-    // restore after 300ms
-    setTimeout(() => {
-      this.sidebarElem.classList.remove(className);
-    }, 300);
-  }
+  useEffect(() => {
+    // this.hackUIController();
+    setMounted(true);
+  }, []);
 
-  backdropClickedHandler = () => {
-    const { navigationContainer } = this.props;
-    navigationContainer.toggleDrawer();
-  }
+  useEffect(() => {
+    toggleDrawerMode(isDrawerMode);
+  }, [isDrawerMode, toggleDrawerMode]);
 
-  itemSelectedHandler = (contentsId) => {
-    const { navigationContainer, navigationUIController } = this.props;
-    const { sidebarContentsId } = navigationContainer.state;
+  const [isHover, setHover] = useState(false);
+  const [isDragging, setDrag] = useState(false);
 
-    // already selected
-    if (sidebarContentsId === contentsId) {
-      navigationUIController.toggleCollapse();
+  const resizableContainer = useRef<HTMLDivElement>(null);
+  const setContentWidth = useCallback((newWidth) => {
+    if (resizableContainer.current == null) {
+      return;
     }
-    // switch and expand
-    else {
-      navigationUIController.expand();
+    resizableContainer.current.style.width = `${newWidth}px`;
+  }, []);
+
+  const hoverHandler = useCallback((isHover: boolean) => {
+    if (!isCollapsed || isDrawerMode) {
+      return;
     }
-  }
 
-  calcViewHeight() {
-    const scrollTargetElem = document.querySelector('#grw-sidebar-contents-scroll-target');
-    return window.innerHeight - scrollTargetElem.getBoundingClientRect().top;
-  }
+    setHover(isHover);
 
-  renderGlobalNavigation = () => (
-    <SidebarNav onItemSelected={this.itemSelectedHandler} />
-  );
+    if (isHover) {
+      setContentWidth(currentProductNavWidth);
+    }
+    if (!isHover) {
+      setContentWidth(sidebarMinimizeWidth);
+    }
+  }, [isCollapsed, isDrawerMode, currentProductNavWidth]);
 
-  renderSidebarContents = () => {
-    const scrollTargetSelector = '#grw-sidebar-contents-scroll-target';
-
-    return (
-      <>
-        <StickyStretchableScroller
-          scrollTargetSelector={scrollTargetSelector}
-          contentsElemSelector="#grw-sidebar-content-container"
-          stickyElemSelector=".grw-sidebar"
-          calcViewHeightFunc={this.calcViewHeight}
-        />
-
-        <div id="grw-sidebar-contents-scroll-target">
-          <div id="grw-sidebar-content-container">
-            <SidebarContents
-              isSharedUser={this.props.appContainer.isSharedUser}
-            />
-          </div>
-        </div>
+  const toggleNavigationBtnClickHandler = useCallback(() => {
+    mutateSidebarCollapsed(!isCollapsed);
+  }, [isCollapsed]);
 
-        <DrawerToggler iconClass="icon-arrow-left" />
-      </>
-    );
-  };
-
-  render() {
-    const { isDrawerOpened } = this.props.navigationContainer.state;
-
-    return (
-      <>
-        <div className={`grw-sidebar d-print-none ${this.isDrawerMode ? 'grw-sidebar-drawer' : ''} ${isDrawerOpened ? 'open' : ''}`}>
-          <ThemeProvider
-            theme={theme => ({
-              ...theme,
-              context: 'product',
-            })}
-          >
-            <LayoutManager
-              globalNavigation={this.renderGlobalNavigation}
-              productNavigation={() => null}
-              containerNavigation={this.renderSidebarContents}
-              experimental_hideNavVisuallyOnCollapse
-              experimental_flyoutOnHover
-              experimental_alternateFlyoutBehaviour
-              experimental_fullWidthFlyout
-              shouldHideGlobalNavShadow
-              showContextualNavigation
-            >
-            </LayoutManager>
-          </ThemeProvider>
-        </div>
+  useEffect(() => {
+    if (isCollapsed) {
+      setContentWidth(sidebarMinimizeWidth);
+    }
+    else {
+      setContentWidth(currentProductNavWidth);
+    }
+  }, [isCollapsed]);
+
+  const draggableAreaMoveHandler = useCallback((event) => {
+    if (isDragging) {
+      event.preventDefault();
+      const newWidth = event.pageX - 60;
+      if (resizableContainer.current != null) {
+        setContentWidth(newWidth);
+        resizableContainer.current.classList.add('dragging');
+      }
+    }
+  }, [isDragging]);
 
-        { isDrawerOpened && (
-          <div className="grw-sidebar-backdrop modal-backdrop show" onClick={this.backdropClickedHandler}></div>
-        ) }
-      </>
-    );
-  }
+  const dragableAreaMouseUpHandler = useCallback(() => {
+    if (resizableContainer.current == null) {
+      return;
+    }
 
-}
+    setDrag(false);
 
+    if (resizableContainer.current.clientWidth < sidebarMinWidth) {
+      // force collapsed
+      mutateSidebarCollapsed(true);
+      mutateProductNavWidth(sidebarMinWidth);
+      // TODO call API and save DB
+    }
+    else {
+      mutateProductNavWidth(resizableContainer.current.clientWidth);
+      // TODO call API and save DB
+    }
 
-const SidebarWithNavigationUIController = withNavigationUIController(Sidebar);
+    resizableContainer.current.classList.remove('dragging');
 
-/**
- * Wrapper component for using unstated
- */
+    document.removeEventListener('mousemove', draggableAreaMoveHandler);
+    document.removeEventListener('mouseup', dragableAreaMouseUpHandler);
 
-const SidebarWithNavigation = (props) => {
-  const { preferDrawerModeByUser: isDrawerModeOnInit } = props.navigationContainer.state;
+  }, [draggableAreaMoveHandler]);
 
-  const initUICForDrawerMode = isDrawerModeOnInit
-    // generate initialUIController for Drawer mode
-    ? {
-      isCollapsed: false,
-      isResizeDisabled: true,
-      productNavWidth: sidebarDefaultWidth,
+  const dragableAreaClickHandler = useCallback(() => {
+    if (isCollapsed || isDrawerMode) {
+      return;
     }
-    // set undefined (should be initialized by cache)
-    : undefined;
+    setDrag(true);
+  }, [isCollapsed, isDrawerMode]);
+
+  useEffect(() => {
+    document.addEventListener('mousemove', draggableAreaMoveHandler);
+    document.addEventListener('mouseup', dragableAreaMouseUpHandler);
+  }, [draggableAreaMoveHandler, dragableAreaMouseUpHandler]);
 
   return (
-    <NavigationProvider initialUIController={initUICForDrawerMode}>
-      <SidebarWithNavigationUIController {...props} isDrawerModeOnInit={isDrawerModeOnInit} />
-    </NavigationProvider>
+    <>
+      <div className={`grw-sidebar d-print-none ${isDrawerMode ? 'grw-sidebar-drawer' : ''} ${isDrawerOpened ? 'open' : ''}`}>
+        <div className="data-layout-container">
+          <div className="navigation">
+            <div className="grw-navigation-wrap">
+              <div className="grw-global-navigation">
+                { isMounted ? <GlobalNavigation></GlobalNavigation> : <GlobalNavigationSkelton></GlobalNavigationSkelton> }
+              </div>
+              <div
+                ref={resizableContainer}
+                className="grw-contextual-navigation"
+                onMouseEnter={() => hoverHandler(true)}
+                onMouseLeave={() => hoverHandler(false)}
+                onMouseMove={draggableAreaMoveHandler}
+                onMouseUp={dragableAreaMouseUpHandler}
+                style={{ width: isCollapsed ? sidebarMinimizeWidth : currentProductNavWidth }}
+              >
+                <div className="grw-contextual-navigation-child">
+                  <div role="group" className={`grw-contextual-navigation-sub ${!isHover && isCollapsed ? 'collapsed' : ''}`}>
+                    { isMounted ? <SidebarContents></SidebarContents> : <SidebarSkeltonContents></SidebarSkeltonContents> }
+                  </div>
+                </div>
+              </div>
+            </div>
+            <div className="grw-navigation-draggable">
+              <div
+                className={`${!isDrawerMode ? 'resizable' : ''} grw-navigation-draggable-hitarea`}
+                onMouseDown={dragableAreaClickHandler}
+              >
+                <div className="grw-navigation-draggable-hitarea-child"></div>
+              </div>
+              <button
+                className={`grw-navigation-resize-button ${!isDrawerMode ? 'resizable' : ''} ${isCollapsed ? 'collapse-state' : 'normal-state'} `}
+                type="button"
+                aria-expanded="true"
+                aria-label="Toggle navigation"
+                disabled={isDrawerMode}
+                onClick={toggleNavigationBtnClickHandler}
+              >
+                <span role="presentation">
+                  <i className="ml-1 fa fa-fw fa-angle-right text-white"></i>
+                </span>
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      { isDrawerOpened && (
+        <div className="grw-sidebar-backdrop modal-backdrop show" onClick={backdropClickedHandler}></div>
+      ) }
+    </>
   );
-};
 
-SidebarWithNavigation.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 
-export default withUnstatedContainers(SidebarWithNavigation, [AppContainer, NavigationContainer]);
+export default Sidebar;

+ 14 - 22
packages/app/src/components/Sidebar/SidebarContents.tsx

@@ -1,24 +1,26 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { withTranslation } from 'react-i18next';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-import NavigationContainer from '~/client/services/NavigationContainer';
+import React, { FC } from 'react';
 
 import RecentChanges from './RecentChanges';
 import CustomSidebar from './CustomSidebar';
+import { useCurrentSidebarContents, SidebarContents as SidebarContentType } from '~/stores/ui';
+
+type Props = {
+  isSharedUser?: boolean,
+};
+
+const SidebarContents: FC<Props> = (props: Props) => {
 
-const SidebarContents = (props) => {
-  const { navigationContainer, isSharedUser } = props;
+  const { data: currentSidebarContents } = useCurrentSidebarContents();
+
+  const { isSharedUser } = props;
 
   if (isSharedUser) {
     return null;
   }
 
   let Contents;
-  switch (navigationContainer.state.sidebarContentsId) {
-    case 'recent':
+  switch (currentSidebarContents) {
+    case SidebarContentType.RECENT:
       Contents = RecentChanges;
       break;
     default:
@@ -31,19 +33,9 @@ const SidebarContents = (props) => {
 
 };
 
-SidebarContents.propTypes = {
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
-
-  isSharedUser: PropTypes.bool,
-};
 
 SidebarContents.defaultProps = {
   isSharedUser: false,
 };
 
-/**
- * Wrapper component for using unstated
- */
-const SidebarContentsWrapper = withUnstatedContainers(SidebarContents, [NavigationContainer]);
-
-export default withTranslation()(SidebarContentsWrapper);
+export default SidebarContents;

+ 79 - 79
packages/app/src/components/Sidebar/SidebarNav.tsx

@@ -1,94 +1,94 @@
-import React from 'react';
-import PropTypes from 'prop-types';
+import React, { FC, useCallback } from 'react';
+import { useCurrentUser, useIsSharedUser } from '~/stores/context';
+import { SidebarContents, useCurrentSidebarContents } from '~/stores/ui';
 
-import { withTranslation } from 'react-i18next';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import NavigationContainer from '~/client/services/NavigationContainer';
 
+type PrimaryItemProps = {
+  contents: SidebarContents,
+  label: string,
+  iconName: string,
+  onItemSelected: (contents: SidebarContents) => void,
+}
 
-class SidebarNav extends React.Component {
+const PrimaryItem: FC<PrimaryItemProps> = (props: PrimaryItemProps) => {
+  const {
+    contents, iconName, onItemSelected,
+  } = props;
 
-  static propTypes = {
-    onItemSelected: PropTypes.func,
-  };
+  // TODO: migrate from NavigationContainer
+  const { data: currentContents, mutate } = useCurrentSidebarContents();
 
-  state = {
-  };
+  const isSelected = contents === currentContents;
 
-  itemSelectedHandler = (contentsId) => {
-    const { navigationContainer, onItemSelected } = this.props;
+  const itemSelectedHandler = useCallback(() => {
+    // const { navigationContainer, onItemSelected } = this.props;
     if (onItemSelected != null) {
-      onItemSelected(contentsId);
+      onItemSelected(contents);
     }
 
-    navigationContainer.selectSidebarContents(contentsId);
-  }
-
-  PrimaryItem = ({ id, label, iconName }) => {
-    const { sidebarContentsId } = this.props.navigationContainer.state;
-    const isSelected = sidebarContentsId === id;
-
-    return (
-      <button
-        type="button"
-        className={`d-block btn btn-primary ${isSelected ? 'active' : ''}`}
-        onClick={() => this.itemSelectedHandler(id)}
-      >
-        <i className="material-icons">{iconName}</i>
-      </button>
-    );
-  }
-
-  SecondaryItem({
-    label, iconName, href, isBlank,
-  }) {
-    return (
-      <a href={href} className="d-block btn btn-primary" target={`${isBlank ? '_blank' : ''}`}>
-        <i className="material-icons">{iconName}</i>
-      </a>
-    );
-  }
-
-  generateIconFactory(classNames) {
-    return () => <i className={classNames}></i>;
-  }
-
-  render() {
-    const { isAdmin, currentUsername, isSharedUser } = this.props.appContainer;
-    const isLoggedIn = currentUsername != null;
-
-    const { PrimaryItem, SecondaryItem } = this;
-
-    return (
-      <div className="grw-sidebar-nav">
-        <div className="grw-sidebar-nav-primary-container">
-          {!isSharedUser && <PrimaryItem id="custom" label="Custom Sidebar" iconName="code" />}
-          {!isSharedUser && <PrimaryItem id="recent" label="Recent Changes" iconName="update" />}
-          {/* <PrimaryItem id="tag" label="Tags" iconName="icon-tag" /> */}
-          {/* <PrimaryItem id="favorite" label="Favorite" iconName="icon-star" /> */}
-        </div>
-        <div className="grw-sidebar-nav-secondary-container">
-          {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
-          {isLoggedIn && <SecondaryItem label="Draft" iconName="file_copy" href="/me/drafts" />}
-          <SecondaryItem label="Help" iconName="help" href="https://docs.growi.org" isBlank />
-          <SecondaryItem label="Trash" iconName="delete" href="/trash" />
-        </div>
-      </div>
-    );
-  }
+    mutate(contents);
+  }, [contents, mutate, onItemSelected]);
+
+  return (
+    <button
+      type="button"
+      className={`d-block btn btn-primary ${isSelected ? 'active' : ''}`}
+      onClick={itemSelectedHandler}
+    >
+      <i className="material-icons">{iconName}</i>
+    </button>
+  );
+};
 
+type SecondaryItemProps = {
+  label: string,
+  href: string,
+  iconName: string,
+  isBlank?: boolean,
 }
 
-SidebarNav.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
+const SecondaryItem: FC<SecondaryItemProps> = (props: SecondaryItemProps) => {
+  const { iconName, href, isBlank } = props;
+
+  return (
+    <a href={href} className="d-block btn btn-primary" target={`${isBlank ? '_blank' : ''}`}>
+      <i className="material-icons">{iconName}</i>
+    </a>
+  );
 };
 
-/**
- * Wrapper component for using unstated
- */
-const SidebarNavWrapper = withUnstatedContainers(SidebarNav, [AppContainer, NavigationContainer]);
 
-export default withTranslation()(SidebarNavWrapper);
+type Props = {
+  onItemSelected: (contents: SidebarContents) => void,
+}
+
+const SidebarNav: FC<Props> = (props: Props) => {
+
+  const { data: isSharedUser } = useIsSharedUser();
+  const { data: currentUser } = useCurrentUser();
+
+  const isAdmin = currentUser?.admin;
+  const isLoggedIn = currentUser != null;
+
+  const { onItemSelected } = props;
+
+  return (
+    <div className="grw-sidebar-nav">
+      <div className="grw-sidebar-nav-primary-container">
+        {!isSharedUser && <PrimaryItem contents={SidebarContents.CUSTOM} label="Custom Sidebar" iconName="code" onItemSelected={onItemSelected} />}
+        {!isSharedUser && <PrimaryItem contents={SidebarContents.RECENT} label="Recent Changes" iconName="update" onItemSelected={onItemSelected} />}
+        {/* <PrimaryItem id="tag" label="Tags" iconName="icon-tag" /> */}
+        {/* <PrimaryItem id="favorite" label="Favorite" iconName="icon-star" /> */}
+      </div>
+      <div className="grw-sidebar-nav-secondary-container">
+        {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
+        {isLoggedIn && <SecondaryItem label="Draft" iconName="file_copy" href="/me/drafts" />}
+        <SecondaryItem label="Help" iconName="help" href="https://docs.growi.org" isBlank />
+        <SecondaryItem label="Trash" iconName="delete" href="/trash" />
+      </div>
+    </div>
+  );
+
+};
+
+export default SidebarNav;