@acusti/use-is-out-of-bounds
Version:
React hook that detects if an element is outside of its parent’s bounds on any of its sides (like collision detection)
131 lines (130 loc) • 5.52 kB
JavaScript
import useBoundingClientRect from "@acusti/use-bounding-client-rect";
import * as React from "react";
const { useRef } = React;
const INITIAL_OUT_OF_BOUNDS = Object.freeze({
bottom: false,
hasLayout: false,
left: false,
maxHeight: null,
maxWidth: null,
right: false,
top: false
});
const INITIAL_OUT_OF_BOUNDS_HAS_LAYOUT = Object.freeze({
...INITIAL_OUT_OF_BOUNDS,
hasLayout: true
});
const getBoundingAncestor = (element) => {
while (element == null ? void 0 : element.parentElement) {
if (element.parentElement.tagName === "BODY") return element.parentElement;
if (getComputedStyle(element.parentElement).overflowX !== "visible") {
return element.parentElement;
}
element = element.parentElement;
}
return null;
};
const getShouldSwitchDirection = (spaceAvailable, spaceAvailableOpposite) => {
if (spaceAvailable > -10) return false;
const difference = spaceAvailableOpposite - spaceAvailable;
if (difference <= 40) return false;
return spaceAvailableOpposite >= spaceAvailable * 0.75;
};
const getIsCurrentDirectionBefore = ({
length,
positionAfter,
positionBefore
}) => {
if (Number.isNaN(positionAfter)) return false;
if (Number.isNaN(positionBefore)) return true;
if (positionBefore >= 0) return false;
return positionAfter < length / 2;
};
const useIsOutOfBounds = (element) => {
const outOfBoundsRef = useRef(INITIAL_OUT_OF_BOUNDS);
const elementRef = useRef(element ?? null);
const computedStyleRef = useRef(null);
const elementRect = useBoundingClientRect(element);
const boundingElement = getBoundingAncestor(element);
const boundingElementRect = useBoundingClientRect(boundingElement);
if (element !== elementRef.current) {
elementRef.current = element ?? null;
computedStyleRef.current = null;
}
if (!element || elementRect.top == null) {
outOfBoundsRef.current = INITIAL_OUT_OF_BOUNDS;
return INITIAL_OUT_OF_BOUNDS;
} else if (boundingElementRect.top == null) {
outOfBoundsRef.current = INITIAL_OUT_OF_BOUNDS_HAS_LAYOUT;
return INITIAL_OUT_OF_BOUNDS_HAS_LAYOUT;
}
const previousOutOfBounds = outOfBoundsRef.current;
const elementBottom = elementRect.bottom;
const elementLeft = elementRect.left;
const elementRight = elementRect.right;
const elementTop = elementRect.top;
const boundingElementBottom = (boundingElement == null ? void 0 : boundingElement.tagName) === "BODY" ? boundingElement.ownerDocument.documentElement.clientHeight : boundingElementRect.bottom;
const boundingElementLeft = boundingElementRect.left;
const boundingElementRight = boundingElementRect.right;
const boundingElementTop = boundingElementRect.top;
const elementHeight = elementBottom - elementTop;
const elementWidth = elementRight - elementLeft;
let bottom = elementBottom > boundingElementBottom || previousOutOfBounds.bottom;
let left = elementLeft < boundingElementLeft || previousOutOfBounds.left;
let right = elementRight > boundingElementRight || previousOutOfBounds.right;
let top = elementTop < boundingElementTop || previousOutOfBounds.top;
if (bottom || left || right || top) {
const style = computedStyleRef.current ?? getComputedStyle(element);
computedStyleRef.current ?? (computedStyleRef.current = style);
const isUpward = getIsCurrentDirectionBefore({
length: elementHeight,
positionAfter: parseFloat(style.getPropertyValue("bottom")),
positionBefore: parseFloat(style.getPropertyValue("top"))
});
const isLeftward = getIsCurrentDirectionBefore({
length: elementWidth,
positionAfter: parseFloat(style.getPropertyValue("right")),
positionBefore: parseFloat(style.getPropertyValue("left"))
});
const offsetBottom = isUpward ? elementHeight : 0;
const offsetLeft = isLeftward ? 0 : elementWidth;
const offsetRight = isLeftward ? elementWidth : 0;
const offsetTop = isUpward ? 0 : elementHeight;
const availableLeft = elementLeft - offsetLeft - boundingElementLeft;
const availableRight = boundingElementRight - (elementRight + offsetRight);
const availableTop = elementTop - offsetTop - boundingElementTop;
const availableBottom = boundingElementBottom - (elementBottom + offsetBottom);
if (isUpward && top) {
top = getShouldSwitchDirection(availableTop, availableBottom);
bottom = !top;
} else if (!isUpward && bottom) {
bottom = getShouldSwitchDirection(availableBottom, availableTop);
top = !bottom;
}
if (isLeftward && left) {
left = getShouldSwitchDirection(availableLeft, availableRight);
right = !left;
} else if (!isLeftward && right) {
right = getShouldSwitchDirection(availableRight, availableLeft);
left = !right;
}
}
const maxHeight = !bottom || top ? boundingElementBottom - elementTop : elementBottom - boundingElementTop;
const maxWidth = !right || left ? boundingElementRight - elementLeft : elementRight - boundingElementLeft;
if (!previousOutOfBounds.hasLayout || bottom !== previousOutOfBounds.bottom || left !== previousOutOfBounds.left || maxHeight !== previousOutOfBounds.maxHeight || maxWidth !== previousOutOfBounds.maxWidth || right !== previousOutOfBounds.right || top !== previousOutOfBounds.top) {
outOfBoundsRef.current = {
bottom,
hasLayout: true,
left,
maxHeight,
maxWidth,
right,
top
};
}
return outOfBoundsRef.current;
};
export {
useIsOutOfBounds as default
};
//# sourceMappingURL=useIsOutOfBounds.js.map