Sidebar.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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 { 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, mutate: mutateProductNavWidth } = useCurrentProductNavWidth();
  31. const { mutate: 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. scheduleToPut({ preferCollapsedModeByUser: false, currentProductNavWidth: newWidth });
  40. }, [mutateProductNavWidth]);
  41. const collapsedByResizableAreaHandler = useCallback(() => {
  42. mutatePreferCollapsedMode(true);
  43. mutateCollapsedContentsOpened(false);
  44. scheduleToPut({ preferCollapsedModeByUser: true });
  45. }, [mutateCollapsedContentsOpened, mutatePreferCollapsedMode]);
  46. // open/close resizable container when drawer mode
  47. useEffect(() => {
  48. if (isDrawerMode()) {
  49. setResizableAreaWidth(undefined);
  50. }
  51. else if (isCollapsedMode()) {
  52. setResizableAreaWidth(sidebarNavCollapsedWidth);
  53. }
  54. else {
  55. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  56. setResizableAreaWidth(currentProductNavWidth!);
  57. }
  58. mutateDrawerOpened(false);
  59. }, [currentProductNavWidth, isCollapsedMode, isDrawerMode, mutateDrawerOpened]);
  60. return (
  61. <ResizableArea
  62. className="flex-expand-vert"
  63. width={resizableAreaWidth}
  64. minWidth={resizableAreaMinWidth}
  65. disabled={!isDockMode()}
  66. onResize={resizeHandler}
  67. onResizeDone={resizeDoneHandler}
  68. onCollapsed={collapsedByResizableAreaHandler}
  69. >
  70. {children}
  71. </ResizableArea>
  72. );
  73. });
  74. type CollapsibleContainerProps = {
  75. Nav: FC<SidebarNavProps>,
  76. className?: string,
  77. children?: React.ReactNode,
  78. }
  79. const CollapsibleContainer = memo((props: CollapsibleContainerProps): JSX.Element => {
  80. const { Nav, className, children } = props;
  81. const { isCollapsedMode } = useSidebarMode();
  82. const { data: currentProductNavWidth } = useCurrentProductNavWidth();
  83. const { data: isCollapsedContentsOpened, mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
  84. // open menu when collapsed mode
  85. const primaryItemHoverHandler = useCallback(() => {
  86. // reject other than collapsed mode
  87. if (!isCollapsedMode()) {
  88. return;
  89. }
  90. mutateCollapsedContentsOpened(true);
  91. }, [isCollapsedMode, mutateCollapsedContentsOpened]);
  92. // close menu when collapsed mode
  93. const mouseLeaveHandler = useCallback(() => {
  94. // reject other than collapsed mode
  95. if (!isCollapsedMode()) {
  96. return;
  97. }
  98. mutateCollapsedContentsOpened(false);
  99. }, [isCollapsedMode, mutateCollapsedContentsOpened]);
  100. const openClass = `${isCollapsedContentsOpened ? 'open' : ''}`;
  101. const collapsibleContentsWidth = isCollapsedMode() ? currentProductNavWidth : undefined;
  102. return (
  103. <div className={`flex-expand-horiz ${className}`} onMouseLeave={mouseLeaveHandler}>
  104. <Nav onPrimaryItemHover={primaryItemHoverHandler} />
  105. <div className={`sidebar-contents-container flex-grow-1 overflow-y-auto ${openClass}`} style={{ width: collapsibleContentsWidth }}>
  106. {children}
  107. </div>
  108. </div>
  109. );
  110. });
  111. type DrawableContainerProps = {
  112. className?: string,
  113. children?: React.ReactNode,
  114. }
  115. const DrawableContainer = memo((props: DrawableContainerProps): JSX.Element => {
  116. const { className, children } = props;
  117. const { data: isDrawerOpened } = useDrawerOpened();
  118. const openClass = `${isDrawerOpened ? 'open' : ''}`;
  119. return (
  120. <div className={`${className} ${openClass}`}>
  121. {children}
  122. </div>
  123. );
  124. });
  125. export const Sidebar = (): JSX.Element => {
  126. const { data: sidebarMode, isCollapsedMode } = useSidebarMode();
  127. // css styles
  128. const grwSidebarClass = styles['grw-sidebar'];
  129. // eslint-disable-next-line no-nested-ternary
  130. let modeClass;
  131. switch (sidebarMode) {
  132. case SidebarMode.DRAWER:
  133. modeClass = 'grw-sidebar-drawer';
  134. break;
  135. case SidebarMode.COLLAPSED:
  136. modeClass = 'grw-sidebar-collapsed';
  137. break;
  138. case SidebarMode.DOCK:
  139. modeClass = 'grw-sidebar-dock';
  140. break;
  141. }
  142. return (
  143. <>
  144. { isCollapsedMode() && <AppTitleOnSubnavigation /> }
  145. <DrawableContainer className={`${grwSidebarClass} ${modeClass} border-end vh-100`} data-testid="grw-sidebar">
  146. <ResizableContainer>
  147. { !isCollapsedMode() && <AppTitleOnSidebarHead /> }
  148. <SidebarHead />
  149. <CollapsibleContainer Nav={SidebarNav} className="border-top">
  150. <SidebarContents />
  151. </CollapsibleContainer>
  152. </ResizableContainer>
  153. </DrawableContainer>
  154. </>
  155. );
  156. };