Sidebar.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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 {
  8. useDrawerMode, useDrawerOpened,
  9. useCollapsedMode, useCollapsedContentsOpened,
  10. useCurrentProductNavWidth,
  11. useSidebarResizeDisabled,
  12. } from '~/stores/ui';
  13. import { ResizableArea } from './ResizableArea/ResizableArea';
  14. import { SidebarHead } from './SidebarHead';
  15. import { SidebarNav, type SidebarNavProps } from './SidebarNav';
  16. import styles from './Sidebar.module.scss';
  17. const SidebarContents = dynamic(() => import('./SidebarContents').then(mod => mod.SidebarContents), { ssr: false });
  18. const resizableAreaMinWidth = 348;
  19. const resizableAreaCollapsedWidth = 48;
  20. type ResizabeSidebarProps = {
  21. children?: React.ReactNode,
  22. }
  23. const ResizableSidebar = memo((props: ResizabeSidebarProps): JSX.Element => {
  24. const { children } = props;
  25. const { data: isDrawerMode } = useDrawerMode();
  26. const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth();
  27. const { data: isCollapsedMode, mutate: mutateCollapsedMode } = useCollapsedMode();
  28. const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
  29. const { data: isResizeDisabled, mutate: mutateSidebarResizeDisabled } = useSidebarResizeDisabled();
  30. const [resizableAreaWidth, setResizableAreaWidth] = useState<number|undefined>(undefined);
  31. const toggleDrawerMode = useCallback((bool) => {
  32. const isStateModified = isResizeDisabled !== bool;
  33. if (!isStateModified) {
  34. return;
  35. }
  36. // Drawer <-- Dock
  37. if (bool) {
  38. // disable resize
  39. mutateSidebarResizeDisabled(true, false);
  40. }
  41. // Drawer --> Dock
  42. else {
  43. // enable resize
  44. mutateSidebarResizeDisabled(false, false);
  45. }
  46. }, [isResizeDisabled, mutateSidebarResizeDisabled]);
  47. const resizeHandler = useCallback((newWidth: number) => {
  48. setResizableAreaWidth(newWidth);
  49. }, []);
  50. const resizeDoneHandler = useCallback((newWidth: number) => {
  51. mutateProductNavWidth(newWidth, false);
  52. scheduleToPut({ preferCollapsedModeByUser: false, currentProductNavWidth: newWidth });
  53. }, [mutateProductNavWidth]);
  54. const collapsedByResizableAreaHandler = useCallback(() => {
  55. mutateCollapsedMode(true);
  56. mutateCollapsedContentsOpened(false);
  57. scheduleToPut({ preferCollapsedModeByUser: true });
  58. }, [mutateCollapsedContentsOpened, mutateCollapsedMode]);
  59. useEffect(() => {
  60. toggleDrawerMode(isDrawerMode);
  61. }, [isDrawerMode, toggleDrawerMode]);
  62. // open/close resizable container when drawer mode
  63. useEffect(() => {
  64. if (isDrawerMode) {
  65. setResizableAreaWidth(undefined);
  66. }
  67. else if (isCollapsedMode) {
  68. setResizableAreaWidth(resizableAreaCollapsedWidth);
  69. }
  70. else {
  71. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  72. setResizableAreaWidth(currentProductNavWidth!);
  73. }
  74. }, [currentProductNavWidth, isCollapsedMode, isDrawerMode]);
  75. const disableResizing = isResizeDisabled || isDrawerMode || isCollapsedMode;
  76. return (
  77. <ResizableArea
  78. className="flex-expand-vert"
  79. width={resizableAreaWidth}
  80. minWidth={resizableAreaMinWidth}
  81. disabled={disableResizing}
  82. onResize={resizeHandler}
  83. onResizeDone={resizeDoneHandler}
  84. onCollapsed={collapsedByResizableAreaHandler}
  85. >
  86. {children}
  87. </ResizableArea>
  88. );
  89. });
  90. type CollapsibleSidebarProps = {
  91. Nav: FC<SidebarNavProps>,
  92. className?: string,
  93. children?: React.ReactNode,
  94. }
  95. const CollapsibleSidebar = memo((props: CollapsibleSidebarProps): JSX.Element => {
  96. const { Nav, className, children } = props;
  97. const { data: currentProductNavWidth } = useCurrentProductNavWidth();
  98. const { data: isCollapsedMode } = useCollapsedMode();
  99. const { data: isCollapsedContentsOpened, mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
  100. // open menu when collapsed mode
  101. const primaryItemHoverHandler = useCallback(() => {
  102. // reject other than collapsed mode
  103. if (!isCollapsedMode) {
  104. return;
  105. }
  106. mutateCollapsedContentsOpened(true);
  107. }, [isCollapsedMode, mutateCollapsedContentsOpened]);
  108. // close menu when collapsed mode
  109. const mouseLeaveHandler = useCallback(() => {
  110. // reject other than collapsed mode
  111. if (!isCollapsedMode) {
  112. return;
  113. }
  114. mutateCollapsedContentsOpened(false);
  115. }, [isCollapsedMode, mutateCollapsedContentsOpened]);
  116. const openClass = `${isCollapsedContentsOpened ? 'open' : ''}`;
  117. const collapsibleContentsWidth = isCollapsedMode ? currentProductNavWidth : undefined;
  118. return (
  119. <div className={`flex-expand-horiz ${className}`} onMouseLeave={mouseLeaveHandler}>
  120. <Nav onPrimaryItemHover={primaryItemHoverHandler} />
  121. <div className={`sidebar-contents-container flex-grow-1 overflow-y-auto ${openClass}`} style={{ width: collapsibleContentsWidth }}>
  122. {children}
  123. </div>
  124. </div>
  125. );
  126. });
  127. type DrawableSidebarProps = {
  128. className?: string,
  129. children?: React.ReactNode,
  130. }
  131. const DrawableSidebar = memo((props: DrawableSidebarProps): JSX.Element => {
  132. const { className, children } = props;
  133. const { data: isDrawerOpened } = useDrawerOpened();
  134. const openClass = `${isDrawerOpened ? 'open' : ''}`;
  135. return (
  136. <div className={`${className} ${openClass}`}>
  137. {children}
  138. </div>
  139. );
  140. });
  141. export const Sidebar = (): JSX.Element => {
  142. const { data: isDrawerMode } = useDrawerMode();
  143. const { data: isCollapsedMode } = useCollapsedMode();
  144. // css styles
  145. const grwSidebarClass = styles['grw-sidebar'];
  146. // eslint-disable-next-line no-nested-ternary
  147. const modeClass = isDrawerMode
  148. ? 'grw-sidebar-drawer'
  149. : isCollapsedMode
  150. ? 'grw-sidebar-collapsed'
  151. : 'grw-sidebar-dock';
  152. return (
  153. <DrawableSidebar className={`${grwSidebarClass} ${modeClass} border-end vh-100`} data-testid="grw-sidebar">
  154. <ResizableSidebar>
  155. <SidebarHead />
  156. <CollapsibleSidebar Nav={SidebarNav} className="border-top">
  157. <SidebarContents />
  158. </CollapsibleSidebar>
  159. </ResizableSidebar>
  160. </DrawableSidebar>
  161. );
  162. };