react-fit
Version:
Fit a popover element on the screen.
209 lines (208 loc) • 9.05 kB
JavaScript
'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 }));
}