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.

209 lines (207 loc) 7.87 kB
'use client'; import * as React from 'react'; import { inertValue } from '@base-ui-components/utils/inertValue'; import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; import { useScrollLock } from '@base-ui-components/utils/useScrollLock'; import { useStableCallback } from '@base-ui-components/utils/useStableCallback'; import { useStore } from '@base-ui-components/utils/store'; import { useSelectRootContext, useSelectFloatingContext } from "../root/SelectRootContext.js"; import { CompositeList } from "../../composite/list/CompositeList.js"; import { popupStateMapping } from "../../utils/popupStateMapping.js"; import { useAnchorPositioning } from "../../utils/useAnchorPositioning.js"; import { SelectPositionerContext } from "./SelectPositionerContext.js"; import { InternalBackdrop } from "../../utils/InternalBackdrop.js"; import { useRenderElement } from "../../utils/useRenderElement.js"; import { DROPDOWN_COLLISION_AVOIDANCE } from "../../utils/constants.js"; import { clearStyles } from "../popup/utils.js"; import { selectors } from "../store.js"; import { createChangeEventDetails } from "../../utils/createBaseUIEventDetails.js"; import { REASONS } from "../../utils/reasons.js"; import { findItemIndex, itemIncludes } from "../../utils/itemEquality.js"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const FIXED = { position: 'fixed' }; /** * Positions the select popup. * Renders a `<div>` element. * * Documentation: [Base UI Select](https://base-ui.com/react/components/select) */ export const SelectPositioner = /*#__PURE__*/React.forwardRef(function SelectPositioner(componentProps, forwardedRef) { const { anchor, positionMethod = 'absolute', className, render, side = 'bottom', align = 'center', sideOffset = 0, alignOffset = 0, collisionBoundary = 'clipping-ancestors', collisionPadding, arrowPadding = 5, sticky = false, disableAnchorTracking, alignItemWithTrigger = true, collisionAvoidance = DROPDOWN_COLLISION_AVOIDANCE, ...elementProps } = componentProps; const { store, listRef, labelsRef, alignItemWithTriggerActiveRef, selectedItemTextRef, valuesRef, initialValueRef, popupRef, setValue } = useSelectRootContext(); const floatingRootContext = useSelectFloatingContext(); const open = useStore(store, selectors.open); const mounted = useStore(store, selectors.mounted); const modal = useStore(store, selectors.modal); const value = useStore(store, selectors.value); const touchModality = useStore(store, selectors.touchModality); const positionerElement = useStore(store, selectors.positionerElement); const triggerElement = useStore(store, selectors.triggerElement); const isItemEqualToValue = useStore(store, selectors.isItemEqualToValue); const scrollUpArrowRef = React.useRef(null); const scrollDownArrowRef = React.useRef(null); const [controlledAlignItemWithTrigger, setControlledAlignItemWithTrigger] = React.useState(alignItemWithTrigger); const alignItemWithTriggerActive = mounted && controlledAlignItemWithTrigger && !touchModality; if (!mounted && controlledAlignItemWithTrigger !== alignItemWithTrigger) { setControlledAlignItemWithTrigger(alignItemWithTrigger); } useIsoLayoutEffect(() => { if (!mounted) { if (selectors.scrollUpArrowVisible(store.state)) { store.set('scrollUpArrowVisible', false); } if (selectors.scrollDownArrowVisible(store.state)) { store.set('scrollDownArrowVisible', false); } } }, [store, mounted]); React.useImperativeHandle(alignItemWithTriggerActiveRef, () => alignItemWithTriggerActive); useScrollLock((alignItemWithTriggerActive || modal) && open && !touchModality, triggerElement); const positioning = useAnchorPositioning({ anchor, floatingRootContext, positionMethod, mounted, side, sideOffset, align, alignOffset, arrowPadding, collisionBoundary, collisionPadding, sticky, disableAnchorTracking: disableAnchorTracking ?? alignItemWithTriggerActive, collisionAvoidance, keepMounted: true }); const renderedSide = alignItemWithTriggerActive ? 'none' : positioning.side; const positionerStyles = alignItemWithTriggerActive ? FIXED : positioning.positionerStyles; const defaultProps = React.useMemo(() => { const hiddenStyles = {}; if (!open) { hiddenStyles.pointerEvents = 'none'; } return { role: 'presentation', hidden: !mounted, style: { ...positionerStyles, ...hiddenStyles } }; }, [open, mounted, positionerStyles]); const state = React.useMemo(() => ({ open, side: renderedSide, align: positioning.align, anchorHidden: positioning.anchorHidden }), [open, renderedSide, positioning.align, positioning.anchorHidden]); const setPositionerElement = useStableCallback(element => { store.set('positionerElement', element); }); const element = useRenderElement('div', componentProps, { ref: [forwardedRef, setPositionerElement], state, stateAttributesMapping: popupStateMapping, props: [defaultProps, elementProps] }); const prevMapSizeRef = React.useRef(0); const onMapChange = useStableCallback(map => { if (map.size === 0 && prevMapSizeRef.current === 0) { return; } if (valuesRef.current.length === 0) { return; } const prevSize = prevMapSizeRef.current; prevMapSizeRef.current = map.size; if (map.size === prevSize) { return; } const eventDetails = createChangeEventDetails(REASONS.none); if (prevSize !== 0 && !store.state.multiple && value !== null) { const valueIndex = findItemIndex(valuesRef.current, value, isItemEqualToValue); if (valueIndex === -1) { const initial = initialValueRef.current; const hasInitial = initial != null && itemIncludes(valuesRef.current, initial, isItemEqualToValue); const nextValue = hasInitial ? initial : null; setValue(nextValue, eventDetails); if (nextValue === null) { store.set('selectedIndex', null); selectedItemTextRef.current = null; } } } if (prevSize !== 0 && store.state.multiple && Array.isArray(value)) { const nextValue = value.filter(v => itemIncludes(valuesRef.current, v, isItemEqualToValue)); if (nextValue.length !== value.length || nextValue.some(v => !itemIncludes(value, v, isItemEqualToValue))) { setValue(nextValue, eventDetails); if (nextValue.length === 0) { store.set('selectedIndex', null); selectedItemTextRef.current = null; } } } if (open && alignItemWithTriggerActive) { store.update({ scrollUpArrowVisible: false, scrollDownArrowVisible: false }); const stylesToClear = { height: '' }; clearStyles(positionerElement, stylesToClear); clearStyles(popupRef.current, stylesToClear); } }); const contextValue = React.useMemo(() => ({ ...positioning, side: renderedSide, alignItemWithTriggerActive, setControlledAlignItemWithTrigger, scrollUpArrowRef, scrollDownArrowRef }), [positioning, renderedSide, alignItemWithTriggerActive, setControlledAlignItemWithTrigger]); return /*#__PURE__*/_jsx(CompositeList, { elementsRef: listRef, labelsRef: labelsRef, onMapChange: onMapChange, children: /*#__PURE__*/_jsxs(SelectPositionerContext.Provider, { value: contextValue, children: [mounted && modal && /*#__PURE__*/_jsx(InternalBackdrop, { inert: inertValue(!open), cutout: triggerElement }), element] }) }); }); if (process.env.NODE_ENV !== "production") SelectPositioner.displayName = "SelectPositioner";