Sidebar.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import React, {
  2. type FC,
  3. memo, useCallback, useEffect, useState,
  4. useRef,
  5. } from 'react';
  6. import dynamic from 'next/dynamic';
  7. import { SidebarMode } from '~/interfaces/ui';
  8. import { useIsSearchPage } from '~/stores/context';
  9. import {
  10. useDrawerOpened,
  11. useCollapsedContentsOpened,
  12. useCurrentProductNavWidth,
  13. usePreferCollapsedMode,
  14. useSidebarMode,
  15. useSidebarScrollerRef,
  16. } from '~/stores/ui';
  17. import { DrawerToggler } from '../Common/DrawerToggler';
  18. import { AppTitleOnSidebarHead, AppTitleOnSubnavigation } from './AppTitle/AppTitle';
  19. import { ResizableArea } from './ResizableArea/ResizableArea';
  20. import { SidebarHead } from './SidebarHead';
  21. import { SidebarNav, type SidebarNavProps } from './SidebarNav';
  22. import styles from './Sidebar.module.scss';
  23. const SidebarContents = dynamic(() => import('./SidebarContents').then(mod => mod.SidebarContents), { ssr: false });
  24. const resizableAreaMinWidth = 348;
  25. const sidebarNavCollapsedWidth = 48;
  26. type ResizableContainerProps = {
  27. children?: React.ReactNode,
  28. }
  29. const ResizableContainer = memo((props: ResizableContainerProps): JSX.Element => {
  30. const { children } = props;
  31. const { isDrawerMode, isCollapsedMode, isDockMode } = useSidebarMode();
  32. const { mutate: mutateDrawerOpened } = useDrawerOpened();
  33. const { data: currentProductNavWidth, mutateAndSave: mutateProductNavWidth } = useCurrentProductNavWidth();
  34. const { mutateAndSave: mutatePreferCollapsedMode } = usePreferCollapsedMode();
  35. const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
  36. const [resizableAreaWidth, setResizableAreaWidth] = useState<number|undefined>(undefined);
  37. const resizeHandler = useCallback((newWidth: number) => {
  38. setResizableAreaWidth(newWidth);
  39. }, []);
  40. const resizeDoneHandler = useCallback((newWidth: number) => {
  41. mutateProductNavWidth(newWidth, false);
  42. }, [mutateProductNavWidth]);
  43. const collapsedByResizableAreaHandler = useCallback(() => {
  44. mutatePreferCollapsedMode(true);
  45. mutateCollapsedContentsOpened(false);
  46. }, [mutateCollapsedContentsOpened, mutatePreferCollapsedMode]);
  47. // open/close resizable container when drawer mode
  48. useEffect(() => {
  49. if (isDrawerMode()) {
  50. setResizableAreaWidth(undefined);
  51. }
  52. else if (isCollapsedMode()) {
  53. setResizableAreaWidth(sidebarNavCollapsedWidth);
  54. }
  55. else {
  56. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  57. setResizableAreaWidth(currentProductNavWidth!);
  58. }
  59. mutateDrawerOpened(false);
  60. }, [currentProductNavWidth, isCollapsedMode, isDrawerMode, mutateDrawerOpened]);
  61. return (
  62. <ResizableArea
  63. className="flex-expand-vert"
  64. width={resizableAreaWidth}
  65. minWidth={resizableAreaMinWidth}
  66. disabled={!isDockMode()}
  67. onResize={resizeHandler}
  68. onResizeDone={resizeDoneHandler}
  69. onCollapsed={collapsedByResizableAreaHandler}
  70. >
  71. {children}
  72. </ResizableArea>
  73. );
  74. });
  75. type CollapsibleContainerProps = {
  76. Nav: FC<SidebarNavProps>,
  77. className?: string,
  78. children?: React.ReactNode,
  79. }
  80. const CollapsibleContainer = memo((props: CollapsibleContainerProps): JSX.Element => {
  81. const { Nav, className, children } = props;
  82. const { isCollapsedMode } = useSidebarMode();
  83. const { data: currentProductNavWidth } = useCurrentProductNavWidth();
  84. const { data: isCollapsedContentsOpened, mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
  85. const sidebarScrollerRef = useRef<HTMLDivElement>(null);
  86. const { mutate: mutateSidebarScroller } = useSidebarScrollerRef();
  87. mutateSidebarScroller(sidebarScrollerRef);
  88. // open menu when collapsed mode
  89. const primaryItemHoverHandler = useCallback(() => {
  90. // reject other than collapsed mode
  91. if (!isCollapsedMode()) {
  92. return;
  93. }
  94. mutateCollapsedContentsOpened(true);
  95. }, [isCollapsedMode, mutateCollapsedContentsOpened]);
  96. // close menu when collapsed mode
  97. const mouseLeaveHandler = useCallback(() => {
  98. // reject other than collapsed mode
  99. if (!isCollapsedMode()) {
  100. return;
  101. }
  102. mutateCollapsedContentsOpened(false);
  103. }, [isCollapsedMode, mutateCollapsedContentsOpened]);
  104. const closedClass = isCollapsedMode() && !isCollapsedContentsOpened ? 'd-none' : '';
  105. const openedClass = isCollapsedMode() && isCollapsedContentsOpened ? 'open' : '';
  106. const collapsibleContentsWidth = isCollapsedMode() ? currentProductNavWidth : undefined;
  107. return (
  108. <div className={`flex-expand-horiz ${className}`} onMouseLeave={mouseLeaveHandler}>
  109. <Nav onPrimaryItemHover={primaryItemHoverHandler} />
  110. <div
  111. ref={sidebarScrollerRef}
  112. className={`sidebar-contents-container flex-grow-1 overflow-y-auto overflow-x-hidden ${closedClass} ${openedClass}`}
  113. style={{ width: collapsibleContentsWidth }}
  114. >
  115. {children}
  116. </div>
  117. </div>
  118. );
  119. });
  120. // for data-* attributes
  121. type HTMLElementProps = JSX.IntrinsicElements &
  122. Record<keyof JSX.IntrinsicElements, { [p: `data-${string}`]: string | number }>;
  123. type DrawableContainerProps = {
  124. divProps?: HTMLElementProps['div'],
  125. className?: string,
  126. children?: React.ReactNode,
  127. }
  128. const DrawableContainer = memo((props: DrawableContainerProps): JSX.Element => {
  129. const { divProps, className, children } = props;
  130. const { data: isDrawerOpened, mutate } = useDrawerOpened();
  131. const openClass = `${isDrawerOpened ? 'open' : ''}`;
  132. return (
  133. <>
  134. <div {...divProps} className={`${className} ${openClass}`}>
  135. {children}
  136. </div>
  137. { isDrawerOpened && (
  138. <div className="modal-backdrop fade show" onClick={() => mutate(false)} />
  139. ) }
  140. </>
  141. );
  142. });
  143. export const Sidebar = (): JSX.Element => {
  144. const {
  145. data: sidebarMode,
  146. isDrawerMode, isCollapsedMode, isDockMode,
  147. } = useSidebarMode();
  148. const { data: isSearchPage } = useIsSearchPage();
  149. // css styles
  150. const grwSidebarClass = styles['grw-sidebar'];
  151. // eslint-disable-next-line no-nested-ternary
  152. let modeClass;
  153. switch (sidebarMode) {
  154. case SidebarMode.DRAWER:
  155. modeClass = 'grw-sidebar-drawer';
  156. break;
  157. case SidebarMode.COLLAPSED:
  158. modeClass = 'grw-sidebar-collapsed';
  159. break;
  160. case SidebarMode.DOCK:
  161. modeClass = 'grw-sidebar-dock';
  162. break;
  163. }
  164. return (
  165. <>
  166. { sidebarMode != null && isDrawerMode() && (
  167. <DrawerToggler className="position-fixed d-none d-md-block">
  168. <span className="material-symbols-outlined">reorder</span>
  169. </DrawerToggler>
  170. ) }
  171. { sidebarMode != null && !isDockMode() && !isSearchPage && <AppTitleOnSubnavigation /> }
  172. <DrawableContainer className={`${grwSidebarClass} ${modeClass} border-end flex-expand-vh-100`} divProps={{ 'data-testid': 'grw-sidebar' }}>
  173. <ResizableContainer>
  174. { sidebarMode != null && !isCollapsedMode() && <AppTitleOnSidebarHead /> }
  175. <SidebarHead />
  176. <CollapsibleContainer Nav={SidebarNav} className="border-top">
  177. <SidebarContents />
  178. </CollapsibleContainer>
  179. </ResizableContainer>
  180. </DrawableContainer>
  181. </>
  182. );
  183. };