Jelajahi Sumber

Merge pull request #2354 from weseek/feat/sidebar-mode-control

Feat/sidebar mode control
Yuki Takei 5 tahun lalu
induk
melakukan
a959e04c9c

+ 8 - 2
resource/locales/en-US/translation.json

@@ -60,8 +60,6 @@
   "No diff": "No diff",
   "No diff": "No diff",
   "Shrink versions that have no diffs": "Shrink versions that have no diffs",
   "Shrink versions that have no diffs": "Shrink versions that have no diffs",
   "User ID": "User ID",
   "User ID": "User ID",
-  "Home": "Home",
-  "Settings": "Settings",
   "User Information": "User information",
   "User Information": "User information",
   "Basic Info": "Basic info",
   "Basic Info": "Basic info",
   "Name": "Name",
   "Name": "Name",
@@ -132,6 +130,14 @@
   "Disassociate": "Disassociate",
   "Disassociate": "Disassociate",
   "Recent Created": "Recent Created",
   "Recent Created": "Recent Created",
   "Recent Changes": "Recent Changes",
   "Recent Changes": "Recent Changes",
+  "personal_dropdown": {
+    "home": "Home",
+    "settings": "Settings",
+    "color_mode": "Color mode",
+    "sidebar_mode": "Sidebar mode",
+    "sidebar_mode_editor": "Sidebar mode on Editor",
+    "use_os_settings": "Use OS settings"
+  },
   "form_validation": {
   "form_validation": {
     "error_message": "Some values ​​are incorrect",
     "error_message": "Some values ​​are incorrect",
     "required": "%s is required",
     "required": "%s is required",

+ 11 - 2
resource/locales/ja/translation.json

@@ -60,8 +60,6 @@
   "No diff": "差分なし",
   "No diff": "差分なし",
   "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
   "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
   "User ID": "ユーザーID",
   "User ID": "ユーザーID",
-  "Home": "ホーム",
-  "Settings": "設定",
   "User Information": "ユーザー情報",
   "User Information": "ユーザー情報",
   "Basic Info": "ユーザーの基本情報",
   "Basic Info": "ユーザーの基本情報",
   "Name": "名前",
   "Name": "名前",
@@ -129,8 +127,19 @@
   "Deleted Pages": "削除済みページ",
   "Deleted Pages": "削除済みページ",
   "Sign out": "ログアウト",
   "Sign out": "ログアウト",
   "Disassociate": "連携解除",
   "Disassociate": "連携解除",
+  "Color mode": "カラーモード",
+  "Sidebar mode": "サイドバーモード",
+  "Sidebar mode on Editor": "サイドバーモード(編集時)",
   "Recent Created": "最新の作成",
   "Recent Created": "最新の作成",
   "Recent Changes": "最新の変更",
   "Recent Changes": "最新の変更",
+  "personal_dropdown": {
+    "home": "ホーム",
+    "settings": "設定",
+    "color_mode": "カラーモード",
+    "sidebar_mode": "サイドバーモード",
+    "sidebar_mode_editor": "サイドバーモード(編集時)",
+    "use_os_settings": "OS設定を利用する"
+  },
   "form_validation": {
   "form_validation": {
     "error_message": "いくつかの値が設定されていません",
     "error_message": "いくつかの値が設定されていません",
     "required": "%sに値を入力してください",
     "required": "%sに値を入力してください",

+ 63 - 8
src/client/js/components/Navbar/PersonalDropdown.jsx

@@ -25,6 +25,14 @@ const PersonalDropdown = (props) => {
     window.location.href = '/logout';
     window.location.href = '/logout';
   };
   };
 
 
+  const preferDrawerModeSwitchModifiedHandler = (bool) => {
+    appContainer.setDrawerModePreference(bool);
+  };
+
+  const preferDrawerModeOnEditSwitchModifiedHandler = (bool) => {
+    appContainer.setDrawerModePreferenceOnEdit(bool);
+  };
+
   const followOsCheckboxModifiedHandler = (bool) => {
   const followOsCheckboxModifiedHandler = (bool) => {
     // reset user preference
     // reset user preference
     if (bool) {
     if (bool) {
@@ -44,7 +52,9 @@ const PersonalDropdown = (props) => {
   /*
   /*
    * render
    * render
    */
    */
-  const { preferDarkModeByMediaQuery, preferDarkModeByUser } = appContainer.state;
+  const {
+    preferDarkModeByMediaQuery, preferDarkModeByUser, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
+  } = appContainer.state;
   const isUserPreferenceExists = preferDarkModeByUser != null;
   const isUserPreferenceExists = preferDarkModeByUser != null;
   const isDarkMode = () => {
   const isDarkMode = () => {
     if (isUserPreferenceExists) {
     if (isUserPreferenceExists) {
@@ -78,16 +88,61 @@ const PersonalDropdown = (props) => {
           </div>
           </div>
 
 
           <div className="btn-group btn-block mt-2" role="group">
           <div className="btn-group btn-block mt-2" role="group">
-            <a className="btn btn-sm btn-outline-secondary" href={`/user/${user.username}`}><i className="icon-fw icon-home"></i>{ t('Home') }</a>
-            <a className="btn btn-sm btn-outline-secondary" href="/me"><i className="icon-fw icon-wrench"></i>{ t('Settings') }</a>
+            <a className="btn btn-sm btn-outline-secondary" href={`/user/${user.username}`}>
+              <i className="icon-fw icon-home"></i>{ t('personal_dropdown.home') }
+            </a>
+            <a className="btn btn-sm btn-outline-secondary" href="/me">
+              <i className="icon-fw icon-wrench"></i>{ t('personal_dropdown.settings') }
+            </a>
           </div>
           </div>
         </div>
         </div>
 
 
         <div className="dropdown-divider"></div>
         <div className="dropdown-divider"></div>
 
 
-        <h6 className="dropdown-header">Color Scheme</h6>
+        <h6 className="dropdown-header">{t('personal_dropdown.sidebar_mode')}</h6>
+        <form className="px-4">
+          <div className="form-row justify-content-center">
+            <div className="form-group col-auto mb-0 d-flex align-items-center">
+              <i className="icon-drawer"></i>
+              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
+                <input
+                  id="swSidebarMode"
+                  className="custom-control-input"
+                  type="checkbox"
+                  checked={!preferDrawerModeByUser}
+                  onChange={e => preferDrawerModeSwitchModifiedHandler(!e.target.checked)}
+                />
+                <label className="custom-control-label" htmlFor="swSidebarMode"></label>
+              </div>
+              <i className="ti-layout-sidebar-left"></i>
+            </div>
+          </div>
+        </form>
+        <h6 className="dropdown-header">{t('personal_dropdown.sidebar_mode_editor')}</h6>
+        <form className="px-4">
+          <div className="form-row justify-content-center">
+            <div className="form-group col-auto mb-0 d-flex align-items-center">
+              <i className="icon-drawer"></i>
+              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
+                <input
+                  id="swSidebarModeOnEditor"
+                  className="custom-control-input"
+                  type="checkbox"
+                  checked={!preferDrawerModeOnEditByUser}
+                  onChange={e => preferDrawerModeOnEditSwitchModifiedHandler(!e.target.checked)}
+                />
+                <label className="custom-control-label" htmlFor="swSidebarModeOnEditor"></label>
+              </div>
+              <i className="ti-layout-sidebar-left"></i>
+            </div>
+          </div>
+        </form>
+
+        <div className="dropdown-divider"></div>
+
+        <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
         <form className="px-4">
         <form className="px-4">
-          <div className="form-row align-items-center">
+          <div className="form-row">
             <div className="form-group col-auto">
             <div className="form-group col-auto">
               <div className="custom-control custom-checkbox">
               <div className="custom-control custom-checkbox">
                 <input
                 <input
@@ -97,12 +152,12 @@ const PersonalDropdown = (props) => {
                   checked={!isUserPreferenceExists}
                   checked={!isUserPreferenceExists}
                   onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
                   onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
                 />
                 />
-                <label className="custom-control-label text-nowrap" htmlFor="cbFollowOs">Use OS Setting</label>
+                <label className="custom-control-label text-nowrap" htmlFor="cbFollowOs">{t('personal_dropdown.use_os_settings')}</label>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
-          <div className="form-row align-items-center">
-            <div className="form-group col-auto mb-0 d-flex">
+          <div className="form-row justify-content-center">
+            <div className="form-group col-auto mb-0 d-flex align-items-center">
               <span className={isUserPreferenceExists ? '' : 'text-muted'}>Light</span>
               <span className={isUserPreferenceExists ? '' : 'text-muted'}>Light</span>
               <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
               <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
                 <input
                 <input

+ 1 - 12
src/client/js/components/Navbar/SearchTop.jsx

@@ -16,7 +16,6 @@ class SearchTop extends React.Component {
     this.state = {
     this.state = {
       text: '',
       text: '',
       isScopeChildren: false,
       isScopeChildren: false,
-      isCollapsed: true,
     };
     };
 
 
     this.onInputChange = this.onInputChange.bind(this);
     this.onInputChange = this.onInputChange.bind(this);
@@ -25,16 +24,6 @@ class SearchTop extends React.Component {
     this.search = this.search.bind(this);
     this.search = this.search.bind(this);
   }
   }
 
 
-  componentWillMount() {
-    this.initBreakpointEvents();
-  }
-
-  initBreakpointEvents() {
-    this.props.appContainer.addBreakpointListener('md', (mql) => {
-      this.setState({ isCollapsed: !mql.matches });
-    }, true);
-  }
-
   onInputChange(text) {
   onInputChange(text) {
     this.setState({ text });
     this.setState({ text });
   }
   }
@@ -62,7 +51,7 @@ class SearchTop extends React.Component {
   }
   }
 
 
   Root = ({ children }) => {
   Root = ({ children }) => {
-    const { isCollapsed } = this.state;
+    const { isDeviceSmallerThanMd: isCollapsed } = this.props.appContainer.state;
 
 
     return isCollapsed
     return isCollapsed
       ? (
       ? (

+ 95 - 31
src/client/js/components/Sidebar.jsx

@@ -8,7 +8,7 @@ import {
   ThemeProvider,
   ThemeProvider,
 } from '@atlaskit/navigation-next';
 } from '@atlaskit/navigation-next';
 
 
-import { createSubscribedElement } from './UnstatedUtils';
+import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
 
 
 import SidebarNav from './Sidebar/SidebarNav';
 import SidebarNav from './Sidebar/SidebarNav';
@@ -23,6 +23,7 @@ class Sidebar extends React.Component {
   static propTypes = {
   static propTypes = {
     appContainer: PropTypes.instanceOf(AppContainer).isRequired,
     appContainer: PropTypes.instanceOf(AppContainer).isRequired,
     navigationUIController: PropTypes.any.isRequired,
     navigationUIController: PropTypes.any.isRequired,
+    isDrawerModeOnInit: PropTypes.bool,
   };
   };
 
 
   state = {
   state = {
@@ -31,7 +32,10 @@ class Sidebar extends React.Component {
 
 
   componentWillMount() {
   componentWillMount() {
     this.hackUIController();
     this.hackUIController();
-    this.initBreakpointEvents();
+  }
+
+  componentDidUpdate(prevProps, prevState) {
+    this.toggleDrawerMode(this.isDrawerMode);
   }
   }
 
 
   /**
   /**
@@ -50,35 +54,75 @@ class Sidebar extends React.Component {
     };
     };
   }
   }
 
 
-  initBreakpointEvents() {
-    const { appContainer, navigationUIController } = this.props;
+  /**
+   * return whether drawer mode or not
+   */
+  get isDrawerMode() {
+    let isDrawerMode = this.props.appContainer.state.isDrawerMode;
+    if (isDrawerMode == null) {
+      isDrawerMode = this.props.isDrawerModeOnInit;
+    }
+    return isDrawerMode;
+  }
+
+  toggleDrawerMode(bool) {
+    const { navigationUIController } = this.props;
+
+    const isStateModified = navigationUIController.state.isResizeDisabled !== bool;
+    if (!isStateModified) {
+      return;
+    }
+
+    // Drawer <-- Dock
+    if (bool) {
+      // cache state
+      this.sidebarCollapsedCached = navigationUIController.state.isCollapsed;
+      this.sidebarWidthCached = navigationUIController.state.productNavWidth;
+
+      // clear transition temporary
+      if (this.sidebarCollapsedCached) {
+        this.clearNavigationTransitionTemporary(this.navigationElem);
+      }
 
 
-    const mdOrAvobeHandler = (mql) => {
-      // sm -> md
-      if (mql.matches) {
-        appContainer.setState({ isDrawerOpened: false });
-        navigationUIController.enableResize();
+      navigationUIController.disableResize();
 
 
-        // restore width
-        if (this.sidebarWidthCached != null) {
-          navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
-        }
+      // fix width
+      navigationUIController.setState({ productNavWidth: sidebarDefaultWidth });
+    }
+    // Drawer --> Dock
+    else {
+      // clear transition temporary when restore collapsed sidebar
+      if (this.sidebarCollapsedCached) {
+        this.clearNavigationTransitionTemporary(this.ctxNavigationElem);
       }
       }
-      // md -> sm
-      else {
-        // cache width
-        this.sidebarWidthCached = navigationUIController.state.productNavWidth;
 
 
-        appContainer.setState({ isDrawerOpened: false });
-        navigationUIController.disableResize();
-        navigationUIController.expand();
+      navigationUIController.enableResize();
 
 
-        // fix width
-        navigationUIController.setState({ productNavWidth: sidebarDefaultWidth });
+      // restore width
+      if (this.sidebarWidthCached != null) {
+        navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
       }
       }
-    };
+    }
+  }
+
+  get navigationElem() {
+    return document.querySelector('div[data-testid="Navigation"]');
+  }
+
+  get ctxNavigationElem() {
+    return document.querySelector('div[data-testid="ContextualNavigation"]');
+  }
+
+  clearNavigationTransitionTemporary(elem) {
+    const transitionCache = elem.style.transition;
+
+    // clear
+    elem.style.transition = undefined;
 
 
-    appContainer.addBreakpointListener('md', mdOrAvobeHandler, true);
+    // restore after 300ms
+    setTimeout(() => {
+      elem.style.transition = transitionCache;
+    }, 300);
   }
   }
 
 
   backdropClickedHandler = () => {
   backdropClickedHandler = () => {
@@ -122,7 +166,7 @@ class Sidebar extends React.Component {
 
 
     return (
     return (
       <>
       <>
-        <div className={`grw-sidebar ${isDrawerOpened ? 'open' : ''}`}>
+        <div className={`grw-sidebar ${this.isDrawerMode ? 'grw-sidebar-drawer' : ''} ${isDrawerOpened ? 'open' : ''}`}>
           <ThemeProvider
           <ThemeProvider
             theme={theme => ({
             theme={theme => ({
               ...theme,
               ...theme,
@@ -153,15 +197,35 @@ class Sidebar extends React.Component {
 
 
 }
 }
 
 
-const SidebarWithNavigationUI = withNavigationUIController(Sidebar);
+
+const SidebarWithNavigationUIController = withNavigationUIController(Sidebar);
 
 
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const SidebarWrapper = (props) => {
-  return createSubscribedElement(SidebarWithNavigationUI, props, [AppContainer]);
+
+const SidebarWithNavigation = (props) => {
+  const { preferDrawerModeByUser: isDrawerModeOnInit } = props.appContainer.state;
+
+  const initUICForDrawerMode = isDrawerModeOnInit
+    // generate initialUIController for Drawer mode
+    ? {
+      isCollapsed: false,
+      isResizeDisabled: true,
+      productNavWidth: sidebarDefaultWidth,
+    }
+    // set undefined (should be initialized by cache)
+    : undefined;
+
+  return (
+    <NavigationProvider initialUIController={initUICForDrawerMode}>
+      <SidebarWithNavigationUIController {...props} isDrawerModeOnInit={isDrawerModeOnInit} />
+    </NavigationProvider>
+  );
+};
+
+SidebarWithNavigation.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 };
 
 
-export default () => (
-  <NavigationProvider><SidebarWrapper /></NavigationProvider>
-);
+export default withUnstatedContainers(SidebarWithNavigation, [AppContainer]);

+ 25 - 0
src/client/js/components/UnstatedUtils.jsx

@@ -59,3 +59,28 @@ export function createSubscribedElement(componentClass, props, containerClasses)
   );
   );
 
 
 }
 }
+
+/**
+ * Return a React component that is injected unstated containers
+ *
+ * @param {object} Component A React.Component or functional component
+ * @param {array} containerClasses unstated container classes to subscribe
+ * @returns returns such like a following element:
+ *  e.g.
+ *  <Subscribe to={containerClasses}>  // containerClasses = [AppContainer, PageContainer]
+ *    { (appContainer, pageContainer) => (
+ *      <Component appContainer={appContainer} pageContainer={pageContainer} {...this.props} />
+ *    )}
+ *  </Subscribe>
+ */
+export function withUnstatedContainers(Component, containerClasses) {
+  return props => (
+    // wrap with <Subscribe></Subscribe>
+    <Subscribe to={containerClasses}>
+      { (...containers) => {
+        const propsForContainers = generateAutoNamedProps(containers);
+        return <Component {...props} {...propsForContainers} />;
+      }}
+    </Subscribe>
+  );
+}

+ 3 - 3
src/client/js/legacy/crowi.js

@@ -250,10 +250,10 @@ $(() => {
 
 
   // tab changing handling
   // tab changing handling
   $('a[href="#revision-body"]').on('show.bs.tab', () => {
   $('a[href="#revision-body"]').on('show.bs.tab', () => {
-    appContainer.setState({ editorMode: null });
+    appContainer.setEditorMode(null);
   });
   });
   $('a[href="#edit"]').on('show.bs.tab', () => {
   $('a[href="#edit"]').on('show.bs.tab', () => {
-    appContainer.setState({ editorMode: 'builtin' });
+    appContainer.setEditorMode('builtin');
     $('body').addClass('on-edit');
     $('body').addClass('on-edit');
     $('body').addClass('builtin-editor');
     $('body').addClass('builtin-editor');
   });
   });
@@ -262,7 +262,7 @@ $(() => {
     $('body').removeClass('builtin-editor');
     $('body').removeClass('builtin-editor');
   });
   });
   $('a[href="#hackmd"]').on('show.bs.tab', () => {
   $('a[href="#hackmd"]').on('show.bs.tab', () => {
-    appContainer.setState({ editorMode: 'hackmd' });
+    appContainer.setEditorMode('hackmd');
     $('body').addClass('on-edit');
     $('body').addClass('on-edit');
     $('body').addClass('hackmd');
     $('body').addClass('hackmd');
   });
   });

+ 85 - 9
src/client/js/services/AppContainer.js

@@ -31,11 +31,17 @@ export default class AppContainer extends Container {
   constructor() {
   constructor() {
     super();
     super();
 
 
+    const { localStorage } = window;
+
     this.state = {
     this.state = {
       editorMode: null,
       editorMode: null,
+      isDeviceSmallerThanMd: null,
       preferDarkModeByMediaQuery: false,
       preferDarkModeByMediaQuery: false,
-      preferDarkModeByUser: null,
-      breakpoint: 'xs',
+      preferDarkModeByUser: localStorage.preferDarkModeByUser === 'true',
+      preferDrawerModeByUser: localStorage.preferDrawerModeByUser === 'true',
+      preferDrawerModeOnEditByUser: // default: true
+        localStorage.preferDrawerModeOnEditByUser == null || localStorage.preferDrawerModeOnEditByUser === 'true',
+      isDrawerMode: null,
       isDrawerOpened: false,
       isDrawerOpened: false,
 
 
       isPageCreateModalShown: false,
       isPageCreateModalShown: false,
@@ -111,10 +117,31 @@ export default class AppContainer extends Container {
   }
   }
 
 
   init() {
   init() {
+    this.initDeviceSize();
     this.initColorScheme();
     this.initColorScheme();
     this.initPlugins();
     this.initPlugins();
   }
   }
 
 
+  initDeviceSize() {
+    const mdOrAvobeHandler = async(mql) => {
+      let isDeviceSmallerThanMd;
+
+      // sm -> md
+      if (mql.matches) {
+        isDeviceSmallerThanMd = false;
+      }
+      // md -> sm
+      else {
+        isDeviceSmallerThanMd = true;
+      }
+
+      this.setState({ isDeviceSmallerThanMd });
+      this.updateDrawerMode({ ...this.state, isDeviceSmallerThanMd }); // generate newest state object
+    };
+
+    this.addBreakpointListener('md', mdOrAvobeHandler, true);
+  }
+
   async initColorScheme() {
   async initColorScheme() {
     const switchStateByMediaQuery = async(mql) => {
     const switchStateByMediaQuery = async(mql) => {
       const preferDarkMode = mql.matches;
       const preferDarkMode = mql.matches;
@@ -127,13 +154,7 @@ export default class AppContainer extends Container {
     // add event listener
     // add event listener
     mqlForDarkMode.addListener(switchStateByMediaQuery);
     mqlForDarkMode.addListener(switchStateByMediaQuery);
 
 
-    // initialize1: restore settings from localStorage
-    const { localStorage } = window;
-    if (localStorage.preferDarkModeByUser != null) {
-      await this.setState({ preferDarkModeByUser: localStorage.preferDarkModeByUser === 'true' });
-    }
-
-    // initialize2: check media query
+    // initialize: check media query
     switchStateByMediaQuery(mqlForDarkMode);
     switchStateByMediaQuery(mqlForDarkMode);
   }
   }
 
 
@@ -361,6 +382,11 @@ export default class AppContainer extends Container {
     return users;
     return users;
   }
   }
 
 
+  setEditorMode(editorMode) {
+    this.setState({ editorMode });
+    this.updateDrawerMode({ ...this.state, editorMode }); // generate newest state object
+  }
+
   toggleDrawer() {
   toggleDrawer() {
     const { isDrawerOpened } = this.state;
     const { isDrawerOpened } = this.state;
     this.setState({ isDrawerOpened: !isDrawerOpened });
     this.setState({ isDrawerOpened: !isDrawerOpened });
@@ -386,6 +412,56 @@ export default class AppContainer extends Container {
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
   }
   }
 
 
+  /**
+   * Set Sidebar mode preference by user
+   * @param {boolean} preferDockMode
+   */
+  async setDrawerModePreference(bool) {
+    this.setState({ preferDrawerModeByUser: bool });
+    this.updateDrawerMode({ ...this.state, preferDrawerModeByUser: bool }); // generate newest state object
+
+    // store settings to localStorage
+    const { localStorage } = window;
+    localStorage.preferDrawerModeByUser = bool;
+  }
+
+  /**
+   * Set Sidebar mode preference by user
+   * @param {boolean} preferDockMode
+   */
+  async setDrawerModePreferenceOnEdit(bool) {
+    this.setState({ preferDrawerModeOnEditByUser: bool });
+    this.updateDrawerMode({ ...this.state, preferDrawerModeOnEditByUser: bool }); // generate newest state object
+
+    // store settings to localStorage
+    const { localStorage } = window;
+    localStorage.preferDrawerModeOnEditByUser = bool;
+  }
+
+  /**
+   * Update drawer related state by specified 'newState' object
+   * @param {object} newState A newest state object
+   *
+   * Specify 'newState' like following code:
+   *
+   *   { ...this.state, overwriteParam: overwriteValue }
+   *
+   * because updating state of unstated container will be delayed unless you use await
+   */
+  updateDrawerMode(newState) {
+    const {
+      editorMode, isDeviceSmallerThanMd, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
+    } = newState;
+
+    // get preference on view or edit
+    const preferDrawerMode = editorMode != null ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
+
+    const isDrawerMode = isDeviceSmallerThanMd || preferDrawerMode;
+    const isDrawerOpened = false; // close Drawer anyway
+
+    this.setState({ isDrawerMode, isDrawerOpened });
+  }
+
   /**
   /**
    * Set color scheme preference by user
    * Set color scheme preference by user
    * @param {boolean} isDarkMode
    * @param {boolean} isDarkMode

+ 35 - 29
src/client/styles/scss/_sidebar.scss

@@ -59,6 +59,8 @@
     // Adjust to be on top of the growi subnavigation
     // Adjust to be on top of the growi subnavigation
     z-index: $zindex-sticky + 5;
     z-index: $zindex-sticky + 5;
 
 
+    transition: left 300ms cubic-bezier(0.25, 1, 0.5, 1);
+
     // css-xxx-Outer
     // css-xxx-Outer
     > div:nth-of-type(2) {
     > div:nth-of-type(2) {
       width: 0;
       width: 0;
@@ -117,42 +119,46 @@
 }
 }
 
 
 // Drawer Mode
 // Drawer Mode
-@include media-breakpoint-down(sm) {
-  .grw-sidebar {
-    position: fixed;
-    z-index: $zindex-fixed - 2;
-
-    // override @atlaskit/navigation-next styles
-    div[data-layout-container='true'] {
-      // css-teprsg
-      > div:nth-of-type(2) {
-        display: none;
-      }
-    }
-    div[data-testid='Navigation'] {
-      // css-xxx-Outer
-      > div:nth-of-type(2) {
-        display: none;
-      }
-    }
+@mixin drawer() {
+  position: fixed;
+  z-index: $zindex-fixed - 2;
 
 
-    &:not(.open) {
-      div[data-testid='Navigation'] {
-        left: -#{$grw-sidebar-nav-width + $grw-sidebar-content-min-width};
-      }
+  // override @atlaskit/navigation-next styles
+  div[data-layout-container='true'] {
+    // css-teprsg
+    > div:nth-of-type(2) {
+      display: none;
     }
     }
-    &.open {
-      div[data-testid='Navigation'] {
-        left: 0;
-      }
+  }
+  div[data-testid='Navigation'] {
+    // css-xxx-Outer
+    > div:nth-of-type(2) {
+      display: none;
     }
     }
+  }
 
 
+  &:not(.open) {
+    div[data-testid='Navigation'] {
+      left: -#{$grw-sidebar-nav-width + $grw-sidebar-content-min-width};
+    }
+  }
+  &.open {
     div[data-testid='Navigation'] {
     div[data-testid='Navigation'] {
-      transition: left 300ms cubic-bezier(0.25, 1, 0.5, 1);
+      left: 0;
     }
     }
   }
   }
+}
+
+.grw-sidebar {
+  &.grw-sidebar-drawer {
+    @include drawer();
+  }
 
 
-  .grw-sidebar-backdrop.modal-backdrop {
-    z-index: $zindex-fixed - 4;
+  @include media-breakpoint-down(sm) {
+    @include drawer();
   }
   }
 }
 }
+
+.grw-sidebar-backdrop.modal-backdrop {
+  z-index: $zindex-fixed - 4;
+}