UNPKG

@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
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