Sidebar.jsx 6.7 KB

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