Sidebar.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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 } = useDrawerOpened();
  119. const openClass = `${isDrawerOpened ? 'open' : ''}`;
  120. return (
  121. <div className={`${className} ${openClass}`}>
  122. {children}
  123. </div>
  124. );
  125. });
  126. export const Sidebar = (): JSX.Element => {
  127. const { data: sidebarMode, isDrawerMode, isDockMode } = useSidebarMode();
  128. // css styles
  129. const grwSidebarClass = styles['grw-sidebar'];
  130. // eslint-disable-next-line no-nested-ternary
  131. let modeClass;
  132. switch (sidebarMode) {
  133. case SidebarMode.DRAWER:
  134. modeClass = 'grw-sidebar-drawer';
  135. break;
  136. case SidebarMode.COLLAPSED:
  137. modeClass = 'grw-sidebar-collapsed';
  138. break;
  139. case SidebarMode.DOCK:
  140. modeClass = 'grw-sidebar-dock';
  141. break;
  142. }
  143. return (
  144. <>
  145. { sidebarMode != null && isDrawerMode() && (
  146. <DrawerToggler className="position-fixed d-none d-md-block">
  147. <span className="material-symbols-outlined">reorder</span>
  148. </DrawerToggler>
  149. ) }
  150. { sidebarMode != null && !isDockMode() && <AppTitleOnSubnavigation /> }
  151. <DrawableContainer className={`${grwSidebarClass} ${modeClass} border-end vh-100`} data-testid="grw-sidebar">
  152. <ResizableContainer>
  153. { sidebarMode != null && isDockMode() && <AppTitleOnSidebarHead /> }
  154. <SidebarHead />
  155. <CollapsibleContainer Nav={SidebarNav} className="border-top">
  156. <SidebarContents />
  157. </CollapsibleContainer>
  158. </ResizableContainer>
  159. </DrawableContainer>
  160. </>
  161. );
  162. };