Sidebar.tsx 6.2 KB

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