import React, { memo, useCallback, useEffect, useRef, useState, } from 'react'; import dynamic from 'next/dynamic'; import { useUserUISettings } from '~/client/services/user-ui-settings'; import { useDrawerMode, useDrawerOpened, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth, useSidebarResizeDisabled, useSidebarScrollerRef, } from '~/stores/ui'; import DrawerToggler from './Navbar/DrawerToggler'; import { NavigationResizeHexagon } from './Sidebar/NavigationResizeHexagon'; import { SidebarNav } from './Sidebar/SidebarNav'; import { SidebarSkeleton } from './Sidebar/Skeleton/SidebarSkeleton'; import { StickyStretchableScrollerProps } from './StickyStretchableScroller'; import styles from './Sidebar.module.scss'; const StickyStretchableScroller = dynamic(() => import('./StickyStretchableScroller') .then(mod => mod.StickyStretchableScroller), { ssr: false }); const SidebarContents = dynamic(() => import('./Sidebar/SidebarContents') .then(mod => mod.SidebarContents), { ssr: false, loading: () => }); const sidebarMinWidth = 240; const sidebarMinimizeWidth = 20; const sidebarFixedWidthInDrawerMode = 320; const GlobalNavigation = memo(() => { const { data: isDrawerMode } = useDrawerMode(); const { data: currentContents } = useCurrentSidebarContents(); const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed(); const { scheduleToPut } = useUserUISettings(); const itemSelectedHandler = useCallback((selectedContents) => { if (isDrawerMode) { return; } let newValue = false; // already selected if (currentContents === selectedContents) { // toggle collapsed newValue = !isCollapsed; } mutateSidebarCollapsed(newValue, false); scheduleToPut({ isSidebarCollapsed: newValue }); }, [currentContents, isCollapsed, isDrawerMode, mutateSidebarCollapsed, scheduleToPut]); return ; }); GlobalNavigation.displayName = 'GlobalNavigation'; const SidebarContentsWrapper = memo(() => { const { mutate: mutateSidebarScroller } = useSidebarScrollerRef(); const calcViewHeight = useCallback(() => { const elem = document.querySelector('#grw-sidebar-contents-wrapper'); return elem != null ? window.innerHeight - elem?.getBoundingClientRect().top : window.innerHeight; }, []); return ( <>
); }); SidebarContentsWrapper.displayName = 'SidebarContentsWrapper'; const Sidebar = memo((): JSX.Element => { const { data: isDrawerMode } = useDrawerMode(); const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened(); const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth(); const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed(); const { data: isResizeDisabled, mutate: mutateSidebarResizeDisabled } = useSidebarResizeDisabled(); const { scheduleToPut } = useUserUISettings(); const [isHover, setHover] = useState(false); const [isHoverOnResizableContainer, setHoverOnResizableContainer] = useState(false); const [isDragging, setDrag] = useState(false); const resizableContainer = useRef(null); const timeoutIdRef = useRef(); const isResizableByDrag = !isResizeDisabled && !isDrawerMode && (!isCollapsed || isHover); const toggleDrawerMode = useCallback((bool) => { const isStateModified = isResizeDisabled !== bool; if (!isStateModified) { return; } // Drawer <-- Dock if (bool) { // disable resize mutateSidebarResizeDisabled(true, false); } // Drawer --> Dock else { // enable resize mutateSidebarResizeDisabled(false, false); } }, [isResizeDisabled, mutateSidebarResizeDisabled]); const backdropClickedHandler = useCallback(() => { mutateDrawerOpened(false, false); }, [mutateDrawerOpened]); const setContentWidth = useCallback((newWidth: number) => { if (resizableContainer.current == null) { return; } resizableContainer.current.style.width = `${newWidth}px`; }, []); const hoverOnHandler = useCallback(() => { if (!isCollapsed || isDrawerMode || isDragging) { return; } setHover(true); }, [isCollapsed, isDragging, isDrawerMode]); const hoverOutHandler = useCallback(() => { if (!isCollapsed || isDrawerMode || isDragging) { return; } setHover(false); }, [isCollapsed, isDragging, isDrawerMode]); const hoverOnResizableContainerHandler = useCallback(() => { if (!isCollapsed || isDrawerMode || isDragging) { return; } setHoverOnResizableContainer(true); }, [isCollapsed, isDrawerMode, isDragging]); const hoverOutResizableContainerHandler = useCallback(() => { if (!isCollapsed || isDrawerMode || isDragging) { return; } setHoverOnResizableContainer(false); }, [isCollapsed, isDrawerMode, isDragging]); const toggleNavigationBtnClickHandler = useCallback(() => { const newValue = !isCollapsed; mutateSidebarCollapsed(newValue, false); scheduleToPut({ isSidebarCollapsed: newValue }); }, [isCollapsed, mutateSidebarCollapsed, scheduleToPut]); useEffect(() => { if (isCollapsed) { setContentWidth(sidebarMinimizeWidth); } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion setContentWidth(currentProductNavWidth!); } }, [currentProductNavWidth, isCollapsed, setContentWidth]); const draggableAreaMoveHandler = useCallback((event: MouseEvent) => { event.preventDefault(); const newWidth = event.pageX - 60; if (resizableContainer.current != null) { setContentWidth(newWidth); resizableContainer.current.classList.add('dragging'); } }, [setContentWidth]); const dragableAreaMouseUpHandler = useCallback(() => { if (resizableContainer.current == null) { return; } setDrag(false); if (resizableContainer.current.clientWidth < sidebarMinWidth) { // force collapsed mutateSidebarCollapsed(true); mutateProductNavWidth(sidebarMinWidth, false); scheduleToPut({ isSidebarCollapsed: true, currentProductNavWidth: sidebarMinWidth }); } else { const newWidth = resizableContainer.current.clientWidth; mutateSidebarCollapsed(false); mutateProductNavWidth(newWidth, false); scheduleToPut({ isSidebarCollapsed: false, currentProductNavWidth: newWidth }); } resizableContainer.current.classList.remove('dragging'); }, [mutateProductNavWidth, mutateSidebarCollapsed, scheduleToPut]); const dragableAreaMouseDownHandler = useCallback((event: React.MouseEvent) => { if (!isResizableByDrag) { return; } event.preventDefault(); setDrag(true); const removeEventListeners = () => { document.removeEventListener('mousemove', draggableAreaMoveHandler); document.removeEventListener('mouseup', dragableAreaMouseUpHandler); document.removeEventListener('mouseup', removeEventListeners); }; document.addEventListener('mousemove', draggableAreaMoveHandler); document.addEventListener('mouseup', dragableAreaMouseUpHandler); document.addEventListener('mouseup', removeEventListeners); }, [dragableAreaMouseUpHandler, draggableAreaMoveHandler, isResizableByDrag]); useEffect(() => { toggleDrawerMode(isDrawerMode); }, [isDrawerMode, toggleDrawerMode]); // open/close resizable container useEffect(() => { if (!isCollapsed) { return; } if (isHoverOnResizableContainer) { // schedule to open timeoutIdRef.current = setTimeout(() => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion setContentWidth(currentProductNavWidth!); }, 70); } else if (timeoutIdRef.current != null) { // cancel schedule to open clearTimeout(timeoutIdRef.current); timeoutIdRef.current = undefined; } // close if (!isHover) { setContentWidth(sidebarMinimizeWidth); timeoutIdRef.current = undefined; } }, [isCollapsed, isHover, isHoverOnResizableContainer, currentProductNavWidth, setContentWidth]); // open/close resizable container when drawer mode useEffect(() => { if (isDrawerMode) { setContentWidth(sidebarFixedWidthInDrawerMode); } else if (isCollapsed) { setContentWidth(sidebarMinimizeWidth); } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion setContentWidth(currentProductNavWidth!); } }, [currentProductNavWidth, isCollapsed, isDrawerMode, setContentWidth]); const showContents = isDrawerMode || isHover || !isCollapsed; // css styles const grwSidebarClass = `grw-sidebar ${styles['grw-sidebar']}`; const sidebarModeClass = `${isDrawerMode ? 'grw-sidebar-drawer' : 'grw-sidebar-dock'}`; const isOpenClass = `${isDrawerOpened ? 'open' : ''}`; return ( <>
{ isResizableByDrag && (
) }
{ isDrawerOpened && (
) } ); }); Sidebar.displayName = 'Sidebar'; export default Sidebar;