UNPKG

@base-ui-components/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

239 lines (236 loc) 11.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useSelectPopup = useSelectPopup; var React = _interopRequireWildcard(require("react")); var _mergeReactProps = require("../../utils/mergeReactProps"); var _SelectRootContext = require("../root/SelectRootContext"); var _useEnhancedEffect = require("../../utils/useEnhancedEffect"); var _owner = require("../../utils/owner"); var _useEventCallback = require("../../utils/useEventCallback"); var _utils = require("./utils"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function useSelectPopup() { const { mounted, id, setOpen, getRootPositionerProps, alignItemToTrigger, triggerElement, positionerElement, valueRef, selectedItemTextRef, popupRef, scrollUpArrowVisible, scrollDownArrowVisible, setScrollUpArrowVisible, setScrollDownArrowVisible, setcontrolledAlignItemToTrigger } = (0, _SelectRootContext.useSelectRootContext)(); const initialHeightRef = React.useRef(0); const reachedMaxHeightRef = React.useRef(false); const maxHeightRef = React.useRef(0); const initialPlacedRef = React.useRef(false); const originalPositionerStylesRef = React.useRef({}); const handleScrollArrowVisibility = (0, _useEventCallback.useEventCallback)(() => { if (!alignItemToTrigger || !popupRef.current) { return; } const isScrolledToTop = popupRef.current.scrollTop < 1; const isScrolledToBottom = popupRef.current.scrollTop + popupRef.current.clientHeight >= popupRef.current.scrollHeight - 1; if (scrollUpArrowVisible !== !isScrolledToTop) { setScrollUpArrowVisible(!isScrolledToTop); } if (scrollDownArrowVisible !== !isScrolledToBottom) { setScrollDownArrowVisible(!isScrolledToBottom); } }); (0, _useEnhancedEffect.useEnhancedEffect)(() => { if (alignItemToTrigger || !positionerElement || Object.keys(originalPositionerStylesRef.current).length) { return; } originalPositionerStylesRef.current = { top: positionerElement.style.top || '0', left: positionerElement.style.left || '0', right: positionerElement.style.right, height: positionerElement.style.height, bottom: positionerElement.style.bottom, minHeight: positionerElement.style.minHeight, maxHeight: positionerElement.style.maxHeight, marginTop: positionerElement.style.marginTop, marginBottom: positionerElement.style.marginBottom }; }, [alignItemToTrigger, positionerElement]); (0, _useEnhancedEffect.useEnhancedEffect)(() => { if (mounted || alignItemToTrigger) { return; } initialPlacedRef.current = false; reachedMaxHeightRef.current = false; initialHeightRef.current = 0; maxHeightRef.current = 0; if (positionerElement) { (0, _utils.clearPositionerStyles)(positionerElement, originalPositionerStylesRef.current); } }, [mounted, alignItemToTrigger, positionerElement]); (0, _useEnhancedEffect.useEnhancedEffect)(() => { if (!mounted || !alignItemToTrigger || !triggerElement || !positionerElement || !popupRef.current) { return; } const positionerStyles = getComputedStyle(positionerElement); const popupStyles = getComputedStyle(popupRef.current); const doc = (0, _owner.ownerDocument)(triggerElement); const triggerRect = triggerElement.getBoundingClientRect(); const positionerRect = positionerElement.getBoundingClientRect(); const triggerX = triggerRect.left; const triggerHeight = triggerRect.height; const scrollHeight = popupRef.current.scrollHeight; const borderBottom = parseFloat(popupStyles.borderBottomWidth); const marginTop = parseFloat(positionerStyles.marginTop) || 10; const marginBottom = parseFloat(positionerStyles.marginBottom) || 10; const minHeight = parseFloat(positionerStyles.minHeight) || 100; const paddingLeft = 5; const paddingRight = 5; const triggerCollisionThreshold = 20; const viewportHeight = doc.documentElement.clientHeight - marginTop - marginBottom; const viewportWidth = doc.documentElement.clientWidth; const availableSpaceBeneathTrigger = viewportHeight - triggerRect.bottom + triggerHeight; const textElement = selectedItemTextRef.current; const valueElement = valueRef.current; let offsetX = 0; let offsetY = 0; if (textElement && valueElement) { const valueRect = valueElement.getBoundingClientRect(); const textRect = textElement.getBoundingClientRect(); const valueLeftFromTriggerLeft = valueRect.left - triggerX; const textLeftFromPositionerLeft = textRect.left - positionerRect.left; const valueCenterFromPositionerTop = valueRect.top - triggerRect.top + valueRect.height / 2; const textCenterFromTriggerTop = textRect.top - positionerRect.top + textRect.height / 2; offsetX = valueLeftFromTriggerLeft - textLeftFromPositionerLeft; offsetY = textCenterFromTriggerTop - valueCenterFromPositionerTop; } const idealHeight = availableSpaceBeneathTrigger + offsetY + marginBottom + borderBottom; let height = Math.min(viewportHeight, idealHeight); const maxHeight = viewportHeight - marginTop - marginBottom; const scrollTop = idealHeight - height; const left = Math.max(paddingLeft, triggerX + offsetX); const maxRight = viewportWidth - paddingRight; const rightOverflow = Math.max(0, left + positionerRect.width - maxRight); positionerElement.style.left = `${left - rightOverflow}px`; positionerElement.style.height = `${height}px`; positionerElement.style.maxHeight = 'auto'; positionerElement.style.marginTop = `${marginTop}px`; positionerElement.style.marginBottom = `${marginBottom}px`; const maxScrollTop = popupRef.current.scrollHeight - popupRef.current.clientHeight; const isTopPositioned = scrollTop >= maxScrollTop; if (isTopPositioned) { height = Math.min(viewportHeight, positionerRect.height) - (scrollTop - maxScrollTop); } // When the trigger is too close to the top or bottom of the viewport, or the minHeight is // reached, we fallback to aligning the popup to the trigger as the UX is poor otherwise. const fallbackToAlignPopupToTrigger = triggerRect.top < triggerCollisionThreshold || triggerRect.bottom > viewportHeight - triggerCollisionThreshold || height < Math.min(scrollHeight, minHeight); if (fallbackToAlignPopupToTrigger) { initialPlacedRef.current = true; (0, _utils.clearPositionerStyles)(positionerElement, originalPositionerStylesRef.current); setcontrolledAlignItemToTrigger(false); return; } if (isTopPositioned) { const topOffset = Math.max(0, viewportHeight - idealHeight); positionerElement.style.top = positionerRect.height >= maxHeight ? '0' : `${topOffset}px`; positionerElement.style.height = `${height}px`; popupRef.current.scrollTop = popupRef.current.scrollHeight - popupRef.current.clientHeight; initialHeightRef.current = Math.max(minHeight, height); } else { positionerElement.style.bottom = '0'; initialHeightRef.current = Math.max(minHeight, height); popupRef.current.scrollTop = scrollTop; } if (initialHeightRef.current === viewportHeight) { reachedMaxHeightRef.current = true; } handleScrollArrowVisibility(); // Avoid the `onScroll` event logic from triggering before the popup is placed. setTimeout(() => { initialPlacedRef.current = true; }); }, [mounted, alignItemToTrigger, positionerElement, triggerElement, valueRef, selectedItemTextRef, popupRef, setScrollUpArrowVisible, setScrollDownArrowVisible, handleScrollArrowVisibility, setcontrolledAlignItemToTrigger]); React.useEffect(() => { if (!alignItemToTrigger || !positionerElement || !mounted) { return undefined; } const win = (0, _owner.ownerWindow)(positionerElement); function handleResize() { setOpen(false); } win.addEventListener('resize', handleResize); return () => { win.removeEventListener('resize', handleResize); }; }, [setOpen, alignItemToTrigger, positionerElement, mounted]); const getPopupProps = React.useCallback((externalProps = {}) => { return (0, _mergeReactProps.mergeReactProps)(getRootPositionerProps(externalProps), { ['data-id']: `${id}-popup`, onScroll(event) { if (!alignItemToTrigger || !positionerElement || !popupRef.current || !initialPlacedRef.current) { return; } if (reachedMaxHeightRef.current || !alignItemToTrigger) { handleScrollArrowVisibility(); return; } const isTopPositioned = positionerElement.style.top === '0px'; const isBottomPositioned = positionerElement.style.bottom === '0px'; const currentHeight = positionerElement.getBoundingClientRect().height; const doc = (0, _owner.ownerDocument)(positionerElement); const positionerStyles = getComputedStyle(positionerElement); const marginTop = parseFloat(positionerStyles.marginTop); const marginBottom = parseFloat(positionerStyles.marginBottom); const viewportHeight = doc.documentElement.clientHeight - marginTop - marginBottom; if (isTopPositioned) { const scrollTop = event.currentTarget.scrollTop; const maxScrollTop = event.currentTarget.scrollHeight - event.currentTarget.clientHeight; const diff = maxScrollTop - scrollTop; const nextHeight = Math.min(currentHeight + diff, viewportHeight); positionerElement.style.height = `${Math.min(currentHeight + diff, viewportHeight)}px`; if (nextHeight !== viewportHeight) { event.currentTarget.scrollTop = maxScrollTop; } else { reachedMaxHeightRef.current = true; } } else if (isBottomPositioned) { const scrollTop = event.currentTarget.scrollTop; const minScrollTop = 0; const diff = scrollTop - minScrollTop; const nextHeight = Math.min(currentHeight + diff, viewportHeight); const idealHeight = currentHeight + diff; const overshoot = idealHeight - viewportHeight; positionerElement.style.height = `${Math.min(idealHeight, viewportHeight)}px`; if (nextHeight !== viewportHeight) { event.currentTarget.scrollTop = 0; } else { reachedMaxHeightRef.current = true; if (event.currentTarget.scrollTop < event.currentTarget.scrollHeight - event.currentTarget.clientHeight) { event.currentTarget.scrollTop -= diff - overshoot; } } } handleScrollArrowVisibility(); }, ...(alignItemToTrigger && { style: { position: 'relative', maxHeight: '100%', overflowX: 'hidden', overflowY: 'auto' } }) }); }, [getRootPositionerProps, id, alignItemToTrigger, positionerElement, popupRef, handleScrollArrowVisibility]); return React.useMemo(() => ({ getPopupProps }), [getPopupProps]); }