UNPKG

@redocly/theme

Version:

Shared UI components lib

93 lines (79 loc) 3.17 kB
import { useNavigate, useLocation } from 'react-router-dom'; import { useCallback, useEffect, useState } from 'react'; import type { MenuItemProps } from '../../types/sidebar'; import { useMenuItemExpanded } from './use-menu-item-expanded'; import { useCollapse } from './use-collapse'; import { loadAndNavigate } from '../../utils/load-and-navigate'; import { withoutPathPrefix, withPathPrefix } from '../../utils/urls'; type NestedMenuProps = MenuItemProps & { labelRef?: React.RefObject<HTMLElement | null>; nestedMenuRef?: React.RefObject<HTMLDivElement | null>; }; export function useNestedMenu({ item, labelRef, nestedMenuRef }: NestedMenuProps) { const [isExpanded, setIsExpanded] = useMenuItemExpanded(item); // we need to know when the item is collapsed after transition to remove children from DOM const [canUnmount, setCanUnmount] = useState(!isExpanded); const navigate = useNavigate(); const location = useLocation(); const { style } = useCollapse({ isExpanded, collapseElRef: nestedMenuRef || { current: null }, onTransitionStateChange: (state) => { if (state === 'collapseEnd') { setCanUnmount(true); } if (state === 'expandStart') { setCanUnmount(false); } // signal that used in e2e tests to wait for the item to be expanded if (state === 'expandEnd') { labelRef?.current?.dispatchEvent(new CustomEvent('menu:expand-end', { bubbles: true })); } }, }); function scrollIfNeeded(el: Element, centerIfNeeded: boolean = false) { const rect = el.getBoundingClientRect(); const isInViewport = rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth); // Only scroll if element is in viewport to prevent page jumping // @ts-ignore if (isInViewport && typeof el.scrollIntoViewIfNeeded === 'function') { // @ts-ignore el.scrollIntoViewIfNeeded(centerIfNeeded); } } // scroll to active element if needed useEffect(() => { if (item.active && labelRef && labelRef.current) { scrollIfNeeded(labelRef.current, true); // center item on the first scroll } }, [labelRef, item.active]); // scroll to expanded element if needed (position could change after collapse) useEffect(() => { if (item.active && isExpanded && labelRef && labelRef.current) { scrollIfNeeded(labelRef.current); } }, [labelRef, isExpanded, item.active]); const handleExpand = useCallback(async () => { if ( item.expanded === 'always' || (item.link && item.hasActiveSubItem && item.link !== withoutPathPrefix(location.pathname)) ) { return; } const [firstChild] = item.items; if (!isExpanded && item.selectFirstItemOnExpand && firstChild.link) { await loadAndNavigate({ navigate, to: withPathPrefix(firstChild.link) }); } setIsExpanded(!isExpanded); }, [item, isExpanded, navigate, location.pathname, setIsExpanded]); return { isExpanded, canUnmount, style, handleExpand, }; }