Sidebar.tsx 10 KB

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