@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.
252 lines (250 loc) • 9.56 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useSelectRoot = useSelectRoot;
var React = _interopRequireWildcard(require("react"));
var _react2 = require("@floating-ui/react");
var _useFieldControlValidation = require("../../field/control/useFieldControlValidation");
var _FieldRootContext = require("../../field/root/FieldRootContext");
var _useBaseUiId = require("../../utils/useBaseUiId");
var _useControlled = require("../../utils/useControlled");
var _utils = require("../../utils");
var _useEnhancedEffect = require("../../utils/useEnhancedEffect");
var _useEventCallback = require("../../utils/useEventCallback");
var _warn = require("../../utils/warn");
var _useAfterExitAnimation = require("../../utils/useAfterExitAnimation");
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 useSelectRoot(params) {
const {
id: idProp,
disabled = false,
readOnly = false,
required = false,
alignItemToTrigger: alignItemToTriggerParam = true,
modal = false
} = params;
const {
setDirty,
validityData,
validationMode,
setControlId
} = (0, _FieldRootContext.useFieldRootContext)();
const fieldControlValidation = (0, _useFieldControlValidation.useFieldControlValidation)();
const id = (0, _useBaseUiId.useBaseUiId)(idProp);
(0, _useEnhancedEffect.useEnhancedEffect)(() => {
setControlId(id);
return () => {
setControlId(undefined);
};
}, [id, setControlId]);
const [value, setValueUnwrapped] = (0, _useControlled.useControlled)({
controlled: params.value,
default: params.defaultValue,
name: 'Select',
state: 'value'
});
const [open, setOpenUnwrapped] = (0, _useControlled.useControlled)({
controlled: params.open,
default: params.defaultOpen,
name: 'Select',
state: 'open'
});
const [controlledAlignItemToTrigger, setcontrolledAlignItemToTrigger] = React.useState(alignItemToTriggerParam);
const listRef = React.useRef([]);
const labelsRef = React.useRef([]);
const popupRef = React.useRef(null);
const valueRef = React.useRef(null);
const valuesRef = React.useRef([]);
const typingRef = React.useRef(false);
const selectedItemTextRef = React.useRef(null);
const selectionRef = React.useRef({
allowSelectedMouseUp: false,
allowUnselectedMouseUp: false,
allowSelect: false
});
const [triggerElement, setTriggerElement] = React.useState(null);
const [positionerElement, setPositionerElement] = React.useState(null);
const [activeIndex, setActiveIndex] = React.useState(null);
const [selectedIndex, setSelectedIndex] = React.useState(null);
const [label, setLabel] = React.useState('');
const [touchModality, setTouchModality] = React.useState(false);
const [scrollUpArrowVisible, setScrollUpArrowVisible] = React.useState(false);
const [scrollDownArrowVisible, setScrollDownArrowVisible] = React.useState(false);
const {
mounted,
setMounted,
transitionStatus
} = (0, _utils.useTransitionStatus)(open);
const alignItemToTrigger = Boolean(mounted && controlledAlignItemToTrigger && !touchModality);
if (!mounted && controlledAlignItemToTrigger !== alignItemToTriggerParam) {
setcontrolledAlignItemToTrigger(alignItemToTriggerParam);
}
if (!alignItemToTriggerParam || !mounted) {
if (scrollUpArrowVisible) {
setScrollUpArrowVisible(false);
}
if (scrollDownArrowVisible) {
setScrollDownArrowVisible(false);
}
}
const setOpen = (0, _useEventCallback.useEventCallback)((nextOpen, event) => {
params.onOpenChange?.(nextOpen, event);
setOpenUnwrapped(nextOpen);
// Workaround `enableFocusInside` in Floating UI setting `tabindex=0` of a non-highlighted
// option upon close when tabbing out due to `keepMounted=true`:
// https://github.com/floating-ui/floating-ui/pull/3004/files#diff-962a7439cdeb09ea98d4b622a45d517bce07ad8c3f866e089bda05f4b0bbd875R194-R199
// This otherwise causes options to retain `tabindex=0` incorrectly when the popup is closed
// when tabbing outside.
if (!nextOpen && activeIndex !== null) {
const activeOption = listRef.current[activeIndex];
// Wait for Floating UI's focus effect to have fired
queueMicrotask(() => {
activeOption?.setAttribute('tabindex', '-1');
});
}
});
(0, _useAfterExitAnimation.useAfterExitAnimation)({
open,
animatedElementRef: popupRef,
onFinished() {
setMounted(false);
setActiveIndex(null);
}
});
const setValue = (0, _useEventCallback.useEventCallback)((nextValue, event) => {
params.onValueChange?.(nextValue, event);
setValueUnwrapped(nextValue);
setDirty(nextValue !== validityData.initialValue);
if (validationMode === 'onChange') {
fieldControlValidation.commitValidation(nextValue);
}
const index = valuesRef.current.indexOf(nextValue);
setSelectedIndex(index);
setLabel(labelsRef.current[index] ?? '');
});
(0, _useEnhancedEffect.useEnhancedEffect)(() => {
// Wait for the items to have registered their values in `valuesRef`.
queueMicrotask(() => {
const stringValue = typeof value === 'string' || value === null ? value : JSON.stringify(value);
const index = valuesRef.current.indexOf(stringValue);
if (index !== -1) {
setSelectedIndex(index);
setLabel(labelsRef.current[index] ?? '');
} else if (value) {
(0, _warn.warn)(`The value \`${stringValue}\` is not present in the select items.`);
}
});
}, [value]);
const floatingRootContext = (0, _react2.useFloatingRootContext)({
open,
onOpenChange: setOpen,
elements: {
reference: triggerElement,
floating: positionerElement
}
});
const click = (0, _react2.useClick)(floatingRootContext, {
enabled: !readOnly,
event: 'mousedown'
});
const dismiss = (0, _react2.useDismiss)(floatingRootContext, {
bubbles: false,
outsidePressEvent: 'mousedown'
});
const role = (0, _react2.useRole)(floatingRootContext, {
role: 'select'
});
const listNavigation = (0, _react2.useListNavigation)(floatingRootContext, {
enabled: !readOnly,
listRef,
activeIndex,
selectedIndex,
onNavigate(nextActiveIndex) {
// Retain the highlight while transitioning out.
if (nextActiveIndex === null && !open) {
return;
}
setActiveIndex(nextActiveIndex);
},
// Implement our own listeners since `onPointerLeave` on each option fires while scrolling with
// the `alignItemToTrigger` prop enabled, causing a performance issue on Chrome.
focusItemOnHover: false
});
const typehaead = (0, _react2.useTypeahead)(floatingRootContext, {
enabled: !readOnly,
listRef: labelsRef,
activeIndex,
selectedIndex,
onMatch(index) {
if (open) {
setActiveIndex(index);
} else {
setValue(valuesRef.current[index]);
}
},
onTypingChange(typing) {
// FIXME: Floating UI doesn't support allowing space to select an item while the popup is
// closed and the trigger isn't a native <button>.
typingRef.current = typing;
}
});
const {
getReferenceProps: getRootTriggerProps,
getFloatingProps: getRootPositionerProps,
getItemProps
} = (0, _react2.useInteractions)([click, dismiss, role, listNavigation, typehaead]);
const rootContext = React.useMemo(() => ({
id,
name: params.name,
required,
disabled,
readOnly,
triggerElement,
setTriggerElement,
positionerElement,
setPositionerElement,
scrollUpArrowVisible,
setScrollUpArrowVisible,
scrollDownArrowVisible,
setScrollDownArrowVisible,
setcontrolledAlignItemToTrigger,
value,
setValue,
open,
setOpen,
mounted,
setMounted,
label,
setLabel,
valueRef,
valuesRef,
labelsRef,
typingRef,
selectionRef,
getRootPositionerProps,
getRootTriggerProps,
getItemProps,
listRef,
popupRef,
selectedItemTextRef,
floatingRootContext,
touchModality,
setTouchModality,
alignItemToTrigger,
transitionStatus,
fieldControlValidation,
modal
}), [id, params.name, required, disabled, readOnly, triggerElement, positionerElement, scrollUpArrowVisible, scrollDownArrowVisible, value, setValue, open, setOpen, mounted, setMounted, label, getRootPositionerProps, getRootTriggerProps, getItemProps, floatingRootContext, touchModality, alignItemToTrigger, transitionStatus, fieldControlValidation, modal]);
const indexContext = React.useMemo(() => ({
activeIndex,
setActiveIndex,
selectedIndex,
setSelectedIndex
}), [activeIndex, selectedIndex, setActiveIndex]);
return React.useMemo(() => ({
rootContext,
indexContext
}), [rootContext, indexContext]);
}