| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- 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<StickyStretchableScrollerProps>(() => import('./StickyStretchableScroller')
- .then(mod => mod.StickyStretchableScroller), { ssr: false });
- const SidebarContents = dynamic(() => import('./Sidebar/SidebarContents')
- .then(mod => mod.SidebarContents), { ssr: false, loading: () => <SidebarSkeleton /> });
- 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 <SidebarNav onItemSelected={itemSelectedHandler} />;
- });
- 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 (
- <>
- <div id="grw-sidebar-contents-wrapper" style={{ minHeight: '100%' }}>
- <StickyStretchableScroller
- simplebarRef={mutateSidebarScroller}
- stickyElemSelector=".grw-sidebar"
- calcViewHeight={calcViewHeight}
- >
- <SidebarContents />
- </StickyStretchableScroller>
- </div>
- <DrawerToggler iconClass="icon-arrow-left" />
- </>
- );
- });
- 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<HTMLDivElement>(null);
- const timeoutIdRef = useRef<NodeJS.Timeout>();
- 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 (
- <>
- <div className={`${grwSidebarClass} ${sidebarModeClass} ${isOpenClass} d-print-none`} data-testid="grw-sidebar">
- <div className="data-layout-container">
- <div
- className='navigation transition-enabled'
- onMouseEnter={hoverOnHandler}
- onMouseLeave={hoverOutHandler}
- >
- <div className="grw-navigation-wrap">
- <div className="grw-global-navigation">
- <GlobalNavigation></GlobalNavigation>
- </div>
- <div
- ref={resizableContainer}
- className="grw-contextual-navigation"
- onMouseEnter={hoverOnResizableContainerHandler}
- onMouseLeave={hoverOutResizableContainerHandler}
- style={{ width: isCollapsed ? sidebarMinimizeWidth : currentProductNavWidth }}
- >
- <div className="grw-contextual-navigation-child">
- <div role="group" data-testid="grw-contextual-navigation-sub" className={`grw-contextual-navigation-sub ${showContents ? '' : 'd-none'}`}>
- <SidebarContentsWrapper></SidebarContentsWrapper>
- </div>
- </div>
- </div>
- </div>
- <div className="grw-navigation-draggable">
- { isResizableByDrag && (
- <div
- className="grw-navigation-draggable-hitarea"
- onMouseDown={dragableAreaMouseDownHandler}
- >
- <div className="grw-navigation-draggable-hitarea-child"></div>
- </div>
- ) }
- <button
- data-testid="grw-navigation-resize-button"
- className={`grw-navigation-resize-button ${!isDrawerMode ? 'resizable' : ''} ${isCollapsed ? 'collapsed' : ''} `}
- type="button"
- aria-expanded="true"
- aria-label="Toggle navigation"
- disabled={isDrawerMode}
- onClick={toggleNavigationBtnClickHandler}
- >
- <span className="hexagon-container" role="presentation">
- <NavigationResizeHexagon />
- </span>
- <span className="hitarea" role="presentation"></span>
- </button>
- </div>
- </div>
- </div>
- </div>
- { isDrawerOpened && (
- <div className={`${styles['grw-sidebar-backdrop']} modal-backdrop show`} onClick={backdropClickedHandler}></div>
- ) }
- </>
- );
- });
- Sidebar.displayName = 'Sidebar';
- export default Sidebar;
|