@redocly/theme
Version:
Shared UI components lib
93 lines (79 loc) • 3.17 kB
text/typescript
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,
};
}