Sidebar.jsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import {
  4. withNavigationUIController,
  5. LayoutManager,
  6. NavigationProvider,
  7. ThemeProvider,
  8. } from '@atlaskit/navigation-next';
  9. import { withUnstatedContainers } from './UnstatedUtils';
  10. import AppContainer from '../services/AppContainer';
  11. import NavigationContainer from '../services/NavigationContainer';
  12. import DrawerToggler from './Navbar/DrawerToggler';
  13. import SidebarNav from './Sidebar/SidebarNav';
  14. import SidebarContents from './Sidebar/SidebarContents';
  15. import StickyStretchableScroller from './StickyStretchableScroller';
  16. const sidebarDefaultWidth = 320;
  17. class Sidebar extends React.Component {
  18. static propTypes = {
  19. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  20. navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
  21. navigationUIController: PropTypes.any.isRequired,
  22. isDrawerModeOnInit: PropTypes.bool,
  23. };
  24. componentWillMount() {
  25. this.hackUIController();
  26. }
  27. componentDidUpdate(prevProps, prevState) {
  28. this.toggleDrawerMode(this.isDrawerMode);
  29. }
  30. /**
  31. * hack and override UIController.storeState
  32. *
  33. * Since UIController is an unstated container, setState() in storeState method should be awaited before writing to cache.
  34. */
  35. hackUIController() {
  36. const { navigationUIController } = this.props;
  37. // see: @atlaskit/navigation-next/dist/esm/ui-controller/UIController.js
  38. const orgStoreState = navigationUIController.storeState;
  39. navigationUIController.storeState = async(state) => {
  40. await navigationUIController.setState(state);
  41. orgStoreState(state);
  42. };
  43. }
  44. /**
  45. * return whether drawer mode or not
  46. */
  47. get isDrawerMode() {
  48. let isDrawerMode = this.props.navigationContainer.state.isDrawerMode;
  49. if (isDrawerMode == null) {
  50. isDrawerMode = this.props.isDrawerModeOnInit;
  51. }
  52. return isDrawerMode;
  53. }
  54. toggleDrawerMode(bool) {
  55. const { navigationUIController } = this.props;
  56. const isStateModified = navigationUIController.state.isResizeDisabled !== bool;
  57. if (!isStateModified) {
  58. return;
  59. }
  60. // Drawer <-- Dock
  61. if (bool) {
  62. // cache state
  63. this.sidebarCollapsedCached = navigationUIController.state.isCollapsed;
  64. this.sidebarWidthCached = navigationUIController.state.productNavWidth;
  65. // clear transition temporary
  66. if (this.sidebarCollapsedCached) {
  67. this.addCssClassTemporary('grw-sidebar-supress-transitions-to-drawer');
  68. }
  69. navigationUIController.disableResize();
  70. // fix width
  71. navigationUIController.setState({ productNavWidth: sidebarDefaultWidth });
  72. }
  73. // Drawer --> Dock
  74. else {
  75. // clear transition temporary
  76. if (this.sidebarCollapsedCached) {
  77. this.addCssClassTemporary('grw-sidebar-supress-transitions-to-dock');
  78. }
  79. navigationUIController.enableResize();
  80. // restore width
  81. if (this.sidebarWidthCached != null) {
  82. navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
  83. }
  84. }
  85. }
  86. get sidebarElem() {
  87. return document.querySelector('.grw-sidebar');
  88. }
  89. addCssClassTemporary(className) {
  90. // clear
  91. this.sidebarElem.classList.add(className);
  92. // restore after 300ms
  93. setTimeout(() => {
  94. this.sidebarElem.classList.remove(className);
  95. }, 300);
  96. }
  97. backdropClickedHandler = () => {
  98. const { navigationContainer } = this.props;
  99. navigationContainer.toggleDrawer();
  100. }
  101. itemSelectedHandler = (contentsId) => {
  102. const { navigationContainer, navigationUIController } = this.props;
  103. const { sidebarContentsId } = navigationContainer.state;
  104. // already selected
  105. if (sidebarContentsId === contentsId) {
  106. navigationUIController.toggleCollapse();
  107. }
  108. // switch and expand
  109. else {
  110. navigationUIController.expand();
  111. }
  112. }
  113. calcViewHeight() {
  114. const scrollTargetElem = document.querySelector('#grw-sidebar-contents-scroll-target');
  115. return window.innerHeight - scrollTargetElem.getBoundingClientRect().top;
  116. }
  117. renderGlobalNavigation = () => (
  118. <SidebarNav onItemSelected={this.itemSelectedHandler} />
  119. );
  120. renderSidebarContents = () => {
  121. const scrollTargetSelector = '#grw-sidebar-contents-scroll-target';
  122. return (
  123. <>
  124. <StickyStretchableScroller
  125. scrollTargetSelector={scrollTargetSelector}
  126. contentsElemSelector="#grw-sidebar-content-container"
  127. stickyElemSelector=".grw-sidebar"
  128. calcViewHeightFunc={this.calcViewHeight}
  129. />
  130. <div id="grw-sidebar-contents-scroll-target">
  131. <div id="grw-sidebar-content-container">
  132. <SidebarContents
  133. isSharedUser={this.props.appContainer.isSharedUser}
  134. />
  135. </div>
  136. </div>
  137. <DrawerToggler iconClass="icon-arrow-left" />
  138. </>
  139. );
  140. };
  141. render() {
  142. const { isDrawerOpened } = this.props.navigationContainer.state;
  143. return (
  144. <>
  145. <div className={`grw-sidebar d-print-none ${this.isDrawerMode ? 'grw-sidebar-drawer' : ''} ${isDrawerOpened ? 'open' : ''}`}>
  146. <ThemeProvider
  147. theme={theme => ({
  148. ...theme,
  149. context: 'product',
  150. })}
  151. >
  152. <LayoutManager
  153. globalNavigation={this.renderGlobalNavigation}
  154. productNavigation={() => null}
  155. containerNavigation={this.renderSidebarContents}
  156. experimental_hideNavVisuallyOnCollapse
  157. experimental_flyoutOnHover
  158. experimental_alternateFlyoutBehaviour
  159. experimental_fullWidthFlyout
  160. shouldHideGlobalNavShadow
  161. showContextualNavigation
  162. >
  163. </LayoutManager>
  164. </ThemeProvider>
  165. </div>
  166. { isDrawerOpened && (
  167. <div className="grw-sidebar-backdrop modal-backdrop show" onClick={this.backdropClickedHandler}></div>
  168. ) }
  169. </>
  170. );
  171. }
  172. }
  173. const SidebarWithNavigationUIController = withNavigationUIController(Sidebar);
  174. /**
  175. * Wrapper component for using unstated
  176. */
  177. const SidebarWithNavigation = (props) => {
  178. const { preferDrawerModeByUser: isDrawerModeOnInit } = props.navigationContainer.state;
  179. const initUICForDrawerMode = isDrawerModeOnInit
  180. // generate initialUIController for Drawer mode
  181. ? {
  182. isCollapsed: false,
  183. isResizeDisabled: true,
  184. productNavWidth: sidebarDefaultWidth,
  185. }
  186. // set undefined (should be initialized by cache)
  187. : undefined;
  188. return (
  189. <NavigationProvider initialUIController={initUICForDrawerMode}>
  190. <SidebarWithNavigationUIController {...props} isDrawerModeOnInit={isDrawerModeOnInit} />
  191. </NavigationProvider>
  192. );
  193. };
  194. SidebarWithNavigation.propTypes = {
  195. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  196. navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
  197. };
  198. export default withUnstatedContainers(SidebarWithNavigation, [AppContainer, NavigationContainer]);