import { type FC, memo, useCallback, useEffect, useState, useRef, type JSX, } from 'react'; import withLoadingProps from 'next-dynamic-loading-props'; import dynamic from 'next/dynamic'; import SimpleBar from 'simplebar-react'; import { useIsomorphicLayoutEffect } from 'usehooks-ts'; import { SidebarMode } from '~/interfaces/ui'; import { useDeviceLargerThanXl } from '~/states/ui/device'; import { useDrawerOpened, usePreferCollapsedMode, useSidebarMode, useCollapsedContentsOpened, useCurrentProductNavWidth, } from '~/states/ui/sidebar'; import { useIsSearchPage } from '~/stores-universal/context'; import { EditorMode, useEditorMode } from '~/stores-universal/ui'; import { useSidebarScrollerRef, useIsDeviceLargerThanMd, } from '~/stores/ui'; import { DrawerToggler } from '../Common/DrawerToggler'; import { AppTitleOnSidebarHead, AppTitleOnEditorSidebarHead, AppTitleOnSubnavigation } from './AppTitle/AppTitle'; import { ResizableAreaFallback } from './ResizableArea/ResizableAreaFallback'; import type { ResizableAreaProps } from './ResizableArea/props'; import { SidebarHead } from './SidebarHead'; import { SidebarNav, type SidebarNavProps } from './SidebarNav'; import 'simplebar-react/dist/simplebar.min.css'; import styles from './Sidebar.module.scss'; const SidebarContents = dynamic(() => import('./SidebarContents').then(mod => mod.SidebarContents), { ssr: false }); const ResizableArea = withLoadingProps(useLoadingProps => dynamic( () => import('./ResizableArea').then(mod => mod.ResizableArea), { ssr: false, loading: () => { // eslint-disable-next-line react-hooks/rules-of-hooks const { children, ...rest } = useLoadingProps(); return {children}; }, }, )); const resizableAreaMinWidth = 348; const sidebarNavCollapsedWidth = 48; const getWidthByMode = (isDrawerMode: boolean, isCollapsedMode: boolean, currentProductNavWidth: number | undefined): number | undefined => { if (isDrawerMode) { return undefined; } if (isCollapsedMode) { return sidebarNavCollapsedWidth; } return currentProductNavWidth; }; type ResizableContainerProps = { children?: React.ReactNode, } const ResizableContainer = memo((props: ResizableContainerProps): JSX.Element => { const { children } = props; const { isDrawerMode, isCollapsedMode, isDockMode } = useSidebarMode(); const [, setIsDrawerOpened] = useDrawerOpened(); const [currentProductNavWidth, setCurrentProductNavWidth] = useCurrentProductNavWidth(); const [, setPreferCollapsedMode] = usePreferCollapsedMode(); const [, setCollapsedContentsOpened] = useCollapsedContentsOpened(); const [isClient, setClient] = useState(false); const [resizableAreaWidth, setResizableAreaWidth] = useState( getWidthByMode(isDrawerMode(), isCollapsedMode(), currentProductNavWidth), ); const resizeHandler = useCallback((newWidth: number) => { setResizableAreaWidth(newWidth); }, []); const resizeDoneHandler = useCallback((newWidth: number) => { setCurrentProductNavWidth(newWidth); }, [setCurrentProductNavWidth]); const collapsedByResizableAreaHandler = useCallback(() => { setPreferCollapsedMode(true); setCollapsedContentsOpened(false); }, [setCollapsedContentsOpened, setPreferCollapsedMode]); useIsomorphicLayoutEffect(() => { setClient(true); }, []); // open/close resizable container when drawer mode useEffect(() => { setResizableAreaWidth(getWidthByMode(isDrawerMode(), isCollapsedMode(), currentProductNavWidth)); setIsDrawerOpened(false); }, [currentProductNavWidth, isCollapsedMode, isDrawerMode, setIsDrawerOpened]); return !isClient ? ( {children} ) : ( {children} ); }); type CollapsibleContainerProps = { Nav: FC, className?: string, children?: React.ReactNode, } const CollapsibleContainer = memo((props: CollapsibleContainerProps): JSX.Element => { const { Nav, className, children } = props; const { isCollapsedMode } = useSidebarMode(); const [currentProductNavWidth] = useCurrentProductNavWidth(); const [isCollapsedContentsOpened, setCollapsedContentsOpened] = useCollapsedContentsOpened(); const sidebarScrollerRef = useRef(null); const { mutate: mutateSidebarScroller } = useSidebarScrollerRef(); mutateSidebarScroller(sidebarScrollerRef); // open menu when collapsed mode const primaryItemHoverHandler = useCallback(() => { // reject other than collapsed mode if (!isCollapsedMode()) { return; } setCollapsedContentsOpened(true); }, [isCollapsedMode, setCollapsedContentsOpened]); // close menu when collapsed mode const mouseLeaveHandler = useCallback(() => { // reject other than collapsed mode if (!isCollapsedMode()) { return; } setCollapsedContentsOpened(false); }, [isCollapsedMode, setCollapsedContentsOpened]); const closedClass = isCollapsedMode() && !isCollapsedContentsOpened ? 'd-none' : ''; const openedClass = isCollapsedMode() && isCollapsedContentsOpened ? 'open' : ''; const collapsibleContentsWidth = isCollapsedMode() ? currentProductNavWidth : undefined; return (
); }); // for data-* attributes type HTMLElementProps = JSX.IntrinsicElements & Record; type DrawableContainerProps = { divProps?: HTMLElementProps['div'], className?: string, children?: React.ReactNode, } const DrawableContainer = memo((props: DrawableContainerProps): JSX.Element => { const { divProps, className, children } = props; const [isDrawerOpened, setIsDrawerOpened] = useDrawerOpened(); const openClass = `${isDrawerOpened ? 'open' : ''}`; return ( <>
{children}
{ isDrawerOpened && (
setIsDrawerOpened(false)} /> ) } ); }); export const Sidebar = (): JSX.Element => { const { sidebarMode, isDrawerMode, isCollapsedMode, isDockMode, } = useSidebarMode(); const { data: isSearchPage } = useIsSearchPage(); const { data: editorMode } = useEditorMode(); const { data: isMdSize } = useIsDeviceLargerThanMd(); const [isXlSize] = useDeviceLargerThanXl(); const isEditorMode = editorMode === EditorMode.Editor; const shouldHideSiteName = isEditorMode && isXlSize; const shouldHideSubnavAppTitle = isEditorMode && isMdSize && (isDrawerMode() || isCollapsedMode()); const shouldShowEditorSidebarHead = isEditorMode && isXlSize; // css styles const grwSidebarClass = styles['grw-sidebar']; // eslint-disable-next-line no-nested-ternary let modeClass; switch (sidebarMode) { case SidebarMode.DRAWER: modeClass = 'grw-sidebar-drawer'; break; case SidebarMode.COLLAPSED: modeClass = 'grw-sidebar-collapsed'; break; case SidebarMode.DOCK: modeClass = 'grw-sidebar-dock'; break; } return ( <> { sidebarMode != null && isDrawerMode() && ( reorder )} { sidebarMode != null && !isDockMode() && !isSearchPage && !shouldHideSubnavAppTitle && ( )} { sidebarMode != null && !isCollapsedMode() && ( )} {shouldShowEditorSidebarHead ? : } ); };