Sidebar.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import React, {
  2. FC, useCallback, useEffect, useRef, useState,
  3. } from 'react';
  4. import {
  5. useDrawerMode, useDrawerOpened, usePreferDrawerModeByUser,
  6. useSidebarCollapsed,
  7. useCurrentSidebarContents,
  8. useCurrentProductNavWidth,
  9. useSidebarResizeDisabled,
  10. } from '~/stores/ui';
  11. import DrawerToggler from './Navbar/DrawerToggler';
  12. import SidebarNav from './Sidebar/SidebarNav';
  13. import SidebarContents from './Sidebar/SidebarContents';
  14. import { scheduleToPutUserUISettings } from '~/services/user-ui-settings';
  15. import { NavigationResizeHexagon } from './Sidebar/NavigationResizeHexagon';
  16. const sidebarMinWidth = 240;
  17. const sidebarMinimizeWidth = 20;
  18. const GlobalNavigation = () => {
  19. const { data: currentContents } = useCurrentSidebarContents();
  20. const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed();
  21. const itemSelectedHandler = useCallback((selectedContents) => {
  22. let newValue = false;
  23. // already selected
  24. if (currentContents === selectedContents) {
  25. // toggle collapsed
  26. newValue = !isCollapsed;
  27. }
  28. mutateSidebarCollapsed(newValue, false);
  29. scheduleToPutUserUISettings({ isSidebarCollapsed: newValue });
  30. }, [currentContents, isCollapsed, mutateSidebarCollapsed]);
  31. return <SidebarNav onItemSelected={itemSelectedHandler} />;
  32. };
  33. // dummy skelton contents
  34. const GlobalNavigationSkelton = () => {
  35. return (
  36. <div className="grw-sidebar-nav">
  37. <div className="grw-sidebar-nav-primary-container">
  38. </div>
  39. <div className="grw-sidebar-nav-secondary-container">
  40. </div>
  41. </div>
  42. );
  43. };
  44. const SidebarContentsWrapper = () => {
  45. const scrollTargetSelector = '#grw-sidebar-contents-scroll-target';
  46. const calcViewHeight = useCallback(() => {
  47. const scrollTargetElem = document.querySelector('#grw-sidebar-contents-scroll-target');
  48. return scrollTargetElem != null
  49. ? window.innerHeight - scrollTargetElem?.getBoundingClientRect().top
  50. : window.innerHeight;
  51. }, []);
  52. return (
  53. <>
  54. {/* <StickyStretchableScroller
  55. scrollTargetSelector={scrollTargetSelector}
  56. contentsElemSelector="#grw-sidebar-content-container"
  57. stickyElemSelector=".grw-sidebar"
  58. calcViewHeightFunc={calcViewHeight}
  59. /> */}
  60. <div id="grw-sidebar-contents-scroll-target">
  61. <div id="grw-sidebar-content-container">
  62. <SidebarContents />
  63. </div>
  64. </div>
  65. <DrawerToggler iconClass="icon-arrow-left" />
  66. </>
  67. );
  68. };
  69. // dummy skelton contents
  70. const SidebarSkeltonContents = () => {
  71. return (
  72. <div>Skelton Contents!!!</div>
  73. );
  74. };
  75. type Props = {
  76. }
  77. const Sidebar: FC<Props> = (props: Props) => {
  78. const { data: isDrawerMode } = useDrawerMode();
  79. const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
  80. const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth();
  81. const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed();
  82. const { data: isResizeDisabled, mutate: mutateSidebarResizeDisabled } = useSidebarResizeDisabled();
  83. const [isHover, setHover] = useState(false);
  84. const [isDragging, setDrag] = useState(false);
  85. const [isMounted, setMounted] = useState(false);
  86. const isResizableByDrag = !isDrawerMode && (!isCollapsed || isHover);
  87. /**
  88. * hack and override UIController.storeState
  89. *
  90. * Since UIController is an unstated container, setState() in storeState method should be awaited before writing to cache.
  91. */
  92. // hackUIController() {
  93. // const { navigationUIController } = this.props;
  94. // // see: @atlaskit/navigation-next/dist/esm/ui-controller/UIController.js
  95. // const orgStoreState = navigationUIController.storeState;
  96. // navigationUIController.storeState = async(state) => {
  97. // await navigationUIController.setState(state);
  98. // orgStoreState(state);
  99. // };
  100. // }
  101. const toggleDrawerMode = useCallback((bool) => {
  102. const isStateModified = isResizeDisabled !== bool;
  103. if (!isStateModified) {
  104. return;
  105. }
  106. // Drawer <-- Dock
  107. if (bool) {
  108. // // cache state
  109. // this.sidebarCollapsedCached = navigationUIController.state.isCollapsed;
  110. // this.sidebarWidthCached = navigationUIController.state.productNavWidth;
  111. // // clear transition temporary
  112. // if (this.sidebarCollapsedCached) {
  113. // this.addCssClassTemporary('grw-sidebar-supress-transitions-to-drawer');
  114. // }
  115. // disable resize
  116. mutateSidebarResizeDisabled(true, false);
  117. }
  118. // Drawer --> Dock
  119. else {
  120. // // clear transition temporary
  121. // if (this.sidebarCollapsedCached) {
  122. // this.addCssClassTemporary('grw-sidebar-supress-transitions-to-dock');
  123. // }
  124. // enable resize
  125. mutateSidebarResizeDisabled(false, false);
  126. // // restore width
  127. // if (this.sidebarWidthCached != null) {
  128. // navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
  129. // }
  130. }
  131. }, [isResizeDisabled, mutateSidebarResizeDisabled]);
  132. // addCssClassTemporary(className) {
  133. // // clear
  134. // this.sidebarElem.classList.add(className);
  135. // // restore after 300ms
  136. // setTimeout(() => {
  137. // this.sidebarElem.classList.remove(className);
  138. // }, 300);
  139. // }
  140. const backdropClickedHandler = useCallback(() => {
  141. mutateDrawerOpened(false, false);
  142. }, [mutateDrawerOpened]);
  143. useEffect(() => {
  144. // this.hackUIController();
  145. setMounted(true);
  146. }, []);
  147. useEffect(() => {
  148. toggleDrawerMode(isDrawerMode);
  149. }, [isDrawerMode, toggleDrawerMode]);
  150. const resizableContainer = useRef<HTMLDivElement>(null);
  151. const setContentWidth = useCallback((newWidth) => {
  152. if (resizableContainer.current == null) {
  153. return;
  154. }
  155. resizableContainer.current.style.width = `${newWidth}px`;
  156. }, []);
  157. const hoverOnResizableContainerHandler = useCallback(() => {
  158. if (!isCollapsed || isDrawerMode || isDragging) {
  159. return;
  160. }
  161. setHover(true);
  162. setContentWidth(currentProductNavWidth);
  163. }, [isCollapsed, isDrawerMode, isDragging, setContentWidth, currentProductNavWidth]);
  164. const hoverOutHandler = useCallback(() => {
  165. if (!isCollapsed || isDrawerMode || isDragging) {
  166. return;
  167. }
  168. setHover(false);
  169. setContentWidth(sidebarMinimizeWidth);
  170. }, [isCollapsed, isDragging, isDrawerMode, setContentWidth]);
  171. const toggleNavigationBtnClickHandler = useCallback(() => {
  172. const newValue = !isCollapsed;
  173. mutateSidebarCollapsed(newValue, false);
  174. scheduleToPutUserUISettings({ isSidebarCollapsed: newValue });
  175. }, [isCollapsed, mutateSidebarCollapsed]);
  176. useEffect(() => {
  177. if (isCollapsed) {
  178. setContentWidth(sidebarMinimizeWidth);
  179. }
  180. else {
  181. setContentWidth(currentProductNavWidth);
  182. }
  183. }, [currentProductNavWidth, isCollapsed, setContentWidth]);
  184. const draggableAreaMoveHandler = useCallback((event: MouseEvent) => {
  185. event.preventDefault();
  186. const newWidth = event.pageX - 60;
  187. if (resizableContainer.current != null) {
  188. setContentWidth(newWidth);
  189. resizableContainer.current.classList.add('dragging');
  190. }
  191. }, [setContentWidth]);
  192. const dragableAreaMouseUpHandler = useCallback(() => {
  193. if (resizableContainer.current == null) {
  194. return;
  195. }
  196. setDrag(false);
  197. if (resizableContainer.current.clientWidth < sidebarMinWidth) {
  198. // force collapsed
  199. mutateSidebarCollapsed(true);
  200. mutateProductNavWidth(sidebarMinWidth, false);
  201. scheduleToPutUserUISettings({ isSidebarCollapsed: true, currentProductNavWidth: sidebarMinWidth });
  202. }
  203. else {
  204. const newWidth = resizableContainer.current.clientWidth;
  205. mutateProductNavWidth(newWidth, false);
  206. scheduleToPutUserUISettings({ currentProductNavWidth: newWidth });
  207. }
  208. resizableContainer.current.classList.remove('dragging');
  209. }, [mutateProductNavWidth, mutateSidebarCollapsed]);
  210. const dragableAreaMouseDownHandler = useCallback((event: React.MouseEvent) => {
  211. if (!isResizableByDrag) {
  212. return;
  213. }
  214. event.preventDefault();
  215. setDrag(true);
  216. const removeEventListeners = () => {
  217. document.removeEventListener('mousemove', draggableAreaMoveHandler);
  218. document.removeEventListener('mouseup', dragableAreaMouseUpHandler);
  219. document.removeEventListener('mouseup', removeEventListeners);
  220. };
  221. document.addEventListener('mousemove', draggableAreaMoveHandler);
  222. document.addEventListener('mouseup', dragableAreaMouseUpHandler);
  223. document.addEventListener('mouseup', removeEventListeners);
  224. }, [dragableAreaMouseUpHandler, draggableAreaMoveHandler, isResizableByDrag]);
  225. return (
  226. <>
  227. <div className={`grw-sidebar d-print-none ${isDrawerMode ? 'grw-sidebar-drawer' : ''} ${isDrawerOpened ? 'open' : ''}`}>
  228. <div className="data-layout-container">
  229. <div className="navigation" onMouseLeave={hoverOutHandler}>
  230. <div className="grw-navigation-wrap">
  231. <div className="grw-global-navigation">
  232. { isMounted ? <GlobalNavigation></GlobalNavigation> : <GlobalNavigationSkelton></GlobalNavigationSkelton> }
  233. </div>
  234. <div
  235. ref={resizableContainer}
  236. className="grw-contextual-navigation"
  237. onMouseEnter={hoverOnResizableContainerHandler}
  238. style={{ width: isCollapsed ? sidebarMinimizeWidth : currentProductNavWidth }}
  239. >
  240. <div className="grw-contextual-navigation-child">
  241. <div role="group" className={`grw-contextual-navigation-sub ${!isHover && isCollapsed ? 'collapsed' : ''}`}>
  242. { isMounted ? <SidebarContentsWrapper></SidebarContentsWrapper> : <SidebarSkeltonContents></SidebarSkeltonContents> }
  243. </div>
  244. </div>
  245. </div>
  246. </div>
  247. <div className="grw-navigation-draggable">
  248. <div
  249. className={`${isResizableByDrag ? 'resizable' : ''} grw-navigation-draggable-hitarea`}
  250. onMouseDown={dragableAreaMouseDownHandler}
  251. >
  252. <div className="grw-navigation-draggable-hitarea-child"></div>
  253. </div>
  254. <button
  255. className={`grw-navigation-resize-button ${!isDrawerMode ? 'resizable' : ''} ${isCollapsed ? 'collapsed' : ''} `}
  256. type="button"
  257. aria-expanded="true"
  258. aria-label="Toggle navigation"
  259. disabled={isDrawerMode}
  260. onClick={toggleNavigationBtnClickHandler}
  261. >
  262. <span className="hexagon-container" role="presentation">
  263. <NavigationResizeHexagon />
  264. </span>
  265. <span className="hitarea" role="presentation"></span>
  266. </button>
  267. </div>
  268. </div>
  269. </div>
  270. </div>
  271. { isDrawerOpened && (
  272. <div className="grw-sidebar-backdrop modal-backdrop show" onClick={backdropClickedHandler}></div>
  273. ) }
  274. </>
  275. );
  276. };
  277. export default Sidebar;