@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.
187 lines (185 loc) • 6.51 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 { useEventCallback } from '@base-ui-components/utils/useEventCallback';
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 { clearPositionerStyles } from "../popup/utils.js";
import { selectors } from "../store.js";
import { useScrollLock } from "../../utils/useScrollLock.js";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const FIXED = {
position: 'fixed'
};
/**
* Positions the select menu 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,
trackAnchor = true,
alignItemWithTrigger = true,
collisionAvoidance = DROPDOWN_COLLISION_AVOIDANCE,
...elementProps
} = componentProps;
const {
store,
listRef,
labelsRef,
alignItemWithTriggerActiveRef,
valuesRef
} = 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 [controlledAlignItemWithTrigger, setControlledAlignItemWithTrigger] = React.useState(alignItemWithTrigger);
const alignItemWithTriggerActive = mounted && controlledAlignItemWithTrigger && !touchModality;
if (!mounted && controlledAlignItemWithTrigger !== alignItemWithTrigger) {
setControlledAlignItemWithTrigger(alignItemWithTrigger);
}
useIsoLayoutEffect(() => {
if (!alignItemWithTrigger || !mounted) {
if (selectors.scrollUpArrowVisible(store.state)) {
store.set('scrollUpArrowVisible', false);
}
if (selectors.scrollDownArrowVisible(store.state)) {
store.set('scrollDownArrowVisible', false);
}
}
}, [store, mounted, alignItemWithTrigger]);
React.useImperativeHandle(alignItemWithTriggerActiveRef, () => alignItemWithTriggerActive);
useScrollLock({
enabled: (alignItemWithTriggerActive || modal) && open && !touchModality,
mounted,
open,
referenceElement: triggerElement
});
const positioning = useAnchorPositioning({
anchor,
floatingRootContext,
positionMethod,
mounted,
side,
sideOffset,
align,
alignOffset,
arrowPadding,
collisionBoundary,
collisionPadding,
sticky,
trackAnchor: trackAnchor ?? !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 = useEventCallback(element => {
store.set('positionerElement', element);
});
const element = useRenderElement('div', componentProps, {
ref: [forwardedRef, setPositionerElement],
state,
customStyleHookMapping: popupStateMapping,
props: [defaultProps, elementProps]
});
const prevMapSizeRef = React.useRef(0);
const onMapChange = useEventCallback(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;
}
if (!store.state.multiple && value !== null) {
const valueIndex = valuesRef.current.indexOf(value);
if (valueIndex === -1) {
store.apply({
label: '',
selectedIndex: null
});
}
}
if (open && alignItemWithTriggerActive) {
store.apply({
scrollUpArrowVisible: false,
scrollDownArrowVisible: false
});
if (positionerElement) {
clearPositionerStyles(positionerElement, {
height: ''
});
}
}
});
const contextValue = React.useMemo(() => ({
...positioning,
side: renderedSide,
alignItemWithTriggerActive,
setControlledAlignItemWithTrigger
}), [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";