@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
JavaScript
'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";