@jobber/components 8.20.2 → 8.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/BottomSheet-cjs.js +9 -58
  2. package/dist/BottomSheet-es.js +2 -47
  3. package/dist/Card/index.cjs +4 -0
  4. package/dist/Card/index.mjs +4 -0
  5. package/dist/ComboboxChipRemove-cjs.js +3418 -0
  6. package/dist/ComboboxChipRemove-es.js +3366 -0
  7. package/dist/ComboboxPrimitive-cjs.js +170 -0
  8. package/dist/ComboboxPrimitive-es.js +168 -0
  9. package/dist/DataDump/index.cjs +4 -0
  10. package/dist/DataDump/index.mjs +4 -0
  11. package/dist/InputNumberExperimental-cjs.js +45 -44
  12. package/dist/InputNumberExperimental-es.js +3 -2
  13. package/dist/InternalBackdrop-cjs.js +4278 -0
  14. package/dist/InternalBackdrop-es.js +4204 -0
  15. package/dist/Menu/index.cjs +4 -0
  16. package/dist/Menu/index.mjs +4 -0
  17. package/dist/Menu-cjs.js +9 -8
  18. package/dist/Menu-es.js +2 -1
  19. package/dist/MenuSubmenuTrigger-cjs.js +208 -1969
  20. package/dist/MenuSubmenuTrigger-es.js +7 -1759
  21. package/dist/NumberFieldInput-cjs.js +56 -439
  22. package/dist/NumberFieldInput-es.js +6 -376
  23. package/dist/Page/index.cjs +4 -0
  24. package/dist/Page/index.mjs +4 -0
  25. package/dist/ScrollAreaViewport-cjs.js +108 -4356
  26. package/dist/ScrollAreaViewport-es.js +6 -4201
  27. package/dist/buttonRenderAdapter-cjs.js +56 -0
  28. package/dist/buttonRenderAdapter-es.js +51 -0
  29. package/dist/clamp-cjs.js +0 -1194
  30. package/dist/clamp-es.js +1 -1091
  31. package/dist/docs/empty-states/empty-states.md +29 -0
  32. package/dist/index.cjs +4 -0
  33. package/dist/index.mjs +4 -0
  34. package/dist/primitives/BottomSheet/index.cjs +5 -2
  35. package/dist/primitives/BottomSheet/index.mjs +5 -2
  36. package/dist/primitives/ComboboxPrimitive/ComboboxPrimitive.d.ts +32 -0
  37. package/dist/primitives/ComboboxPrimitive/ComboboxPrimitive.types.d.ts +30 -0
  38. package/dist/primitives/ComboboxPrimitive/index.cjs +28 -0
  39. package/dist/primitives/ComboboxPrimitive/index.d.ts +2 -0
  40. package/dist/primitives/ComboboxPrimitive/index.mjs +22 -0
  41. package/dist/primitives/InputNumberExperimental/index.cjs +4 -2
  42. package/dist/primitives/InputNumberExperimental/index.mjs +4 -2
  43. package/dist/primitives/index.cjs +11 -2
  44. package/dist/primitives/index.d.ts +1 -0
  45. package/dist/primitives/index.mjs +10 -2
  46. package/dist/styles.css +767 -0
  47. package/dist/unstyledPrimitives/index.cjs +237 -3588
  48. package/dist/unstyledPrimitives/index.mjs +96 -3447
  49. package/dist/useButton-cjs.js +1196 -0
  50. package/dist/useButton-es.js +1091 -0
  51. package/dist/useCompositeListItem-cjs.js +1792 -0
  52. package/dist/useCompositeListItem-es.js +1762 -0
  53. package/dist/useLabel-cjs.js +411 -0
  54. package/dist/useLabel-es.js +378 -0
  55. package/package.json +2 -2
@@ -1,3081 +1,128 @@
1
1
  import * as React from 'react';
2
- import { j as useControlled, a as useStableCallback, k as useValueAsRef, c as useTransitionStatus, d as useIsoLayoutEffect, x as createGenericEventDetails, r as inputClear, q as inputChange, m as createChangeEventDetails, _ as focusOut, a4 as outsidePress, g as getTarget, a3 as itemPress, f as useOpenChangeComplete, B as useValueChanged, n as none, ao as inputPress, v as visuallyHiddenInput, p as visuallyHidden, L as contains, ap as pointer, b as useTimeout, A as useButton, o as ownerDocument, C as stopEvent, W as listNavigation, a2 as closePress, u as useBaseUiId, am as isAndroid, aq as isFirefox, P as escapeKey, ar as clearPress, t as transitionStatusMapping, a7 as imperativeAction, i as clamp, as as swipe, aa as useAnimationFrame, ak as TransitionStatusDataAttributes, R as triggerHover, a5 as useAnimationsFinished, S as SafeReact, e as error, at as chipRemovePress, au as scrub, ac as isWebKit } from '../clamp-es.js';
3
- import { K as createSelector, ad as useStore, T as useOpenInteractionType, a3 as useClick, Y as useDismiss, $ as useInteractions, O as useOnFirstRender, ae as Store, a8 as pressableTriggerOpenStateMapping, v as useDirection, ac as triggerOpenStateMapping, A as FloatingPortal, y as popupStateMapping, W as useScrollLock, I as InternalBackdrop, E as inertValue, H as DROPDOWN_COLLISION_AVOIDANCE, z as FloatingFocusManager, x as useDialogRootContext, af as useDrawerProviderContext, ag as DrawerBackdropCssVars, ah as DrawerPopupCssVars, ai as DrawerProviderContext, aj as CommonPopupDataAttributes, ak as useDialogPortalContext, al as DialogStore, am as useDrawerRootContext, ab as useTriggerRegistration, an as DrawerPopupDataAttributes, ao as useDrawerSnapPoints, ap as DrawerViewportContext, aq as BASE_UI_SWIPE_IGNORE_SELECTOR, ar as DRAWER_CONTENT_ATTRIBUTE, c as DrawerBackdrop, f as DrawerContent, d as DrawerPopup, b as DrawerPortal, a as DrawerRoot, D as DrawerTrigger, as as useScrollAreaRootContext, at as getOffset, au as ScrollAreaRootCssVars, av as scrollAreaStateAttributesMapping, aw as useScrollAreaViewportContext, S as ScrollAreaRoot, e as ScrollAreaViewport } from '../ScrollAreaViewport-es.js';
4
- import { f as formatErrorMessage, d as EMPTY_ARRAY, a as useRefWithInit, b as useMergedRefs, N as NOOP, E as EMPTY_OBJECT, u as useRenderElement, m as mergeProps } from '../useRenderElement-es.js';
2
+ import { a as useStableCallback, ar as pointer, u as useBaseUiId, d as useIsoLayoutEffect, t as transitionStatusMapping, n as createChangeEventDetails, Q as imperativeAction, o as ownerDocument, m as contains, g as getTarget, b as useTimeout, as as swipe, ac as useAnimationFrame, al as TransitionStatusDataAttributes, a4 as triggerHover, B as useControlled, a6 as itemPress, c as useTransitionStatus, f as useOpenChangeComplete, a8 as useAnimationsFinished, ao as isFirefox, at as scrub, K as createGenericEventDetails, V as isWebKit } from '../useButton-es.js';
3
+ import { A as useCoreFilter, B as AriaCombobox, D as stringifyAsLabel, E as useComboboxInputValueContext, l as ComboboxArrow, i as ComboboxBackdrop, x as ComboboxClear, v as ComboboxCollection, w as ComboboxEmpty, n as ComboboxGroup, o as ComboboxGroupLabel, m as ComboboxIcon, c as ComboboxInput, d as ComboboxInputGroup, p as ComboboxItem, f as ComboboxList, k as ComboboxPopup, h as ComboboxPortal, j as ComboboxPositioner, u as ComboboxRow, g as ComboboxStatus, e as ComboboxTrigger, z as useFilteredItems, s as ComboboxChip, t as ComboboxChipRemove, r as ComboboxChips, q as ComboboxItemIndicator, a as ComboboxLabel, C as ComboboxRoot, b as ComboboxValue, y as useComboboxFilter } from '../ComboboxChipRemove-es.js';
5
4
  import { jsx, jsxs } from 'react/jsx-runtime';
6
- import { a as useFormContext, b as useFieldRootContext, j as useLabelableId, k as useField, f as fieldValidityMapping, u as useLabelableContext, l as DEFAULT_FIELD_STATE_ATTRIBUTES, c as useLabel, m as useNumberFieldRootContext, n as DEFAULT_STEP, s as stateAttributesMapping$9, i as NumberFieldDecrement, d as NumberFieldGroup, h as NumberFieldIncrement, e as NumberFieldInput, N as NumberFieldRoot } from '../NumberFieldInput-es.js';
7
- import { u as useFloatingRootContext, o as useListNavigation, p as useTypeahead, q as getPseudoElementBounds, C as CompositeList, r as useAnchorPositioning, s as getDisabledMountTransitionStyles, t as useCompositeListItem, I as IndexGuessBehavior, a as DrawerClose, D as DrawerTitle, v as useMenuRootContext, w as useMenuPositionerContext, x as useContextMenuRootContext, y as useMenuItem, R as REGULAR_ITEM, z as itemMapping, A as MenuStore, l as MenuGroup, n as MenuGroupLabel, c as MenuItem, b as MenuLinkItem, j as MenuPopup, h as MenuPortal, i as MenuPositioner, k as MenuRadioGroup, d as MenuRadioItem, e as MenuRadioItemIndicator, g as MenuRoot, m as MenuSubmenuRoot, f as MenuSubmenuTrigger, M as MenuTrigger } from '../MenuSubmenuTrigger-es.js';
8
- import * as ReactDOM from 'react-dom';
9
5
  import { S as Separator } from '../Separator-es.js';
6
+ import { u as useDialogRootContext, n as useDrawerProviderContext, o as DrawerBackdropCssVars, q as DrawerPopupCssVars, r as DrawerProviderContext, s as useDialogPortalContext, t as DialogStore, v as useDrawerRootContext, m as useTriggerRegistration, w as DrawerPopupDataAttributes, x as useDrawerSnapPoints, y as DrawerViewportContext, z as DRAWER_CONTENT_ATTRIBUTE, c as DrawerBackdrop, f as DrawerContent, d as DrawerPopup, b as DrawerPortal, a as DrawerRoot, D as DrawerTrigger, A as useScrollAreaRootContext, B as getOffset, E as ScrollAreaRootCssVars, F as scrollAreaStateAttributesMapping, G as useScrollAreaViewportContext, S as ScrollAreaRoot, e as ScrollAreaViewport } from '../ScrollAreaViewport-es.js';
7
+ import { a as DrawerClose, D as DrawerTitle, u as useMenuRootContext, o as useMenuPositionerContext, p as useContextMenuRootContext, q as useMenuItem, R as REGULAR_ITEM, r as itemMapping, s as MenuStore, l as MenuGroup, n as MenuGroupLabel, c as MenuItem, b as MenuLinkItem, j as MenuPopup, h as MenuPortal, i as MenuPositioner, k as MenuRadioGroup, d as MenuRadioItem, e as MenuRadioItemIndicator, g as MenuRoot, m as MenuSubmenuRoot, f as MenuSubmenuTrigger, M as MenuTrigger } from '../MenuSubmenuTrigger-es.js';
8
+ import { u as useRenderElement, m as mergeProps, f as formatErrorMessage, N as NOOP, E as EMPTY_OBJECT } from '../useRenderElement-es.js';
10
9
  import { a as isHTMLElement, f as getComputedStyle, b as isElement, k as getWindow } from '../floating-ui.utils.dom-es.js';
10
+ import { c as clamp } from '../clamp-es.js';
11
+ import { C as CommonPopupDataAttributes, p as popupStateMapping, $ as BASE_UI_SWIPE_IGNORE_SELECTOR, m as useDirection, r as inertValue } from '../InternalBackdrop-es.js';
12
+ import * as ReactDOM from 'react-dom';
13
+ import { u as useCompositeListItem } from '../useCompositeListItem-es.js';
11
14
  import { y as round } from '../index.esm-es.js';
15
+ import { u as useNumberFieldRootContext, D as DEFAULT_STEP, s as stateAttributesMapping$6, d as NumberFieldDecrement, a as NumberFieldGroup, c as NumberFieldIncrement, b as NumberFieldInput, N as NumberFieldRoot } from '../NumberFieldInput-es.js';
16
+ import '../useLabel-es.js';
12
17
  import '../floating-ui.react-dom-es.js';
13
18
 
14
- const ComboboxRootContext = /*#__PURE__*/React.createContext(undefined);
15
- if (process.env.NODE_ENV !== "production") ComboboxRootContext.displayName = "ComboboxRootContext";
16
- const ComboboxFloatingContext = /*#__PURE__*/React.createContext(undefined);
17
- if (process.env.NODE_ENV !== "production") ComboboxFloatingContext.displayName = "ComboboxFloatingContext";
18
- const ComboboxDerivedItemsContext = /*#__PURE__*/React.createContext(undefined);
19
- // `inputValue` can't be placed in the store.
20
- // https://github.com/mui/base-ui/issues/2703
21
- if (process.env.NODE_ENV !== "production") ComboboxDerivedItemsContext.displayName = "ComboboxDerivedItemsContext";
22
- const ComboboxInputValueContext = /*#__PURE__*/React.createContext('');
23
- if (process.env.NODE_ENV !== "production") ComboboxInputValueContext.displayName = "ComboboxInputValueContext";
24
- function useComboboxRootContext() {
25
- const context = React.useContext(ComboboxRootContext);
26
- if (!context) {
27
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: ComboboxRootContext is missing. Combobox parts must be placed within <Combobox.Root>.' : formatErrorMessage(22));
28
- }
29
- return context;
30
- }
31
- function useComboboxFloatingContext() {
32
- const context = React.useContext(ComboboxFloatingContext);
33
- if (!context) {
34
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: ComboboxFloatingContext is missing. Combobox parts must be placed within <Combobox.Root>.' : formatErrorMessage(23));
35
- }
36
- return context;
37
- }
38
- function useComboboxDerivedItemsContext() {
39
- const context = React.useContext(ComboboxDerivedItemsContext);
40
- if (!context) {
41
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: ComboboxItemsContext is missing. Combobox parts must be placed within <Combobox.Root>.' : formatErrorMessage(24));
42
- }
43
- return context;
44
- }
45
- function useComboboxInputValueContext() {
46
- return React.useContext(ComboboxInputValueContext);
47
- }
48
-
49
- const defaultItemEquality = (itemValue, selectedValue) => Object.is(itemValue, selectedValue);
50
- function compareItemEquality(itemValue, selectedValue, comparer) {
51
- if (itemValue == null || selectedValue == null) {
52
- return Object.is(itemValue, selectedValue);
53
- }
54
- return comparer(itemValue, selectedValue);
55
- }
56
- function selectedValueIncludes(selectedValues, itemValue, comparer) {
57
- if (!selectedValues || selectedValues.length === 0) {
58
- return false;
59
- }
60
- return selectedValues.some(selectedValue => {
61
- if (selectedValue === undefined) {
62
- return false;
63
- }
64
- return compareItemEquality(itemValue, selectedValue, comparer);
65
- });
66
- }
67
- function findItemIndex(itemValues, selectedValue, comparer) {
68
- if (!itemValues || itemValues.length === 0) {
69
- return -1;
70
- }
71
- return itemValues.findIndex(itemValue => {
72
- if (itemValue === undefined) {
73
- return false;
74
- }
75
- return compareItemEquality(itemValue, selectedValue, comparer);
76
- });
77
- }
78
- function removeItem(selectedValues, itemValue, comparer) {
79
- return selectedValues.filter(selectedValue => !compareItemEquality(itemValue, selectedValue, comparer));
80
- }
81
-
82
- function serializeValue(value) {
83
- if (value == null) {
84
- return '';
85
- }
86
- if (typeof value === 'string') {
87
- return value;
88
- }
89
- try {
90
- return JSON.stringify(value);
91
- } catch {
92
- return String(value);
93
- }
94
- }
95
-
96
- function isGroupedItems(items) {
97
- return items != null && items.length > 0 && typeof items[0] === 'object' && items[0] != null && 'items' in items[0];
98
- }
99
-
100
- /**
101
- * Checks if the items array contains an item with a null value that has a non-null label.
102
- */
103
- function hasNullItemLabel(items) {
104
- if (!Array.isArray(items)) {
105
- return items != null && 'null' in items;
106
- }
107
- const arrayItems = items;
108
- if (isGroupedItems(arrayItems)) {
109
- for (const group of arrayItems) {
110
- for (const item of group.items) {
111
- if (item && item.value == null && item.label != null) {
112
- return true;
113
- }
114
- }
115
- }
116
- return false;
117
- }
118
- for (const item of arrayItems) {
119
- if (item && item.value == null && item.label != null) {
120
- return true;
121
- }
122
- }
123
- return false;
124
- }
125
- function stringifyAsLabel(item, itemToStringLabel) {
126
- if (itemToStringLabel && item != null) {
127
- return itemToStringLabel(item) ?? '';
128
- }
129
- if (item && typeof item === 'object') {
130
- if ('label' in item && item.label != null) {
131
- return String(item.label);
132
- }
133
- if ('value' in item) {
134
- return String(item.value);
135
- }
136
- }
137
- return serializeValue(item);
138
- }
139
- function stringifyAsValue(item, itemToStringValue) {
140
- if (itemToStringValue && item != null) {
141
- return itemToStringValue(item) ?? '';
142
- }
143
- if (item && typeof item === 'object' && 'value' in item && 'label' in item) {
144
- return serializeValue(item.value);
145
- }
146
- return serializeValue(item);
147
- }
148
- function resolveSelectedLabel(value, items, itemToStringLabel) {
149
- function fallback() {
150
- return stringifyAsLabel(value, itemToStringLabel);
151
- }
152
- if (itemToStringLabel && value != null) {
153
- return itemToStringLabel(value);
154
- }
155
-
156
- // Custom object with explicit label takes precedence
157
- if (value && typeof value === 'object' && 'label' in value && value.label != null) {
158
- return value.label;
159
- }
160
-
161
- // Items provided as plain record map
162
- if (items && !Array.isArray(items)) {
163
- return items[value] ?? fallback();
164
- }
165
-
166
- // Items provided as array (flat or grouped)
167
- if (Array.isArray(items)) {
168
- const arrayItems = items;
169
- const flatItems = isGroupedItems(arrayItems) ? arrayItems.flatMap(group => group.items) : arrayItems;
170
- if (value == null || typeof value !== 'object') {
171
- const match = flatItems.find(item => item.value === value);
172
- if (match && match.label != null) {
173
- return match.label;
174
- }
175
- return fallback();
176
- }
177
-
178
- // Object without explicit label: try matching by its `value` property
179
- if ('value' in value) {
180
- const match = flatItems.find(item => item && item.value === value.value);
181
- if (match && match.label != null) {
182
- return match.label;
183
- }
184
- }
185
- }
186
- return fallback();
187
- }
188
- function resolveMultipleLabels(values, items, itemToStringLabel) {
189
- return values.reduce((acc, value, index) => {
190
- if (index > 0) {
191
- acc.push(', ');
192
- }
193
- acc.push(/*#__PURE__*/jsx(React.Fragment, {
194
- children: resolveSelectedLabel(value, items, itemToStringLabel)
195
- }, index));
196
- return acc;
197
- }, []);
198
- }
199
-
200
- const selectors = {
201
- id: createSelector(state => state.id),
202
- labelId: createSelector(state => state.labelId),
203
- query: createSelector(state => state.query),
204
- items: createSelector(state => state.items),
205
- selectedValue: createSelector(state => state.selectedValue),
206
- hasSelectionChips: createSelector(state => {
207
- const selectedValue = state.selectedValue;
208
- return Array.isArray(selectedValue) && selectedValue.length > 0;
209
- }),
210
- hasSelectedValue: createSelector(state => {
211
- const {
212
- selectedValue,
213
- selectionMode
214
- } = state;
215
- if (selectedValue == null) {
216
- return false;
217
- }
218
- if (selectionMode === 'multiple' && Array.isArray(selectedValue)) {
219
- return selectedValue.length > 0;
220
- }
221
- return true;
222
- }),
223
- hasNullItemLabel: createSelector((state, enabled) => {
224
- return enabled ? hasNullItemLabel(state.items) : false;
225
- }),
226
- open: createSelector(state => state.open),
227
- mounted: createSelector(state => state.mounted),
228
- forceMounted: createSelector(state => state.forceMounted),
229
- inline: createSelector(state => state.inline),
230
- activeIndex: createSelector(state => state.activeIndex),
231
- selectedIndex: createSelector(state => state.selectedIndex),
232
- isActive: createSelector((state, index) => state.activeIndex === index),
233
- isSelected: createSelector((state, itemValue) => {
234
- const comparer = state.isItemEqualToValue;
235
- const selectedValue = state.selectedValue;
236
- if (Array.isArray(selectedValue)) {
237
- return selectedValue.some(selectedItem => compareItemEquality(itemValue, selectedItem, comparer));
238
- }
239
- return compareItemEquality(itemValue, selectedValue, comparer);
240
- }),
241
- transitionStatus: createSelector(state => state.transitionStatus),
242
- popupProps: createSelector(state => state.popupProps),
243
- inputProps: createSelector(state => state.inputProps),
244
- triggerProps: createSelector(state => state.triggerProps),
245
- getItemProps: createSelector(state => state.getItemProps),
246
- positionerElement: createSelector(state => state.positionerElement),
247
- listElement: createSelector(state => state.listElement),
248
- triggerElement: createSelector(state => state.triggerElement),
249
- inputElement: createSelector(state => state.inputElement),
250
- inputGroupElement: createSelector(state => state.inputGroupElement),
251
- popupSide: createSelector(state => state.popupSide),
252
- openMethod: createSelector(state => state.openMethod),
253
- inputInsidePopup: createSelector(state => state.inputInsidePopup),
254
- selectionMode: createSelector(state => state.selectionMode),
255
- listRef: createSelector(state => state.listRef),
256
- labelsRef: createSelector(state => state.labelsRef),
257
- popupRef: createSelector(state => state.popupRef),
258
- emptyRef: createSelector(state => state.emptyRef),
259
- inputRef: createSelector(state => state.inputRef),
260
- keyboardActiveRef: createSelector(state => state.keyboardActiveRef),
261
- chipsContainerRef: createSelector(state => state.chipsContainerRef),
262
- clearRef: createSelector(state => state.clearRef),
263
- valuesRef: createSelector(state => state.valuesRef),
264
- allValuesRef: createSelector(state => state.allValuesRef),
265
- name: createSelector(state => state.name),
266
- disabled: createSelector(state => state.disabled),
267
- readOnly: createSelector(state => state.readOnly),
268
- required: createSelector(state => state.required),
269
- grid: createSelector(state => state.grid),
270
- isGrouped: createSelector(state => state.isGrouped),
271
- virtualized: createSelector(state => state.virtualized),
272
- onOpenChangeComplete: createSelector(state => state.onOpenChangeComplete),
273
- openOnInputClick: createSelector(state => state.openOnInputClick),
274
- itemToStringLabel: createSelector(state => state.itemToStringLabel),
275
- isItemEqualToValue: createSelector(state => state.isItemEqualToValue),
276
- modal: createSelector(state => state.modal),
277
- autoHighlight: createSelector(state => state.autoHighlight),
278
- submitOnItemClick: createSelector(state => state.submitOnItemClick)
279
- };
280
-
281
- /**
282
- * Enhanced filter using Intl.Collator for more robust string matching.
283
- * Uses the provided `itemToStringLabel` function if available, otherwise falls back to:
284
- * • When `item` is an object with a `value` property, that property is used.
285
- * • When `item` is a primitive (e.g. `string`), it is used directly.
286
- */
287
- function createCollatorItemFilter(collatorFilter, itemToStringLabel) {
288
- return (item, query) => {
289
- if (item == null) {
290
- return false;
291
- }
292
- const itemString = stringifyAsLabel(item, itemToStringLabel);
293
- return collatorFilter.contains(itemString, query);
294
- };
295
- }
296
-
297
- /**
298
- * Enhanced filter for single selection mode using Intl.Collator that shows all items
299
- * when query is empty or matches the current selection, making it easier to browse options.
300
- */
301
- function createSingleSelectionCollatorFilter(collatorFilter, itemToStringLabel, selectedValue) {
302
- return (item, query) => {
303
- if (item == null) {
304
- return false;
305
- }
306
- if (!query) {
307
- return true;
308
- }
309
- const itemString = stringifyAsLabel(item, itemToStringLabel);
310
- const selectedString = selectedValue != null ? stringifyAsLabel(selectedValue, itemToStringLabel) : '';
311
-
312
- // Handle case-insensitive matching consistently
313
- if (selectedString && collatorFilter.contains(selectedString, query) && selectedString.length === query.length) {
314
- return true;
315
- }
316
- return collatorFilter.contains(itemString, query);
317
- };
318
- }
319
-
320
- const filterCache = new Map();
321
- function stringifyLocale(locale) {
322
- if (Array.isArray(locale)) {
323
- return locale.map(value => stringifyLocale(value)).join(',');
324
- }
325
- if (locale == null) {
326
- return '';
327
- }
328
- return String(locale);
329
- }
330
- function getFilter(options = {}) {
331
- const mergedOptions = {
332
- usage: 'search',
333
- sensitivity: 'base',
334
- ignorePunctuation: true,
335
- ...options
336
- };
337
- const cacheKey = `${stringifyLocale(options.locale)}|${JSON.stringify(mergedOptions)}`;
338
- const cachedFilter = filterCache.get(cacheKey);
339
- if (cachedFilter) {
340
- return cachedFilter;
341
- }
342
- const collator = new Intl.Collator(options.locale, mergedOptions);
343
- const filter = {
344
- contains(item, query, itemToString) {
345
- if (!query) {
346
- return true;
347
- }
348
- const itemString = stringifyAsLabel(item, itemToString);
349
- for (let i = 0; i <= itemString.length - query.length; i += 1) {
350
- if (collator.compare(itemString.slice(i, i + query.length), query) === 0) {
351
- return true;
352
- }
353
- }
354
- return false;
355
- },
356
- startsWith(item, query, itemToString) {
357
- if (!query) {
358
- return true;
359
- }
360
- const itemString = stringifyAsLabel(item, itemToString);
361
- return collator.compare(itemString.slice(0, query.length), query) === 0;
362
- },
363
- endsWith(item, query, itemToString) {
364
- if (!query) {
365
- return true;
366
- }
367
- const itemString = stringifyAsLabel(item, itemToString);
368
- const queryLength = query.length;
369
- return itemString.length >= queryLength && collator.compare(itemString.slice(itemString.length - queryLength), query) === 0;
370
- }
371
- };
372
- filterCache.set(cacheKey, filter);
373
- return filter;
374
- }
375
-
376
- /**
377
- * Matches items against a query using `Intl.Collator` for robust string matching.
378
- */
379
- const useCoreFilter = getFilter;
380
- /**
381
- * Matches items against a query using `Intl.Collator` for robust string matching.
382
- */
383
- function useComboboxFilter(options = {}) {
19
+ function AutocompleteRoot(props) {
384
20
  const {
385
- multiple = false,
21
+ openOnInputClick = false,
386
22
  value,
387
- ...collatorOptions
388
- } = options;
389
- const coreFilter = getFilter(collatorOptions);
390
- const contains = React.useCallback((item, query, itemToString) => {
391
- if (multiple) {
392
- return createCollatorItemFilter(coreFilter, itemToString)(item, query);
393
- }
394
- return createSingleSelectionCollatorFilter(coreFilter, itemToString, value)(item, query);
395
- }, [coreFilter, value, multiple]);
396
- return React.useMemo(() => ({
397
- contains,
398
- startsWith: coreFilter.startsWith,
399
- endsWith: coreFilter.endsWith
400
- }), [contains, coreFilter]);
401
- }
402
-
403
- const NO_ACTIVE_VALUE = Symbol('none');
404
- const INITIAL_LAST_HIGHLIGHT = {
405
- value: NO_ACTIVE_VALUE,
406
- index: -1
407
- };
408
-
409
- function AriaCombobox(props) {
410
- const {
411
- id: idProp,
412
- onOpenChangeComplete: onOpenChangeCompleteProp,
413
- defaultSelectedValue = null,
414
- selectedValue: selectedValueProp,
415
- onSelectedValueChange,
416
- defaultInputValue: defaultInputValueProp,
417
- inputValue: inputValueProp,
418
- selectionMode = 'none',
419
- onItemHighlighted: onItemHighlightedProp,
420
- name: nameProp,
421
- disabled: disabledProp = false,
422
- readOnly = false,
423
- required = false,
424
- inputRef: inputRefProp,
425
- grid = false,
426
- items,
427
- filteredItems: filteredItemsProp,
428
- filter: filterProp,
429
- openOnInputClick = true,
430
- autoHighlight = false,
431
- keepHighlight = false,
432
- highlightItemOnHover = true,
433
- loopFocus = true,
434
- itemToStringLabel,
23
+ defaultValue,
24
+ onValueChange,
25
+ mode = 'list',
435
26
  itemToStringValue,
436
- isItemEqualToValue = defaultItemEquality,
437
- virtualized = false,
438
- inline: inlineProp = false,
439
- fillInputOnItemPress = true,
440
- modal = false,
441
- limit = -1,
442
- autoComplete = 'list',
443
- formAutoComplete,
444
- locale,
445
- submitOnItemClick = false
27
+ ...other
446
28
  } = props;
447
- const {
448
- clearErrors
449
- } = useFormContext();
450
- const {
451
- setDirty,
452
- validityData,
453
- shouldValidateOnChange,
454
- setFilled,
455
- name: fieldName,
456
- disabled: fieldDisabled,
457
- setTouched,
458
- setFocused,
459
- validationMode,
460
- validation
461
- } = useFieldRootContext();
462
- const id = useLabelableId({
463
- id: idProp
464
- });
465
- const collatorFilter = useCoreFilter({
466
- locale
467
- });
468
- const [queryChangedAfterOpen, setQueryChangedAfterOpen] = React.useState(false);
469
- const [closeQuery, setCloseQuery] = React.useState(null);
470
- const listRef = React.useRef([]);
471
- const labelsRef = React.useRef([]);
472
- const popupRef = React.useRef(null);
473
- const inputRef = React.useRef(null);
474
- const startDismissRef = React.useRef(null);
475
- const endDismissRef = React.useRef(null);
476
- const emptyRef = React.useRef(null);
477
- const keyboardActiveRef = React.useRef(true);
478
- const hadInputClearRef = React.useRef(false);
479
- const chipsContainerRef = React.useRef(null);
480
- const clearRef = React.useRef(null);
481
- const selectionEventRef = React.useRef(null);
482
- const lastHighlightRef = React.useRef(INITIAL_LAST_HIGHLIGHT);
483
- const pendingQueryHighlightRef = React.useRef(null);
484
-
485
- /**
486
- * Contains the currently visible list of item values post-filtering.
487
- */
488
- const valuesRef = React.useRef([]);
489
- /**
490
- * Contains all item values in a stable, unfiltered order.
491
- * This is only used when `items` prop is not provided.
492
- * It accumulates values on first mount and does not remove them on unmount due to
493
- * filtering, providing a stable index for selected value tracking.
494
- */
495
- const allValuesRef = React.useRef([]);
496
- const disabled = fieldDisabled || disabledProp;
497
- const name = fieldName ?? nameProp;
498
- const multiple = selectionMode === 'multiple';
499
- const single = selectionMode === 'single';
500
- const hasInputValue = inputValueProp !== undefined || defaultInputValueProp !== undefined;
501
- const hasItems = items !== undefined;
502
- const hasFilteredItemsProp = filteredItemsProp !== undefined;
503
- let autoHighlightMode;
504
- if (autoHighlight === 'always') {
505
- autoHighlightMode = 'always';
506
- } else {
507
- autoHighlightMode = autoHighlight ? 'input-change' : false;
508
- }
509
- const [selectedValue, setSelectedValueUnwrapped] = useControlled({
510
- controlled: selectedValueProp,
511
- default: multiple ? defaultSelectedValue ?? EMPTY_ARRAY : defaultSelectedValue,
512
- name: 'Combobox',
513
- state: 'selectedValue'
514
- });
515
- const filter = React.useMemo(() => {
516
- if (filterProp === null) {
517
- return () => true;
518
- }
519
- if (filterProp !== undefined) {
520
- return filterProp;
521
- }
522
- if (single && !queryChangedAfterOpen) {
523
- return createSingleSelectionCollatorFilter(collatorFilter, itemToStringLabel, selectedValue);
524
- }
525
- return createCollatorItemFilter(collatorFilter, itemToStringLabel);
526
- }, [filterProp, single, selectedValue, queryChangedAfterOpen, collatorFilter, itemToStringLabel]);
29
+ const enableInline = mode === 'inline' || mode === 'both';
30
+ const staticItems = mode === 'inline' || mode === 'none';
527
31
 
528
- // If neither inputValue nor defaultInputValue are provided, derive it from the
529
- // selected value for single mode so the input reflects the selection on mount.
530
- const initialDefaultInputValue = useRefWithInit(() => {
531
- if (hasInputValue) {
532
- return defaultInputValueProp ?? '';
533
- }
534
- if (single) {
535
- return stringifyAsLabel(selectedValue, itemToStringLabel);
536
- }
537
- return '';
538
- }).current;
539
- const [inputValue, setInputValueUnwrapped] = useControlled({
540
- controlled: inputValueProp,
541
- default: initialDefaultInputValue,
542
- name: 'Combobox',
543
- state: 'inputValue'
544
- });
545
- const [open, setOpenUnwrapped] = useControlled({
546
- controlled: props.open,
547
- default: props.defaultOpen,
548
- name: 'Combobox',
549
- state: 'open'
550
- });
551
- const isGrouped = isGroupedItems(items);
552
- const query = closeQuery ?? (inputValue === '' ? '' : String(inputValue).trim());
553
- const selectedLabelString = single ? stringifyAsLabel(selectedValue, itemToStringLabel) : '';
554
- const shouldBypassFiltering = single && !queryChangedAfterOpen && query !== '' && selectedLabelString !== '' && selectedLabelString.length === query.length && collatorFilter.contains(selectedLabelString, query);
555
- const filterQuery = shouldBypassFiltering ? '' : query;
556
- const shouldIgnoreExternalFiltering = hasItems && hasFilteredItemsProp && shouldBypassFiltering;
557
- const flatItems = React.useMemo(() => {
558
- if (!items) {
559
- return EMPTY_ARRAY;
560
- }
561
- if (isGrouped) {
562
- return items.flatMap(group => group.items);
563
- }
564
- return items;
565
- }, [items, isGrouped]);
566
- const filteredItems = React.useMemo(() => {
567
- if (filteredItemsProp && !shouldIgnoreExternalFiltering) {
568
- return filteredItemsProp;
569
- }
570
- if (!items) {
571
- return EMPTY_ARRAY;
572
- }
573
- if (isGrouped) {
574
- const groupedItems = items;
575
- const resultingGroups = [];
576
- let currentCount = 0;
577
- for (const group of groupedItems) {
578
- if (limit > -1 && currentCount >= limit) {
579
- break;
580
- }
581
- const candidateItems = filterQuery === '' ? group.items : group.items.filter(item => filter(item, filterQuery, itemToStringLabel));
582
- if (candidateItems.length === 0) {
583
- continue;
584
- }
585
- const remainingLimit = limit > -1 ? limit - currentCount : Infinity;
586
- const itemsToTake = candidateItems.slice(0, remainingLimit);
587
- if (itemsToTake.length > 0) {
588
- const newGroup = {
589
- ...group,
590
- items: itemsToTake
591
- };
592
- resultingGroups.push(newGroup);
593
- currentCount += itemsToTake.length;
594
- }
595
- }
596
- return resultingGroups;
597
- }
598
- if (filterQuery === '') {
599
- return limit > -1 ? flatItems.slice(0, limit) :
600
- // The cast here is done as `flatItems` is readonly.
601
- // valuesRef.current, a mutable ref, can be set to `flatFilteredItems`, which may
602
- // reference this exact readonly value, creating a mutation risk.
603
- // However, <Combobox.Item> can never mutate this value as the mutating effect
604
- // bails early when `items` is provided, and this is only ever returned
605
- // when `items` is provided due to the early return at the top of this hook.
606
- flatItems;
607
- }
608
- const limitedItems = [];
609
- for (const item of flatItems) {
610
- if (limit > -1 && limitedItems.length >= limit) {
611
- break;
612
- }
613
- if (filter(item, filterQuery, itemToStringLabel)) {
614
- limitedItems.push(item);
615
- }
616
- }
617
- return limitedItems;
618
- }, [filteredItemsProp, shouldIgnoreExternalFiltering, items, isGrouped, filterQuery, limit, filter, itemToStringLabel, flatItems]);
619
- const flatFilteredItems = React.useMemo(() => {
620
- if (isGrouped) {
621
- const groups = filteredItems;
622
- return groups.flatMap(g => g.items);
623
- }
624
- return filteredItems;
625
- }, [filteredItems, isGrouped]);
626
- const store = useRefWithInit(() => new Store({
627
- id,
628
- labelId: undefined,
629
- selectedValue,
630
- open,
631
- filter,
632
- query,
633
- items,
634
- selectionMode,
635
- listRef,
636
- labelsRef,
637
- popupRef,
638
- emptyRef,
639
- inputRef,
640
- startDismissRef,
641
- endDismissRef,
642
- keyboardActiveRef,
643
- chipsContainerRef,
644
- clearRef,
645
- valuesRef,
646
- allValuesRef,
647
- selectionEventRef,
648
- name,
649
- disabled,
650
- readOnly,
651
- required,
652
- grid,
653
- isGrouped,
654
- virtualized,
655
- openOnInputClick,
656
- itemToStringLabel,
657
- isItemEqualToValue,
658
- modal,
659
- autoHighlight: autoHighlightMode,
660
- submitOnItemClick,
661
- hasInputValue,
662
- mounted: false,
663
- forceMounted: false,
664
- transitionStatus: 'idle',
665
- inline: inlineProp,
666
- activeIndex: null,
667
- selectedIndex: null,
668
- popupProps: {},
669
- inputProps: {},
670
- triggerProps: {},
671
- positionerElement: null,
672
- listElement: null,
673
- triggerElement: null,
674
- inputElement: null,
675
- inputGroupElement: null,
676
- popupSide: null,
677
- openMethod: null,
678
- inputInsidePopup: true,
679
- onOpenChangeComplete: onOpenChangeCompleteProp || NOOP,
680
- // Placeholder callbacks replaced on first render
681
- setOpen: NOOP,
682
- setInputValue: NOOP,
683
- setSelectedValue: NOOP,
684
- setIndices: NOOP,
685
- onItemHighlighted: NOOP,
686
- handleSelection: NOOP,
687
- getItemProps: () => EMPTY_OBJECT,
688
- forceMount: NOOP,
689
- requestSubmit: NOOP
690
- })).current;
691
- const fieldRawValue = selectionMode === 'none' ? inputValue : selectedValue;
692
- const fieldStringValue = React.useMemo(() => {
693
- if (selectionMode === 'none') {
694
- return fieldRawValue;
695
- }
696
- if (Array.isArray(selectedValue)) {
697
- return selectedValue.map(value => stringifyAsValue(value, itemToStringValue));
698
- }
699
- return stringifyAsValue(selectedValue, itemToStringValue);
700
- }, [fieldRawValue, itemToStringValue, selectionMode, selectedValue]);
701
- const onItemHighlighted = useStableCallback(onItemHighlightedProp);
702
- const onOpenChangeComplete = useStableCallback(onOpenChangeCompleteProp);
703
- const activeIndex = useStore(store, selectors.activeIndex);
704
- const selectedIndex = useStore(store, selectors.selectedIndex);
705
- const positionerElement = useStore(store, selectors.positionerElement);
706
- const listElement = useStore(store, selectors.listElement);
707
- const triggerElement = useStore(store, selectors.triggerElement);
708
- const inputElement = useStore(store, selectors.inputElement);
709
- const inputGroupElement = useStore(store, selectors.inputGroupElement);
710
- const inline = useStore(store, selectors.inline);
711
- const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
712
- const triggerRef = useValueAsRef(triggerElement);
713
- const {
714
- mounted,
715
- setMounted,
716
- transitionStatus
717
- } = useTransitionStatus(open);
718
- const {
719
- openMethod,
720
- triggerProps
721
- } = useOpenInteractionType(open);
722
- useField({
723
- id,
724
- name,
725
- commit: validation.commit,
726
- value: fieldRawValue,
727
- controlRef: inputInsidePopup ? triggerRef : inputRef,
728
- getValue: () => fieldStringValue
729
- });
730
- const forceMount = useStableCallback(() => {
731
- if (items) {
732
- // Ensure typeahead works on a closed list.
733
- labelsRef.current = flatFilteredItems.map(item => stringifyAsLabel(item, itemToStringLabel));
734
- } else {
735
- store.set('forceMounted', true);
736
- }
737
- });
738
- const initialSelectedValueRef = React.useRef(selectedValue);
739
- useIsoLayoutEffect(() => {
740
- // Ensure the values and labels are registered for programmatic value changes.
741
- if (selectedValue !== initialSelectedValueRef.current) {
742
- forceMount();
743
- }
744
- }, [forceMount, selectedValue]);
745
- const setIndices = useStableCallback(options => {
746
- store.update(options);
747
- const type = options.type || 'none';
748
- if (options.activeIndex === undefined) {
749
- return;
750
- }
751
- if (options.activeIndex === null) {
752
- if (lastHighlightRef.current !== INITIAL_LAST_HIGHLIGHT) {
753
- lastHighlightRef.current = INITIAL_LAST_HIGHLIGHT;
754
- onItemHighlighted(undefined, createGenericEventDetails(type, undefined, {
755
- index: -1
756
- }));
757
- }
758
- } else {
759
- const activeValue = valuesRef.current[options.activeIndex];
760
- lastHighlightRef.current = {
761
- value: activeValue,
762
- index: options.activeIndex
763
- };
764
- onItemHighlighted(activeValue, createGenericEventDetails(type, undefined, {
765
- index: options.activeIndex
766
- }));
767
- }
768
- });
769
- const setInputValue = useStableCallback((next, eventDetails) => {
770
- hadInputClearRef.current = eventDetails.reason === inputClear;
771
- props.onInputValueChange?.(next, eventDetails);
772
- if (eventDetails.isCanceled) {
773
- return;
32
+ // Mirror the typed value for uncontrolled usage so we can compose the temporary
33
+ // inline input value.
34
+ const isControlled = value !== undefined;
35
+ const [internalValue, setInternalValue] = React.useState(defaultValue ?? '');
36
+ const [inlineInputValue, setInlineInputValue] = React.useState('');
37
+ React.useEffect(() => {
38
+ if (isControlled) {
39
+ setInlineInputValue('');
774
40
  }
41
+ }, [value, isControlled]);
775
42
 
776
- // If user is typing, ensure we don't auto-highlight on open due to a race
777
- // with the post-open effect that sets this flag.
778
- if (eventDetails.reason === inputChange) {
779
- const event = eventDetails.event;
780
- const inputType = event.inputType;
781
- // Treat composition commits as typed input; autofill may omit `inputType` or
782
- // report `insertReplacementText`.
783
- const isTypedInput = event.type === 'compositionend' || inputType != null && inputType !== '' && inputType !== 'insertReplacementText';
784
- if (isTypedInput) {
785
- const hasQuery = next.trim() !== '';
786
- if (hasQuery) {
787
- setQueryChangedAfterOpen(true);
788
- }
789
- // Defer index updates until after the filtered items have been derived to ensure
790
- // `onItemHighlighted` receives the latest item.
791
- pendingQueryHighlightRef.current = {
792
- hasQuery
793
- };
794
- if (hasQuery && autoHighlightMode && store.state.activeIndex == null) {
795
- store.set('activeIndex', 0);
796
- }
797
- }
43
+ // Compose the input value shown to the user: inline value takes precedence when present.
44
+ let resolvedInputValue;
45
+ if (enableInline && inlineInputValue !== '') {
46
+ resolvedInputValue = inlineInputValue;
47
+ } else if (isControlled) {
48
+ resolvedInputValue = value ?? '';
49
+ } else {
50
+ resolvedInputValue = internalValue;
51
+ }
52
+ const handleValueChange = useStableCallback((nextValue, eventDetails) => {
53
+ setInlineInputValue('');
54
+ if (!isControlled) {
55
+ setInternalValue(nextValue);
798
56
  }
799
- setInputValueUnwrapped(next);
57
+ onValueChange?.(nextValue, eventDetails);
800
58
  });
801
- const setOpen = useStableCallback((nextOpen, eventDetails) => {
802
- if (open === nextOpen) {
803
- return;
59
+ const collator = useCoreFilter();
60
+ const baseFilter = React.useMemo(() => {
61
+ if (other.filter !== undefined) {
62
+ return other.filter;
804
63
  }
64
+ return collator.contains;
65
+ }, [other.filter, collator]);
66
+ const resolvedQuery = String(isControlled ? value : internalValue).trim();
805
67
 
806
- // If the `Empty` component is not used, the positioner or popup should be hidden
807
- // with CSS. In this case, allow the Escape key to bubble to close a parent popup
808
- // if there are no items to show.
809
- if (eventDetails.reason === 'escape-key' && hasItems && flatFilteredItems.length === 0 && !store.state.emptyRef.current) {
810
- eventDetails.allowPropagation();
811
- }
812
- props.onOpenChange?.(nextOpen, eventDetails);
813
- if (eventDetails.isCanceled) {
814
- return;
815
- }
816
- if (!nextOpen && queryChangedAfterOpen) {
817
- if (single) {
818
- if (!inline) {
819
- setCloseQuery(query);
820
- }
821
- // Avoid a flicker when closing the popup with an empty query.
822
- if (query === '') {
823
- setQueryChangedAfterOpen(false);
824
- }
825
- } else if (multiple) {
826
- if (inline || inputInsidePopup) {
827
- setIndices({
828
- activeIndex: null
829
- });
830
- } else {
831
- // Freeze the current query so filtering remains stable while exiting.
832
- setCloseQuery(query);
833
- }
834
- // Clear the input immediately on close while retaining filtering via closeQuery for exit animations
835
- // if the input is outside the popup.
836
- setInputValue('', createChangeEventDetails(inputClear, eventDetails.event));
837
- }
838
- }
839
- setOpenUnwrapped(nextOpen);
840
- if (!nextOpen && inputInsidePopup && (eventDetails.reason === focusOut || eventDetails.reason === outsidePress)) {
841
- setTouched(true);
842
- setFocused(false);
843
- if (validationMode === 'onBlur') {
844
- const valueToValidate = selectionMode === 'none' ? inputValue : selectedValue;
845
- validation.commit(valueToValidate);
846
- }
847
- }
848
- });
849
- const setSelectedValue = useStableCallback((nextValue, eventDetails) => {
850
- // Cast to `any` due to conditional value type (single vs. multiple).
851
- // The runtime implementation already ensures the correct value shape.
852
- onSelectedValueChange?.(nextValue, eventDetails);
853
- if (eventDetails.isCanceled) {
854
- return;
855
- }
856
- setSelectedValueUnwrapped(nextValue);
857
- const shouldFillInput = selectionMode === 'none' && popupRef.current && fillInputOnItemPress || single && !store.state.inputInsidePopup;
858
- if (shouldFillInput) {
859
- setInputValue(stringifyAsLabel(nextValue, itemToStringLabel), createChangeEventDetails(eventDetails.reason, eventDetails.event));
860
- }
861
- if (single && nextValue != null && eventDetails.reason !== inputChange && queryChangedAfterOpen && !inline) {
862
- setCloseQuery(query);
863
- }
864
- });
865
- const handleSelection = useStableCallback((event, passedValue) => {
866
- let itemValue = passedValue;
867
- if (itemValue === undefined) {
868
- if (activeIndex === null) {
869
- return;
870
- }
871
- itemValue = valuesRef.current[activeIndex];
68
+ // In "both", wrap filtering to use only the typed value, ignoring the inline value.
69
+ const resolvedFilter = React.useMemo(() => {
70
+ if (mode !== 'both') {
71
+ return staticItems ? null : baseFilter;
872
72
  }
873
- const targetEl = getTarget(event);
874
- const overrideEvent = selectionEventRef.current ?? event;
875
- selectionEventRef.current = null;
876
- const eventDetails = createChangeEventDetails(itemPress, overrideEvent);
877
-
878
- // Let the link handle the click.
879
- const href = targetEl?.closest('a')?.getAttribute('href');
880
- if (href) {
881
- if (href.startsWith('#')) {
882
- setOpen(false, eventDetails);
883
- }
884
- return;
885
- }
886
- if (multiple) {
887
- const currentSelectedValue = Array.isArray(selectedValue) ? selectedValue : [];
888
- const isCurrentlySelected = selectedValueIncludes(currentSelectedValue, itemValue, store.state.isItemEqualToValue);
889
- const nextValue = isCurrentlySelected ? removeItem(currentSelectedValue, itemValue, store.state.isItemEqualToValue) : [...currentSelectedValue, itemValue];
890
- setSelectedValue(nextValue, eventDetails);
891
- const wasFiltering = inputRef.current ? inputRef.current.value.trim() !== '' : false;
892
- if (!wasFiltering) {
893
- return;
894
- }
895
- if (store.state.inputInsidePopup) {
896
- setInputValue('', createChangeEventDetails(inputClear, eventDetails.event));
897
- } else {
898
- setOpen(false, eventDetails);
899
- }
900
- } else {
901
- setSelectedValue(itemValue, eventDetails);
902
- setOpen(false, eventDetails);
903
- }
904
- });
905
- const requestSubmit = useStableCallback(() => {
906
- if (!store.state.submitOnItemClick) {
907
- return;
908
- }
909
- const form = store.state.inputElement?.form;
910
- if (form && typeof form.requestSubmit === 'function') {
911
- form.requestSubmit();
912
- }
913
- });
914
- const handleUnmount = useStableCallback(() => {
915
- setMounted(false);
916
- onOpenChangeComplete?.(false);
917
- setQueryChangedAfterOpen(false);
918
- setCloseQuery(null);
919
- if (selectionMode === 'none') {
920
- setIndices({
921
- activeIndex: null,
922
- selectedIndex: null
923
- });
924
- } else {
925
- setIndices({
926
- activeIndex: null
927
- });
928
- }
929
-
930
- // Multiple selection mode:
931
- // If the user typed a filter and didn't select in multiple mode, clear the input
932
- // after close completes to avoid mid-exit flicker and start fresh on next open.
933
- if (multiple && inputRef.current && inputRef.current.value !== '' && !hadInputClearRef.current) {
934
- setInputValue('', createChangeEventDetails(inputClear));
935
- }
936
-
937
- // Single selection mode:
938
- // - If input is rendered inside the popup, clear it so the next open is blank
939
- // - If input is outside the popup, sync it to the selected value
940
- if (single) {
941
- if (store.state.inputInsidePopup) {
942
- if (inputRef.current && inputRef.current.value !== '') {
943
- setInputValue('', createChangeEventDetails(inputClear));
944
- }
945
- } else {
946
- const stringVal = stringifyAsLabel(selectedValue, itemToStringLabel);
947
- if (inputRef.current && inputRef.current.value !== stringVal) {
948
- // If no selection was made, treat this as clearing the typed filter.
949
- const reason = stringVal === '' ? inputClear : none;
950
- setInputValue(stringVal, createChangeEventDetails(reason));
951
- }
952
- }
953
- }
954
- });
955
-
956
- // Support composing the Dialog component around an inline combobox.
957
- // `[role="dialog"]` is more interoperable than using a context, e.g. it can work
958
- // with third-party modal libraries, though the limitation is that the closest
959
- // `role=dialog` part must be the animated element.
960
- const resolvedPopupRef = React.useMemo(() => {
961
- if (inline && positionerElement) {
962
- return {
963
- current: positionerElement.closest('[role="dialog"]')
964
- };
965
- }
966
- return popupRef;
967
- }, [inline, positionerElement]);
968
- useOpenChangeComplete({
969
- enabled: !props.actionsRef,
970
- open,
971
- ref: resolvedPopupRef,
972
- onComplete() {
973
- if (!open) {
974
- handleUnmount();
975
- }
976
- }
977
- });
978
- React.useImperativeHandle(props.actionsRef, () => ({
979
- unmount: handleUnmount
980
- }), [handleUnmount]);
981
- useIsoLayoutEffect(function syncSelectedIndex() {
982
- if (open || selectionMode === 'none') {
983
- return;
984
- }
985
- const registry = items ? flatItems : allValuesRef.current;
986
- if (multiple) {
987
- const currentValue = Array.isArray(selectedValue) ? selectedValue : [];
988
- const lastValue = currentValue[currentValue.length - 1];
989
- const lastIndex = findItemIndex(registry, lastValue, isItemEqualToValue);
990
- setIndices({
991
- selectedIndex: lastIndex === -1 ? null : lastIndex
992
- });
993
- } else {
994
- const index = findItemIndex(registry, selectedValue, isItemEqualToValue);
995
- setIndices({
996
- selectedIndex: index === -1 ? null : index
997
- });
998
- }
999
- }, [open, selectedValue, items, selectionMode, flatItems, multiple, isItemEqualToValue, setIndices]);
1000
- useIsoLayoutEffect(() => {
1001
- if (items) {
1002
- valuesRef.current = flatFilteredItems;
1003
- listRef.current.length = flatFilteredItems.length;
1004
- }
1005
- }, [items, flatFilteredItems]);
1006
- useIsoLayoutEffect(() => {
1007
- const pendingHighlight = pendingQueryHighlightRef.current;
1008
- if (pendingHighlight) {
1009
- if (pendingHighlight.hasQuery) {
1010
- if (autoHighlightMode) {
1011
- store.set('activeIndex', 0);
1012
- }
1013
- } else if (autoHighlightMode === 'always') {
1014
- store.set('activeIndex', 0);
1015
- }
1016
- pendingQueryHighlightRef.current = null;
1017
- }
1018
- if (!open && !inline) {
1019
- return;
1020
- }
1021
- const shouldUseFlatFilteredItems = hasItems || hasFilteredItemsProp;
1022
- const candidateItems = shouldUseFlatFilteredItems ? flatFilteredItems : valuesRef.current;
1023
- const storeActiveIndex = store.state.activeIndex;
1024
- if (storeActiveIndex == null) {
1025
- if (autoHighlightMode === 'always' && candidateItems.length > 0) {
1026
- store.set('activeIndex', 0);
1027
- return;
1028
- }
1029
- if (lastHighlightRef.current !== INITIAL_LAST_HIGHLIGHT) {
1030
- lastHighlightRef.current = INITIAL_LAST_HIGHLIGHT;
1031
- store.state.onItemHighlighted(undefined, createGenericEventDetails(none, undefined, {
1032
- index: -1
1033
- }));
1034
- }
1035
- return;
1036
- }
1037
- if (storeActiveIndex >= candidateItems.length) {
1038
- if (lastHighlightRef.current !== INITIAL_LAST_HIGHLIGHT) {
1039
- lastHighlightRef.current = INITIAL_LAST_HIGHLIGHT;
1040
- store.state.onItemHighlighted(undefined, createGenericEventDetails(none, undefined, {
1041
- index: -1
1042
- }));
1043
- }
1044
- store.set('activeIndex', null);
1045
- return;
1046
- }
1047
- const itemValue = candidateItems[storeActiveIndex];
1048
- const previouslyHighlightedItemValue = lastHighlightRef.current.value;
1049
- const isSameItem = previouslyHighlightedItemValue !== NO_ACTIVE_VALUE && compareItemEquality(itemValue, previouslyHighlightedItemValue, store.state.isItemEqualToValue);
1050
- if (lastHighlightRef.current.index !== storeActiveIndex || !isSameItem) {
1051
- lastHighlightRef.current = {
1052
- value: itemValue,
1053
- index: storeActiveIndex
1054
- };
1055
- store.state.onItemHighlighted(itemValue, createGenericEventDetails(none, undefined, {
1056
- index: storeActiveIndex
1057
- }));
1058
- }
1059
- }, [activeIndex, autoHighlightMode, hasFilteredItemsProp, hasItems, flatFilteredItems, inline, open, store]);
1060
- useIsoLayoutEffect(() => {
1061
- if (selectionMode === 'none') {
1062
- setFilled(String(inputValue) !== '');
1063
- return;
1064
- }
1065
- setFilled(multiple ? Array.isArray(selectedValue) && selectedValue.length > 0 : selectedValue != null);
1066
- }, [setFilled, selectionMode, inputValue, selectedValue, multiple]);
1067
-
1068
- // Ensures that the active index is not set to 0 when the list is empty.
1069
- // This avoids needing to press ArrowDown twice under certain conditions.
1070
- React.useEffect(() => {
1071
- if (hasItems && autoHighlightMode && flatFilteredItems.length === 0) {
1072
- setIndices({
1073
- activeIndex: null
1074
- });
1075
- }
1076
- }, [hasItems, autoHighlightMode, flatFilteredItems.length, setIndices]);
1077
- useValueChanged(query, () => {
1078
- if (!open || query === '' || query === String(initialDefaultInputValue)) {
1079
- return;
1080
- }
1081
- setQueryChangedAfterOpen(true);
1082
- });
1083
- useValueChanged(selectedValue, () => {
1084
- if (selectionMode === 'none') {
1085
- return;
1086
- }
1087
- clearErrors(name);
1088
- setDirty(selectedValue !== validityData.initialValue);
1089
- if (shouldValidateOnChange()) {
1090
- validation.commit(selectedValue);
1091
- } else {
1092
- validation.commit(selectedValue, true);
1093
- }
1094
- if (single && !hasInputValue && !inputInsidePopup) {
1095
- const nextInputValue = stringifyAsLabel(selectedValue, itemToStringLabel);
1096
- if (inputValue !== nextInputValue) {
1097
- setInputValue(nextInputValue, createChangeEventDetails(none));
1098
- }
1099
- }
1100
- });
1101
- useValueChanged(inputValue, () => {
1102
- if (selectionMode !== 'none') {
1103
- return;
1104
- }
1105
- clearErrors(name);
1106
- setDirty(inputValue !== validityData.initialValue);
1107
- if (shouldValidateOnChange()) {
1108
- validation.commit(inputValue);
1109
- } else {
1110
- validation.commit(inputValue, true);
1111
- }
1112
- });
1113
- useValueChanged(items, () => {
1114
- if (!single || hasInputValue || inputInsidePopup || queryChangedAfterOpen) {
1115
- return;
1116
- }
1117
- const nextInputValue = stringifyAsLabel(selectedValue, itemToStringLabel);
1118
- if (inputValue !== nextInputValue) {
1119
- setInputValue(nextInputValue, createChangeEventDetails(none));
1120
- }
1121
- });
1122
- const floatingRootContext = useFloatingRootContext({
1123
- open: inline ? true : open,
1124
- onOpenChange: setOpen,
1125
- elements: {
1126
- reference: inputInsidePopup ? triggerElement : inputElement,
1127
- floating: positionerElement
1128
- }
1129
- });
1130
- let ariaHasPopup;
1131
- let ariaExpanded;
1132
- if (!inline) {
1133
- ariaHasPopup = grid ? 'grid' : 'listbox';
1134
- ariaExpanded = open ? 'true' : 'false';
1135
- }
1136
- const role = React.useMemo(() => {
1137
- const isPlainInput = inputElement?.tagName === 'INPUT';
1138
- // During SSR and initial hydration, the input ref is not available yet.
1139
- // Assume an input-like control so combobox ARIA attributes are present.
1140
- const shouldTreatAsInput = inputElement == null || isPlainInput;
1141
- const shouldApplyAria = shouldTreatAsInput || open;
1142
- const reference = shouldTreatAsInput ? {
1143
- autoComplete: 'off',
1144
- spellCheck: 'false',
1145
- autoCorrect: 'off',
1146
- autoCapitalize: 'none'
1147
- } : {};
1148
- if (shouldApplyAria) {
1149
- reference.role = 'combobox';
1150
- reference['aria-expanded'] = ariaExpanded;
1151
- reference['aria-haspopup'] = ariaHasPopup;
1152
- reference['aria-controls'] = open ? listElement?.id : undefined;
1153
- reference['aria-autocomplete'] = autoComplete;
1154
- }
1155
- return {
1156
- reference,
1157
- floating: {
1158
- role: 'presentation'
1159
- }
1160
- };
1161
- }, [inputElement, open, ariaExpanded, ariaHasPopup, listElement?.id, autoComplete]);
1162
- const click = useClick(floatingRootContext, {
1163
- enabled: !readOnly && !disabled && openOnInputClick,
1164
- event: 'mousedown-only',
1165
- toggle: false,
1166
- // Apply a small delay for touch to let iOS viewport centering settle.
1167
- // This avoids top-bottom flip flickers if the preferred position is "top" when first tapping.
1168
- touchOpenDelay: inputInsidePopup ? 0 : 50,
1169
- reason: inputPress
1170
- });
1171
- const dismiss = useDismiss(floatingRootContext, {
1172
- enabled: !readOnly && !disabled && !inline,
1173
- outsidePressEvent: {
1174
- mouse: 'sloppy',
1175
- // The visual viewport (affected by the mobile software keyboard) can be
1176
- // somewhat small. The user may want to scroll the screen to see more of
1177
- // the popup.
1178
- touch: 'intentional'
1179
- },
1180
- // Without a popup, let the Escape key bubble the event up to other popups' handlers.
1181
- bubbles: inline ? true : undefined,
1182
- outsidePress(event) {
1183
- const target = getTarget(event);
1184
- return !contains(triggerElement, target) && !contains(clearRef.current, target) && !contains(chipsContainerRef.current, target) && !contains(inputGroupElement, target);
1185
- }
1186
- });
1187
- const listNavigation = useListNavigation(floatingRootContext, {
1188
- enabled: !readOnly && !disabled,
1189
- id,
1190
- listRef,
1191
- activeIndex,
1192
- selectedIndex,
1193
- virtual: true,
1194
- loopFocus,
1195
- allowEscape: loopFocus && !autoHighlightMode,
1196
- focusItemOnOpen: queryChangedAfterOpen || selectionMode === 'none' && !autoHighlightMode ? false : 'auto',
1197
- focusItemOnHover: highlightItemOnHover,
1198
- resetOnPointerLeave: !keepHighlight,
1199
- // `cols` > 1 enables grid navigation.
1200
- // Since <Combobox.Row> infers column sizes (and is required when building a grid),
1201
- // it works correctly even with a value of `2`.
1202
- // Floating UI tests don't require `role="row"` wrappers, so retains the number API.
1203
- cols: grid ? 2 : 1,
1204
- orientation: grid ? 'horizontal' : undefined,
1205
- disabledIndices: EMPTY_ARRAY,
1206
- onNavigate(nextActiveIndex, event) {
1207
- // Retain the highlight only while actually transitioning out or closed.
1208
- if (!event && !open || transitionStatus === 'ending') {
1209
- return;
1210
- }
1211
- if (!event) {
1212
- setIndices({
1213
- activeIndex: nextActiveIndex
1214
- });
1215
- } else {
1216
- setIndices({
1217
- activeIndex: nextActiveIndex,
1218
- type: keyboardActiveRef.current ? 'keyboard' : 'pointer'
1219
- });
1220
- }
1221
- }
1222
- });
1223
- const {
1224
- getReferenceProps,
1225
- getFloatingProps,
1226
- getItemProps
1227
- } = useInteractions([role, click, dismiss, listNavigation]);
1228
- useOnFirstRender(() => {
1229
- store.update({
1230
- inline: inlineProp,
1231
- popupProps: getFloatingProps(),
1232
- inputProps: getReferenceProps(),
1233
- triggerProps,
1234
- getItemProps,
1235
- setOpen,
1236
- setInputValue,
1237
- setSelectedValue,
1238
- setIndices,
1239
- onItemHighlighted,
1240
- handleSelection,
1241
- forceMount,
1242
- requestSubmit
1243
- });
1244
- });
1245
- useIsoLayoutEffect(() => {
1246
- store.update({
1247
- id,
1248
- selectedValue,
1249
- open,
1250
- mounted,
1251
- transitionStatus,
1252
- items,
1253
- inline: inlineProp,
1254
- popupProps: getFloatingProps(),
1255
- inputProps: getReferenceProps(),
1256
- triggerProps,
1257
- openMethod,
1258
- getItemProps,
1259
- selectionMode,
1260
- name,
1261
- disabled,
1262
- readOnly,
1263
- required,
1264
- grid,
1265
- isGrouped,
1266
- virtualized,
1267
- onOpenChangeComplete,
1268
- openOnInputClick,
1269
- itemToStringLabel,
1270
- modal,
1271
- autoHighlight: autoHighlightMode,
1272
- isItemEqualToValue,
1273
- submitOnItemClick,
1274
- hasInputValue,
1275
- requestSubmit
1276
- });
1277
- }, [store, id, selectedValue, open, mounted, transitionStatus, items, getFloatingProps, getReferenceProps, getItemProps, openMethod, triggerProps, selectionMode, name, disabled, readOnly, required, validation, grid, isGrouped, virtualized, onOpenChangeComplete, openOnInputClick, itemToStringLabel, modal, isItemEqualToValue, submitOnItemClick, hasInputValue, inlineProp, requestSubmit, autoHighlightMode]);
1278
- const hiddenInputRef = useMergedRefs(inputRefProp, validation.inputRef);
1279
- const itemsContextValue = React.useMemo(() => ({
1280
- query,
1281
- hasItems,
1282
- filteredItems,
1283
- flatFilteredItems
1284
- }), [query, hasItems, filteredItems, flatFilteredItems]);
1285
- const serializedValue = React.useMemo(() => {
1286
- if (Array.isArray(fieldRawValue)) {
1287
- return '';
1288
- }
1289
- return stringifyAsValue(fieldRawValue, itemToStringValue);
1290
- }, [fieldRawValue, itemToStringValue]);
1291
- const hasMultipleSelection = multiple && Array.isArray(selectedValue) && selectedValue.length > 0;
1292
- const hiddenInputName = multiple || selectionMode === 'none' ? undefined : name;
1293
- const hiddenInputs = React.useMemo(() => {
1294
- if (!multiple || !Array.isArray(selectedValue) || !name) {
1295
- return null;
1296
- }
1297
- return selectedValue.map(value => {
1298
- const currentSerializedValue = stringifyAsValue(value, itemToStringValue);
1299
- return /*#__PURE__*/jsx("input", {
1300
- type: "hidden",
1301
- name: name,
1302
- value: currentSerializedValue
1303
- }, currentSerializedValue);
1304
- });
1305
- }, [multiple, selectedValue, name, itemToStringValue]);
1306
- const children = /*#__PURE__*/jsxs(React.Fragment, {
1307
- children: [props.children, /*#__PURE__*/jsx("input", {
1308
- ...validation.getInputValidationProps({
1309
- // Move focus when the hidden input is focused.
1310
- onFocus() {
1311
- if (inputInsidePopup) {
1312
- triggerElement?.focus();
1313
- return;
1314
- }
1315
- (inputRef.current || triggerElement)?.focus();
1316
- },
1317
- // Handle browser autofill.
1318
- onChange(event) {
1319
- // Workaround for https://github.com/facebook/react/issues/9023
1320
- if (event.nativeEvent.defaultPrevented) {
1321
- return;
1322
- }
1323
- const nextValue = event.target.value;
1324
- const details = createChangeEventDetails(none, event.nativeEvent);
1325
- function handleChange() {
1326
- // Browser autofill only writes a single scalar value.
1327
- if (multiple) {
1328
- return;
1329
- }
1330
- if (selectionMode === 'none') {
1331
- setDirty(nextValue !== validityData.initialValue);
1332
- setInputValue(nextValue, details);
1333
- if (shouldValidateOnChange()) {
1334
- validation.commit(nextValue);
1335
- }
1336
- return;
1337
- }
1338
- const matchingValue = valuesRef.current.find(v => {
1339
- const candidate = stringifyAsValue(v, itemToStringValue);
1340
- if (candidate.toLowerCase() === nextValue.toLowerCase()) {
1341
- return true;
1342
- }
1343
- return false;
1344
- });
1345
- if (matchingValue != null) {
1346
- setDirty(matchingValue !== validityData.initialValue);
1347
- setSelectedValue?.(matchingValue, details);
1348
- if (shouldValidateOnChange()) {
1349
- validation.commit(matchingValue);
1350
- }
1351
- }
1352
- }
1353
- if (items) {
1354
- handleChange();
1355
- } else {
1356
- forceMount();
1357
- queueMicrotask(handleChange);
1358
- }
1359
- }
1360
- }),
1361
- id: id && hiddenInputName == null ? `${id}-hidden-input` : undefined,
1362
- name: hiddenInputName,
1363
- autoComplete: formAutoComplete,
1364
- disabled: disabled,
1365
- required: required && !hasMultipleSelection,
1366
- readOnly: readOnly,
1367
- value: serializedValue,
1368
- ref: hiddenInputRef,
1369
- style: hiddenInputName ? visuallyHiddenInput : visuallyHidden,
1370
- tabIndex: -1,
1371
- "aria-hidden": true
1372
- }), hiddenInputs]
1373
- });
1374
- return /*#__PURE__*/jsx(ComboboxRootContext.Provider, {
1375
- value: store,
1376
- children: /*#__PURE__*/jsx(ComboboxFloatingContext.Provider, {
1377
- value: floatingRootContext,
1378
- children: /*#__PURE__*/jsx(ComboboxDerivedItemsContext.Provider, {
1379
- value: itemsContextValue,
1380
- children: /*#__PURE__*/jsx(ComboboxInputValueContext.Provider, {
1381
- value: inputValue,
1382
- children: children
1383
- })
1384
- })
1385
- })
1386
- });
1387
- }
1388
-
1389
- function AutocompleteRoot(props) {
1390
- const {
1391
- openOnInputClick = false,
1392
- value,
1393
- defaultValue,
1394
- onValueChange,
1395
- mode = 'list',
1396
- itemToStringValue,
1397
- ...other
1398
- } = props;
1399
- const enableInline = mode === 'inline' || mode === 'both';
1400
- const staticItems = mode === 'inline' || mode === 'none';
1401
-
1402
- // Mirror the typed value for uncontrolled usage so we can compose the temporary
1403
- // inline input value.
1404
- const isControlled = value !== undefined;
1405
- const [internalValue, setInternalValue] = React.useState(defaultValue ?? '');
1406
- const [inlineInputValue, setInlineInputValue] = React.useState('');
1407
- React.useEffect(() => {
1408
- if (isControlled) {
1409
- setInlineInputValue('');
1410
- }
1411
- }, [value, isControlled]);
1412
-
1413
- // Compose the input value shown to the user: inline value takes precedence when present.
1414
- let resolvedInputValue;
1415
- if (enableInline && inlineInputValue !== '') {
1416
- resolvedInputValue = inlineInputValue;
1417
- } else if (isControlled) {
1418
- resolvedInputValue = value ?? '';
1419
- } else {
1420
- resolvedInputValue = internalValue;
1421
- }
1422
- const handleValueChange = useStableCallback((nextValue, eventDetails) => {
1423
- setInlineInputValue('');
1424
- if (!isControlled) {
1425
- setInternalValue(nextValue);
1426
- }
1427
- onValueChange?.(nextValue, eventDetails);
1428
- });
1429
- const collator = useCoreFilter();
1430
- const baseFilter = React.useMemo(() => {
1431
- if (other.filter !== undefined) {
1432
- return other.filter;
1433
- }
1434
- return collator.contains;
1435
- }, [other.filter, collator]);
1436
- const resolvedQuery = String(isControlled ? value : internalValue).trim();
1437
-
1438
- // In "both", wrap filtering to use only the typed value, ignoring the inline value.
1439
- const resolvedFilter = React.useMemo(() => {
1440
- if (mode !== 'both') {
1441
- return staticItems ? null : baseFilter;
1442
- }
1443
- if (baseFilter === null) {
1444
- return null;
73
+ if (baseFilter === null) {
74
+ return null;
1445
75
  }
1446
76
  return (item, _query, toString) => {
1447
77
  return baseFilter(item, resolvedQuery, toString);
1448
78
  };
1449
- }, [baseFilter, mode, resolvedQuery, staticItems]);
1450
- const handleItemHighlighted = useStableCallback((highlightedValue, eventDetails) => {
1451
- props.onItemHighlighted?.(highlightedValue, eventDetails);
1452
- if (eventDetails.reason === pointer) {
1453
- return;
1454
- }
1455
- if (enableInline) {
1456
- if (highlightedValue == null) {
1457
- setInlineInputValue('');
1458
- } else {
1459
- setInlineInputValue(stringifyAsLabel(highlightedValue, itemToStringValue));
1460
- }
1461
- } else {
1462
- setInlineInputValue('');
1463
- }
1464
- });
1465
- return /*#__PURE__*/jsx(AriaCombobox, {
1466
- ...other,
1467
- itemToStringLabel: itemToStringValue,
1468
- openOnInputClick: openOnInputClick,
1469
- selectionMode: "none",
1470
- fillInputOnItemPress: true,
1471
- filter: resolvedFilter,
1472
- autoComplete: mode,
1473
- inputValue: resolvedInputValue,
1474
- defaultInputValue: defaultValue,
1475
- onInputValueChange: handleValueChange,
1476
- onItemHighlighted: handleItemHighlighted
1477
- });
1478
- }
1479
-
1480
- function AutocompleteValue(props) {
1481
- const {
1482
- children
1483
- } = props;
1484
- const inputValue = useComboboxInputValueContext();
1485
- let returnValue = null;
1486
- if (typeof children === 'function') {
1487
- returnValue = children(String(inputValue));
1488
- } else if (children != null) {
1489
- returnValue = children;
1490
- } else {
1491
- returnValue = inputValue;
1492
- }
1493
- return /*#__PURE__*/jsx(React.Fragment, {
1494
- children: returnValue
1495
- });
1496
- }
1497
-
1498
- const triggerStateAttributesMapping = {
1499
- ...pressableTriggerOpenStateMapping,
1500
- ...fieldValidityMapping,
1501
- popupSide: side => side ? {
1502
- 'data-popup-side': side
1503
- } : null,
1504
- listEmpty: empty => empty ? {
1505
- 'data-list-empty': ''
1506
- } : null
1507
- };
1508
-
1509
- function getDefaultLabelId(id) {
1510
- return id == null ? undefined : `${id}-label`;
1511
- }
1512
- function resolveAriaLabelledBy(fieldLabelId, localLabelId) {
1513
- return fieldLabelId ?? localLabelId;
1514
- }
1515
-
1516
- const BOUNDARY_OFFSET = 2;
1517
-
1518
- /**
1519
- * A button that opens the popup.
1520
- * Renders a `<button>` element.
1521
- */
1522
- const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(componentProps, forwardedRef) {
1523
- const {
1524
- render,
1525
- className,
1526
- nativeButton = true,
1527
- disabled: disabledProp = false,
1528
- id: idProp,
1529
- ...elementProps
1530
- } = componentProps;
1531
- const {
1532
- state: fieldState,
1533
- disabled: fieldDisabled,
1534
- setTouched,
1535
- setFocused,
1536
- validationMode,
1537
- validation
1538
- } = useFieldRootContext();
1539
- const {
1540
- labelId: fieldLabelId
1541
- } = useLabelableContext();
1542
- const store = useComboboxRootContext();
1543
- const {
1544
- filteredItems
1545
- } = useComboboxDerivedItemsContext();
1546
- const selectionMode = useStore(store, selectors.selectionMode);
1547
- const comboboxDisabled = useStore(store, selectors.disabled);
1548
- const readOnly = useStore(store, selectors.readOnly);
1549
- const required = useStore(store, selectors.required);
1550
- const mounted = useStore(store, selectors.mounted);
1551
- const popupSideValue = useStore(store, selectors.popupSide);
1552
- const positionerElement = useStore(store, selectors.positionerElement);
1553
- const listElement = useStore(store, selectors.listElement);
1554
- const triggerProps = useStore(store, selectors.triggerProps);
1555
- const triggerElement = useStore(store, selectors.triggerElement);
1556
- const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
1557
- const rootId = useStore(store, selectors.id);
1558
- const comboboxLabelId = useStore(store, selectors.labelId);
1559
- const open = useStore(store, selectors.open);
1560
- const selectedValue = useStore(store, selectors.selectedValue);
1561
- const activeIndex = useStore(store, selectors.activeIndex);
1562
- const selectedIndex = useStore(store, selectors.selectedIndex);
1563
- const hasSelectedValue = useStore(store, selectors.hasSelectedValue);
1564
- const floatingRootContext = useComboboxFloatingContext();
1565
- const inputValue = useComboboxInputValueContext();
1566
- const focusTimeout = useTimeout();
1567
- const disabled = fieldDisabled || comboboxDisabled || disabledProp;
1568
- const listEmpty = filteredItems.length === 0;
1569
- const popupSide = mounted && positionerElement ? popupSideValue : null;
1570
- useLabelableId({
1571
- id: inputInsidePopup ? idProp : undefined
1572
- });
1573
- const id = inputInsidePopup ? idProp ?? rootId : idProp;
1574
- const ariaLabelledBy = resolveAriaLabelledBy(fieldLabelId, comboboxLabelId);
1575
- const currentPointerTypeRef = React.useRef('');
1576
- function trackPointerType(event) {
1577
- currentPointerTypeRef.current = event.pointerType;
1578
- }
1579
- const domReference = floatingRootContext.useState('domReferenceElement');
1580
-
1581
- // Update the floating root context to use the trigger element when it differs from the current reference.
1582
- // This ensures useClick and useTypeahead attach handlers to the correct element.
1583
- React.useEffect(() => {
1584
- if (!inputInsidePopup) {
1585
- return;
1586
- }
1587
- if (triggerElement && triggerElement !== domReference) {
1588
- floatingRootContext.set('domReferenceElement', triggerElement);
1589
- }
1590
- }, [triggerElement, domReference, floatingRootContext, inputInsidePopup]);
1591
- const {
1592
- reference: triggerTypeaheadProps
1593
- } = useTypeahead(floatingRootContext, {
1594
- enabled: !open && !readOnly && !comboboxDisabled && selectionMode === 'single',
1595
- listRef: store.state.labelsRef,
1596
- activeIndex,
1597
- selectedIndex,
1598
- onMatch(index) {
1599
- const nextSelectedValue = store.state.valuesRef.current[index];
1600
- if (nextSelectedValue !== undefined) {
1601
- store.state.setSelectedValue(nextSelectedValue, createChangeEventDetails('none'));
1602
- }
1603
- }
1604
- });
1605
- const {
1606
- reference: triggerClickProps
1607
- } = useClick(floatingRootContext, {
1608
- enabled: !readOnly && !comboboxDisabled,
1609
- event: 'mousedown'
1610
- });
1611
- const {
1612
- buttonRef,
1613
- getButtonProps
1614
- } = useButton({
1615
- native: nativeButton,
1616
- disabled
1617
- });
1618
- const state = {
1619
- ...fieldState,
1620
- open,
1621
- disabled,
1622
- popupSide,
1623
- listEmpty,
1624
- placeholder: !hasSelectedValue
1625
- };
1626
- const setTriggerElement = useStableCallback(element => {
1627
- store.set('triggerElement', element);
1628
- });
1629
- const element = useRenderElement('button', componentProps, {
1630
- ref: [forwardedRef, buttonRef, setTriggerElement],
1631
- state,
1632
- props: [triggerProps, triggerClickProps, triggerTypeaheadProps, {
1633
- id,
1634
- tabIndex: inputInsidePopup ? 0 : -1,
1635
- role: inputInsidePopup ? 'combobox' : undefined,
1636
- 'aria-expanded': open ? 'true' : 'false',
1637
- 'aria-haspopup': inputInsidePopup ? 'dialog' : 'listbox',
1638
- 'aria-controls': open ? listElement?.id : undefined,
1639
- 'aria-required': inputInsidePopup ? required || undefined : undefined,
1640
- 'aria-labelledby': ariaLabelledBy,
1641
- onPointerDown: trackPointerType,
1642
- onPointerEnter: trackPointerType,
1643
- onFocus() {
1644
- setFocused(true);
1645
- if (disabled || readOnly) {
1646
- return;
1647
- }
1648
- focusTimeout.start(0, store.state.forceMount);
1649
- },
1650
- onBlur(event) {
1651
- // If focus is moving into the popup, don't count it as a blur.
1652
- if (contains(positionerElement, event.relatedTarget)) {
1653
- return;
1654
- }
1655
- setTouched(true);
1656
- setFocused(false);
1657
- if (validationMode === 'onBlur') {
1658
- const valueToValidate = selectionMode === 'none' ? inputValue : selectedValue;
1659
- validation.commit(valueToValidate);
1660
- }
1661
- },
1662
- onMouseDown(event) {
1663
- if (disabled || readOnly) {
1664
- return;
1665
- }
1666
- if (!inputInsidePopup) {
1667
- floatingRootContext.set('domReferenceElement', event.currentTarget);
1668
- }
1669
-
1670
- // Ensure items are registered for initial selection highlight.
1671
- store.state.forceMount();
1672
- if (currentPointerTypeRef.current !== 'touch') {
1673
- store.state.inputRef.current?.focus();
1674
- if (!inputInsidePopup) {
1675
- event.preventDefault();
1676
- }
1677
- }
1678
- if (open) {
1679
- return;
1680
- }
1681
- const doc = ownerDocument(event.currentTarget);
1682
- function handleMouseUp(mouseEvent) {
1683
- if (!triggerElement) {
1684
- return;
1685
- }
1686
- const mouseUpTarget = getTarget(mouseEvent);
1687
- const positioner = store.state.positionerElement;
1688
- const list = store.state.listElement;
1689
- if (contains(triggerElement, mouseUpTarget) || contains(positioner, mouseUpTarget) || contains(list, mouseUpTarget) || mouseUpTarget === triggerElement) {
1690
- return;
1691
- }
1692
- const bounds = getPseudoElementBounds(triggerElement);
1693
- const withinHorizontal = mouseEvent.clientX >= bounds.left - BOUNDARY_OFFSET && mouseEvent.clientX <= bounds.right + BOUNDARY_OFFSET;
1694
- const withinVertical = mouseEvent.clientY >= bounds.top - BOUNDARY_OFFSET && mouseEvent.clientY <= bounds.bottom + BOUNDARY_OFFSET;
1695
- if (withinHorizontal && withinVertical) {
1696
- return;
1697
- }
1698
- store.state.setOpen(false, createChangeEventDetails('cancel-open', mouseEvent));
1699
- }
1700
- if (inputInsidePopup) {
1701
- doc.addEventListener('mouseup', handleMouseUp, {
1702
- once: true
1703
- });
1704
- }
1705
- },
1706
- onKeyDown(event) {
1707
- if (disabled || readOnly) {
1708
- return;
1709
- }
1710
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
1711
- stopEvent(event);
1712
- store.state.setOpen(true, createChangeEventDetails(listNavigation, event.nativeEvent));
1713
- store.state.inputRef.current?.focus();
1714
- }
1715
- }
1716
- }, validation ? validation.getValidationProps(elementProps) : elementProps, getButtonProps],
1717
- stateAttributesMapping: triggerStateAttributesMapping
1718
- });
1719
- return element;
1720
- });
1721
- if (process.env.NODE_ENV !== "production") ComboboxTrigger.displayName = "ComboboxTrigger";
1722
-
1723
- const ComboboxChipsContext = /*#__PURE__*/React.createContext(undefined);
1724
- if (process.env.NODE_ENV !== "production") ComboboxChipsContext.displayName = "ComboboxChipsContext";
1725
- function useComboboxChipsContext() {
1726
- return React.useContext(ComboboxChipsContext);
1727
- }
1728
-
1729
- const ComboboxPositionerContext = /*#__PURE__*/React.createContext(undefined);
1730
- if (process.env.NODE_ENV !== "production") ComboboxPositionerContext.displayName = "ComboboxPositionerContext";
1731
- function useComboboxPositionerContext(optional) {
1732
- const context = React.useContext(ComboboxPositionerContext);
1733
- if (context === undefined && !optional) {
1734
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Combobox.Popup> and <Combobox.Arrow> must be used within the <Combobox.Positioner> component' : formatErrorMessage(21));
1735
- }
1736
- return context;
1737
- }
1738
-
1739
- /**
1740
- * @internal
1741
- */
1742
- const ComboboxInternalDismissButton = /*#__PURE__*/React.forwardRef(function ComboboxInternalDismissButton(_, forwardedRef) {
1743
- const store = useComboboxRootContext();
1744
- const {
1745
- buttonRef,
1746
- getButtonProps
1747
- } = useButton({
1748
- native: false
1749
- });
1750
- const mergedRef = useMergedRefs(forwardedRef, buttonRef);
1751
- const handleDismiss = useStableCallback(event => {
1752
- store.state.setOpen(false, createChangeEventDetails(closePress, event.nativeEvent, event.currentTarget));
1753
- });
1754
- const dismissProps = getButtonProps({
1755
- onClick: handleDismiss
1756
- });
1757
- return /*#__PURE__*/jsx("span", {
1758
- ref: mergedRef,
1759
- ...dismissProps,
1760
- "aria-label": "Dismiss",
1761
- tabIndex: undefined,
1762
- style: visuallyHiddenInput
1763
- });
1764
- });
1765
- if (process.env.NODE_ENV !== "production") ComboboxInternalDismissButton.displayName = "ComboboxInternalDismissButton";
1766
-
1767
- const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(componentProps, forwardedRef) {
1768
- const {
1769
- render,
1770
- className,
1771
- disabled: disabledProp = false,
1772
- id: idProp,
1773
- ...elementProps
1774
- } = componentProps;
1775
- const {
1776
- state: fieldState,
1777
- disabled: fieldDisabled,
1778
- setTouched,
1779
- setFocused,
1780
- validationMode,
1781
- validation
1782
- } = useFieldRootContext();
1783
- const {
1784
- labelId: fieldLabelId
1785
- } = useLabelableContext();
1786
- const comboboxChipsContext = useComboboxChipsContext();
1787
- const positioning = useComboboxPositionerContext(true);
1788
- const hasPositionerParent = Boolean(positioning);
1789
- const store = useComboboxRootContext();
1790
- const {
1791
- filteredItems
1792
- } = useComboboxDerivedItemsContext();
1793
- // `inputValue` can't be placed in the store.
1794
- // https://github.com/mui/base-ui/issues/2703
1795
- const inputValue = useComboboxInputValueContext();
1796
- const direction = useDirection();
1797
- const required = useStore(store, selectors.required);
1798
- const comboboxDisabled = useStore(store, selectors.disabled);
1799
- const readOnly = useStore(store, selectors.readOnly);
1800
- const name = useStore(store, selectors.name);
1801
- const selectionMode = useStore(store, selectors.selectionMode);
1802
- const autoHighlightMode = useStore(store, selectors.autoHighlight);
1803
- const inputProps = useStore(store, selectors.inputProps);
1804
- const triggerProps = useStore(store, selectors.triggerProps);
1805
- const open = useStore(store, selectors.open);
1806
- const mounted = useStore(store, selectors.mounted);
1807
- const selectedValue = useStore(store, selectors.selectedValue);
1808
- const popupSideValue = useStore(store, selectors.popupSide);
1809
- const positionerElement = useStore(store, selectors.positionerElement);
1810
- const rootId = useStore(store, selectors.id);
1811
- const inline = useStore(store, selectors.inline);
1812
- const modal = useStore(store, selectors.modal);
1813
- const autoHighlightEnabled = Boolean(autoHighlightMode);
1814
- const popupSide = mounted && positionerElement ? popupSideValue : null;
1815
- const disabled = fieldDisabled || comboboxDisabled || disabledProp;
1816
- const listEmpty = filteredItems.length === 0;
1817
- const isInsidePopup = hasPositionerParent || inline;
1818
- const focusManagerModal = !isInsidePopup || modal;
1819
- const id = useBaseUiId(idProp ?? (!isInsidePopup ? rootId : undefined));
1820
- const ariaLabelledBy = resolveAriaLabelledBy(fieldLabelId, undefined);
1821
- const fieldStateForInput = hasPositionerParent ? DEFAULT_FIELD_STATE_ATTRIBUTES : fieldState;
1822
- const [composingValue, setComposingValue] = React.useState(null);
1823
- const isComposingRef = React.useRef(false);
1824
- const lastActiveIndexRef = React.useRef(null);
1825
- const shouldRestoreActiveIndexRef = React.useRef(false);
1826
- const setInputElement = useStableCallback(element => {
1827
- const nextIsInsidePopup = hasPositionerParent || store.state.inline;
1828
- if (nextIsInsidePopup && !store.state.hasInputValue) {
1829
- store.state.setInputValue('', createChangeEventDetails(none));
1830
- }
1831
- store.update({
1832
- inputElement: element,
1833
- inputInsidePopup: nextIsInsidePopup
1834
- });
1835
- });
1836
- const validationProps = hasPositionerParent || !validation ? elementProps : validation.getValidationProps(elementProps);
1837
- const state = {
1838
- ...fieldStateForInput,
1839
- open,
1840
- disabled,
1841
- readOnly,
1842
- popupSide,
1843
- listEmpty
1844
- };
1845
- function handleKeyDown(event) {
1846
- if (!comboboxChipsContext) {
1847
- return undefined;
1848
- }
1849
- let nextIndex;
1850
- const {
1851
- highlightedChipIndex
1852
- } = comboboxChipsContext;
1853
- if (highlightedChipIndex !== undefined) {
1854
- if (event.key === 'ArrowLeft') {
1855
- event.preventDefault();
1856
- if (highlightedChipIndex > 0) {
1857
- nextIndex = highlightedChipIndex - 1;
1858
- } else {
1859
- nextIndex = undefined;
1860
- }
1861
- } else if (event.key === 'ArrowRight') {
1862
- event.preventDefault();
1863
- if (highlightedChipIndex < selectedValue.length - 1) {
1864
- nextIndex = highlightedChipIndex + 1;
1865
- } else {
1866
- nextIndex = undefined;
1867
- }
1868
- } else if (event.key === 'Backspace' || event.key === 'Delete') {
1869
- event.preventDefault();
1870
- // Move highlight appropriately after removal.
1871
- const computedNextIndex = highlightedChipIndex >= selectedValue.length - 1 ? selectedValue.length - 2 : highlightedChipIndex;
1872
- // If the computed index is negative, treat it as no highlight.
1873
- nextIndex = computedNextIndex >= 0 ? computedNextIndex : undefined;
1874
- store.state.setIndices({
1875
- activeIndex: null,
1876
- selectedIndex: null,
1877
- type: 'keyboard'
1878
- });
1879
- }
1880
- return nextIndex;
1881
- }
1882
-
1883
- // Handle navigation when no chip is highlighted
1884
- if (event.key === 'ArrowLeft' && (event.currentTarget.selectionStart ?? 0) === 0 && selectedValue.length > 0) {
1885
- event.preventDefault();
1886
- const lastChipIndex = Math.max(selectedValue.length - 1, 0);
1887
- nextIndex = lastChipIndex;
1888
- } else if (event.key === 'Backspace' && event.currentTarget.value === '' && selectedValue.length > 0) {
1889
- store.state.setIndices({
1890
- activeIndex: null,
1891
- selectedIndex: null,
1892
- type: 'keyboard'
1893
- });
1894
- event.preventDefault();
1895
- }
1896
- return nextIndex;
1897
- }
1898
- const element = useRenderElement('input', componentProps, {
1899
- state,
1900
- ref: [forwardedRef, store.state.inputRef, setInputElement],
1901
- props: [inputProps, triggerProps, {
1902
- type: 'text',
1903
- value: componentProps.value ?? composingValue ?? inputValue,
1904
- 'aria-readonly': readOnly || undefined,
1905
- 'aria-required': required || undefined,
1906
- 'aria-labelledby': ariaLabelledBy,
1907
- disabled,
1908
- readOnly,
1909
- required: selectionMode === 'none' ? required : undefined,
1910
- ...(selectionMode === 'none' && name && {
1911
- name
1912
- }),
1913
- id,
1914
- onFocus() {
1915
- setFocused(true);
1916
- if (!inline || !shouldRestoreActiveIndexRef.current) {
1917
- return;
1918
- }
1919
- shouldRestoreActiveIndexRef.current = false;
1920
- const nextActiveIndex = lastActiveIndexRef.current;
1921
- if (nextActiveIndex == null ||
1922
- // `valuesRef` can be sparse, so guard against restoring a removed slot.
1923
- !Object.hasOwn(store.state.valuesRef.current, nextActiveIndex)) {
1924
- return;
1925
- }
1926
- store.state.setIndices({
1927
- activeIndex: nextActiveIndex
1928
- });
1929
- },
1930
- onBlur() {
1931
- setTouched(true);
1932
- setFocused(false);
1933
- const activeIndex = store.state.activeIndex;
1934
- if (inline && activeIndex !== null && autoHighlightMode !== 'always') {
1935
- lastActiveIndexRef.current = activeIndex;
1936
- shouldRestoreActiveIndexRef.current = true;
1937
- store.state.setIndices({
1938
- activeIndex: null
1939
- });
1940
- }
1941
- if (validationMode === 'onBlur') {
1942
- const valueToValidate = selectionMode === 'none' ? inputValue : selectedValue;
1943
- validation.commit(valueToValidate);
1944
- }
1945
- },
1946
- onCompositionStart(event) {
1947
- if (isAndroid) {
1948
- return;
1949
- }
1950
- isComposingRef.current = true;
1951
- setComposingValue(event.currentTarget.value);
1952
- },
1953
- onCompositionEnd(event) {
1954
- isComposingRef.current = false;
1955
- const next = event.currentTarget.value;
1956
- setComposingValue(null);
1957
- store.state.setInputValue(next, createChangeEventDetails(inputChange, event.nativeEvent));
1958
- },
1959
- onChange(event) {
1960
- // Autofill may not provide `inputType` (Chrome) or may report
1961
- // `insertReplacementText` (Firefox).
1962
- const inputType = event.nativeEvent.inputType;
1963
- const autofillLikeInput = !inputType || inputType === 'insertReplacementText';
1964
- const shouldOpenOnInput = isComposingRef.current || !autofillLikeInput;
1965
-
1966
- // During IME composition, avoid propagating controlled updates to prevent
1967
- // filtering the options prematurely so `Empty` won't show incorrectly.
1968
- // We can't rely on this check for Android due to how it handles composition
1969
- // events with some keyboards (e.g. Samsung keyboard with predictive text on
1970
- // treats all text as always-composing).
1971
- // https://github.com/mui/base-ui/issues/2942
1972
- if (isComposingRef.current) {
1973
- const nextVal = event.currentTarget.value;
1974
- setComposingValue(nextVal);
1975
- if (nextVal === '' && !store.state.openOnInputClick && !store.state.inputInsidePopup) {
1976
- store.state.setOpen(false, createChangeEventDetails(inputClear, event.nativeEvent));
1977
- }
1978
- const trimmed = nextVal.trim();
1979
- const shouldMaintainHighlight = autoHighlightEnabled && trimmed !== '';
1980
- if (!readOnly && !disabled && trimmed) {
1981
- if (shouldOpenOnInput) {
1982
- store.state.setOpen(true, createChangeEventDetails(inputChange, event.nativeEvent));
1983
- if (!autoHighlightEnabled) {
1984
- store.state.setIndices({
1985
- activeIndex: null,
1986
- selectedIndex: null,
1987
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
1988
- });
1989
- }
1990
- }
1991
- }
1992
- if (open && store.state.activeIndex !== null && !shouldMaintainHighlight) {
1993
- store.state.setIndices({
1994
- activeIndex: null,
1995
- selectedIndex: null,
1996
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
1997
- });
1998
- }
1999
- return;
2000
- }
2001
- store.state.setInputValue(event.currentTarget.value, createChangeEventDetails(inputChange, event.nativeEvent));
2002
- const empty = event.currentTarget.value === '';
2003
- const clearDetails = createChangeEventDetails(inputClear, event.nativeEvent);
2004
- if (empty && !store.state.inputInsidePopup) {
2005
- if (selectionMode === 'single') {
2006
- store.state.setSelectedValue(null, clearDetails);
2007
- }
2008
- if (!store.state.openOnInputClick) {
2009
- store.state.setOpen(false, clearDetails);
2010
- }
2011
- }
2012
- const trimmed = event.currentTarget.value.trim();
2013
- if (!readOnly && !disabled && trimmed) {
2014
- if (shouldOpenOnInput) {
2015
- store.state.setOpen(true, createChangeEventDetails(inputChange, event.nativeEvent));
2016
- // When autoHighlight is enabled, keep the highlight (will be set to 0 in root).
2017
- if (!autoHighlightEnabled) {
2018
- store.state.setIndices({
2019
- activeIndex: null,
2020
- selectedIndex: null,
2021
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
2022
- });
2023
- }
2024
- }
2025
- }
2026
-
2027
- // When the user types, ensure the list resets its highlight so that
2028
- // virtual focus returns to the input (aria-activedescendant is
2029
- // cleared).
2030
- if (open && store.state.activeIndex !== null && !autoHighlightEnabled) {
2031
- store.state.setIndices({
2032
- activeIndex: null,
2033
- selectedIndex: null,
2034
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
2035
- });
2036
- }
2037
- },
2038
- onKeyDown(event) {
2039
- if (disabled || readOnly) {
2040
- return;
2041
- }
2042
- if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
2043
- return;
2044
- }
2045
- store.state.keyboardActiveRef.current = true;
2046
- const input = event.currentTarget;
2047
- const scrollAmount = input.scrollWidth - input.clientWidth;
2048
- const isRTL = direction === 'rtl';
2049
- if (event.key === 'Home') {
2050
- stopEvent(event);
2051
- const cursor = isFirefox && isRTL ? input.value.length : 0;
2052
- input.setSelectionRange(cursor, cursor);
2053
- input.scrollLeft = 0;
2054
- return;
2055
- }
2056
- if (event.key === 'End') {
2057
- stopEvent(event);
2058
- const cursor = isFirefox && isRTL ? 0 : input.value.length;
2059
- input.setSelectionRange(cursor, cursor);
2060
- input.scrollLeft = isRTL ? -scrollAmount : scrollAmount;
2061
- return;
2062
- }
2063
- if (!mounted && event.key === 'Escape') {
2064
- const isClear = selectionMode === 'multiple' && Array.isArray(selectedValue) ? selectedValue.length === 0 : selectedValue === null;
2065
- const details = createChangeEventDetails(escapeKey, event.nativeEvent);
2066
- const value = selectionMode === 'multiple' ? [] : null;
2067
- store.state.setInputValue('', details);
2068
- store.state.setSelectedValue(value, details);
2069
- if (!isClear && !store.state.inline && !details.isPropagationAllowed) {
2070
- event.stopPropagation();
2071
- }
2072
- return;
2073
- }
2074
-
2075
- // Handle deletion when no chip is highlighted and the input is empty.
2076
- if (comboboxChipsContext && event.key === 'Backspace' && input.value === '' && comboboxChipsContext.highlightedChipIndex === undefined && Array.isArray(selectedValue) && selectedValue.length > 0) {
2077
- const newValue = selectedValue.slice(0, -1);
2078
- // If the removed item was also the active (highlighted) item, clear highlight
2079
- store.state.setIndices({
2080
- activeIndex: null,
2081
- selectedIndex: null,
2082
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
2083
- });
2084
- store.state.setSelectedValue(newValue, createChangeEventDetails(none, event.nativeEvent));
2085
- return;
2086
- }
2087
- const hadHighlightedChip = comboboxChipsContext?.highlightedChipIndex !== undefined;
2088
- const nextIndex = handleKeyDown(event);
2089
- comboboxChipsContext?.setHighlightedChipIndex(nextIndex);
2090
- if (nextIndex !== undefined) {
2091
- comboboxChipsContext?.chipsRef.current[nextIndex]?.focus();
2092
- } else if (hadHighlightedChip) {
2093
- store.state.inputRef.current?.focus();
2094
- }
2095
-
2096
- // event.isComposing
2097
- if (event.which === 229) {
2098
- return;
2099
- }
2100
- if (event.key === 'Enter' && open) {
2101
- const activeIndex = store.state.activeIndex;
2102
- const nativeEvent = event.nativeEvent;
2103
- if (activeIndex === null) {
2104
- if (inline) {
2105
- return;
2106
- }
2107
-
2108
- // Allow form submission when no item is highlighted.
2109
- store.state.setOpen(false, createChangeEventDetails(none, nativeEvent));
2110
- return;
2111
- }
2112
- stopEvent(event);
2113
- const listItem = store.state.listRef.current[activeIndex];
2114
- if (listItem) {
2115
- store.state.selectionEventRef.current = nativeEvent;
2116
- listItem.click();
2117
- store.state.selectionEventRef.current = null;
2118
- }
2119
- }
2120
- },
2121
- onPointerMove() {
2122
- store.state.keyboardActiveRef.current = false;
2123
- },
2124
- onPointerDown() {
2125
- store.state.keyboardActiveRef.current = false;
2126
- }
2127
- }, validationProps],
2128
- stateAttributesMapping: triggerStateAttributesMapping
2129
- });
2130
- return /*#__PURE__*/jsxs(React.Fragment, {
2131
- children: [open && focusManagerModal && /*#__PURE__*/jsx(ComboboxInternalDismissButton, {
2132
- ref: store.state.startDismissRef
2133
- }), element]
2134
- });
2135
- });
2136
- if (process.env.NODE_ENV !== "production") ComboboxInput.displayName = "ComboboxInput";
2137
-
2138
- /**
2139
- * A wrapper for the input and its associated controls.
2140
- * Renders a `<div>` element.
2141
- */
2142
- const ComboboxInputGroup = /*#__PURE__*/React.forwardRef(function ComboboxInputGroup(componentProps, forwardedRef) {
2143
- const {
2144
- render,
2145
- className,
2146
- ...elementProps
2147
- } = componentProps;
2148
- const {
2149
- state: fieldState,
2150
- disabled: fieldDisabled
2151
- } = useFieldRootContext();
2152
- const store = useComboboxRootContext();
2153
- const {
2154
- filteredItems
2155
- } = useComboboxDerivedItemsContext();
2156
- const open = useStore(store, selectors.open);
2157
- const mounted = useStore(store, selectors.mounted);
2158
- const popupSideValue = useStore(store, selectors.popupSide);
2159
- const positionerElement = useStore(store, selectors.positionerElement);
2160
- const comboboxDisabled = useStore(store, selectors.disabled);
2161
- const readOnly = useStore(store, selectors.readOnly);
2162
- const hasSelectedValue = useStore(store, selectors.hasSelectedValue);
2163
- const selectionMode = useStore(store, selectors.selectionMode);
2164
- const popupSide = mounted && positionerElement ? popupSideValue : null;
2165
- const disabled = fieldDisabled || comboboxDisabled;
2166
- const listEmpty = filteredItems.length === 0;
2167
- const placeholder = selectionMode === 'none' ? false : !hasSelectedValue;
2168
- const state = {
2169
- ...fieldState,
2170
- open,
2171
- disabled,
2172
- readOnly,
2173
- popupSide,
2174
- listEmpty,
2175
- placeholder
2176
- };
2177
- const setInputGroupElement = useStableCallback(element => {
2178
- store.set('inputGroupElement', element);
2179
- });
2180
- return useRenderElement('div', componentProps, {
2181
- ref: [forwardedRef, setInputGroupElement],
2182
- props: [{
2183
- role: 'group'
2184
- }, elementProps],
2185
- state,
2186
- stateAttributesMapping: triggerStateAttributesMapping
2187
- });
2188
- });
2189
- if (process.env.NODE_ENV !== "production") ComboboxInputGroup.displayName = "ComboboxInputGroup";
2190
-
2191
- /**
2192
- * An icon that indicates that the trigger button opens the popup.
2193
- * Renders a `<span>` element.
2194
- */
2195
- const ComboboxIcon = /*#__PURE__*/React.forwardRef(function ComboboxIcon(componentProps, forwardedRef) {
2196
- const {
2197
- className,
2198
- render,
2199
- ...elementProps
2200
- } = componentProps;
2201
- const element = useRenderElement('span', componentProps, {
2202
- ref: forwardedRef,
2203
- props: [{
2204
- 'aria-hidden': true,
2205
- children: '▼'
2206
- }, elementProps]
2207
- });
2208
- return element;
2209
- });
2210
- if (process.env.NODE_ENV !== "production") ComboboxIcon.displayName = "ComboboxIcon";
2211
-
2212
- const stateAttributesMapping$8 = {
2213
- ...transitionStatusMapping,
2214
- ...triggerOpenStateMapping
2215
- };
2216
-
2217
- /**
2218
- * Clears the value when clicked.
2219
- * Renders a `<button>` element.
2220
- */
2221
- const ComboboxClear = /*#__PURE__*/React.forwardRef(function ComboboxClear(componentProps, forwardedRef) {
2222
- const {
2223
- render,
2224
- className,
2225
- disabled: disabledProp = false,
2226
- nativeButton = true,
2227
- keepMounted = false,
2228
- ...elementProps
2229
- } = componentProps;
2230
- const {
2231
- disabled: fieldDisabled
2232
- } = useFieldRootContext();
2233
- const store = useComboboxRootContext();
2234
- const selectionMode = useStore(store, selectors.selectionMode);
2235
- const comboboxDisabled = useStore(store, selectors.disabled);
2236
- const readOnly = useStore(store, selectors.readOnly);
2237
- const open = useStore(store, selectors.open);
2238
- const selectedValue = useStore(store, selectors.selectedValue);
2239
- const hasSelectionChips = useStore(store, selectors.hasSelectionChips);
2240
- const inputValue = useComboboxInputValueContext();
2241
- let visible = false;
2242
- if (selectionMode === 'none') {
2243
- visible = inputValue !== '';
2244
- } else if (selectionMode === 'single') {
2245
- visible = selectedValue != null;
2246
- } else {
2247
- visible = hasSelectionChips;
2248
- }
2249
- const disabled = fieldDisabled || comboboxDisabled || disabledProp;
2250
- const {
2251
- buttonRef,
2252
- getButtonProps
2253
- } = useButton({
2254
- native: nativeButton,
2255
- disabled
2256
- });
2257
- const {
2258
- mounted,
2259
- transitionStatus,
2260
- setMounted
2261
- } = useTransitionStatus(visible);
2262
- const state = {
2263
- disabled,
2264
- open,
2265
- transitionStatus
2266
- };
2267
- useOpenChangeComplete({
2268
- open: visible,
2269
- ref: store.state.clearRef,
2270
- onComplete() {
2271
- if (!visible) {
2272
- setMounted(false);
2273
- }
2274
- }
2275
- });
2276
- const element = useRenderElement('button', componentProps, {
2277
- state,
2278
- ref: [forwardedRef, buttonRef, store.state.clearRef],
2279
- props: [{
2280
- tabIndex: -1,
2281
- children: 'x',
2282
- // Avoid stealing focus from the input.
2283
- onMouseDown(event) {
2284
- event.preventDefault();
2285
- },
2286
- onClick(event) {
2287
- if (disabled || readOnly) {
2288
- return;
2289
- }
2290
- const keyboardActiveRef = store.state.keyboardActiveRef;
2291
- store.state.setInputValue('', createChangeEventDetails(clearPress, event.nativeEvent));
2292
- if (selectionMode !== 'none') {
2293
- store.state.setSelectedValue(Array.isArray(selectedValue) ? [] : null, createChangeEventDetails(clearPress, event.nativeEvent));
2294
- store.state.setIndices({
2295
- activeIndex: null,
2296
- selectedIndex: null,
2297
- type: keyboardActiveRef.current ? 'keyboard' : 'pointer'
2298
- });
2299
- } else {
2300
- store.state.setIndices({
2301
- activeIndex: null,
2302
- type: keyboardActiveRef.current ? 'keyboard' : 'pointer'
2303
- });
2304
- }
2305
- store.state.inputRef.current?.focus();
2306
- }
2307
- }, elementProps, getButtonProps],
2308
- stateAttributesMapping: stateAttributesMapping$8
2309
- });
2310
- const shouldRender = keepMounted || mounted;
2311
- if (!shouldRender) {
2312
- return null;
2313
- }
2314
- return element;
2315
- });
2316
- if (process.env.NODE_ENV !== "production") ComboboxClear.displayName = "ComboboxClear";
2317
-
2318
- const GroupCollectionContext = /*#__PURE__*/React.createContext(null);
2319
- if (process.env.NODE_ENV !== "production") GroupCollectionContext.displayName = "GroupCollectionContext";
2320
- function useGroupCollectionContext() {
2321
- return React.useContext(GroupCollectionContext);
2322
- }
2323
- function GroupCollectionProvider(props) {
2324
- const {
2325
- children,
2326
- items
2327
- } = props;
2328
- const contextValue = React.useMemo(() => ({
2329
- items
2330
- }), [items]);
2331
- return /*#__PURE__*/jsx(GroupCollectionContext.Provider, {
2332
- value: contextValue,
2333
- children: children
2334
- });
2335
- }
2336
-
2337
- function ComboboxCollection(props) {
2338
- const {
2339
- children
2340
- } = props;
2341
- const {
2342
- filteredItems
2343
- } = useComboboxDerivedItemsContext();
2344
- const groupContext = useGroupCollectionContext();
2345
- const itemsToRender = groupContext ? groupContext.items : filteredItems;
2346
- if (!itemsToRender) {
2347
- return null;
2348
- }
2349
- return /*#__PURE__*/jsx(React.Fragment, {
2350
- children: itemsToRender.map(children)
2351
- });
2352
- }
2353
-
2354
- const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(componentProps, forwardedRef) {
2355
- var _ComboboxCollection;
2356
- const {
2357
- render,
2358
- className,
2359
- children,
2360
- ...elementProps
2361
- } = componentProps;
2362
- const store = useComboboxRootContext();
2363
- const floatingRootContext = useComboboxFloatingContext();
2364
- const hasPositionerContext = Boolean(useComboboxPositionerContext(true));
2365
- const {
2366
- filteredItems
2367
- } = useComboboxDerivedItemsContext();
2368
- const items = useStore(store, selectors.items);
2369
- const labelsRef = useStore(store, selectors.labelsRef);
2370
- const listRef = useStore(store, selectors.listRef);
2371
- const selectionMode = useStore(store, selectors.selectionMode);
2372
- const grid = useStore(store, selectors.grid);
2373
- const popupProps = useStore(store, selectors.popupProps);
2374
- const disabled = useStore(store, selectors.disabled);
2375
- const readOnly = useStore(store, selectors.readOnly);
2376
- const virtualized = useStore(store, selectors.virtualized);
2377
- const multiple = selectionMode === 'multiple';
2378
- const empty = filteredItems.length === 0;
2379
- const setPositionerElement = useStableCallback(element => {
2380
- store.set('positionerElement', element);
2381
- });
2382
- const setListElement = useStableCallback(element => {
2383
- store.set('listElement', element);
2384
- });
2385
-
2386
- // Support "closed template" API: if children is a function, implicitly wrap it
2387
- // with a Combobox.Collection that reads items from context/root.
2388
- // Ensures this component's `popupProps` subscription does not cause <Combobox.Item>
2389
- // to re-render on every active index change.
2390
- const resolvedChildren = React.useMemo(() => {
2391
- if (typeof children === 'function') {
2392
- return _ComboboxCollection || (_ComboboxCollection = /*#__PURE__*/jsx(ComboboxCollection, {
2393
- children: children
2394
- }));
2395
- }
2396
- return children;
2397
- }, [children]);
2398
- const state = {
2399
- empty
2400
- };
2401
- const floatingId = floatingRootContext.useState('floatingId');
2402
- const element = useRenderElement('div', componentProps, {
2403
- state,
2404
- ref: [forwardedRef, setListElement, hasPositionerContext ? null : setPositionerElement],
2405
- props: [popupProps, {
2406
- children: resolvedChildren,
2407
- tabIndex: -1,
2408
- id: floatingId,
2409
- role: grid ? 'grid' : 'listbox',
2410
- 'aria-multiselectable': multiple ? 'true' : undefined,
2411
- onKeyDown(event) {
2412
- if (disabled || readOnly) {
2413
- return;
2414
- }
2415
- if (event.key === 'Enter') {
2416
- const activeIndex = store.state.activeIndex;
2417
- if (activeIndex == null) {
2418
- // Allow form submission when no item is highlighted.
2419
- return;
2420
- }
2421
- stopEvent(event);
2422
- const nativeEvent = event.nativeEvent;
2423
- const listItem = store.state.listRef.current[activeIndex];
2424
- if (listItem) {
2425
- store.state.selectionEventRef.current = nativeEvent;
2426
- listItem.click();
2427
- store.state.selectionEventRef.current = null;
2428
- }
2429
- }
2430
- },
2431
- onKeyDownCapture() {
2432
- store.state.keyboardActiveRef.current = true;
2433
- },
2434
- onPointerMoveCapture() {
2435
- store.state.keyboardActiveRef.current = false;
2436
- }
2437
- }, elementProps]
2438
- });
2439
- if (virtualized) {
2440
- return element;
2441
- }
2442
- return /*#__PURE__*/jsx(CompositeList, {
2443
- elementsRef: listRef,
2444
- labelsRef: items ? undefined : labelsRef,
2445
- children: element
2446
- });
2447
- });
2448
- if (process.env.NODE_ENV !== "production") ComboboxList.displayName = "ComboboxList";
2449
-
2450
- /**
2451
- * Displays a status message whose content changes are announced politely to screen readers.
2452
- * Useful for conveying the status of an asynchronously loaded list.
2453
- * Renders a `<div>` element.
2454
- */
2455
- const ComboboxStatus = /*#__PURE__*/React.forwardRef(function ComboboxStatus(componentProps, forwardedRef) {
2456
- const {
2457
- render,
2458
- className,
2459
- ...elementProps
2460
- } = componentProps;
2461
- return useRenderElement('div', componentProps, {
2462
- ref: forwardedRef,
2463
- props: [{
2464
- role: 'status',
2465
- 'aria-live': 'polite',
2466
- 'aria-atomic': true
2467
- }, elementProps]
2468
- });
2469
- });
2470
- if (process.env.NODE_ENV !== "production") ComboboxStatus.displayName = "ComboboxStatus";
2471
-
2472
- const ComboboxPortalContext = /*#__PURE__*/React.createContext(undefined);
2473
- if (process.env.NODE_ENV !== "production") ComboboxPortalContext.displayName = "ComboboxPortalContext";
2474
- function useComboboxPortalContext() {
2475
- const context = React.useContext(ComboboxPortalContext);
2476
- if (context === undefined) {
2477
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Combobox.Portal> is missing.' : formatErrorMessage(20));
2478
- }
2479
- return context;
2480
- }
2481
-
2482
- const ComboboxPortal = /*#__PURE__*/React.forwardRef(function ComboboxPortal(props, forwardedRef) {
2483
- const {
2484
- keepMounted = false,
2485
- ...portalProps
2486
- } = props;
2487
- const store = useComboboxRootContext();
2488
- const mounted = useStore(store, selectors.mounted);
2489
- const forceMounted = useStore(store, selectors.forceMounted);
2490
- const shouldRender = mounted || keepMounted || forceMounted;
2491
- if (!shouldRender) {
2492
- return null;
2493
- }
2494
- return /*#__PURE__*/jsx(ComboboxPortalContext.Provider, {
2495
- value: keepMounted,
2496
- children: /*#__PURE__*/jsx(FloatingPortal, {
2497
- ref: forwardedRef,
2498
- ...portalProps
2499
- })
2500
- });
2501
- });
2502
- if (process.env.NODE_ENV !== "production") ComboboxPortal.displayName = "ComboboxPortal";
2503
-
2504
- const stateAttributesMapping$7 = {
2505
- ...popupStateMapping,
2506
- ...transitionStatusMapping
2507
- };
2508
-
2509
- /**
2510
- * An overlay displayed beneath the popup.
2511
- * Renders a `<div>` element.
2512
- */
2513
- const ComboboxBackdrop = /*#__PURE__*/React.forwardRef(function ComboboxBackdrop(componentProps, forwardedRef) {
2514
- const {
2515
- className,
2516
- render,
2517
- ...elementProps
2518
- } = componentProps;
2519
- const store = useComboboxRootContext();
2520
- const open = useStore(store, selectors.open);
2521
- const mounted = useStore(store, selectors.mounted);
2522
- const transitionStatus = useStore(store, selectors.transitionStatus);
2523
- const state = {
2524
- open,
2525
- transitionStatus
2526
- };
2527
- return useRenderElement('div', componentProps, {
2528
- state,
2529
- ref: forwardedRef,
2530
- stateAttributesMapping: stateAttributesMapping$7,
2531
- props: [{
2532
- role: 'presentation',
2533
- hidden: !mounted,
2534
- style: {
2535
- userSelect: 'none',
2536
- WebkitUserSelect: 'none'
2537
- }
2538
- }, elementProps]
2539
- });
2540
- });
2541
- if (process.env.NODE_ENV !== "production") ComboboxBackdrop.displayName = "ComboboxBackdrop";
2542
-
2543
- const ComboboxPositioner = /*#__PURE__*/React.forwardRef(function ComboboxPositioner(componentProps, forwardedRef) {
2544
- const {
2545
- render,
2546
- className,
2547
- anchor,
2548
- positionMethod = 'absolute',
2549
- side = 'bottom',
2550
- align = 'center',
2551
- sideOffset = 0,
2552
- alignOffset = 0,
2553
- collisionBoundary = 'clipping-ancestors',
2554
- collisionPadding = 5,
2555
- arrowPadding = 5,
2556
- sticky = false,
2557
- disableAnchorTracking = false,
2558
- collisionAvoidance = DROPDOWN_COLLISION_AVOIDANCE,
2559
- ...elementProps
2560
- } = componentProps;
2561
- const store = useComboboxRootContext();
2562
- const {
2563
- filteredItems
2564
- } = useComboboxDerivedItemsContext();
2565
- const floatingRootContext = useComboboxFloatingContext();
2566
- const keepMounted = useComboboxPortalContext();
2567
- const modal = useStore(store, selectors.modal);
2568
- const open = useStore(store, selectors.open);
2569
- const mounted = useStore(store, selectors.mounted);
2570
- const openMethod = useStore(store, selectors.openMethod);
2571
- const triggerElement = useStore(store, selectors.triggerElement);
2572
- const inputElement = useStore(store, selectors.inputElement);
2573
- const inputGroupElement = useStore(store, selectors.inputGroupElement);
2574
- const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
2575
- const transitionStatus = useStore(store, selectors.transitionStatus);
2576
- const empty = filteredItems.length === 0;
2577
- const resolvedAnchor = anchor ?? (inputInsidePopup ? triggerElement : inputGroupElement ?? inputElement);
2578
- const positioning = useAnchorPositioning({
2579
- anchor: resolvedAnchor,
2580
- floatingRootContext,
2581
- positionMethod,
2582
- mounted,
2583
- side,
2584
- sideOffset,
2585
- align,
2586
- alignOffset,
2587
- arrowPadding,
2588
- collisionBoundary,
2589
- collisionPadding,
2590
- sticky,
2591
- disableAnchorTracking,
2592
- keepMounted,
2593
- collisionAvoidance,
2594
- lazyFlip: true
2595
- });
2596
- useScrollLock(open && modal && openMethod !== 'touch', triggerElement);
2597
- const defaultProps = React.useMemo(() => {
2598
- const style = {
2599
- ...positioning.positionerStyles
2600
- };
2601
- if (!open) {
2602
- style.pointerEvents = 'none';
2603
- }
2604
- return {
2605
- role: 'presentation',
2606
- hidden: !mounted,
2607
- style
2608
- };
2609
- }, [open, mounted, positioning.positionerStyles]);
2610
- const state = {
2611
- open,
2612
- side: positioning.side,
2613
- align: positioning.align,
2614
- anchorHidden: positioning.anchorHidden,
2615
- empty
2616
- };
2617
- useIsoLayoutEffect(() => {
2618
- store.set('popupSide', positioning.side);
2619
- }, [store, positioning.side]);
2620
- const contextValue = React.useMemo(() => ({
2621
- side: positioning.side,
2622
- align: positioning.align,
2623
- arrowRef: positioning.arrowRef,
2624
- arrowUncentered: positioning.arrowUncentered,
2625
- arrowStyles: positioning.arrowStyles,
2626
- anchorHidden: positioning.anchorHidden,
2627
- isPositioned: positioning.isPositioned
2628
- }), [positioning.side, positioning.align, positioning.arrowRef, positioning.arrowUncentered, positioning.arrowStyles, positioning.anchorHidden, positioning.isPositioned]);
2629
- const setPositionerElement = useStableCallback(element => {
2630
- store.set('positionerElement', element);
2631
- });
2632
- const element = useRenderElement('div', componentProps, {
2633
- state,
2634
- ref: [forwardedRef, setPositionerElement],
2635
- props: [defaultProps, getDisabledMountTransitionStyles(transitionStatus), elementProps],
2636
- stateAttributesMapping: popupStateMapping
2637
- });
2638
- return /*#__PURE__*/jsxs(ComboboxPositionerContext.Provider, {
2639
- value: contextValue,
2640
- children: [mounted && modal && /*#__PURE__*/jsx(InternalBackdrop, {
2641
- inert: inertValue(!open),
2642
- cutout: inputGroupElement ?? inputElement ?? triggerElement
2643
- }), element]
2644
- });
2645
- });
2646
- if (process.env.NODE_ENV !== "production") ComboboxPositioner.displayName = "ComboboxPositioner";
2647
-
2648
- const stateAttributesMapping$6 = {
2649
- ...popupStateMapping,
2650
- ...transitionStatusMapping
2651
- };
2652
-
2653
- /**
2654
- * A container for the list.
2655
- * Renders a `<div>` element.
2656
- */
2657
- const ComboboxPopup = /*#__PURE__*/React.forwardRef(function ComboboxPopup(componentProps, forwardedRef) {
2658
- const {
2659
- render,
2660
- className,
2661
- initialFocus,
2662
- finalFocus,
2663
- ...elementProps
2664
- } = componentProps;
2665
- const store = useComboboxRootContext();
2666
- const positioning = useComboboxPositionerContext();
2667
- const floatingRootContext = useComboboxFloatingContext();
2668
- const {
2669
- filteredItems
2670
- } = useComboboxDerivedItemsContext();
2671
- const mounted = useStore(store, selectors.mounted);
2672
- const open = useStore(store, selectors.open);
2673
- const openMethod = useStore(store, selectors.openMethod);
2674
- const transitionStatus = useStore(store, selectors.transitionStatus);
2675
- const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
2676
- const inputElement = useStore(store, selectors.inputElement);
2677
- const modal = useStore(store, selectors.modal);
2678
- const empty = filteredItems.length === 0;
2679
- useOpenChangeComplete({
2680
- open,
2681
- ref: store.state.popupRef,
2682
- onComplete() {
2683
- if (open) {
2684
- store.state.onOpenChangeComplete(true);
2685
- }
2686
- }
2687
- });
2688
- const state = {
2689
- open,
2690
- side: positioning.side,
2691
- align: positioning.align,
2692
- anchorHidden: positioning.anchorHidden,
2693
- transitionStatus,
2694
- empty
2695
- };
2696
- const element = useRenderElement('div', componentProps, {
2697
- state,
2698
- ref: [forwardedRef, store.state.popupRef],
2699
- props: [{
2700
- role: inputInsidePopup ? 'dialog' : 'presentation',
2701
- tabIndex: -1,
2702
- onFocus(event) {
2703
- const target = getTarget(event.nativeEvent);
2704
- if (openMethod !== 'touch' && (contains(store.state.listElement, target) || target === event.currentTarget)) {
2705
- store.state.inputRef.current?.focus();
2706
- }
2707
- }
2708
- }, getDisabledMountTransitionStyles(transitionStatus), elementProps],
2709
- stateAttributesMapping: stateAttributesMapping$6
2710
- });
2711
-
2712
- // Default initial focus logic:
2713
- // If opened by touch, focus the popup element to prevent the virtual keyboard from opening
2714
- // (this is required for Android specifically as iOS handles this automatically).
2715
- const computedDefaultInitialFocus = inputInsidePopup ? interactionType => interactionType === 'touch' ? store.state.popupRef.current : inputElement : false;
2716
- const resolvedInitialFocus = initialFocus === undefined ? computedDefaultInitialFocus : initialFocus;
2717
- let resolvedFinalFocus;
2718
- if (finalFocus != null) {
2719
- resolvedFinalFocus = finalFocus;
2720
- } else {
2721
- resolvedFinalFocus = inputInsidePopup ? undefined : false;
2722
- }
2723
- const focusManagerModal = !inputInsidePopup || modal;
2724
- return /*#__PURE__*/jsx(FloatingFocusManager, {
2725
- context: floatingRootContext,
2726
- disabled: !mounted,
2727
- modal: focusManagerModal,
2728
- openInteractionType: openMethod,
2729
- initialFocus: resolvedInitialFocus,
2730
- returnFocus: resolvedFinalFocus,
2731
- getInsideElements: () => [store.state.startDismissRef.current, store.state.endDismissRef.current],
2732
- children: /*#__PURE__*/jsxs(React.Fragment, {
2733
- children: [element, focusManagerModal && /*#__PURE__*/jsx(ComboboxInternalDismissButton, {
2734
- ref: store.state.endDismissRef
2735
- })]
2736
- })
2737
- });
2738
- });
2739
- if (process.env.NODE_ENV !== "production") ComboboxPopup.displayName = "ComboboxPopup";
2740
-
2741
- /**
2742
- * Displays an element positioned against the anchor.
2743
- * Renders a `<div>` element.
2744
- */
2745
- const ComboboxArrow = /*#__PURE__*/React.forwardRef(function ComboboxArrow(componentProps, forwardedRef) {
2746
- const {
2747
- className,
2748
- render,
2749
- ...elementProps
2750
- } = componentProps;
2751
- const store = useComboboxRootContext();
2752
- const {
2753
- arrowRef,
2754
- side,
2755
- align,
2756
- arrowUncentered,
2757
- arrowStyles
2758
- } = useComboboxPositionerContext();
2759
- const open = useStore(store, selectors.open);
2760
- const state = {
2761
- open,
2762
- side,
2763
- align,
2764
- uncentered: arrowUncentered
2765
- };
2766
- return useRenderElement('div', componentProps, {
2767
- ref: [arrowRef, forwardedRef],
2768
- stateAttributesMapping: popupStateMapping,
2769
- state,
2770
- props: {
2771
- style: arrowStyles,
2772
- 'aria-hidden': true,
2773
- ...elementProps
2774
- }
2775
- });
2776
- });
2777
- if (process.env.NODE_ENV !== "production") ComboboxArrow.displayName = "ComboboxArrow";
2778
-
2779
- const ComboboxGroupContext = /*#__PURE__*/React.createContext(undefined);
2780
- if (process.env.NODE_ENV !== "production") ComboboxGroupContext.displayName = "ComboboxGroupContext";
2781
- function useComboboxGroupContext() {
2782
- const context = React.useContext(ComboboxGroupContext);
2783
- if (context === undefined) {
2784
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: ComboboxGroupContext is missing. ComboboxGroup parts must be placed within <Combobox.Group>.' : formatErrorMessage(18));
2785
- }
2786
- return context;
2787
- }
2788
-
2789
- const ComboboxGroup = /*#__PURE__*/React.forwardRef(function ComboboxGroup(componentProps, forwardedRef) {
2790
- const {
2791
- render,
2792
- className,
2793
- items,
2794
- ...elementProps
2795
- } = componentProps;
2796
- const [labelId, setLabelId] = React.useState();
2797
- const contextValue = React.useMemo(() => ({
2798
- labelId,
2799
- setLabelId,
2800
- items
2801
- }), [labelId, setLabelId, items]);
2802
- const element = useRenderElement('div', componentProps, {
2803
- ref: forwardedRef,
2804
- props: [{
2805
- role: 'group',
2806
- 'aria-labelledby': labelId
2807
- }, elementProps]
2808
- });
2809
- const wrappedElement = /*#__PURE__*/jsx(ComboboxGroupContext.Provider, {
2810
- value: contextValue,
2811
- children: element
2812
- });
2813
- if (items) {
2814
- return /*#__PURE__*/jsx(GroupCollectionProvider, {
2815
- items: items,
2816
- children: wrappedElement
2817
- });
2818
- }
2819
- return wrappedElement;
2820
- });
2821
- if (process.env.NODE_ENV !== "production") ComboboxGroup.displayName = "ComboboxGroup";
2822
-
2823
- /**
2824
- * An accessible label that is automatically associated with its parent group.
2825
- * Renders a `<div>` element.
2826
- */
2827
- const ComboboxGroupLabel = /*#__PURE__*/React.forwardRef(function ComboboxGroupLabel(componentProps, forwardedRef) {
2828
- const {
2829
- render,
2830
- className,
2831
- id: idProp,
2832
- ...elementProps
2833
- } = componentProps;
2834
- const {
2835
- setLabelId
2836
- } = useComboboxGroupContext();
2837
- const id = useBaseUiId(idProp);
2838
- useIsoLayoutEffect(() => {
2839
- setLabelId(id);
2840
- return () => {
2841
- setLabelId(undefined);
2842
- };
2843
- }, [id, setLabelId]);
2844
- const element = useRenderElement('div', componentProps, {
2845
- ref: forwardedRef,
2846
- props: [{
2847
- id
2848
- }, elementProps]
2849
- });
2850
- return element;
2851
- });
2852
- if (process.env.NODE_ENV !== "production") ComboboxGroupLabel.displayName = "ComboboxGroupLabel";
2853
-
2854
- const ComboboxItemContext = /*#__PURE__*/React.createContext(undefined);
2855
- if (process.env.NODE_ENV !== "production") ComboboxItemContext.displayName = "ComboboxItemContext";
2856
- function useComboboxItemContext() {
2857
- const context = React.useContext(ComboboxItemContext);
2858
- if (!context) {
2859
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: ComboboxItemContext is missing. ComboboxItem parts must be placed within <Combobox.Item>.' : formatErrorMessage(19));
2860
- }
2861
- return context;
2862
- }
2863
-
2864
- const ComboboxRowContext = /*#__PURE__*/React.createContext(false);
2865
- if (process.env.NODE_ENV !== "production") ComboboxRowContext.displayName = "ComboboxRowContext";
2866
- function useComboboxRowContext() {
2867
- return React.useContext(ComboboxRowContext);
2868
- }
2869
-
2870
- const ComboboxItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(function ComboboxItem(componentProps, forwardedRef) {
2871
- const {
2872
- render,
2873
- className,
2874
- value: itemValue = null,
2875
- index: indexProp,
2876
- disabled = false,
2877
- nativeButton = false,
2878
- ...elementProps
2879
- } = componentProps;
2880
- const didPointerDownRef = React.useRef(false);
2881
- const textRef = React.useRef(null);
2882
- const listItem = useCompositeListItem({
2883
- index: indexProp,
2884
- textRef,
2885
- indexGuessBehavior: IndexGuessBehavior.GuessFromOrder
2886
- });
2887
- const store = useComboboxRootContext();
2888
- const isRow = useComboboxRowContext();
2889
- const {
2890
- flatFilteredItems,
2891
- hasItems
2892
- } = useComboboxDerivedItemsContext();
2893
- const open = useStore(store, selectors.open);
2894
- const selectionMode = useStore(store, selectors.selectionMode);
2895
- const readOnly = useStore(store, selectors.readOnly);
2896
- const virtualized = useStore(store, selectors.virtualized);
2897
- const isItemEqualToValue = useStore(store, selectors.isItemEqualToValue);
2898
- const selectable = selectionMode !== 'none';
2899
- const index = indexProp ?? (virtualized ? findItemIndex(flatFilteredItems, itemValue, isItemEqualToValue) : listItem.index);
2900
- const hasRegistered = listItem.index !== -1;
2901
- const rootId = useStore(store, selectors.id);
2902
- const highlighted = useStore(store, selectors.isActive, index);
2903
- const matchesSelectedValue = useStore(store, selectors.isSelected, itemValue);
2904
- const getItemProps = useStore(store, selectors.getItemProps);
2905
- const itemRef = React.useRef(null);
2906
- const id = rootId != null && hasRegistered ? `${rootId}-${index}` : undefined;
2907
- const selected = matchesSelectedValue && selectable;
2908
- useIsoLayoutEffect(() => {
2909
- const shouldRun = hasRegistered && (virtualized || indexProp != null);
2910
- if (!shouldRun) {
2911
- return undefined;
2912
- }
2913
- const list = store.state.listRef.current;
2914
- list[index] = itemRef.current;
2915
- return () => {
2916
- delete list[index];
2917
- };
2918
- }, [hasRegistered, virtualized, index, indexProp, store]);
2919
- useIsoLayoutEffect(() => {
2920
- if (!hasRegistered || hasItems) {
2921
- return undefined;
2922
- }
2923
- const visibleMap = store.state.valuesRef.current;
2924
- visibleMap[index] = itemValue;
2925
-
2926
- // Stable registry that doesn't depend on filtering. Assume that no
2927
- // filtering had occurred at this point; otherwise, an `items` prop is
2928
- // required.
2929
- if (selectionMode !== 'none') {
2930
- store.state.allValuesRef.current.push(itemValue);
2931
- }
2932
- return () => {
2933
- delete visibleMap[index];
2934
- };
2935
- }, [hasRegistered, hasItems, index, itemValue, store, selectionMode]);
2936
- useIsoLayoutEffect(() => {
2937
- if (!open) {
2938
- didPointerDownRef.current = false;
2939
- return;
2940
- }
2941
- if (!hasRegistered || hasItems) {
2942
- return;
2943
- }
2944
- const selectedValue = store.state.selectedValue;
2945
- const lastSelectedValue = Array.isArray(selectedValue) ? selectedValue[selectedValue.length - 1] : selectedValue;
2946
- if (compareItemEquality(itemValue, lastSelectedValue, isItemEqualToValue)) {
2947
- store.set('selectedIndex', index);
2948
- }
2949
- }, [hasRegistered, hasItems, open, store, index, itemValue, isItemEqualToValue]);
2950
- const state = {
2951
- disabled,
2952
- selected,
2953
- highlighted
2954
- };
2955
- const rootProps = getItemProps({
2956
- active: highlighted,
2957
- selected
2958
- });
2959
- rootProps.id = undefined;
2960
- rootProps.onFocus = undefined;
2961
- const {
2962
- getButtonProps,
2963
- buttonRef
2964
- } = useButton({
2965
- disabled,
2966
- focusableWhenDisabled: true,
2967
- native: nativeButton,
2968
- composite: true
2969
- });
2970
- function commitSelection(nativeEvent) {
2971
- function selectItem() {
2972
- store.state.handleSelection(nativeEvent, itemValue);
2973
- }
2974
- if (store.state.submitOnItemClick) {
2975
- ReactDOM.flushSync(selectItem);
2976
- store.state.requestSubmit();
2977
- } else {
2978
- selectItem();
79
+ }, [baseFilter, mode, resolvedQuery, staticItems]);
80
+ const handleItemHighlighted = useStableCallback((highlightedValue, eventDetails) => {
81
+ props.onItemHighlighted?.(highlightedValue, eventDetails);
82
+ if (eventDetails.reason === pointer) {
83
+ return;
2979
84
  }
2980
- }
2981
- const defaultProps = {
2982
- id,
2983
- role: isRow ? 'gridcell' : 'option',
2984
- 'aria-selected': selectable ? selected : undefined,
2985
- // Focusable items steal focus from the input upon mouseup.
2986
- // Warn if the user renders a natively focusable element like `<button>`,
2987
- // as it should be a `<div>` instead.
2988
- tabIndex: undefined,
2989
- onPointerDownCapture(event) {
2990
- didPointerDownRef.current = true;
2991
- event.preventDefault();
2992
- },
2993
- onClick(event) {
2994
- if (disabled || readOnly) {
2995
- return;
2996
- }
2997
- commitSelection(event.nativeEvent);
2998
- },
2999
- onMouseUp(event) {
3000
- const pointerStartedOnItem = didPointerDownRef.current;
3001
- didPointerDownRef.current = false;
3002
- if (disabled || readOnly || event.button !== 0 || pointerStartedOnItem || !highlighted) {
3003
- return;
85
+ if (enableInline) {
86
+ if (highlightedValue == null) {
87
+ setInlineInputValue('');
88
+ } else {
89
+ setInlineInputValue(stringifyAsLabel(highlightedValue, itemToStringValue));
3004
90
  }
3005
- commitSelection(event.nativeEvent);
91
+ } else {
92
+ setInlineInputValue('');
3006
93
  }
3007
- };
3008
- const element = useRenderElement('div', componentProps, {
3009
- ref: [buttonRef, forwardedRef, listItem.ref, itemRef],
3010
- state,
3011
- props: [rootProps, defaultProps, elementProps, getButtonProps]
3012
- });
3013
- const contextValue = React.useMemo(() => ({
3014
- selected,
3015
- textRef
3016
- }), [selected, textRef]);
3017
- return /*#__PURE__*/jsx(ComboboxItemContext.Provider, {
3018
- value: contextValue,
3019
- children: element
3020
- });
3021
- }));
3022
- if (process.env.NODE_ENV !== "production") ComboboxItem.displayName = "ComboboxItem";
3023
-
3024
- const ComboboxRow = /*#__PURE__*/React.forwardRef(function ComboboxRow(componentProps, forwardedRef) {
3025
- const {
3026
- render,
3027
- className,
3028
- ...elementProps
3029
- } = componentProps;
3030
- const element = useRenderElement('div', componentProps, {
3031
- ref: forwardedRef,
3032
- props: [{
3033
- role: 'row'
3034
- }, elementProps]
3035
94
  });
3036
- return /*#__PURE__*/jsx(ComboboxRowContext.Provider, {
3037
- value: true,
3038
- children: element
95
+ return /*#__PURE__*/jsx(AriaCombobox, {
96
+ ...other,
97
+ itemToStringLabel: itemToStringValue,
98
+ openOnInputClick: openOnInputClick,
99
+ selectionMode: "none",
100
+ fillInputOnItemPress: true,
101
+ filter: resolvedFilter,
102
+ autoComplete: mode,
103
+ inputValue: resolvedInputValue,
104
+ defaultInputValue: defaultValue,
105
+ onInputValueChange: handleValueChange,
106
+ onItemHighlighted: handleItemHighlighted
3039
107
  });
3040
- });
3041
- if (process.env.NODE_ENV !== "production") ComboboxRow.displayName = "ComboboxRow";
108
+ }
3042
109
 
3043
- /**
3044
- * Renders its children only when the list is empty.
3045
- * Requires the `items` prop on the root component.
3046
- * Announces changes politely to screen readers.
3047
- * Renders a `<div>` element.
3048
- */
3049
- const ComboboxEmpty = /*#__PURE__*/React.forwardRef(function ComboboxEmpty(componentProps, forwardedRef) {
3050
- const {
3051
- render,
3052
- className,
3053
- children: childrenProp,
3054
- ...elementProps
3055
- } = componentProps;
110
+ function AutocompleteValue(props) {
3056
111
  const {
3057
- filteredItems
3058
- } = useComboboxDerivedItemsContext();
3059
- const store = useComboboxRootContext();
3060
- const children = filteredItems.length === 0 ? childrenProp : null;
3061
- return useRenderElement('div', componentProps, {
3062
- ref: [forwardedRef, store.state.emptyRef],
3063
- props: [{
3064
- children,
3065
- role: 'status',
3066
- 'aria-live': 'polite',
3067
- 'aria-atomic': true
3068
- }, elementProps]
112
+ children
113
+ } = props;
114
+ const inputValue = useComboboxInputValueContext();
115
+ let returnValue = null;
116
+ if (typeof children === 'function') {
117
+ returnValue = children(String(inputValue));
118
+ } else if (children != null) {
119
+ returnValue = children;
120
+ } else {
121
+ returnValue = inputValue;
122
+ }
123
+ return /*#__PURE__*/jsx(React.Fragment, {
124
+ children: returnValue
3069
125
  });
3070
- });
3071
- if (process.env.NODE_ENV !== "production") ComboboxEmpty.displayName = "ComboboxEmpty";
3072
-
3073
- /**
3074
- * Returns the internally filtered items.
3075
- */
3076
- function useFilteredItems() {
3077
- const items = useComboboxDerivedItemsContext();
3078
- return items.filteredItems;
3079
126
  }
3080
127
 
3081
128
  var index_parts$5 = /*#__PURE__*/Object.freeze({
@@ -7018,404 +4065,6 @@ var index_parts$2 = /*#__PURE__*/Object.freeze({
7018
4065
  Viewport: ScrollAreaViewport
7019
4066
  });
7020
4067
 
7021
- function ComboboxRoot(props) {
7022
- const {
7023
- multiple = false,
7024
- defaultValue,
7025
- value,
7026
- onValueChange,
7027
- autoComplete,
7028
- ...other
7029
- } = props;
7030
- return /*#__PURE__*/jsx(AriaCombobox, {
7031
- ...other,
7032
- selectionMode: multiple ? 'multiple' : 'single',
7033
- selectedValue: value,
7034
- defaultSelectedValue: defaultValue,
7035
- onSelectedValueChange: onValueChange,
7036
- formAutoComplete: autoComplete
7037
- });
7038
- }
7039
-
7040
- /**
7041
- * An accessible label that is automatically associated with the combobox trigger.
7042
- * Renders a `<div>` element.
7043
- *
7044
- * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
7045
- */
7046
- const ComboboxLabel = /*#__PURE__*/React.forwardRef(function ComboboxLabel(componentProps, forwardedRef) {
7047
- const {
7048
- render,
7049
- className,
7050
- ...elementProps
7051
- } = componentProps;
7052
- // Keep label id derived from the root and ignore runtime `id` overrides from untyped consumers.
7053
- const elementPropsWithoutId = elementProps;
7054
- delete elementPropsWithoutId.id;
7055
- const fieldRootContext = useFieldRootContext();
7056
- const store = useComboboxRootContext();
7057
- const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
7058
- const triggerElement = useStore(store, selectors.triggerElement);
7059
- const inputElement = useStore(store, selectors.inputElement);
7060
- const rootId = useStore(store, selectors.id);
7061
- const defaultLabelId = getDefaultLabelId(rootId);
7062
- const localControlId = triggerElement?.id ?? (inputInsidePopup ? rootId : undefined);
7063
- if (process.env.NODE_ENV !== 'production') {
7064
- // eslint-disable-next-line react-hooks/rules-of-hooks
7065
- React.useEffect(() => {
7066
- if (!inputElement || inputInsidePopup) {
7067
- return;
7068
- }
7069
- const ownerStackMessage = SafeReact.captureOwnerStack?.() || '';
7070
- const message = '<Combobox.Label> labels <Combobox.Trigger> only. ' + 'When <Combobox.Input> is the form control, use a native <label> or <Field.Label> instead.';
7071
- error(`${message}${ownerStackMessage}`);
7072
- }, [inputElement, inputInsidePopup]);
7073
- }
7074
- const labelProps = useLabel({
7075
- id: defaultLabelId,
7076
- fallbackControlId: localControlId,
7077
- setLabelId(nextLabelId) {
7078
- store.set('labelId', nextLabelId);
7079
- }
7080
- });
7081
- return useRenderElement('div', componentProps, {
7082
- ref: forwardedRef,
7083
- state: fieldRootContext.state,
7084
- props: [labelProps, elementProps],
7085
- stateAttributesMapping: fieldValidityMapping
7086
- });
7087
- });
7088
- if (process.env.NODE_ENV !== "production") ComboboxLabel.displayName = "ComboboxLabel";
7089
-
7090
- function ComboboxValue(props) {
7091
- const {
7092
- children: childrenProp,
7093
- placeholder
7094
- } = props;
7095
- const store = useComboboxRootContext();
7096
- const itemToStringLabel = useStore(store, selectors.itemToStringLabel);
7097
- const selectedValue = useStore(store, selectors.selectedValue);
7098
- const items = useStore(store, selectors.items);
7099
- const multiple = useStore(store, selectors.selectionMode) === 'multiple';
7100
- const hasSelectedValue = useStore(store, selectors.hasSelectedValue);
7101
- const shouldCheckNullItemLabel = !hasSelectedValue && placeholder != null && childrenProp == null;
7102
- const hasNullLabel = useStore(store, selectors.hasNullItemLabel, shouldCheckNullItemLabel);
7103
- let children = null;
7104
- if (typeof childrenProp === 'function') {
7105
- children = childrenProp(selectedValue);
7106
- } else if (childrenProp != null) {
7107
- children = childrenProp;
7108
- } else if (!hasSelectedValue && placeholder != null && !hasNullLabel) {
7109
- children = placeholder;
7110
- } else if (multiple && Array.isArray(selectedValue)) {
7111
- children = resolveMultipleLabels(selectedValue, items, itemToStringLabel);
7112
- } else {
7113
- children = resolveSelectedLabel(selectedValue, items, itemToStringLabel);
7114
- }
7115
- return /*#__PURE__*/jsx(React.Fragment, {
7116
- children: children
7117
- });
7118
- }
7119
-
7120
- const ComboboxItemIndicator = /*#__PURE__*/React.forwardRef(function ComboboxItemIndicator(componentProps, forwardedRef) {
7121
- const keepMounted = componentProps.keepMounted ?? false;
7122
- const {
7123
- selected
7124
- } = useComboboxItemContext();
7125
- const shouldRender = keepMounted || selected;
7126
- if (!shouldRender) {
7127
- return null;
7128
- }
7129
-
7130
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
7131
- return /*#__PURE__*/jsx(Inner, {
7132
- ...componentProps,
7133
- ref: forwardedRef
7134
- });
7135
- });
7136
-
7137
- /** The core implementation of the indicator is split here to avoid paying the hooks
7138
- * costs unless the element needs to be mounted. */
7139
- if (process.env.NODE_ENV !== "production") ComboboxItemIndicator.displayName = "ComboboxItemIndicator";
7140
- const Inner = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef((componentProps, forwardedRef) => {
7141
- const {
7142
- render,
7143
- className,
7144
- keepMounted,
7145
- ...elementProps
7146
- } = componentProps;
7147
- const {
7148
- selected
7149
- } = useComboboxItemContext();
7150
- const indicatorRef = React.useRef(null);
7151
- const {
7152
- transitionStatus,
7153
- setMounted
7154
- } = useTransitionStatus(selected);
7155
- const state = {
7156
- selected,
7157
- transitionStatus
7158
- };
7159
- const element = useRenderElement('span', componentProps, {
7160
- ref: [forwardedRef, indicatorRef],
7161
- state,
7162
- props: [{
7163
- 'aria-hidden': true,
7164
- children: '✔️'
7165
- }, elementProps],
7166
- stateAttributesMapping: transitionStatusMapping
7167
- });
7168
- useOpenChangeComplete({
7169
- open: selected,
7170
- ref: indicatorRef,
7171
- onComplete() {
7172
- if (!selected) {
7173
- setMounted(false);
7174
- }
7175
- }
7176
- });
7177
- return element;
7178
- }));
7179
- if (process.env.NODE_ENV !== "production") Inner.displayName = "Inner";
7180
-
7181
- const ComboboxChips = /*#__PURE__*/React.forwardRef(function ComboboxChips(componentProps, forwardedRef) {
7182
- const {
7183
- render,
7184
- className,
7185
- ...elementProps
7186
- } = componentProps;
7187
- const store = useComboboxRootContext();
7188
- const open = useStore(store, selectors.open);
7189
- const hasSelectionChips = useStore(store, selectors.hasSelectionChips);
7190
- const [highlightedChipIndex, setHighlightedChipIndex] = React.useState(undefined);
7191
- if (open && highlightedChipIndex !== undefined) {
7192
- setHighlightedChipIndex(undefined);
7193
- }
7194
- const chipsRef = React.useRef([]);
7195
- const element = useRenderElement('div', componentProps, {
7196
- ref: [forwardedRef, store.state.chipsContainerRef],
7197
- // NVDA enters browse mode instead of staying in focus mode when navigating with
7198
- // arrow keys inside a container unless it has a toolbar role.
7199
- props: [hasSelectionChips ? {
7200
- role: 'toolbar'
7201
- } : EMPTY_OBJECT, elementProps]
7202
- });
7203
- const contextValue = React.useMemo(() => ({
7204
- highlightedChipIndex,
7205
- setHighlightedChipIndex,
7206
- chipsRef
7207
- }), [highlightedChipIndex, setHighlightedChipIndex, chipsRef]);
7208
- return /*#__PURE__*/jsx(ComboboxChipsContext.Provider, {
7209
- value: contextValue,
7210
- children: /*#__PURE__*/jsx(CompositeList, {
7211
- elementsRef: chipsRef,
7212
- children: element
7213
- })
7214
- });
7215
- });
7216
- if (process.env.NODE_ENV !== "production") ComboboxChips.displayName = "ComboboxChips";
7217
-
7218
- const ComboboxChipContext = /*#__PURE__*/React.createContext(undefined);
7219
- if (process.env.NODE_ENV !== "production") ComboboxChipContext.displayName = "ComboboxChipContext";
7220
- function useComboboxChipContext() {
7221
- const context = React.useContext(ComboboxChipContext);
7222
- if (!context) {
7223
- throw new Error(process.env.NODE_ENV !== "production" ? 'useComboboxChipContext must be used within a ComboboxChip' : formatErrorMessage(17));
7224
- }
7225
- return context;
7226
- }
7227
-
7228
- const ComboboxChip = /*#__PURE__*/React.forwardRef(function ComboboxChip(componentProps, forwardedRef) {
7229
- const {
7230
- render,
7231
- className,
7232
- ...elementProps
7233
- } = componentProps;
7234
- const store = useComboboxRootContext();
7235
- const {
7236
- setHighlightedChipIndex,
7237
- chipsRef
7238
- } = useComboboxChipsContext();
7239
- const disabled = useStore(store, selectors.disabled);
7240
- const readOnly = useStore(store, selectors.readOnly);
7241
- const selectedValue = useStore(store, selectors.selectedValue);
7242
- const {
7243
- ref,
7244
- index
7245
- } = useCompositeListItem();
7246
- function handleKeyDown(event) {
7247
- let nextIndex = index;
7248
- if (event.key === 'ArrowLeft') {
7249
- event.preventDefault();
7250
- if (index > 0) {
7251
- nextIndex = index - 1;
7252
- } else {
7253
- nextIndex = undefined;
7254
- }
7255
- } else if (event.key === 'ArrowRight') {
7256
- event.preventDefault();
7257
- if (index < selectedValue.length - 1) {
7258
- nextIndex = index + 1;
7259
- } else {
7260
- nextIndex = undefined;
7261
- }
7262
- } else if (event.key === 'Backspace' || event.key === 'Delete') {
7263
- const computedNextIndex = index >= selectedValue.length - 1 ? selectedValue.length - 2 : index;
7264
- nextIndex = computedNextIndex >= 0 ? computedNextIndex : undefined;
7265
- stopEvent(event);
7266
- store.state.setIndices({
7267
- activeIndex: null,
7268
- selectedIndex: null,
7269
- type: 'keyboard'
7270
- });
7271
- store.state.setSelectedValue(selectedValue.filter((_, i) => i !== index), createChangeEventDetails(none, event.nativeEvent));
7272
- } else if (event.key === 'Enter' || event.key === ' ') {
7273
- stopEvent(event);
7274
- nextIndex = undefined;
7275
- } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
7276
- stopEvent(event);
7277
- store.state.setOpen(true, createChangeEventDetails(listNavigation, event.nativeEvent));
7278
- nextIndex = undefined;
7279
- } else if (
7280
- // Check for printable characters (letters, numbers, symbols)
7281
- event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
7282
- nextIndex = undefined;
7283
- }
7284
- return nextIndex;
7285
- }
7286
- const state = {
7287
- disabled
7288
- };
7289
- const element = useRenderElement('div', componentProps, {
7290
- ref: [forwardedRef, ref],
7291
- state,
7292
- props: [{
7293
- tabIndex: -1,
7294
- 'aria-disabled': disabled || undefined,
7295
- 'aria-readonly': readOnly || undefined,
7296
- onKeyDown(event) {
7297
- if (disabled || readOnly) {
7298
- return;
7299
- }
7300
- const nextIndex = handleKeyDown(event);
7301
- ReactDOM.flushSync(() => {
7302
- setHighlightedChipIndex(nextIndex);
7303
- });
7304
- if (nextIndex === undefined) {
7305
- store.state.inputRef.current?.focus();
7306
- } else {
7307
- chipsRef.current[nextIndex]?.focus();
7308
- }
7309
- },
7310
- onMouseDown(event) {
7311
- if (readOnly) {
7312
- return;
7313
- }
7314
- event.preventDefault();
7315
- if (disabled) {
7316
- return;
7317
- }
7318
- store.state.inputRef.current?.focus();
7319
- }
7320
- }, elementProps]
7321
- });
7322
- const contextValue = React.useMemo(() => ({
7323
- index
7324
- }), [index]);
7325
- return /*#__PURE__*/jsx(ComboboxChipContext.Provider, {
7326
- value: contextValue,
7327
- children: element
7328
- });
7329
- });
7330
- if (process.env.NODE_ENV !== "production") ComboboxChip.displayName = "ComboboxChip";
7331
-
7332
- /**
7333
- * A button to remove a chip.
7334
- * Renders a `<button>` element.
7335
- */
7336
- const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRemove(componentProps, forwardedRef) {
7337
- const {
7338
- render,
7339
- className,
7340
- disabled: disabledProp = false,
7341
- nativeButton = true,
7342
- ...elementProps
7343
- } = componentProps;
7344
- const store = useComboboxRootContext();
7345
- const {
7346
- index
7347
- } = useComboboxChipContext();
7348
- const comboboxDisabled = useStore(store, selectors.disabled);
7349
- const readOnly = useStore(store, selectors.readOnly);
7350
- const selectedValue = useStore(store, selectors.selectedValue);
7351
- const isItemEqualToValue = useStore(store, selectors.isItemEqualToValue);
7352
- const disabled = comboboxDisabled || disabledProp;
7353
- const {
7354
- buttonRef,
7355
- getButtonProps
7356
- } = useButton({
7357
- native: nativeButton,
7358
- disabled: disabled || readOnly,
7359
- focusableWhenDisabled: true
7360
- });
7361
- const state = {
7362
- disabled
7363
- };
7364
- function clearActiveIndexForRemovedItem(removedItem) {
7365
- const activeIndex = store.state.activeIndex;
7366
- if (activeIndex == null) {
7367
- return;
7368
- }
7369
-
7370
- // Try current visible list first; if not found, it's filtered out.
7371
- // No need to clear highlight in that case since it can't equal activeIndex.
7372
- const removedIndex = findItemIndex(store.state.valuesRef.current, removedItem, isItemEqualToValue);
7373
- if (removedIndex !== -1 && activeIndex === removedIndex) {
7374
- store.state.setIndices({
7375
- activeIndex: null,
7376
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
7377
- });
7378
- }
7379
- }
7380
- function removeChip(event) {
7381
- const eventDetails = createChangeEventDetails(chipRemovePress, event.nativeEvent);
7382
- const removedItem = selectedValue[index];
7383
- clearActiveIndexForRemovedItem(removedItem);
7384
- store.state.setSelectedValue(selectedValue.filter((_, i) => i !== index), eventDetails);
7385
- store.state.inputRef.current?.focus();
7386
- return eventDetails;
7387
- }
7388
- const element = useRenderElement('button', componentProps, {
7389
- ref: [forwardedRef, buttonRef],
7390
- state,
7391
- props: [{
7392
- tabIndex: -1,
7393
- onClick(event) {
7394
- if (disabled || readOnly) {
7395
- return;
7396
- }
7397
- const eventDetails = removeChip(event);
7398
- if (!eventDetails.isPropagationAllowed) {
7399
- event.stopPropagation();
7400
- }
7401
- },
7402
- onKeyDown(event) {
7403
- if (disabled || readOnly) {
7404
- return;
7405
- }
7406
- if (event.key === 'Enter' || event.key === ' ') {
7407
- const eventDetails = removeChip(event);
7408
- if (!eventDetails.isPropagationAllowed) {
7409
- stopEvent(event);
7410
- }
7411
- }
7412
- }
7413
- }, elementProps, getButtonProps]
7414
- });
7415
- return element;
7416
- });
7417
- if (process.env.NODE_ENV !== "production") ComboboxChipRemove.displayName = "ComboboxChipRemove";
7418
-
7419
4068
  var index_parts$1 = /*#__PURE__*/Object.freeze({
7420
4069
  __proto__: null,
7421
4070
  Arrow: ComboboxArrow,
@@ -7739,7 +4388,7 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
7739
4388
  ref: [forwardedRef, scrubAreaRef],
7740
4389
  state,
7741
4390
  props: [defaultProps, elementProps],
7742
- stateAttributesMapping: stateAttributesMapping$9
4391
+ stateAttributesMapping: stateAttributesMapping$6
7743
4392
  });
7744
4393
  const contextValue = React.useMemo(() => ({
7745
4394
  isScrubbing,
@@ -7797,7 +4446,7 @@ const NumberFieldScrubAreaCursor = /*#__PURE__*/React.forwardRef(function Number
7797
4446
  pointerEvents: 'none'
7798
4447
  }
7799
4448
  }, elementProps],
7800
- stateAttributesMapping: stateAttributesMapping$9
4449
+ stateAttributesMapping: stateAttributesMapping$6
7801
4450
  });
7802
4451
  return element && /*#__PURE__*/ReactDOM.createPortal(element, ownerDocument(domElement).body);
7803
4452
  });