UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

140 lines (139 loc) 6.6 kB
import { useProvidedRefOrCreate } from "./useProvidedRefOrCreate.js"; import useIsomorphicLayoutEffect from "../utils/useIsomorphicLayoutEffect.js"; import { useResizeObserver } from "./useResizeObserver.js"; import React from "react"; import { getAnchoredPosition } from "@primer/behaviors"; //#region src/hooks/useAnchoredPosition.ts /** * Returns all scrollable ancestor elements of the given element, plus the window. * An element is scrollable if its computed overflow/overflow-x/overflow-y is * 'auto', 'scroll', or 'overlay'. */ function getScrollableAncestors(element) { const scrollables = []; let current = element.parentElement; while (current) { const style = getComputedStyle(current); const overflowY = style.overflowY; const overflowX = style.overflowX; if (/auto|scroll|overlay/.test(overflowY) || /auto|scroll|overlay/.test(overflowX)) scrollables.push(current); current = current.parentElement; } scrollables.push(window); return scrollables; } /** * Calculates the top and left values for an absolutely-positioned floating element * to be anchored to some anchor element. Returns refs for the floating element * and the anchor element, along with the position. * @param settings Settings for calculating the anchored position. * @param dependencies Dependencies to determine when to re-calculate the position. * @returns An object of {top: number, left: number} to absolutely-position the * floating element. */ function useAnchoredPosition(settings, dependencies = []) { var _settings$enabled; const floatingElementRef = useProvidedRefOrCreate(settings === null || settings === void 0 ? void 0 : settings.floatingElementRef); const anchorElementRef = useProvidedRefOrCreate(settings === null || settings === void 0 ? void 0 : settings.anchorElementRef); const enabled = (_settings$enabled = settings === null || settings === void 0 ? void 0 : settings.enabled) !== null && _settings$enabled !== void 0 ? _settings$enabled : true; const savedOnPositionChange = React.useRef(settings === null || settings === void 0 ? void 0 : settings.onPositionChange); const [position, setPosition] = React.useState(void 0); const [_, setPrevHeight] = React.useState(void 0); const topPositionChanged = (prevPosition, newPosition) => { return prevPosition && ["outside-top", "inside-top"].includes(prevPosition.anchorSide) && (prevPosition.anchorSide !== newPosition.anchorSide || prevPosition.top < newPosition.top); }; const updateElementHeight = () => { let heightUpdated = false; setPrevHeight((prevHeight) => { var _floatingElementRef$c, _floatingElementRef$c2; if (prevHeight && prevHeight > ((_floatingElementRef$c = (_floatingElementRef$c2 = floatingElementRef.current) === null || _floatingElementRef$c2 === void 0 ? void 0 : _floatingElementRef$c2.clientHeight) !== null && _floatingElementRef$c !== void 0 ? _floatingElementRef$c : 0)) { requestAnimationFrame(() => { floatingElementRef.current.style.height = `${prevHeight}px`; }); heightUpdated = true; } return prevHeight; }); return heightUpdated; }; const updatePosition = React.useCallback(() => { var _floatingElementRef$c5; if (!enabled) return; if (floatingElementRef.current instanceof Element && anchorElementRef.current instanceof Element) { const newPosition = getAnchoredPosition(floatingElementRef.current, anchorElementRef.current, settings); setPosition((prev) => { if (settings !== null && settings !== void 0 && settings.pinPosition && topPositionChanged(prev, newPosition)) { var _anchorElementRef$cur, _anchorElementRef$cur2, _floatingElementRef$c3, _floatingElementRef$c4; if (((_anchorElementRef$cur = (_anchorElementRef$cur2 = anchorElementRef.current) === null || _anchorElementRef$cur2 === void 0 ? void 0 : _anchorElementRef$cur2.getBoundingClientRect().top) !== null && _anchorElementRef$cur !== void 0 ? _anchorElementRef$cur : 0) > ((_floatingElementRef$c3 = (_floatingElementRef$c4 = floatingElementRef.current) === null || _floatingElementRef$c4 === void 0 ? void 0 : _floatingElementRef$c4.clientHeight) !== null && _floatingElementRef$c3 !== void 0 ? _floatingElementRef$c3 : 0) && updateElementHeight()) return prev; } if (prev && prev.anchorSide === newPosition.anchorSide) { var _savedOnPositionChang; (_savedOnPositionChang = savedOnPositionChange.current) === null || _savedOnPositionChang === void 0 || _savedOnPositionChang.call(savedOnPositionChange, newPosition); } return newPosition; }); } else { var _savedOnPositionChang2; setPosition(void 0); (_savedOnPositionChang2 = savedOnPositionChange.current) === null || _savedOnPositionChang2 === void 0 || _savedOnPositionChang2.call(savedOnPositionChange, void 0); } setPrevHeight((_floatingElementRef$c5 = floatingElementRef.current) === null || _floatingElementRef$c5 === void 0 ? void 0 : _floatingElementRef$c5.clientHeight); }, [ floatingElementRef, anchorElementRef, enabled, ...dependencies ]); useIsomorphicLayoutEffect(() => { savedOnPositionChange.current = settings === null || settings === void 0 ? void 0 : settings.onPositionChange; }, [settings === null || settings === void 0 ? void 0 : settings.onPositionChange]); const hasMountedRef = React.useRef(false); useIsomorphicLayoutEffect(() => { if (floatingElementRef.current instanceof Element && anchorElementRef.current instanceof Element) { hasMountedRef.current = true; updatePosition(); } }, [ updatePosition, floatingElementRef, anchorElementRef ]); React.useEffect(() => { if (!hasMountedRef.current) { hasMountedRef.current = true; updatePosition(); } }, [updatePosition]); useResizeObserver(updatePosition, void 0, [], enabled); useResizeObserver(updatePosition, floatingElementRef, [], enabled); React.useEffect(() => { if (!enabled) return; const anchorEl = anchorElementRef.current; if (!anchorEl) return; let rafId = null; const handleScroll = () => { if (rafId !== null) return; rafId = requestAnimationFrame(() => { rafId = null; updatePosition(); }); }; const scrollables = getScrollableAncestors(anchorEl); for (const scrollable of scrollables) scrollable.addEventListener("scroll", handleScroll); return () => { for (const scrollable of scrollables) scrollable.removeEventListener("scroll", handleScroll); if (rafId !== null) cancelAnimationFrame(rafId); }; }, [ anchorElementRef, updatePosition, enabled ]); return { floatingElementRef, anchorElementRef, position }; } //#endregion export { useAnchoredPosition };