Sidebar.tsx 6.4 KB

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