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.

252 lines (250 loc) 9.56 kB
"use strict"; 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]); }