Sidebar.tsx 11 KB

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