@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)
161 lines • 7.61 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(Object.assign(Object.assign({}, INITIAL_OUT_OF_BOUNDS), { hasLayout: true }));
const getBoundingAncestor = (element) => {
while (element === null || element === void 0 ? void 0 : element.parentElement) {
// If we’ve reached the body, use that as boundingElement
if (element.parentElement.tagName === 'BODY')
return element.parentElement;
// Only need to check one overflow direction, because if either direction
// is not visible, neither can be visible
if (getComputedStyle(element.parentElement).overflowX !== 'visible') {
return element.parentElement;
}
element = element.parentElement;
}
return null;
};
// Opposite direction needs to have much more available space then current direction
const getShouldSwitchDirection = (spaceAvailable, spaceAvailableOpposite) => {
// Give preference to current direction if it is within 10px out of bounds
if (spaceAvailable > -10)
return false;
const difference = spaceAvailableOpposite - spaceAvailable;
// If opposite direction doesn’t have a lot more available space, don’t switch
if (difference <= 40)
return false;
// To be worth switching, opposite direction has to be at least ¼ less out of bounds
return spaceAvailableOpposite >= spaceAvailable * 0.75;
};
const getIsCurrentDirectionBefore = ({ length, positionAfter, positionBefore, }) => {
if (Number.isNaN(positionAfter))
return false;
if (Number.isNaN(positionBefore))
return true;
// If before position is already positive, it’s direction isn’t before
if (positionBefore >= 0)
return false;
// Consider element in negative direction if more than ½ its length is before origin
return positionAfter < length / 2;
};
const useIsOutOfBounds = (element) => {
var _a;
const outOfBoundsRef = useRef(INITIAL_OUT_OF_BOUNDS);
const elementRef = useRef(element !== null && element !== void 0 ? 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 && element !== void 0 ? 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;
// If boundingElement is the <body> element, use viewport height as bottom
const boundingElementBottom = (boundingElement === null || boundingElement === void 0 ? 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;
// If direction isn’t currently out-of-bounds, default to previous value.
// This prevents us erroneously switching back if direction changed and new
// direction causes the previous direction to seem to now have available space.
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 = (_a = computedStyleRef.current) !== null && _a !== void 0 ? _a : getComputedStyle(element);
if (!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')),
});
// Identify available space in each direction
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; // prettier-ignore
const availableRight = boundingElementRight - (elementRight + offsetRight); // prettier-ignore
const availableTop = (elementTop - offsetTop) - boundingElementTop; // prettier-ignore
const availableBottom = boundingElementBottom - (elementBottom + offsetBottom); // prettier-ignore
// If element is out-of-bounds in direction it’s rendering, check if should switch
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;
// Only overwrite outOfBoundsRef if one of the values has changed
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 default useIsOutOfBounds;
//# sourceMappingURL=useIsOutOfBounds.js.map