UNPKG

@chayns-components/core

Version:

A set of beautiful React components for developing your own applications with chayns.

204 lines (198 loc) • 8.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useDropdownPosition = exports.useDropdownListener = exports.useDropdownAlignment = exports.useDropdown = void 0; var _react = require("react"); var _dropdown = require("../types/dropdown"); const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? _react.useLayoutEffect : _react.useEffect; const useDropdownListener = ({ onClick, onClose, onTouchEnd, onTouchStart, shouldCaptureEvents }) => { (0, _react.useEffect)(() => { document.addEventListener('click', onClick, shouldCaptureEvents); document.addEventListener('touchend', onTouchEnd, shouldCaptureEvents); document.addEventListener('touchstart', onTouchStart, shouldCaptureEvents); window.addEventListener('blur', onClose); return () => { document.removeEventListener('click', onClick, shouldCaptureEvents); document.removeEventListener('touchend', onTouchEnd, shouldCaptureEvents); document.removeEventListener('touchstart', onTouchStart, shouldCaptureEvents); window.removeEventListener('blur', onClose); }; }, [onClick, onClose, onTouchEnd, onTouchStart]); }; exports.useDropdownListener = useDropdownListener; const useDropdownAlignment = ({ anchorElement, contentWidth, direction, shouldUseTopAlignment }) => { const [translateX, setTranslateX] = (0, _react.useState)('0px'); const [translateY, setTranslateY] = (0, _react.useState)('0px'); (0, _react.useEffect)(() => { if ([_dropdown.DropdownDirection.BOTTOM_LEFT, _dropdown.DropdownDirection.TOP_LEFT, _dropdown.DropdownDirection.LEFT].includes(direction)) { const difference = anchorElement.clientWidth - contentWidth; setTranslateX(`${difference}px`); } else { setTranslateX('0px'); } }, [anchorElement.clientWidth, contentWidth, direction]); (0, _react.useEffect)(() => { const useTopAlignment = shouldUseTopAlignment || [_dropdown.DropdownDirection.TOP, _dropdown.DropdownDirection.TOP_LEFT, _dropdown.DropdownDirection.TOP_RIGHT].includes(direction); if (useTopAlignment) { setTranslateY('-100%'); } else { setTranslateY('0px'); } }, [direction, shouldUseTopAlignment]); return (0, _react.useMemo)(() => ({ x: translateX, y: translateY }), [translateX, translateY]); }; exports.useDropdownAlignment = useDropdownAlignment; /** * The space (in pixels) that should be kept between the dropdown content and the edge of the * container when calculating the available maximum height. */ const AVAILABLE_HEIGHT_SPACING = 16; const useDropdownPosition = ({ anchorElement, container, contentHeight = 0, direction, shouldShowDropdown }) => { const [coordinates, setCoordinates] = (0, _react.useState)({ x: 0, y: 0 }); const [shouldUseTopAlignment, setShouldUseTopAlignment] = (0, _react.useState)(false); const [availableMaxHeight, setAvailableMaxHeight] = (0, _react.useState)(0); // Stores the alignment decision (top/bottom) that was made when the dropdown opened. The // decision depends on the content height, but the content height in turn is limited by the // available height (which depends on the alignment). Recalculating the alignment on every // change would create a feedback loop that makes the alignment flip and the height oscillate. // We therefore lock the alignment while the dropdown stays open and reset it on close. const lockedTopAlignmentRef = (0, _react.useRef)(null); const calculateCoordinates = (0, _react.useCallback)(() => { // While the dropdown is closing (or closed) we must not recalculate the position and the // available height. Otherwise layout changes underneath the dropdown (e.g. content that // appears after a selection) would move or resize the dropdown body while it is still fading // out, causing it to visibly jump. Freezing the last calculated values keeps the closing // animation stable. if (!shouldShowDropdown) { return; } if (container) { const { left: anchorLeft, top: anchorTop, height: anchorHeight } = anchorElement.getBoundingClientRect(); const { left, top, height } = container.getBoundingClientRect(); const x = anchorLeft - left + container.scrollLeft; const y = anchorTop - top + container.scrollTop; let useTopAlignment = [_dropdown.DropdownDirection.TOP, _dropdown.DropdownDirection.TOP_LEFT, _dropdown.DropdownDirection.TOP_RIGHT].includes(direction); const hasBottomAlignment = [_dropdown.DropdownDirection.BOTTOM, _dropdown.DropdownDirection.BOTTOM_LEFT, _dropdown.DropdownDirection.BOTTOM_RIGHT].includes(direction); // The available space above and below the anchor within the container. const spaceToTop = y; const spaceToBottom = height - (y + anchorHeight); if (lockedTopAlignmentRef.current !== null) { // Keep the alignment that was decided when the dropdown opened. useTopAlignment = lockedTopAlignmentRef.current; } else if (!hasBottomAlignment && y + anchorHeight + contentHeight > height) { // The content does not fit below the anchor. Only flip to the top when there is // actually more space above than below. Otherwise the content would be cut off less // when opened downwards (the content is limited to the available height anyway), so // we keep the downward alignment. This avoids opening upwards for a content that, // once limited to the available space, would fit below just fine. useTopAlignment = spaceToTop > spaceToBottom; } lockedTopAlignmentRef.current = useTopAlignment; setShouldUseTopAlignment(useTopAlignment); // Calculate the space that is available for the dropdown content. When the dropdown is // opened to the top, the available space reaches from the anchor to the top edge of the // container. When it is opened to the bottom, it reaches from the bottom of the anchor // to the bottom edge of the container. A small spacing is subtracted so the content does // not touch the container edge (e.g. to leave room for shadows). const nextAvailableMaxHeight = Math.max(Math.round((useTopAlignment ? spaceToTop : spaceToBottom) - AVAILABLE_HEIGHT_SPACING), 0); // Ignore sub-pixel fluctuations so tiny changes from getBoundingClientRect do not // repeatedly trigger re-renders that could keep the height oscillating. setAvailableMaxHeight(currentAvailableMaxHeight => Math.abs(currentAvailableMaxHeight - nextAvailableMaxHeight) <= 1 ? currentAvailableMaxHeight : nextAvailableMaxHeight); setCoordinates({ x, y: useTopAlignment ? y : y + anchorHeight }); } }, [anchorElement, container, contentHeight, direction, shouldShowDropdown]); useIsomorphicLayoutEffect(() => { // Reset the locked alignment whenever the dropdown is closed, so the next time it opens the // alignment (top/bottom) is decided freshly based on the then-available space. if (!shouldShowDropdown) { lockedTopAlignmentRef.current = null; } const handleResize = () => { calculateCoordinates(); setTimeout(calculateCoordinates, 300); }; handleResize(); if (shouldShowDropdown) { window.addEventListener('resize', handleResize); } return () => { window.removeEventListener('resize', handleResize); }; }, [calculateCoordinates, shouldShowDropdown]); return (0, _react.useMemo)(() => ({ shouldUseTopAlignment, coordinates, availableMaxHeight }), [availableMaxHeight, coordinates, shouldUseTopAlignment]); }; exports.useDropdownPosition = useDropdownPosition; const useDropdown = ({ anchorElement, container, contentHeight, contentWidth, direction, shouldShowDropdown }) => { const { shouldUseTopAlignment, coordinates, availableMaxHeight } = useDropdownPosition({ anchorElement, container, contentHeight, direction, shouldShowDropdown }); const transform = useDropdownAlignment({ anchorElement, contentWidth, direction, shouldUseTopAlignment }); const width = anchorElement.clientWidth; return (0, _react.useMemo)(() => ({ coordinates, transform, width, availableMaxHeight }), [availableMaxHeight, coordinates, transform, width]); }; exports.useDropdown = useDropdown; //# sourceMappingURL=dropdown.js.map