Sidebar.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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. const SidebarContentsWrapper = () => {
  35. const [resetKey, setResetKey] = useState(0);
  36. const scrollTargetSelector = '#grw-sidebar-contents-scroll-target';
  37. const calcViewHeight = useCallback(() => {
  38. const scrollTargetElem = document.querySelector('#grw-sidebar-contents-scroll-target');
  39. return scrollTargetElem != null
  40. ? window.innerHeight - scrollTargetElem?.getBoundingClientRect().top
  41. : window.innerHeight;
  42. }, []);
  43. return (
  44. <>
  45. <StickyStretchableScroller
  46. scrollTargetSelector={scrollTargetSelector}
  47. contentsElemSelector="#grw-sidebar-content-container"
  48. stickyElemSelector=".grw-sidebar"
  49. calcViewHeightFunc={calcViewHeight}
  50. resetKey={resetKey}
  51. />
  52. <div id="grw-sidebar-contents-scroll-target" style={{ minHeight: '100%' }}>
  53. <div id="grw-sidebar-content-container" onLoad={() => setResetKey(Math.random())}>
  54. <SidebarContents />
  55. </div>
  56. </div>
  57. <DrawerToggler iconClass="icon-arrow-left" />
  58. </>
  59. );
  60. };
  61. type Props = {
  62. }
  63. const Sidebar: FC<Props> = (props: Props) => {
  64. const { data: isDrawerMode } = useDrawerMode();
  65. const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
  66. const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth();
  67. const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed();
  68. const { data: isResizeDisabled, mutate: mutateSidebarResizeDisabled } = useSidebarResizeDisabled();
  69. const [isTransitionEnabled, setTransitionEnabled] = useState(false);
  70. const [isHover, setHover] = useState(false);
  71. const [isDragging, setDrag] = useState(false);
  72. const isResizableByDrag = !isResizeDisabled && !isDrawerMode && (!isCollapsed || isHover);
  73. const toggleDrawerMode = useCallback((bool) => {
  74. const isStateModified = isResizeDisabled !== bool;
  75. if (!isStateModified) {
  76. return;
  77. }
  78. // Drawer <-- Dock
  79. if (bool) {
  80. // disable resize
  81. mutateSidebarResizeDisabled(true, false);
  82. }
  83. // Drawer --> Dock
  84. else {
  85. // enable resize
  86. mutateSidebarResizeDisabled(false, false);
  87. }
  88. }, [isResizeDisabled, mutateSidebarResizeDisabled]);
  89. const backdropClickedHandler = useCallback(() => {
  90. mutateDrawerOpened(false, false);
  91. }, [mutateDrawerOpened]);
  92. useEffect(() => {
  93. setTimeout(() => {
  94. setTransitionEnabled(true);
  95. }, 1000);
  96. }, []);
  97. useEffect(() => {
  98. toggleDrawerMode(isDrawerMode);
  99. }, [isDrawerMode, toggleDrawerMode]);
  100. const resizableContainer = useRef<HTMLDivElement>(null);
  101. const setContentWidth = useCallback((newWidth) => {
  102. if (resizableContainer.current == null) {
  103. return;
  104. }
  105. resizableContainer.current.style.width = `${newWidth}px`;
  106. }, []);
  107. const hoverOnResizableContainerHandler = useCallback(() => {
  108. if (!isCollapsed || isDrawerMode || isDragging) {
  109. return;
  110. }
  111. setHover(true);
  112. setContentWidth(currentProductNavWidth);
  113. }, [isCollapsed, isDrawerMode, isDragging, setContentWidth, currentProductNavWidth]);
  114. const hoverOutHandler = useCallback(() => {
  115. if (!isCollapsed || isDrawerMode || isDragging) {
  116. return;
  117. }
  118. setHover(false);
  119. setContentWidth(sidebarMinimizeWidth);
  120. }, [isCollapsed, isDragging, isDrawerMode, setContentWidth]);
  121. const toggleNavigationBtnClickHandler = useCallback(() => {
  122. const newValue = !isCollapsed;
  123. mutateSidebarCollapsed(newValue, false);
  124. scheduleToPutUserUISettings({ isSidebarCollapsed: newValue });
  125. }, [isCollapsed, mutateSidebarCollapsed]);
  126. useEffect(() => {
  127. if (isCollapsed) {
  128. setContentWidth(sidebarMinimizeWidth);
  129. }
  130. else {
  131. setContentWidth(currentProductNavWidth);
  132. }
  133. }, [currentProductNavWidth, isCollapsed, setContentWidth]);
  134. const draggableAreaMoveHandler = useCallback((event: MouseEvent) => {
  135. event.preventDefault();
  136. const newWidth = event.pageX - 60;
  137. if (resizableContainer.current != null) {
  138. setContentWidth(newWidth);
  139. resizableContainer.current.classList.add('dragging');
  140. }
  141. }, [setContentWidth]);
  142. const dragableAreaMouseUpHandler = useCallback(() => {
  143. if (resizableContainer.current == null) {
  144. return;
  145. }
  146. setDrag(false);
  147. if (resizableContainer.current.clientWidth < sidebarMinWidth) {
  148. // force collapsed
  149. mutateSidebarCollapsed(true);
  150. mutateProductNavWidth(sidebarMinWidth, false);
  151. scheduleToPutUserUISettings({ isSidebarCollapsed: true, currentProductNavWidth: sidebarMinWidth });
  152. }
  153. else {
  154. const newWidth = resizableContainer.current.clientWidth;
  155. mutateSidebarCollapsed(false);
  156. mutateProductNavWidth(newWidth, false);
  157. scheduleToPutUserUISettings({ isSidebarCollapsed: false, currentProductNavWidth: newWidth });
  158. }
  159. resizableContainer.current.classList.remove('dragging');
  160. }, [mutateProductNavWidth, mutateSidebarCollapsed]);
  161. const dragableAreaMouseDownHandler = useCallback((event: React.MouseEvent) => {
  162. if (!isResizableByDrag) {
  163. return;
  164. }
  165. event.preventDefault();
  166. setDrag(true);
  167. const removeEventListeners = () => {
  168. document.removeEventListener('mousemove', draggableAreaMoveHandler);
  169. document.removeEventListener('mouseup', dragableAreaMouseUpHandler);
  170. document.removeEventListener('mouseup', removeEventListeners);
  171. };
  172. document.addEventListener('mousemove', draggableAreaMoveHandler);
  173. document.addEventListener('mouseup', dragableAreaMouseUpHandler);
  174. document.addEventListener('mouseup', removeEventListeners);
  175. }, [dragableAreaMouseUpHandler, draggableAreaMoveHandler, isResizableByDrag]);
  176. return (
  177. <>
  178. <div className={`grw-sidebar d-print-none ${isDrawerMode ? 'grw-sidebar-drawer' : ''} ${isDrawerOpened ? 'open' : ''}`}>
  179. <div className="data-layout-container">
  180. <div className={`navigation ${isTransitionEnabled ? 'transition-enabled' : ''}`} onMouseLeave={hoverOutHandler}>
  181. <div className="grw-navigation-wrap">
  182. <div className="grw-global-navigation">
  183. <GlobalNavigation></GlobalNavigation>
  184. </div>
  185. <div
  186. ref={resizableContainer}
  187. className="grw-contextual-navigation"
  188. onMouseEnter={hoverOnResizableContainerHandler}
  189. style={{ width: isCollapsed ? sidebarMinimizeWidth : currentProductNavWidth }}
  190. >
  191. <div className="grw-contextual-navigation-child">
  192. <div role="group" className={`grw-contextual-navigation-sub ${!isHover && isCollapsed ? 'collapsed' : ''}`}>
  193. <SidebarContentsWrapper></SidebarContentsWrapper>
  194. </div>
  195. </div>
  196. </div>
  197. </div>
  198. <div className="grw-navigation-draggable">
  199. { isResizableByDrag && (
  200. <div
  201. className="grw-navigation-draggable-hitarea"
  202. onMouseDown={dragableAreaMouseDownHandler}
  203. >
  204. <div className="grw-navigation-draggable-hitarea-child"></div>
  205. </div>
  206. ) }
  207. <button
  208. className={`grw-navigation-resize-button ${!isDrawerMode ? 'resizable' : ''} ${isCollapsed ? 'collapsed' : ''} `}
  209. type="button"
  210. aria-expanded="true"
  211. aria-label="Toggle navigation"
  212. disabled={isDrawerMode}
  213. onClick={toggleNavigationBtnClickHandler}
  214. >
  215. <span className="hexagon-container" role="presentation">
  216. <NavigationResizeHexagon />
  217. </span>
  218. <span className="hitarea" role="presentation"></span>
  219. </button>
  220. </div>
  221. </div>
  222. </div>
  223. </div>
  224. { isDrawerOpened && (
  225. <div className="grw-sidebar-backdrop modal-backdrop show" onClick={backdropClickedHandler}></div>
  226. ) }
  227. </>
  228. );
  229. };
  230. export default Sidebar;