UNPKG

react-fit

Version:

Fit a popover element on the screen.

209 lines (208 loc) 9.05 kB
'use client'; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { jsx as _jsx } from "react/jsx-runtime"; import { Children, useCallback, useEffect, useRef } from 'react'; import detectElementOverflow from 'detect-element-overflow'; import warning from 'warning'; const isBrowser = typeof window !== 'undefined'; const isMutationObserverSupported = isBrowser && 'MutationObserver' in window; function capitalize(string) { return (string.charAt(0).toUpperCase() + string.slice(1)); } function findScrollContainer(element) { let parent = element.parentElement; while (parent) { const { overflow } = window.getComputedStyle(parent); if (overflow.split(' ').every((o) => o === 'auto' || o === 'scroll')) { return parent; } parent = parent.parentElement; } return document.documentElement; } function alignAxis({ axis, container, element, invertAxis, scrollContainer, secondary, spacing, }) { const style = window.getComputedStyle(element); const parent = container.parentElement; if (!parent) { return; } const scrollContainerCollisions = detectElementOverflow(parent, scrollContainer); const documentCollisions = detectElementOverflow(parent, document.documentElement); const isX = axis === 'x'; const startProperty = isX ? 'left' : 'top'; const endProperty = isX ? 'right' : 'bottom'; const sizeProperty = isX ? 'width' : 'height'; const overflowStartProperty = `overflow${capitalize(startProperty)}`; const overflowEndProperty = `overflow${capitalize(endProperty)}`; const scrollProperty = `scroll${capitalize(startProperty)}`; const uppercasedSizeProperty = capitalize(sizeProperty); const offsetSizeProperty = `offset${uppercasedSizeProperty}`; const clientSizeProperty = `client${uppercasedSizeProperty}`; const minSizeProperty = `min-${sizeProperty}`; const scrollbarWidth = scrollContainer[offsetSizeProperty] - scrollContainer[clientSizeProperty]; const startSpacing = typeof spacing === 'object' ? spacing[startProperty] : spacing; let availableStartSpace = -Math.max(scrollContainerCollisions[overflowStartProperty], documentCollisions[overflowStartProperty] + document.documentElement[scrollProperty]) - startSpacing; const endSpacing = typeof spacing === 'object' ? spacing[endProperty] : spacing; let availableEndSpace = -Math.max(scrollContainerCollisions[overflowEndProperty], documentCollisions[overflowEndProperty] - document.documentElement[scrollProperty]) - endSpacing - scrollbarWidth; if (secondary) { availableStartSpace += parent[clientSizeProperty]; availableEndSpace += parent[clientSizeProperty]; } const offsetSize = element[offsetSizeProperty]; function displayStart() { element.style[startProperty] = 'auto'; element.style[endProperty] = secondary ? '0' : '100%'; } function displayEnd() { element.style[startProperty] = secondary ? '0' : '100%'; element.style[endProperty] = 'auto'; } function displayIfFits(availableSpace, display) { const fits = offsetSize <= availableSpace; if (fits) { display(); } return fits; } function displayStartIfFits() { return displayIfFits(availableStartSpace, displayStart); } function displayEndIfFits() { return displayIfFits(availableEndSpace, displayEnd); } function displayWhereverShrinkedFits() { const moreSpaceStart = availableStartSpace > availableEndSpace; const rawMinSize = style.getPropertyValue(minSizeProperty); const minSize = rawMinSize ? Number.parseInt(rawMinSize, 10) : null; function shrinkToSize(size) { warning(!minSize || size >= minSize, `<Fit />'s child will not fit anywhere with its current ${minSizeProperty} of ${minSize}px.`); const newSize = Math.max(size, minSize || 0); warning(false, `<Fit />'s child needed to have its ${sizeProperty} decreased to ${newSize}px.`); element.style[sizeProperty] = `${newSize}px`; } if (moreSpaceStart) { shrinkToSize(availableStartSpace); displayStart(); } else { shrinkToSize(availableEndSpace); displayEnd(); } } let fits; if (invertAxis) { fits = displayStartIfFits() || displayEndIfFits(); } else { fits = displayEndIfFits() || displayStartIfFits(); } if (!fits) { displayWhereverShrinkedFits(); } } function alignMainAxis(args) { alignAxis(args); } function alignSecondaryAxis(args) { alignAxis(Object.assign(Object.assign({}, args), { axis: args.axis === 'x' ? 'y' : 'x', secondary: true })); } function alignBothAxis(args) { const { invertAxis, invertSecondaryAxis } = args, commonArgs = __rest(args, ["invertAxis", "invertSecondaryAxis"]); alignMainAxis(Object.assign(Object.assign({}, commonArgs), { invertAxis })); alignSecondaryAxis(Object.assign(Object.assign({}, commonArgs), { invertAxis: invertSecondaryAxis })); } export default function Fit({ children, invertAxis, invertSecondaryAxis, mainAxis = 'y', spacing = 8, }) { const container = useRef(undefined); const element = useRef(undefined); const elementWidth = useRef(undefined); const elementHeight = useRef(undefined); const scrollContainer = useRef(undefined); const fit = useCallback(() => { if (!scrollContainer.current || !container.current || !element.current) { return; } const currentElementWidth = element.current.clientWidth; const currentElementHeight = element.current.clientHeight; // No need to recalculate - already did that for current dimensions if (elementWidth.current === currentElementWidth && elementHeight.current === currentElementHeight) { return; } // Save the dimensions so that we know we don't need to repeat the function if unchanged elementWidth.current = currentElementWidth; elementHeight.current = currentElementHeight; const parent = container.current.parentElement; // Container was unmounted if (!parent) { return; } /** * We need to ensure that <Fit />'s child has a absolute position. Otherwise, * we wouldn't be able to place the child in the correct position. */ const style = window.getComputedStyle(element.current); const { position } = style; if (position !== 'absolute') { element.current.style.position = 'absolute'; } /** * We need to ensure that <Fit />'s parent has a relative or absolute position. Otherwise, * we wouldn't be able to place the child in the correct position. */ const parentStyle = window.getComputedStyle(parent); const { position: parentPosition } = parentStyle; if (parentPosition !== 'relative' && parentPosition !== 'absolute') { parent.style.position = 'relative'; } alignBothAxis({ axis: mainAxis, container: container.current, element: element.current, invertAxis, invertSecondaryAxis, scrollContainer: scrollContainer.current, spacing, }); }, [invertAxis, invertSecondaryAxis, mainAxis, spacing]); const child = Children.only(children); useEffect(() => { fit(); function onMutation() { fit(); } if (isMutationObserverSupported && element.current) { const mutationObserver = new MutationObserver(onMutation); mutationObserver.observe(element.current, { attributes: true, attributeFilter: ['class', 'style'], }); } }, [fit]); function assignRefs(domElement) { if (!domElement || !(domElement instanceof HTMLElement)) { return; } element.current = domElement; scrollContainer.current = findScrollContainer(domElement); } return (_jsx("span", { ref: (domContainer) => { if (!domContainer) { return; } container.current = domContainer; const domElement = domContainer === null || domContainer === void 0 ? void 0 : domContainer.firstElementChild; assignRefs(domElement); }, style: { display: 'contents' }, children: child })); }