import React, { useCallback, useMemo, useRef } from "react"; import { useForwardedRef } from "@bedrock-layout/use-forwarded-ref"; import { useStatefulRef } from "@bedrock-layout/use-stateful-ref"; import { Box } from "../box"; import { Icons } from "../icons"; import { useIsomorphicLayoutEffect } from "../../utils"; import type { Breadcrumbs } from "./"; const defaultDivider = ( ); // eslint-disable-next-line max-lines-per-function export const useBreadcrumbs = ({ crumbs, customDivider, ref, }: { customDivider: React.ComponentProps["divider"]; ref: React.ComponentProps["ref"]; crumbs: React.ComponentProps["crumbs"]; }) => { const divider = useMemo(() => customDivider ?? defaultDivider, [customDivider]); const rootRef = useForwardedRef(ref); const overflowAtIndexRef = useStatefulRef(undefined); const isOverflowRef = useRef(typeof overflowAtIndexRef.current === "number"); isOverflowRef.current = typeof overflowAtIndexRef.current === "number"; const previousContainerWidth = useRef(0); // eslint-disable-next-line max-statements, max-lines-per-function const handleOverflow = useCallback(() => { const rootElem = rootRef.current; const crumbsListCloneElem = rootElem.children[1]?.children[0]; const firstCrumb = crumbsListCloneElem?.children[0]; const dividerElem = crumbsListCloneElem?.children[1]; const lastCrumb = crumbsListCloneElem?.children[crumbsListCloneElem.children.length - 1]; const subCrumbsListTrigger = rootElem.children[0]?.children[0]?.children[2]; if ( !(crumbsListCloneElem instanceof HTMLElement) || !(firstCrumb instanceof HTMLElement) || !(dividerElem instanceof HTMLElement) || !(lastCrumb instanceof HTMLElement) || !(subCrumbsListTrigger instanceof HTMLElement) ) { return; } const firstCrumbWidth = Math.ceil(firstCrumb.getBoundingClientRect().width); const lastCrumbWidth = Math.ceil(lastCrumb.getBoundingClientRect().width); const dividerWidth = Math.ceil(dividerElem.getBoundingClientRect().width); const gap = parseInt(getComputedStyle(crumbsListCloneElem).gap); const staticallyOccupiedContainerSpace = firstCrumbWidth + gap + dividerWidth + gap + gap + dividerWidth + gap + lastCrumbWidth; const subCrumbsListTriggerWidth = subCrumbsListTrigger.offsetWidth; const rootElemStyle = getComputedStyle(rootElem); const rootElemPaddingLeft = parseInt(rootElemStyle.paddingLeft); const rootElemPaddingRight = parseInt(rootElemStyle.paddingRight); let shadowListContainerWidth = parseInt(rootElemStyle.width) - rootElemPaddingLeft - rootElemPaddingRight - staticallyOccupiedContainerSpace; const isGrowing = shadowListContainerWidth > previousContainerWidth.current; previousContainerWidth.current = shadowListContainerWidth; const crumbsCount = crumbsListCloneElem.children.length; shadowListContainerWidth -= isOverflowRef.current && ((overflowAtIndexRef.current ?? 0) > 1 || !isGrowing) ? subCrumbsListTriggerWidth + gap : 0; let totalWidth = 0; for (let i = crumbsCount - 3; i > 1; i--) { const crumb = crumbsListCloneElem.children[i]; if (!(crumb instanceof HTMLElement)) return; const crumbWidth = Math.ceil(crumb.getBoundingClientRect().width) + (i !== crumbsCount - 3 ? gap : 0); totalWidth += crumbWidth; if (totalWidth > shadowListContainerWidth) { overflowAtIndexRef.current = Math.ceil(i / 2); return; } } overflowAtIndexRef.current = undefined; }, [rootRef, overflowAtIndexRef]); const observerRef = useRef( typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(() => { handleOverflow(); }) ); useIsomorphicLayoutEffect(() => { const observer = observerRef.current; const rootElem = rootRef.current; observer?.observe(rootElem); handleOverflow(); return () => { observer?.unobserve(rootElem); }; }, [rootRef, crumbs.length, handleOverflow]); return { rootRef, overflowAtIndex: overflowAtIndexRef.current, divider }; };