Sidebar.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. import React, {
  2. memo, useCallback, useEffect, useRef, useState,
  3. } from 'react';
  4. import dynamic from 'next/dynamic';
  5. import { useUserUISettings } from '~/client/services/user-ui-settings';
  6. import {
  7. useDrawerMode, useDrawerOpened,
  8. useSidebarCollapsed,
  9. useCurrentSidebarContents,
  10. useCurrentProductNavWidth,
  11. useSidebarResizeDisabled,
  12. } from '~/stores/ui';
  13. import DrawerToggler from '../Navbar/DrawerToggler';
  14. import { NavigationResizeHexagon } from './NavigationResizeHexagon';
  15. import { SidebarNav } from './SidebarNav';
  16. import styles from './Sidebar.module.scss';
  17. const SidebarContents = dynamic(() => import('./SidebarContents').then(mod => mod.SidebarContents), { ssr: false });
  18. const sidebarMinWidth = 240;
  19. const sidebarMinimizeWidth = 20;
  20. const sidebarFixedWidthInDrawerMode = 320;
  21. export const Sidebar = memo((): JSX.Element => {
  22. const { data: isDrawerMode } = useDrawerMode();
  23. const { data: isDrawerOpened } = useDrawerOpened();
  24. const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth();
  25. const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed();
  26. const { data: isResizeDisabled, mutate: mutateSidebarResizeDisabled } = useSidebarResizeDisabled();
  27. const { scheduleToPut } = useUserUISettings();
  28. const [isHover, setHover] = useState(false);
  29. const [isHoverOnResizableContainer, setHoverOnResizableContainer] = useState(false);
  30. const [isDragging, setDrag] = useState(false);
  31. const resizableContainer = useRef<HTMLDivElement>(null);
  32. const timeoutIdRef = useRef<NodeJS.Timeout>();
  33. const isResizableByDrag = !isResizeDisabled && !isDrawerMode && (!isCollapsed || isHover);
  34. const toggleDrawerMode = useCallback((bool) => {
  35. const isStateModified = isResizeDisabled !== bool;
  36. if (!isStateModified) {
  37. return;
  38. }
  39. // Drawer <-- Dock
  40. if (bool) {
  41. // disable resize
  42. mutateSidebarResizeDisabled(true, false);
  43. }
  44. // Drawer --> Dock
  45. else {
  46. // enable resize
  47. mutateSidebarResizeDisabled(false, false);
  48. }
  49. }, [isResizeDisabled, mutateSidebarResizeDisabled]);
  50. const setContentWidth = useCallback((newWidth: number) => {
  51. if (resizableContainer.current == null) {
  52. return;
  53. }
  54. resizableContainer.current.style.width = `${newWidth}px`;
  55. }, []);
  56. const hoverOnHandler = useCallback(() => {
  57. if (!isCollapsed || isDrawerMode || isDragging) {
  58. return;
  59. }
  60. setHover(true);
  61. }, [isCollapsed, isDragging, isDrawerMode]);
  62. const hoverOutHandler = useCallback(() => {
  63. if (!isCollapsed || isDrawerMode || isDragging) {
  64. return;
  65. }
  66. setHover(false);
  67. }, [isCollapsed, isDragging, isDrawerMode]);
  68. const hoverOnResizableContainerHandler = useCallback(() => {
  69. if (!isCollapsed || isDrawerMode || isDragging) {
  70. return;
  71. }
  72. setHoverOnResizableContainer(true);
  73. }, [isCollapsed, isDrawerMode, isDragging]);
  74. const hoverOutResizableContainerHandler = useCallback(() => {
  75. if (!isCollapsed || isDrawerMode || isDragging) {
  76. return;
  77. }
  78. setHoverOnResizableContainer(false);
  79. }, [isCollapsed, isDrawerMode, isDragging]);
  80. const toggleNavigationBtnClickHandler = useCallback(() => {
  81. const newValue = !isCollapsed;
  82. mutateSidebarCollapsed(newValue, false);
  83. scheduleToPut({ isSidebarCollapsed: newValue });
  84. }, [isCollapsed, mutateSidebarCollapsed, scheduleToPut]);
  85. useEffect(() => {
  86. if (isCollapsed) {
  87. setContentWidth(sidebarMinimizeWidth);
  88. }
  89. else {
  90. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  91. setContentWidth(currentProductNavWidth!);
  92. }
  93. }, [currentProductNavWidth, isCollapsed, setContentWidth]);
  94. const draggableAreaMoveHandler = useCallback((event: MouseEvent) => {
  95. event.preventDefault();
  96. const newWidth = event.pageX - 60;
  97. if (resizableContainer.current != null) {
  98. setContentWidth(newWidth);
  99. resizableContainer.current.classList.add('dragging');
  100. }
  101. }, [setContentWidth]);
  102. const dragableAreaMouseUpHandler = useCallback(() => {
  103. if (resizableContainer.current == null) {
  104. return;
  105. }
  106. setDrag(false);
  107. if (resizableContainer.current.clientWidth < sidebarMinWidth) {
  108. // force collapsed
  109. mutateSidebarCollapsed(true);
  110. mutateProductNavWidth(sidebarMinWidth, false);
  111. scheduleToPut({ isSidebarCollapsed: true, currentProductNavWidth: sidebarMinWidth });
  112. }
  113. else {
  114. const newWidth = resizableContainer.current.clientWidth;
  115. mutateSidebarCollapsed(false);
  116. mutateProductNavWidth(newWidth, false);
  117. scheduleToPut({ isSidebarCollapsed: false, currentProductNavWidth: newWidth });
  118. }
  119. resizableContainer.current.classList.remove('dragging');
  120. }, [mutateProductNavWidth, mutateSidebarCollapsed, scheduleToPut]);
  121. const dragableAreaMouseDownHandler = useCallback((event: React.MouseEvent) => {
  122. if (!isResizableByDrag) {
  123. return;
  124. }
  125. event.preventDefault();
  126. setDrag(true);
  127. const removeEventListeners = () => {
  128. document.removeEventListener('mousemove', draggableAreaMoveHandler);
  129. document.removeEventListener('mouseup', dragableAreaMouseUpHandler);
  130. document.removeEventListener('mouseup', removeEventListeners);
  131. };
  132. document.addEventListener('mousemove', draggableAreaMoveHandler);
  133. document.addEventListener('mouseup', dragableAreaMouseUpHandler);
  134. document.addEventListener('mouseup', removeEventListeners);
  135. }, [dragableAreaMouseUpHandler, draggableAreaMoveHandler, isResizableByDrag]);
  136. useEffect(() => {
  137. toggleDrawerMode(isDrawerMode);
  138. }, [isDrawerMode, toggleDrawerMode]);
  139. // open/close resizable container
  140. useEffect(() => {
  141. if (!isCollapsed) {
  142. return;
  143. }
  144. if (isHoverOnResizableContainer) {
  145. // schedule to open
  146. timeoutIdRef.current = setTimeout(() => {
  147. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  148. setContentWidth(currentProductNavWidth!);
  149. }, 70);
  150. }
  151. else if (timeoutIdRef.current != null) {
  152. // cancel schedule to open
  153. clearTimeout(timeoutIdRef.current);
  154. timeoutIdRef.current = undefined;
  155. }
  156. // close
  157. if (!isHover) {
  158. setContentWidth(sidebarMinimizeWidth);
  159. timeoutIdRef.current = undefined;
  160. }
  161. }, [isCollapsed, isHover, isHoverOnResizableContainer, currentProductNavWidth, setContentWidth]);
  162. // open/close resizable container when drawer mode
  163. useEffect(() => {
  164. if (isDrawerMode) {
  165. setContentWidth(sidebarFixedWidthInDrawerMode);
  166. }
  167. else if (isCollapsed) {
  168. setContentWidth(sidebarMinimizeWidth);
  169. }
  170. else {
  171. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  172. setContentWidth(currentProductNavWidth!);
  173. }
  174. }, [currentProductNavWidth, isCollapsed, isDrawerMode, setContentWidth]);
  175. const showContents = isDrawerMode || isHover || !isCollapsed;
  176. // css styles
  177. const grwSidebarClass = `grw-sidebar ${styles['grw-sidebar']}`;
  178. const sidebarModeClass = `${isDrawerMode ? 'grw-sidebar-drawer' : 'grw-sidebar-dock'}`;
  179. const isOpenClass = `${isDrawerOpened ? 'open' : ''}`;
  180. return (
  181. <>
  182. <div className={`${grwSidebarClass} ${sidebarModeClass} ${isOpenClass} d-print-none`} data-testid="grw-sidebar">
  183. <div className="data-layout-container">
  184. <div
  185. className="navigation transition-enabled"
  186. onMouseEnter={hoverOnHandler}
  187. onMouseLeave={hoverOutHandler}
  188. >
  189. <div className="grw-navigation-wrap">
  190. <div className="grw-global-navigation">
  191. <SidebarNav />
  192. </div>
  193. <div
  194. ref={resizableContainer}
  195. className="grw-contextual-navigation"
  196. onMouseEnter={hoverOnResizableContainerHandler}
  197. onMouseLeave={hoverOutResizableContainerHandler}
  198. style={{ width: isCollapsed ? sidebarMinimizeWidth : currentProductNavWidth }}
  199. >
  200. <div className={`grw-contextual-navigation-child ${showContents ? '' : 'd-none'}`} data-testid="grw-contextual-navigation-child">
  201. <SidebarContents />
  202. <DrawerToggler iconClass="icon-arrow-left" />
  203. </div>
  204. </div>
  205. </div>
  206. <div className="grw-navigation-draggable">
  207. { isResizableByDrag && (
  208. <div
  209. className="grw-navigation-draggable-hitarea"
  210. onMouseDown={dragableAreaMouseDownHandler}
  211. >
  212. <div className="grw-navigation-draggable-hitarea-child"></div>
  213. </div>
  214. ) }
  215. <button
  216. data-testid="grw-navigation-resize-button"
  217. className={`grw-navigation-resize-button ${!isDrawerMode ? 'resizable' : ''} ${isCollapsed ? 'collapsed' : ''} `}
  218. type="button"
  219. aria-expanded="true"
  220. aria-label="Toggle navigation"
  221. disabled={isDrawerMode}
  222. onClick={toggleNavigationBtnClickHandler}
  223. >
  224. <span className="hexagon-container" role="presentation">
  225. <NavigationResizeHexagon />
  226. </span>
  227. <span className="hitarea" role="presentation"></span>
  228. </button>
  229. </div>
  230. </div>
  231. </div>
  232. </div>
  233. </>
  234. );
  235. });
  236. Sidebar.displayName = 'Sidebar';