@jobber/components 9.9.0 → 9.10.1
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.
- package/dist/Autocomplete/index.cjs +0 -2
- package/dist/Autocomplete/index.mjs +0 -2
- package/dist/Autocomplete-cjs.js +4 -5
- package/dist/Autocomplete-es.js +1 -2
- package/dist/Card/index.cjs +1 -2
- package/dist/Card/index.mjs +1 -2
- package/dist/Chip/index.cjs +0 -2
- package/dist/Chip/index.mjs +0 -2
- package/dist/Chips/InternalChipDismissible/hooks/index.cjs +0 -2
- package/dist/Chips/InternalChipDismissible/hooks/index.mjs +0 -2
- package/dist/Chips/InternalChipDismissible/index.cjs +0 -2
- package/dist/Chips/InternalChipDismissible/index.mjs +0 -2
- package/dist/Chips/index.cjs +0 -2
- package/dist/Chips/index.mjs +0 -2
- package/dist/Combobox/index.cjs +2 -4
- package/dist/Combobox/index.mjs +2 -4
- package/dist/ComboboxPrimitive-cjs.js +29 -30
- package/dist/ComboboxPrimitive-es.js +2 -3
- package/dist/{ComboboxChipRemove-cjs.js → ComboboxSeparator-cjs.js} +501 -455
- package/dist/{ComboboxChipRemove-es.js → ComboboxSeparator-es.js} +500 -456
- package/dist/ConfirmationModal/index.cjs +0 -2
- package/dist/ConfirmationModal/index.mjs +0 -2
- package/dist/DataDump/index.cjs +1 -2
- package/dist/DataDump/index.mjs +1 -2
- package/dist/DataList/components/DataListActions/index.cjs +0 -2
- package/dist/DataList/components/DataListActions/index.mjs +0 -2
- package/dist/DataList/components/DataListBulkActions/index.cjs +0 -2
- package/dist/DataList/components/DataListBulkActions/index.mjs +0 -2
- package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +0 -2
- package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +0 -2
- package/dist/DataList/components/DataListFilters/index.cjs +0 -2
- package/dist/DataList/components/DataListFilters/index.mjs +0 -2
- package/dist/DataList/components/DataListHeader/index.cjs +0 -2
- package/dist/DataList/components/DataListHeader/index.mjs +0 -2
- package/dist/DataList/components/DataListItem/index.cjs +0 -2
- package/dist/DataList/components/DataListItem/index.mjs +0 -2
- package/dist/DataList/components/DataListItemActions/index.cjs +0 -2
- package/dist/DataList/components/DataListItemActions/index.mjs +0 -2
- package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +0 -2
- package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +0 -2
- package/dist/DataList/components/DataListItems/index.cjs +0 -2
- package/dist/DataList/components/DataListItems/index.mjs +0 -2
- package/dist/DataList/components/DataListLayout/index.cjs +0 -2
- package/dist/DataList/components/DataListLayout/index.mjs +0 -2
- package/dist/DataList/components/DataListLayoutActions/index.cjs +0 -2
- package/dist/DataList/components/DataListLayoutActions/index.mjs +0 -2
- package/dist/DataList/index.cjs +0 -2
- package/dist/DataList/index.mjs +0 -2
- package/dist/DatePicker/index.cjs +0 -2
- package/dist/DatePicker/index.mjs +0 -2
- package/dist/DatePicker-cjs.js +4 -5
- package/dist/DatePicker-es.js +1 -2
- package/dist/Dialog/index.cjs +1 -2
- package/dist/Dialog/index.mjs +1 -2
- package/dist/DrawerDescription-cjs.js +268 -99
- package/dist/DrawerDescription-es.js +270 -103
- package/dist/DrawerTitle-cjs.js +2 -6
- package/dist/DrawerTitle-es.js +3 -7
- package/dist/FieldDescription-cjs.js +184 -181
- package/dist/FieldDescription-es.js +187 -184
- package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +0 -2
- package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +0 -2
- package/dist/FilterPicker/components/FilterPickerContent/index.cjs +0 -2
- package/dist/FilterPicker/components/FilterPickerContent/index.mjs +0 -2
- package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +0 -2
- package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +0 -2
- package/dist/FilterPicker/index.cjs +0 -2
- package/dist/FilterPicker/index.mjs +0 -2
- package/dist/FilterPickerContent-cjs.js +4 -5
- package/dist/FilterPickerContent-es.js +1 -2
- package/dist/FormatFile/index.cjs +0 -2
- package/dist/FormatFile/index.mjs +0 -2
- package/dist/Gallery/GalleryTypes.d.ts +2 -1
- package/dist/Gallery/index.cjs +19 -2
- package/dist/Gallery/index.d.ts +1 -0
- package/dist/Gallery/index.mjs +19 -2
- package/dist/Gallery-cjs.js +5 -1
- package/dist/Gallery-es.js +5 -1
- package/dist/InputDate/index.cjs +0 -2
- package/dist/InputDate/index.mjs +0 -2
- package/dist/InputNumber/index.cjs +1 -2
- package/dist/InputNumber/index.mjs +1 -2
- package/dist/LightBox/LightBox.d.ts +12 -0
- package/dist/LightBox/LightBox.messages.d.ts +4 -0
- package/dist/LightBox/LightBox.types.d.ts +18 -0
- package/dist/LightBox/index.cjs +19 -2
- package/dist/LightBox/index.d.ts +1 -1
- package/dist/LightBox/index.mjs +19 -2
- package/dist/LightBox-cjs.js +34 -5
- package/dist/LightBox-es.js +34 -5
- package/dist/Menu/index.cjs +1 -2
- package/dist/Menu/index.mjs +1 -2
- package/dist/Menu-cjs.js +4 -5
- package/dist/Menu-es.js +1 -2
- package/dist/MenuSubmenuTrigger-cjs.js +206 -164
- package/dist/MenuSubmenuTrigger-es.js +194 -152
- package/dist/Modal/index.cjs +0 -2
- package/dist/Modal/index.mjs +1 -3
- package/dist/NumberFieldInput-cjs.js +187 -308
- package/dist/NumberFieldInput-es.js +187 -308
- package/dist/Page/index.cjs +1 -2
- package/dist/Page/index.mjs +1 -2
- package/dist/Popover/index.cjs +0 -2
- package/dist/Popover/index.mjs +0 -2
- package/dist/Popover-cjs.js +7 -8
- package/dist/Popover-es.js +1 -2
- package/dist/ScrollAreaViewport-cjs.js +747 -1093
- package/dist/ScrollAreaViewport-es.js +734 -1081
- package/dist/Select/index.cjs +1 -2
- package/dist/Select/index.mjs +1 -2
- package/dist/SelectGroupLabel-cjs.js +207 -306
- package/dist/SelectGroupLabel-es.js +203 -302
- package/dist/Tooltip/index.cjs +0 -2
- package/dist/Tooltip/index.mjs +0 -2
- package/dist/Tooltip-cjs.js +6 -7
- package/dist/Tooltip-es.js +1 -2
- package/dist/dialogReturnFocus-cjs.js +1 -1
- package/dist/dialogReturnFocus-es.js +2 -2
- package/dist/docs/Gallery/Gallery.md +8 -0
- package/dist/docs/LightBox/LightBox.md +17 -3
- package/dist/floating-ui.react-cjs.js +2557 -185
- package/dist/floating-ui.react-es.js +2512 -149
- package/dist/index.cjs +3 -4
- package/dist/index.mjs +3 -4
- package/dist/primitives/BottomSheet/index.cjs +1 -1
- package/dist/primitives/BottomSheet/index.mjs +1 -1
- package/dist/primitives/ComboboxPrimitive/index.cjs +2 -4
- package/dist/primitives/ComboboxPrimitive/index.mjs +2 -4
- package/dist/primitives/SelectPrimitive/index.cjs +0 -2
- package/dist/primitives/SelectPrimitive/index.mjs +0 -2
- package/dist/primitives/index.cjs +2 -3
- package/dist/primitives/index.mjs +2 -3
- package/dist/scrollEdges-cjs.js +1 -11
- package/dist/scrollEdges-es.js +1 -11
- package/dist/scrollable-cjs.js +58 -0
- package/dist/scrollable-es.js +54 -0
- package/dist/styles.css +21 -1
- package/dist/unstyledPrimitives/index.cjs +720 -478
- package/dist/unstyledPrimitives/index.mjs +663 -421
- package/dist/useCompositeListItem-cjs.js +2108 -361
- package/dist/useCompositeListItem-es.js +2076 -331
- package/dist/useLabel-cjs.js +2 -3
- package/dist/useLabel-es.js +2 -3
- package/dist/useRenderElement-cjs.js +9 -7
- package/dist/useRenderElement-es.js +9 -7
- package/dist/useScrollLock-cjs.js +819 -344
- package/dist/useScrollLock-es.js +732 -296
- package/dist/useScrollToActive-cjs.js +4 -5
- package/dist/useScrollToActive-es.js +1 -2
- package/dist/utils/meta/meta.json +1 -0
- package/package.json +3 -3
- package/dist/floating-ui.react-dom-cjs.js +0 -2103
- package/dist/floating-ui.react-dom-es.js +0 -2072
- package/dist/floating-ui.utils-cjs.js +0 -342
- package/dist/floating-ui.utils-es.js +0 -300
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { bU as getGridNavigatedIndex, aO as isIndexOutOfListBounds, a7 as useDirection, v as useControlled, bV as Store, d as useStableCallback, U as useStore, w as useValueAsRef, s as useTransitionStatus, R as useOpenInteractionType, G as createGenericEventDetails, z as none, M as inputClear, O as inputChange, aE as getOverflowAncestors, i as isHTMLElement, a0 as contains, bu as escapeKey, h as createChangeEventDetails, Y as focusOut, Z as outsidePress, e as getTarget, ad as itemPress, l as useOpenChangeComplete, c as useIsoLayoutEffect, L as useValueChanged, V as useClick, bW as inputPress, W as useDismiss, F as FOCUSABLE_POPUP_PROPS, B as visuallyHiddenInput, C as visuallyHidden, P as keyboard, bX as pointer, _ as pressableTriggerOpenStateMapping, q as useTimeout, f as useButton, o as ownerDocument, aR as stopEvent, aS as listNavigation, a1 as cancelOpen, j as closePress, u as useBaseUiId, bg as android, bY as gecko, au as isElement, bv as isInteractiveElement, bZ as clearPress, t as transitionStatusMapping, be as triggerOpenStateMapping, x as ios, a2 as FloatingPortal, a6 as popupStateMapping, a3 as InternalBackdrop, a4 as inertValue, a5 as DROPDOWN_COLLISION_AVOIDANCE, n as FloatingFocusManager, S as SafeReact, r as error, b_ as chipRemovePress } from './useScrollLock-es.js';
|
|
3
3
|
import { f as formatErrorMessage, c as EMPTY_ARRAY, a as useRefWithInit, E as EMPTY_OBJECT, N as NOOP, m as mergeProps, b as useMergedRefs, u as useRenderElement } from './useRenderElement-es.js';
|
|
4
|
-
import { h as hasNullItemLabel, e as compareItemEquality,
|
|
4
|
+
import { h as hasNullItemLabel, e as compareItemEquality, n as stringifyAsLabel, a as useFormContext, b as useFieldRootContext, c as useLabelableId, E as isGroupedItems, G as findSelectionIndex, s as stringifyAsValue, d as useRegisterFieldControl, H as selectedValueIncludes, B as removeItem, i as useFloatingRootContext, j as useListNavigation, l as useOnFirstRender, m as defaultItemEquality, o as areArraysEqual, f as fieldValidityMapping, u as useLabelableContext, k as useTypeahead, r as resolveAriaLabelledBy, p as isMouseWithinBounds, F as FieldRootContext, I as DEFAULT_FIELD_ROOT_CONTEXT, J as DEFAULT_FIELD_STATE_ATTRIBUTES, C as CompositeList, w as useAnchorPositioning, v as useAnchoredPopupScrollLock, x as usePositioner, z as getDisabledMountTransitionStyles, A as useCompositeListItem, g as findItemIndex, K as getDefaultLabelId, q as resolveMultipleLabels, t as resolveSelectedLabel } from './useCompositeListItem-es.js';
|
|
5
5
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
6
6
|
import * as ReactDOM from 'react-dom';
|
|
7
7
|
import { s as stringifyLocale } from './stringifyLocale-es.js';
|
|
8
|
+
import { i as isScrollableY } from './scrollable-es.js';
|
|
8
9
|
import { u as useLabel } from './useLabel-es.js';
|
|
9
|
-
import { b as isElement } from './floating-ui.utils-es.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Positional arguments are deliberate: property names of an options object
|
|
@@ -82,15 +82,15 @@ function useComboboxHasItemsContext() {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
const selectors = {
|
|
85
|
-
id:
|
|
86
|
-
labelId:
|
|
87
|
-
items:
|
|
88
|
-
selectedValue:
|
|
89
|
-
hasSelectionChips:
|
|
85
|
+
id: state => state.id,
|
|
86
|
+
labelId: state => state.labelId,
|
|
87
|
+
items: state => state.items,
|
|
88
|
+
selectedValue: state => state.selectedValue,
|
|
89
|
+
hasSelectionChips: state => {
|
|
90
90
|
const selectedValue = state.selectedValue;
|
|
91
91
|
return Array.isArray(selectedValue) && selectedValue.length > 0;
|
|
92
|
-
}
|
|
93
|
-
hasSelectedValue:
|
|
92
|
+
},
|
|
93
|
+
hasSelectedValue: state => {
|
|
94
94
|
const {
|
|
95
95
|
selectedValue,
|
|
96
96
|
selectionMode
|
|
@@ -102,53 +102,53 @@ const selectors = {
|
|
|
102
102
|
return selectedValue.length > 0;
|
|
103
103
|
}
|
|
104
104
|
return true;
|
|
105
|
-
}
|
|
106
|
-
hasNullItemLabel:
|
|
105
|
+
},
|
|
106
|
+
hasNullItemLabel: (state, enabled) => {
|
|
107
107
|
return enabled ? hasNullItemLabel(state.items) : false;
|
|
108
|
-
}
|
|
109
|
-
open:
|
|
110
|
-
mounted:
|
|
111
|
-
forceMounted:
|
|
112
|
-
inline:
|
|
113
|
-
activeIndex:
|
|
114
|
-
selectedIndex:
|
|
115
|
-
isActive:
|
|
116
|
-
isSelected:
|
|
108
|
+
},
|
|
109
|
+
open: state => state.open,
|
|
110
|
+
mounted: state => state.mounted,
|
|
111
|
+
forceMounted: state => state.forceMounted,
|
|
112
|
+
inline: state => state.inline,
|
|
113
|
+
activeIndex: state => state.activeIndex,
|
|
114
|
+
selectedIndex: state => state.selectedIndex,
|
|
115
|
+
isActive: (state, index) => state.activeIndex === index,
|
|
116
|
+
isSelected: (state, itemValue) => {
|
|
117
117
|
const comparer = state.isItemEqualToValue;
|
|
118
118
|
const selectedValue = state.selectedValue;
|
|
119
119
|
if (Array.isArray(selectedValue)) {
|
|
120
120
|
return selectedValue.some(selectedItem => compareItemEquality(itemValue, selectedItem, comparer));
|
|
121
121
|
}
|
|
122
122
|
return compareItemEquality(itemValue, selectedValue, comparer);
|
|
123
|
-
}
|
|
124
|
-
transitionStatus:
|
|
125
|
-
popupProps:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
123
|
+
},
|
|
124
|
+
transitionStatus: state => state.transitionStatus,
|
|
125
|
+
popupProps: state => state.popupProps,
|
|
126
|
+
listProps: state => state.listProps,
|
|
127
|
+
inputProps: state => state.inputProps,
|
|
128
|
+
triggerProps: state => state.triggerProps,
|
|
129
|
+
itemProps: state => state.itemProps,
|
|
130
|
+
positionerElement: state => state.positionerElement,
|
|
131
|
+
listElement: state => state.listElement,
|
|
132
|
+
popupId: state => state.popupId,
|
|
133
|
+
triggerElement: state => state.triggerElement,
|
|
134
|
+
inputElement: state => state.inputElement,
|
|
135
|
+
inputGroupElement: state => state.inputGroupElement,
|
|
136
|
+
popupSide: state => state.popupSide,
|
|
137
|
+
openMethod: state => state.openMethod,
|
|
138
|
+
inputInsidePopup: state => state.inputInsidePopup,
|
|
139
|
+
inputOwnsFormValue: state => state.inputOwnsFormValue,
|
|
140
|
+
selectionMode: state => state.selectionMode,
|
|
141
|
+
name: state => state.name,
|
|
142
|
+
form: state => state.form,
|
|
143
|
+
disabled: state => state.disabled,
|
|
144
|
+
readOnly: state => state.readOnly,
|
|
145
|
+
required: state => state.required,
|
|
146
|
+
grid: state => state.grid,
|
|
147
|
+
virtualized: state => state.virtualized,
|
|
148
|
+
itemToStringLabel: state => state.itemToStringLabel,
|
|
149
|
+
isItemEqualToValue: state => state.isItemEqualToValue,
|
|
150
|
+
modal: state => state.modal,
|
|
151
|
+
autoHighlight: state => state.autoHighlight
|
|
152
152
|
};
|
|
153
153
|
|
|
154
154
|
/**
|
|
@@ -171,8 +171,7 @@ function createCollatorItemFilter(collatorFilter, itemToStringLabel) {
|
|
|
171
171
|
if (item == null) {
|
|
172
172
|
return false;
|
|
173
173
|
}
|
|
174
|
-
|
|
175
|
-
return collatorFilter.contains(itemString, query);
|
|
174
|
+
return collatorFilter.contains(item, query, itemToStringLabel);
|
|
176
175
|
};
|
|
177
176
|
}
|
|
178
177
|
|
|
@@ -188,14 +187,13 @@ function createSingleSelectionCollatorFilter(collatorFilter, itemToStringLabel,
|
|
|
188
187
|
if (!query) {
|
|
189
188
|
return true;
|
|
190
189
|
}
|
|
191
|
-
const itemString = stringifyAsLabel(item, itemToStringLabel);
|
|
192
190
|
const selectedString = selectedValue != null ? stringifyAsLabel(selectedValue, itemToStringLabel) : '';
|
|
193
191
|
|
|
194
192
|
// Handle case-insensitive matching consistently
|
|
195
193
|
if (selectedString && collatorFilter.contains(selectedString, query) && selectedString.length === query.length) {
|
|
196
194
|
return true;
|
|
197
195
|
}
|
|
198
|
-
return collatorFilter.contains(
|
|
196
|
+
return collatorFilter.contains(item, query, itemToStringLabel);
|
|
199
197
|
};
|
|
200
198
|
}
|
|
201
199
|
|
|
@@ -267,9 +265,8 @@ function useComboboxFilter(options = {}) {
|
|
|
267
265
|
return createSingleSelectionCollatorFilter(coreFilter, itemToString, value)(item, query);
|
|
268
266
|
}, [coreFilter, value, multiple]);
|
|
269
267
|
return React.useMemo(() => ({
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
endsWith: coreFilter.endsWith
|
|
268
|
+
...coreFilter,
|
|
269
|
+
contains
|
|
273
270
|
}), [contains, coreFilter]);
|
|
274
271
|
}
|
|
275
272
|
|
|
@@ -290,7 +287,7 @@ function AriaCombobox(props) {
|
|
|
290
287
|
inputValue: inputValueProp,
|
|
291
288
|
open: openProp,
|
|
292
289
|
defaultOpen = false,
|
|
293
|
-
selectionMode
|
|
290
|
+
selectionMode,
|
|
294
291
|
onItemHighlighted: onItemHighlightedProp,
|
|
295
292
|
name: nameProp,
|
|
296
293
|
form,
|
|
@@ -363,12 +360,10 @@ function AriaCombobox(props) {
|
|
|
363
360
|
*/
|
|
364
361
|
const valuesRef = React.useRef([]);
|
|
365
362
|
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* It accumulates values on first mount and does not remove them on unmount due to
|
|
369
|
-
* filtering, providing a stable index for selected value tracking.
|
|
363
|
+
* The item element that received the last `pointerdown`, used to detect whether a
|
|
364
|
+
* `mouseup` on an item belongs to a drag-select gesture that started elsewhere.
|
|
370
365
|
*/
|
|
371
|
-
const
|
|
366
|
+
const pointerDownItemRef = React.useRef(null);
|
|
372
367
|
const disabled = fieldDisabled || disabledProp;
|
|
373
368
|
const name = fieldName ?? nameProp;
|
|
374
369
|
const multiple = selectionMode === 'multiple';
|
|
@@ -395,11 +390,10 @@ function AriaCombobox(props) {
|
|
|
395
390
|
if (filterProp !== undefined) {
|
|
396
391
|
return filterProp;
|
|
397
392
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
393
|
+
// `shouldBypassFiltering` already empties the query whenever a single selection's label
|
|
394
|
+
// matches it exactly, so the filter never needs a selection-aware variant here.
|
|
401
395
|
return createCollatorItemFilter(collatorFilter, itemToStringLabel);
|
|
402
|
-
}, [filterProp,
|
|
396
|
+
}, [filterProp, collatorFilter, itemToStringLabel]);
|
|
403
397
|
|
|
404
398
|
// If neither inputValue nor defaultInputValue are provided, derive it from the
|
|
405
399
|
// selected value for single mode so the input reflects the selection on mount.
|
|
@@ -425,9 +419,9 @@ function AriaCombobox(props) {
|
|
|
425
419
|
state: 'open'
|
|
426
420
|
});
|
|
427
421
|
const isGrouped = isGroupedItems(items);
|
|
428
|
-
const query = closeQuery ??
|
|
422
|
+
const query = closeQuery ?? String(inputValue).trim();
|
|
429
423
|
const selectedLabelString = single ? stringifyAsLabel(selectedValue, itemToStringLabel) : '';
|
|
430
|
-
const shouldBypassFiltering = single && !queryChangedAfterOpen && query !== '' && selectedLabelString
|
|
424
|
+
const shouldBypassFiltering = single && !queryChangedAfterOpen && query !== '' && selectedLabelString.length === query.length && collatorFilter.contains(selectedLabelString, query);
|
|
431
425
|
const filterQuery = shouldBypassFiltering ? '' : query;
|
|
432
426
|
const shouldIgnoreExternalFiltering = hasItems && hasFilteredItemsProp && shouldBypassFiltering;
|
|
433
427
|
const flatItems = React.useMemo(() => {
|
|
@@ -454,12 +448,18 @@ function AriaCombobox(props) {
|
|
|
454
448
|
if (limit > -1 && currentCount >= limit) {
|
|
455
449
|
break;
|
|
456
450
|
}
|
|
457
|
-
const candidateItems = filterQuery === '' ? group.items : group.items.filter(item => filter(item, filterQuery, itemToStringLabel));
|
|
458
|
-
if (candidateItems.length === 0) {
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
451
|
const remainingLimit = limit > -1 ? limit - currentCount : Infinity;
|
|
462
|
-
const itemsToTake =
|
|
452
|
+
const itemsToTake = filterQuery === '' ? group.items.slice(0, remainingLimit) : [];
|
|
453
|
+
if (filterQuery !== '') {
|
|
454
|
+
for (const item of group.items) {
|
|
455
|
+
if (itemsToTake.length >= remainingLimit) {
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
if (filter(item, filterQuery, itemToStringLabel)) {
|
|
459
|
+
itemsToTake.push(item);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
463
|
if (itemsToTake.length > 0) {
|
|
464
464
|
const newGroup = {
|
|
465
465
|
...group,
|
|
@@ -499,76 +499,85 @@ function AriaCombobox(props) {
|
|
|
499
499
|
}
|
|
500
500
|
return filteredItems;
|
|
501
501
|
}, [filteredItems, isGrouped]);
|
|
502
|
-
const store = useRefWithInit(() =>
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
selectionMode
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
502
|
+
const store = useRefWithInit(() => {
|
|
503
|
+
// An inline list open on the first render never gets a closed pass of the closed-state
|
|
504
|
+
// sync effect below, and `items`-prop lists don't self-register their index the way
|
|
505
|
+
// individually rendered `<Combobox.Item>`s do, so the selected item was never highlighted.
|
|
506
|
+
// Seeding the index here lets list navigation highlight and scroll to the selection on
|
|
507
|
+
// mount. Computed once by construction, so a selection or list that resolves after mount
|
|
508
|
+
// doesn't move an existing highlight or scroll the list away.
|
|
509
|
+
let initialSelectedIndex = null;
|
|
510
|
+
if (inlineProp && open && hasItems && selectionMode !== 'none') {
|
|
511
|
+
initialSelectedIndex = findSelectionIndex(flatFilteredItems, selectedValue, isItemEqualToValue, multiple);
|
|
512
|
+
}
|
|
513
|
+
return new Store({
|
|
514
|
+
id,
|
|
515
|
+
labelId: undefined,
|
|
516
|
+
selectedValue,
|
|
517
|
+
open,
|
|
518
|
+
items,
|
|
519
|
+
selectionMode,
|
|
520
|
+
listRef,
|
|
521
|
+
labelsRef,
|
|
522
|
+
popupRef,
|
|
523
|
+
emptyRef,
|
|
524
|
+
inputRef,
|
|
525
|
+
startDismissRef,
|
|
526
|
+
endDismissRef,
|
|
527
|
+
keyboardActiveRef,
|
|
528
|
+
chipsContainerRef,
|
|
529
|
+
clearRef,
|
|
530
|
+
valuesRef,
|
|
531
|
+
pointerDownItemRef,
|
|
532
|
+
selectionEventRef,
|
|
533
|
+
name,
|
|
534
|
+
form,
|
|
535
|
+
disabled,
|
|
536
|
+
readOnly,
|
|
537
|
+
required,
|
|
538
|
+
grid,
|
|
539
|
+
virtualized,
|
|
540
|
+
openOnInputClick,
|
|
541
|
+
itemToStringLabel,
|
|
542
|
+
isItemEqualToValue,
|
|
543
|
+
modal,
|
|
544
|
+
autoHighlight: autoHighlightMode,
|
|
545
|
+
submitOnItemClick,
|
|
546
|
+
hasInputValue,
|
|
547
|
+
mounted: false,
|
|
548
|
+
forceMounted: false,
|
|
549
|
+
transitionStatus: 'idle',
|
|
550
|
+
inline: inlineProp,
|
|
551
|
+
activeIndex: null,
|
|
552
|
+
selectedIndex: initialSelectedIndex,
|
|
553
|
+
popupProps: {},
|
|
554
|
+
listProps: {},
|
|
555
|
+
inputProps: {},
|
|
556
|
+
triggerProps: {},
|
|
557
|
+
itemProps: EMPTY_OBJECT,
|
|
558
|
+
positionerElement: null,
|
|
559
|
+
listElement: null,
|
|
560
|
+
popupId: undefined,
|
|
561
|
+
triggerElement: null,
|
|
562
|
+
inputElement: null,
|
|
563
|
+
inputGroupElement: null,
|
|
564
|
+
popupSide: null,
|
|
565
|
+
openMethod: null,
|
|
566
|
+
inputInsidePopup: true,
|
|
567
|
+
// Avoid duplicate names in the server HTML. Popup inputs aren't rendered
|
|
568
|
+
// until after hydration, so the hidden input takes over then if needed.
|
|
569
|
+
inputOwnsFormValue: selectionMode === 'none',
|
|
570
|
+
// Placeholder callbacks replaced on first render
|
|
571
|
+
onOpenChangeComplete: NOOP,
|
|
572
|
+
setOpen: NOOP,
|
|
573
|
+
setInputValue: NOOP,
|
|
574
|
+
setSelectedValue: NOOP,
|
|
575
|
+
setIndices: NOOP,
|
|
576
|
+
handleSelection: NOOP,
|
|
577
|
+
forceMount: NOOP,
|
|
578
|
+
requestSubmit: NOOP
|
|
579
|
+
});
|
|
580
|
+
}).current;
|
|
572
581
|
const fieldRawValue = selectionMode === 'none' ? inputValue : selectedValue;
|
|
573
582
|
const fieldStringValue = React.useMemo(() => {
|
|
574
583
|
if (selectionMode === 'none') {
|
|
@@ -611,35 +620,38 @@ function AriaCombobox(props) {
|
|
|
611
620
|
store.set('forceMounted', true);
|
|
612
621
|
}
|
|
613
622
|
});
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Emits `onItemHighlighted` for the item at `index`, or clears the highlight when `index` is `-1`
|
|
626
|
+
* (a no-op if nothing was highlighted). Keeps `lastHighlightRef` in sync with what was emitted.
|
|
627
|
+
*/
|
|
628
|
+
const emitHighlight = useStableCallback((value, index, type) => {
|
|
629
|
+
if (index === -1) {
|
|
630
|
+
if (lastHighlightRef.current === INITIAL_LAST_HIGHLIGHT) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
lastHighlightRef.current = INITIAL_LAST_HIGHLIGHT;
|
|
634
|
+
} else {
|
|
635
|
+
lastHighlightRef.current = {
|
|
636
|
+
value,
|
|
637
|
+
index
|
|
638
|
+
};
|
|
619
639
|
}
|
|
620
|
-
|
|
640
|
+
onItemHighlighted(value, createGenericEventDetails(type, undefined, {
|
|
641
|
+
index
|
|
642
|
+
}));
|
|
643
|
+
});
|
|
621
644
|
const setIndices = useStableCallback(options => {
|
|
622
645
|
store.update(options);
|
|
623
|
-
const
|
|
624
|
-
if (
|
|
646
|
+
const activeIndexOption = options.activeIndex;
|
|
647
|
+
if (activeIndexOption === undefined) {
|
|
625
648
|
return;
|
|
626
649
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
onItemHighlighted(undefined, createGenericEventDetails(type, undefined, {
|
|
631
|
-
index: -1
|
|
632
|
-
}));
|
|
633
|
-
}
|
|
650
|
+
const type = options.type || none;
|
|
651
|
+
if (activeIndexOption === null) {
|
|
652
|
+
emitHighlight(undefined, -1, type);
|
|
634
653
|
} else {
|
|
635
|
-
|
|
636
|
-
lastHighlightRef.current = {
|
|
637
|
-
value: activeValue,
|
|
638
|
-
index: options.activeIndex
|
|
639
|
-
};
|
|
640
|
-
onItemHighlighted(activeValue, createGenericEventDetails(type, undefined, {
|
|
641
|
-
index: options.activeIndex
|
|
642
|
-
}));
|
|
654
|
+
emitHighlight(valuesRef.current[activeIndexOption], activeIndexOption, type);
|
|
643
655
|
}
|
|
644
656
|
});
|
|
645
657
|
const setInputValue = useStableCallback((next, eventDetails) => {
|
|
@@ -652,6 +664,11 @@ function AriaCombobox(props) {
|
|
|
652
664
|
// If user is typing, ensure we don't auto-highlight on open due to a race
|
|
653
665
|
// with the post-open effect that sets this flag.
|
|
654
666
|
if (eventDetails.reason === inputChange) {
|
|
667
|
+
// A controlled popup may ignore a close request. Resuming input proves the popup
|
|
668
|
+
// is remaining open, so release the query captured for an exit animation.
|
|
669
|
+
if (open && closeQuery !== null) {
|
|
670
|
+
setCloseQuery(null);
|
|
671
|
+
}
|
|
655
672
|
const event = eventDetails.event;
|
|
656
673
|
const inputType = event.inputType;
|
|
657
674
|
// Treat composition commits as typed input; autofill may omit `inputType` or
|
|
@@ -667,10 +684,34 @@ function AriaCombobox(props) {
|
|
|
667
684
|
pendingQueryHighlightRef.current = {
|
|
668
685
|
hasQuery
|
|
669
686
|
};
|
|
670
|
-
|
|
687
|
+
|
|
688
|
+
// Virtualized lists own their scroller. Reset regular lists directly so a stale
|
|
689
|
+
// composite registry cannot select a reordered item and scrolling cannot escape
|
|
690
|
+
// the popup.
|
|
691
|
+
const list = store.state.listElement;
|
|
692
|
+
if (!store.state.virtualized && list) {
|
|
693
|
+
const popup = popupRef.current;
|
|
694
|
+
for (const ancestor of getOverflowAncestors(list.firstElementChild ?? list)) {
|
|
695
|
+
if (!isHTMLElement(ancestor) || (popup ? !contains(popup, ancestor) : ancestor.getAttribute('role') === 'dialog')) {
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
if (isScrollableY(ancestor)) {
|
|
699
|
+
ancestor.scrollTop = 0;
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (hasQuery && autoHighlightMode && store.state.activeIndex == null && (open || inline)) {
|
|
671
705
|
store.set('activeIndex', 0);
|
|
672
706
|
}
|
|
673
707
|
}
|
|
708
|
+
} else if (eventDetails.reason === inputClear && next === '' && store.state.inputInsidePopup) {
|
|
709
|
+
// A programmatic clear of an active query (e.g. after selecting an item with the
|
|
710
|
+
// input inside the popup): restore the highlight to the selected item.
|
|
711
|
+
pendingQueryHighlightRef.current = {
|
|
712
|
+
hasQuery: false,
|
|
713
|
+
selection: true
|
|
714
|
+
};
|
|
674
715
|
}
|
|
675
716
|
setInputValueUnwrapped(next);
|
|
676
717
|
});
|
|
@@ -682,7 +723,7 @@ function AriaCombobox(props) {
|
|
|
682
723
|
// If the `Empty` component is not used, the positioner or popup should be hidden
|
|
683
724
|
// with CSS. In this case, allow the Escape key to bubble to close a parent popup
|
|
684
725
|
// if there are no items to show.
|
|
685
|
-
if (eventDetails.reason ===
|
|
726
|
+
if (eventDetails.reason === escapeKey && hasItems && flatFilteredItems.length === 0 && !emptyRef.current) {
|
|
686
727
|
eventDetails.allowPropagation();
|
|
687
728
|
}
|
|
688
729
|
props.onOpenChange?.(nextOpen, eventDetails);
|
|
@@ -692,10 +733,10 @@ function AriaCombobox(props) {
|
|
|
692
733
|
|
|
693
734
|
// If reopening interrupts the close animation, handleUnmount won't run to clear the
|
|
694
735
|
// frozen closeQuery and pending popup input.
|
|
695
|
-
if (nextOpen &&
|
|
736
|
+
if (nextOpen && inputInsidePopup && !inline && closeQuery !== null) {
|
|
696
737
|
setQueryChangedAfterOpen(false);
|
|
697
738
|
setCloseQuery(null);
|
|
698
|
-
if (inputValue !== '') {
|
|
739
|
+
if (inputValue !== '' && eventDetails.reason !== inputChange) {
|
|
699
740
|
setInputValue('', createChangeEventDetails(inputClear, eventDetails.event));
|
|
700
741
|
}
|
|
701
742
|
}
|
|
@@ -749,18 +790,8 @@ function AriaCombobox(props) {
|
|
|
749
790
|
if (shouldFillInput) {
|
|
750
791
|
setInputValue(stringifyAsLabel(nextValue, itemToStringLabel), createChangeEventDetails(eventDetails.reason, eventDetails.event));
|
|
751
792
|
}
|
|
752
|
-
if (single && nextValue != null && eventDetails.reason !== inputChange && queryChangedAfterOpen && !inline) {
|
|
753
|
-
setCloseQuery(query);
|
|
754
|
-
}
|
|
755
793
|
});
|
|
756
|
-
const handleSelection = useStableCallback((event,
|
|
757
|
-
let itemValue = passedValue;
|
|
758
|
-
if (itemValue === undefined) {
|
|
759
|
-
if (activeIndex === null) {
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
itemValue = valuesRef.current[activeIndex];
|
|
763
|
-
}
|
|
794
|
+
const handleSelection = useStableCallback((event, itemValue) => {
|
|
764
795
|
const targetEl = getTarget(event);
|
|
765
796
|
const overrideEvent = selectionEventRef.current ?? event;
|
|
766
797
|
selectionEventRef.current = null;
|
|
@@ -776,8 +807,8 @@ function AriaCombobox(props) {
|
|
|
776
807
|
}
|
|
777
808
|
if (multiple) {
|
|
778
809
|
const currentSelectedValue = Array.isArray(selectedValue) ? selectedValue : [];
|
|
779
|
-
const isCurrentlySelected = selectedValueIncludes(currentSelectedValue, itemValue,
|
|
780
|
-
const nextValue = isCurrentlySelected ? removeItem(currentSelectedValue, itemValue,
|
|
810
|
+
const isCurrentlySelected = selectedValueIncludes(currentSelectedValue, itemValue, isItemEqualToValue);
|
|
811
|
+
const nextValue = isCurrentlySelected ? removeItem(currentSelectedValue, itemValue, isItemEqualToValue) : [...currentSelectedValue, itemValue];
|
|
781
812
|
setSelectedValue(nextValue, eventDetails);
|
|
782
813
|
if (eventDetails.isCanceled) {
|
|
783
814
|
return;
|
|
@@ -800,9 +831,6 @@ function AriaCombobox(props) {
|
|
|
800
831
|
}
|
|
801
832
|
});
|
|
802
833
|
const requestSubmit = useStableCallback(() => {
|
|
803
|
-
if (!store.state.submitOnItemClick) {
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
834
|
const formElement = validation.inputRef.current?.form ?? store.state.inputElement?.form;
|
|
807
835
|
if (formElement && typeof formElement.requestSubmit === 'function') {
|
|
808
836
|
formElement.requestSubmit();
|
|
@@ -875,25 +903,29 @@ function AriaCombobox(props) {
|
|
|
875
903
|
React.useImperativeHandle(props.actionsRef, () => ({
|
|
876
904
|
unmount: handleUnmount
|
|
877
905
|
}), [handleUnmount]);
|
|
878
|
-
useIsoLayoutEffect(function
|
|
879
|
-
if (open
|
|
906
|
+
useIsoLayoutEffect(function syncClosedState() {
|
|
907
|
+
if (open) {
|
|
880
908
|
return;
|
|
881
909
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
selectedIndex: lastIndex === -1 ? null : lastIndex
|
|
889
|
-
});
|
|
890
|
-
} else {
|
|
891
|
-
const index = findItemIndex(registry, selectedValue, isItemEqualToValue);
|
|
892
|
-
setIndices({
|
|
893
|
-
selectedIndex: index === -1 ? null : index
|
|
894
|
-
});
|
|
910
|
+
|
|
911
|
+
// State-driven (not tied to the internal event path) so controlled closes
|
|
912
|
+
// also clear a pointerdown that never received a matching item mouseup.
|
|
913
|
+
pointerDownItemRef.current = null;
|
|
914
|
+
if (selectionMode === 'none') {
|
|
915
|
+
return;
|
|
895
916
|
}
|
|
896
|
-
|
|
917
|
+
|
|
918
|
+
// Without `items`, look the selection up in the live registry of mounted item
|
|
919
|
+
// values (the list stays mounted while closed when closed-state features need
|
|
920
|
+
// it — trigger interaction and rendered-label autofill force-mount it). Mounted
|
|
921
|
+
// items re-assert the index themselves when their registration moves; when
|
|
922
|
+
// nothing is mounted the lookup resolves to `null` and each item re-registers
|
|
923
|
+
// the index on the next open.
|
|
924
|
+
const registry = hasItems ? flatItems : valuesRef.current;
|
|
925
|
+
setIndices({
|
|
926
|
+
selectedIndex: findSelectionIndex(registry, selectedValue, isItemEqualToValue, multiple)
|
|
927
|
+
});
|
|
928
|
+
}, [open, selectedValue, selectionMode, multiple, hasItems, flatItems, isItemEqualToValue, setIndices]);
|
|
897
929
|
useIsoLayoutEffect(() => {
|
|
898
930
|
if (items) {
|
|
899
931
|
valuesRef.current = flatFilteredItems;
|
|
@@ -903,14 +935,52 @@ function AriaCombobox(props) {
|
|
|
903
935
|
useIsoLayoutEffect(() => {
|
|
904
936
|
const pendingHighlight = pendingQueryHighlightRef.current;
|
|
905
937
|
if (pendingHighlight) {
|
|
938
|
+
// A directly rendered list remains visible when the popup state is closed, while a
|
|
939
|
+
// kept-mounted Positioner is hidden and should stay inert.
|
|
940
|
+
const listIsNavigable = open || inline || store.state.positionerElement?.hidden === false;
|
|
906
941
|
if (pendingHighlight.hasQuery) {
|
|
907
|
-
if (autoHighlightMode) {
|
|
942
|
+
if (autoHighlightMode && listIsNavigable) {
|
|
908
943
|
store.set('activeIndex', 0);
|
|
909
944
|
}
|
|
910
|
-
|
|
911
|
-
|
|
945
|
+
pendingQueryHighlightRef.current = null;
|
|
946
|
+
} else if (String(inputValue).trim() === '') {
|
|
947
|
+
// Only handle the clear once it has committed (a controlled input may reject it),
|
|
948
|
+
// so a restore cannot fire while a query is still active.
|
|
949
|
+
pendingQueryHighlightRef.current = null;
|
|
950
|
+
if (listIsNavigable) {
|
|
951
|
+
const clearedBySelection = pendingHighlight.selection;
|
|
952
|
+
if (autoHighlightMode === 'always' && !clearedBySelection && store.state.selectionMode === 'none') {
|
|
953
|
+
// There is no selection to restore in Autocomplete. Keep the first-item reset
|
|
954
|
+
// synchronous so list navigation sees it before a directly rendered list closes.
|
|
955
|
+
store.set('activeIndex', 0);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Items re-mounted by the clear publish their composite indices in a follow-up
|
|
959
|
+
// commit, so the item registries are mid-update here. Defer past React's cascade.
|
|
960
|
+
queueMicrotask(() => {
|
|
961
|
+
if (!store.state.open && !store.state.inline || inputRef.current && inputRef.current.value.trim() !== '') {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Return the highlight to the selected item, the same anchor the popup uses
|
|
966
|
+
// when it first opens. Read the selection through the store so consumers can
|
|
967
|
+
// pass an inline `isItemEqualToValue` or a fresh `selectedValue` array without
|
|
968
|
+
// re-running this effect on every render.
|
|
969
|
+
const currentSelectedValue = store.state.selectedValue;
|
|
970
|
+
const isMultiple = store.state.selectionMode === 'multiple';
|
|
971
|
+
const lastSelectedValue = isMultiple && Array.isArray(currentSelectedValue) ? currentSelectedValue[currentSelectedValue.length - 1] : currentSelectedValue;
|
|
972
|
+
const hasSelection = store.state.selectionMode !== 'none' && lastSelectedValue != null;
|
|
973
|
+
if (hasSelection || clearedBySelection) {
|
|
974
|
+
const registry = hasItems || hasFilteredItemsProp ? flatFilteredItems : valuesRef.current;
|
|
975
|
+
// A selection that is no longer in the list drops the highlight rather than
|
|
976
|
+
// leaving it on whichever item now occupies that index.
|
|
977
|
+
store.set('activeIndex', hasSelection ? findSelectionIndex(registry, currentSelectedValue, store.state.isItemEqualToValue, isMultiple) : null);
|
|
978
|
+
} else if (autoHighlightMode === 'always') {
|
|
979
|
+
store.set('activeIndex', 0);
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
}
|
|
912
983
|
}
|
|
913
|
-
pendingQueryHighlightRef.current = null;
|
|
914
984
|
}
|
|
915
985
|
if (!open && !inline) {
|
|
916
986
|
return;
|
|
@@ -923,21 +993,11 @@ function AriaCombobox(props) {
|
|
|
923
993
|
store.set('activeIndex', 0);
|
|
924
994
|
return;
|
|
925
995
|
}
|
|
926
|
-
|
|
927
|
-
lastHighlightRef.current = INITIAL_LAST_HIGHLIGHT;
|
|
928
|
-
store.state.onItemHighlighted(undefined, createGenericEventDetails(none, undefined, {
|
|
929
|
-
index: -1
|
|
930
|
-
}));
|
|
931
|
-
}
|
|
996
|
+
emitHighlight(undefined, -1, none);
|
|
932
997
|
return;
|
|
933
998
|
}
|
|
934
999
|
if (storeActiveIndex >= candidateItems.length) {
|
|
935
|
-
|
|
936
|
-
lastHighlightRef.current = INITIAL_LAST_HIGHLIGHT;
|
|
937
|
-
store.state.onItemHighlighted(undefined, createGenericEventDetails(none, undefined, {
|
|
938
|
-
index: -1
|
|
939
|
-
}));
|
|
940
|
-
}
|
|
1000
|
+
emitHighlight(undefined, -1, none);
|
|
941
1001
|
store.set('activeIndex', null);
|
|
942
1002
|
return;
|
|
943
1003
|
}
|
|
@@ -945,15 +1005,12 @@ function AriaCombobox(props) {
|
|
|
945
1005
|
const previouslyHighlightedItemValue = lastHighlightRef.current.value;
|
|
946
1006
|
const isSameItem = previouslyHighlightedItemValue !== NO_ACTIVE_VALUE && compareItemEquality(itemValue, previouslyHighlightedItemValue, store.state.isItemEqualToValue);
|
|
947
1007
|
if (lastHighlightRef.current.index !== storeActiveIndex || !isSameItem) {
|
|
948
|
-
|
|
949
|
-
value: itemValue,
|
|
950
|
-
index: storeActiveIndex
|
|
951
|
-
};
|
|
952
|
-
store.state.onItemHighlighted(itemValue, createGenericEventDetails(none, undefined, {
|
|
953
|
-
index: storeActiveIndex
|
|
954
|
-
}));
|
|
1008
|
+
emitHighlight(itemValue, storeActiveIndex, none);
|
|
955
1009
|
}
|
|
956
|
-
}, [activeIndex, autoHighlightMode, hasFilteredItemsProp, hasItems, flatFilteredItems, inline, open, store
|
|
1010
|
+
}, [activeIndex, autoHighlightMode, emitHighlight, hasFilteredItemsProp, hasItems, flatFilteredItems, inline, open, store,
|
|
1011
|
+
// Reruns the effect when the query changes without affecting the deps above, such as
|
|
1012
|
+
// clearing the input when no items are filtered out (individually rendered items).
|
|
1013
|
+
inputValue]);
|
|
957
1014
|
useIsoLayoutEffect(() => {
|
|
958
1015
|
if (selectionMode === 'none') {
|
|
959
1016
|
setFilled(String(inputValue) !== '');
|
|
@@ -984,6 +1041,12 @@ function AriaCombobox(props) {
|
|
|
984
1041
|
}
|
|
985
1042
|
setQueryChangedAfterOpen(true);
|
|
986
1043
|
});
|
|
1044
|
+
function syncInputToSelectedLabel() {
|
|
1045
|
+
const nextInputValue = stringifyAsLabel(selectedValue, itemToStringLabel);
|
|
1046
|
+
if (inputValue !== nextInputValue) {
|
|
1047
|
+
setInputValue(nextInputValue, createChangeEventDetails(none));
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
987
1050
|
useValueChanged(selectedValue, () => {
|
|
988
1051
|
if (selectionMode === 'none') {
|
|
989
1052
|
return;
|
|
@@ -992,10 +1055,7 @@ function AriaCombobox(props) {
|
|
|
992
1055
|
setDirty(isSelectedValueDirty(selectedValue));
|
|
993
1056
|
validation.change(selectedValue);
|
|
994
1057
|
if (single && !hasInputValue && !inputInsidePopup) {
|
|
995
|
-
|
|
996
|
-
if (inputValue !== nextInputValue) {
|
|
997
|
-
setInputValue(nextInputValue, createChangeEventDetails(none));
|
|
998
|
-
}
|
|
1058
|
+
syncInputToSelectedLabel();
|
|
999
1059
|
}
|
|
1000
1060
|
});
|
|
1001
1061
|
useValueChanged(inputValue, () => {
|
|
@@ -1010,10 +1070,7 @@ function AriaCombobox(props) {
|
|
|
1010
1070
|
if (!single || hasInputValue || inputInsidePopup || queryChangedAfterOpen) {
|
|
1011
1071
|
return;
|
|
1012
1072
|
}
|
|
1013
|
-
|
|
1014
|
-
if (inputValue !== nextInputValue) {
|
|
1015
|
-
setInputValue(nextInputValue, createChangeEventDetails(none));
|
|
1016
|
-
}
|
|
1073
|
+
syncInputToSelectedLabel();
|
|
1017
1074
|
});
|
|
1018
1075
|
const floatingRootContext = useFloatingRootContext({
|
|
1019
1076
|
open: inline ? true : open,
|
|
@@ -1023,18 +1080,19 @@ function AriaCombobox(props) {
|
|
|
1023
1080
|
floating: positionerElement
|
|
1024
1081
|
}
|
|
1025
1082
|
});
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
}
|
|
1083
|
+
const ariaHasPopup = grid ? 'grid' : 'listbox';
|
|
1084
|
+
// An inline list isn't gated on `open`: it renders for as long as it's in the tree, so the
|
|
1085
|
+
// combobox is permanently expanded even while the internal open state is `false`.
|
|
1086
|
+
const expanded = open || inline;
|
|
1087
|
+
const ariaExpanded = expanded ? 'true' : 'false';
|
|
1032
1088
|
const role = React.useMemo(() => {
|
|
1033
1089
|
const isPlainInput = inputElement?.tagName === 'INPUT';
|
|
1034
1090
|
// During SSR and initial hydration, the input ref is not available yet.
|
|
1035
1091
|
// Assume an input-like control so combobox ARIA attributes are present.
|
|
1036
1092
|
const shouldTreatAsInput = inputElement == null || isPlainInput;
|
|
1037
|
-
|
|
1093
|
+
// A non-input control only takes on combobox semantics while the list is exposed, which for
|
|
1094
|
+
// an inline list is the whole time.
|
|
1095
|
+
const shouldApplyAria = shouldTreatAsInput || expanded;
|
|
1038
1096
|
const reference = shouldTreatAsInput ? {
|
|
1039
1097
|
autoComplete: 'off',
|
|
1040
1098
|
spellCheck: 'false',
|
|
@@ -1045,7 +1103,7 @@ function AriaCombobox(props) {
|
|
|
1045
1103
|
reference.role = 'combobox';
|
|
1046
1104
|
reference['aria-expanded'] = ariaExpanded;
|
|
1047
1105
|
reference['aria-haspopup'] = ariaHasPopup;
|
|
1048
|
-
reference['aria-controls'] =
|
|
1106
|
+
reference['aria-controls'] = expanded ? listElement?.id : undefined;
|
|
1049
1107
|
reference['aria-autocomplete'] = autoComplete;
|
|
1050
1108
|
}
|
|
1051
1109
|
return {
|
|
@@ -1054,7 +1112,7 @@ function AriaCombobox(props) {
|
|
|
1054
1112
|
role: 'presentation'
|
|
1055
1113
|
}
|
|
1056
1114
|
};
|
|
1057
|
-
}, [inputElement,
|
|
1115
|
+
}, [inputElement, expanded, ariaExpanded, ariaHasPopup, listElement?.id, autoComplete]);
|
|
1058
1116
|
const click = useClick(floatingRootContext, {
|
|
1059
1117
|
enabled: !readOnly && !disabled && openOnInputClick,
|
|
1060
1118
|
event: 'mousedown-only',
|
|
@@ -1108,7 +1166,7 @@ function AriaCombobox(props) {
|
|
|
1108
1166
|
} else {
|
|
1109
1167
|
setIndices({
|
|
1110
1168
|
activeIndex: nextActiveIndex,
|
|
1111
|
-
type: keyboardActiveRef.current ?
|
|
1169
|
+
type: keyboardActiveRef.current ? keyboard : pointer
|
|
1112
1170
|
});
|
|
1113
1171
|
}
|
|
1114
1172
|
}
|
|
@@ -1123,7 +1181,8 @@ function AriaCombobox(props) {
|
|
|
1123
1181
|
}
|
|
1124
1182
|
}
|
|
1125
1183
|
}, dismiss.reference, click.reference, role.reference), [listNavigation.reference, dismiss.reference, click.reference, role.reference, grid, store]);
|
|
1126
|
-
const popupProps = React.useMemo(() => mergeProps(FOCUSABLE_POPUP_PROPS,
|
|
1184
|
+
const popupProps = React.useMemo(() => mergeProps(FOCUSABLE_POPUP_PROPS, dismiss.floating), [dismiss.floating]);
|
|
1185
|
+
const listProps = React.useMemo(() => mergeProps(listNavigation.floating, role.floating), [listNavigation.floating, role.floating]);
|
|
1127
1186
|
const itemProps = React.useMemo(() => {
|
|
1128
1187
|
const listNavigationItemProps = listNavigation.item;
|
|
1129
1188
|
if (!listNavigationItemProps) {
|
|
@@ -1141,6 +1200,7 @@ function AriaCombobox(props) {
|
|
|
1141
1200
|
store.update({
|
|
1142
1201
|
inline: inlineProp,
|
|
1143
1202
|
popupProps,
|
|
1203
|
+
listProps,
|
|
1144
1204
|
inputProps,
|
|
1145
1205
|
triggerProps,
|
|
1146
1206
|
itemProps,
|
|
@@ -1148,10 +1208,10 @@ function AriaCombobox(props) {
|
|
|
1148
1208
|
setInputValue,
|
|
1149
1209
|
setSelectedValue,
|
|
1150
1210
|
setIndices,
|
|
1151
|
-
onItemHighlighted,
|
|
1152
1211
|
handleSelection,
|
|
1153
1212
|
forceMount,
|
|
1154
|
-
requestSubmit
|
|
1213
|
+
requestSubmit,
|
|
1214
|
+
onOpenChangeComplete
|
|
1155
1215
|
});
|
|
1156
1216
|
});
|
|
1157
1217
|
useIsoLayoutEffect(() => {
|
|
@@ -1164,6 +1224,7 @@ function AriaCombobox(props) {
|
|
|
1164
1224
|
items,
|
|
1165
1225
|
inline: inlineProp,
|
|
1166
1226
|
popupProps,
|
|
1227
|
+
listProps,
|
|
1167
1228
|
inputProps,
|
|
1168
1229
|
triggerProps,
|
|
1169
1230
|
openMethod,
|
|
@@ -1175,9 +1236,7 @@ function AriaCombobox(props) {
|
|
|
1175
1236
|
readOnly,
|
|
1176
1237
|
required,
|
|
1177
1238
|
grid,
|
|
1178
|
-
isGrouped,
|
|
1179
1239
|
virtualized,
|
|
1180
|
-
onOpenChangeComplete,
|
|
1181
1240
|
openOnInputClick,
|
|
1182
1241
|
itemToStringLabel,
|
|
1183
1242
|
modal,
|
|
@@ -1185,10 +1244,9 @@ function AriaCombobox(props) {
|
|
|
1185
1244
|
isItemEqualToValue,
|
|
1186
1245
|
submitOnItemClick,
|
|
1187
1246
|
hasInputValue,
|
|
1188
|
-
requestSubmit,
|
|
1189
1247
|
inputOwnsFormValue: selectionMode === 'none' && (inlineProp || !store.state.inputInsidePopup)
|
|
1190
1248
|
});
|
|
1191
|
-
}, [store, id, selectedValue, open, mounted, transitionStatus, items, popupProps, inputProps, itemProps, openMethod, triggerProps, selectionMode, name, disabled, readOnly, required,
|
|
1249
|
+
}, [store, id, selectedValue, open, mounted, transitionStatus, items, popupProps, listProps, inputProps, itemProps, openMethod, triggerProps, selectionMode, name, disabled, readOnly, required, grid, virtualized, openOnInputClick, itemToStringLabel, modal, isItemEqualToValue, submitOnItemClick, hasInputValue, inlineProp, autoHighlightMode, form]);
|
|
1192
1250
|
const hiddenInputRef = useMergedRefs(inputRefProp, validation.inputRef);
|
|
1193
1251
|
const itemsContextValue = React.useMemo(() => ({
|
|
1194
1252
|
query,
|
|
@@ -1232,7 +1290,7 @@ function AriaCombobox(props) {
|
|
|
1232
1290
|
},
|
|
1233
1291
|
// Handle browser autofill.
|
|
1234
1292
|
onChange(event) {
|
|
1235
|
-
// Workaround for https://github.com/
|
|
1293
|
+
// Workaround for https://github.com/react/react/issues/9023
|
|
1236
1294
|
if (event.nativeEvent.defaultPrevented || disabled || readOnly) {
|
|
1237
1295
|
return;
|
|
1238
1296
|
}
|
|
@@ -1327,7 +1385,51 @@ const triggerStateAttributesMapping = {
|
|
|
1327
1385
|
} : null
|
|
1328
1386
|
};
|
|
1329
1387
|
|
|
1330
|
-
|
|
1388
|
+
/**
|
|
1389
|
+
* The popup side is only meaningful while the positioner is mounted, as the store retains the
|
|
1390
|
+
* last resolved side after the popup unmounts.
|
|
1391
|
+
*/
|
|
1392
|
+
function usePopupSide(store) {
|
|
1393
|
+
const mounted = useStore(store, selectors.mounted);
|
|
1394
|
+
const popupSide = useStore(store, selectors.popupSide);
|
|
1395
|
+
const positionerElement = useStore(store, selectors.positionerElement);
|
|
1396
|
+
return mounted && positionerElement ? popupSide : null;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Whether the filtered list has no items to show.
|
|
1401
|
+
*/
|
|
1402
|
+
function useListEmpty() {
|
|
1403
|
+
return useComboboxDerivedItemsContext().filteredItems.length === 0;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* The arrow keys that move the chip highlight backwards and forwards, in that order.
|
|
1408
|
+
*/
|
|
1409
|
+
function getChipNavigationKeys(direction) {
|
|
1410
|
+
return direction === 'rtl' ? ['ArrowRight', 'ArrowLeft'] : ['ArrowLeft', 'ArrowRight'];
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* Where the highlight lands once the chip at `index` is removed, or `undefined` for no highlight.
|
|
1415
|
+
*/
|
|
1416
|
+
function getIndexAfterChipRemoval(index, chipCount) {
|
|
1417
|
+
const nextIndex = index >= chipCount - 1 ? chipCount - 2 : index;
|
|
1418
|
+
return nextIndex >= 0 ? nextIndex : undefined;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Commits the highlighted item by clicking it, tagging the originating event so the item's
|
|
1423
|
+
* handler can attribute the selection to it.
|
|
1424
|
+
*/
|
|
1425
|
+
function clickHighlightedItem(store, activeIndex, nativeEvent) {
|
|
1426
|
+
const listItem = store.state.listRef.current[activeIndex];
|
|
1427
|
+
if (listItem) {
|
|
1428
|
+
store.state.selectionEventRef.current = nativeEvent;
|
|
1429
|
+
listItem.click();
|
|
1430
|
+
store.state.selectionEventRef.current = null;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1331
1433
|
|
|
1332
1434
|
/**
|
|
1333
1435
|
* A button that opens the popup.
|
|
@@ -1357,20 +1459,14 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
|
|
|
1357
1459
|
labelId: fieldLabelId
|
|
1358
1460
|
} = useLabelableContext();
|
|
1359
1461
|
const store = useComboboxRootContext();
|
|
1360
|
-
const {
|
|
1361
|
-
filteredItems
|
|
1362
|
-
} = useComboboxDerivedItemsContext();
|
|
1363
1462
|
const selectionMode = useStore(store, selectors.selectionMode);
|
|
1364
1463
|
const comboboxDisabled = useStore(store, selectors.disabled);
|
|
1365
1464
|
const readOnly = useStore(store, selectors.readOnly);
|
|
1366
1465
|
const required = useStore(store, selectors.required);
|
|
1367
|
-
const mounted = useStore(store, selectors.mounted);
|
|
1368
|
-
const popupSideValue = useStore(store, selectors.popupSide);
|
|
1369
1466
|
const positionerElement = useStore(store, selectors.positionerElement);
|
|
1370
1467
|
const listElement = useStore(store, selectors.listElement);
|
|
1371
1468
|
const storedPopupId = useStore(store, selectors.popupId);
|
|
1372
1469
|
const triggerProps = useStore(store, selectors.triggerProps);
|
|
1373
|
-
const triggerElement = useStore(store, selectors.triggerElement);
|
|
1374
1470
|
const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
|
|
1375
1471
|
const rootId = useStore(store, selectors.id);
|
|
1376
1472
|
const comboboxLabelId = useStore(store, selectors.labelId);
|
|
@@ -1383,8 +1479,8 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
|
|
|
1383
1479
|
const inputValue = useComboboxInputValueContext();
|
|
1384
1480
|
const focusTimeout = useTimeout();
|
|
1385
1481
|
const disabled = fieldDisabled || comboboxDisabled || disabledProp;
|
|
1386
|
-
const listEmpty =
|
|
1387
|
-
const popupSide =
|
|
1482
|
+
const listEmpty = useListEmpty();
|
|
1483
|
+
const popupSide = usePopupSide(store);
|
|
1388
1484
|
useLabelableId({
|
|
1389
1485
|
id: inputInsidePopup ? idProp : undefined
|
|
1390
1486
|
});
|
|
@@ -1402,18 +1498,6 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
|
|
|
1402
1498
|
function trackPointerType(event) {
|
|
1403
1499
|
currentPointerTypeRef.current = event.pointerType;
|
|
1404
1500
|
}
|
|
1405
|
-
const domReference = floatingRootContext.useState('domReferenceElement');
|
|
1406
|
-
|
|
1407
|
-
// Update the floating root context to use the trigger element when it differs from the current reference.
|
|
1408
|
-
// This ensures useClick and useTypeahead attach handlers to the correct element.
|
|
1409
|
-
React.useEffect(() => {
|
|
1410
|
-
if (!inputInsidePopup) {
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1413
|
-
if (triggerElement && triggerElement !== domReference) {
|
|
1414
|
-
floatingRootContext.set('domReferenceElement', triggerElement);
|
|
1415
|
-
}
|
|
1416
|
-
}, [triggerElement, domReference, floatingRootContext, inputInsidePopup]);
|
|
1417
1501
|
const {
|
|
1418
1502
|
reference: triggerTypeaheadProps
|
|
1419
1503
|
} = useTypeahead(floatingRootContext, {
|
|
@@ -1424,7 +1508,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
|
|
|
1424
1508
|
onMatch(index) {
|
|
1425
1509
|
const nextSelectedValue = store.state.valuesRef.current[index];
|
|
1426
1510
|
if (nextSelectedValue !== undefined) {
|
|
1427
|
-
store.state.setSelectedValue(nextSelectedValue, createChangeEventDetails(
|
|
1511
|
+
store.state.setSelectedValue(nextSelectedValue, createChangeEventDetails(none));
|
|
1428
1512
|
}
|
|
1429
1513
|
}
|
|
1430
1514
|
});
|
|
@@ -1459,7 +1543,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
|
|
|
1459
1543
|
id,
|
|
1460
1544
|
tabIndex: inputInsidePopup ? 0 : -1,
|
|
1461
1545
|
role: inputInsidePopup ? 'combobox' : undefined,
|
|
1462
|
-
'aria-expanded': open
|
|
1546
|
+
'aria-expanded': open,
|
|
1463
1547
|
'aria-haspopup': inputInsidePopup ? 'dialog' : 'listbox',
|
|
1464
1548
|
'aria-controls': ariaControls,
|
|
1465
1549
|
'aria-required': inputInsidePopup ? required || undefined : undefined,
|
|
@@ -1506,22 +1590,20 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
|
|
|
1506
1590
|
}
|
|
1507
1591
|
const doc = ownerDocument(event.currentTarget);
|
|
1508
1592
|
function handleMouseUp(mouseEvent) {
|
|
1509
|
-
|
|
1593
|
+
const currentTriggerElement = store.state.triggerElement;
|
|
1594
|
+
if (!currentTriggerElement) {
|
|
1510
1595
|
return;
|
|
1511
1596
|
}
|
|
1512
1597
|
const mouseUpTarget = getTarget(mouseEvent);
|
|
1513
1598
|
const positioner = store.state.positionerElement;
|
|
1514
1599
|
const list = store.state.listElement;
|
|
1515
|
-
if (contains(
|
|
1600
|
+
if (contains(currentTriggerElement, mouseUpTarget) || contains(positioner, mouseUpTarget) || contains(list, mouseUpTarget)) {
|
|
1516
1601
|
return;
|
|
1517
1602
|
}
|
|
1518
|
-
|
|
1519
|
-
const withinHorizontal = mouseEvent.clientX >= bounds.left - BOUNDARY_OFFSET && mouseEvent.clientX <= bounds.right + BOUNDARY_OFFSET;
|
|
1520
|
-
const withinVertical = mouseEvent.clientY >= bounds.top - BOUNDARY_OFFSET && mouseEvent.clientY <= bounds.bottom + BOUNDARY_OFFSET;
|
|
1521
|
-
if (withinHorizontal && withinVertical) {
|
|
1603
|
+
if (isMouseWithinBounds(mouseEvent, currentTriggerElement)) {
|
|
1522
1604
|
return;
|
|
1523
1605
|
}
|
|
1524
|
-
store.state.setOpen(false, createChangeEventDetails(
|
|
1606
|
+
store.state.setOpen(false, createChangeEventDetails(cancelOpen, mouseEvent));
|
|
1525
1607
|
}
|
|
1526
1608
|
if (inputInsidePopup) {
|
|
1527
1609
|
doc.addEventListener('mouseup', handleMouseUp, {
|
|
@@ -1530,7 +1612,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
|
|
|
1530
1612
|
}
|
|
1531
1613
|
},
|
|
1532
1614
|
onKeyDown(event) {
|
|
1533
|
-
if (
|
|
1615
|
+
if (readOnly) {
|
|
1534
1616
|
return;
|
|
1535
1617
|
}
|
|
1536
1618
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
@@ -1539,7 +1621,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
|
|
|
1539
1621
|
store.state.inputRef.current?.focus();
|
|
1540
1622
|
}
|
|
1541
1623
|
}
|
|
1542
|
-
}, validation
|
|
1624
|
+
}, validation.getValidationProps(disabled, elementProps), getButtonProps],
|
|
1543
1625
|
stateAttributesMapping: triggerStateAttributesMapping
|
|
1544
1626
|
});
|
|
1545
1627
|
return element;
|
|
@@ -1614,9 +1696,6 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1614
1696
|
const positioning = useComboboxPositionerContext(true);
|
|
1615
1697
|
const hasPositionerParent = Boolean(positioning);
|
|
1616
1698
|
const store = useComboboxRootContext();
|
|
1617
|
-
const {
|
|
1618
|
-
filteredItems
|
|
1619
|
-
} = useComboboxDerivedItemsContext();
|
|
1620
1699
|
// `inputValue` can't be placed in the store.
|
|
1621
1700
|
// https://github.com/mui/base-ui/issues/2703
|
|
1622
1701
|
const inputValue = useComboboxInputValueContext();
|
|
@@ -1633,19 +1712,16 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1633
1712
|
const open = useStore(store, selectors.open);
|
|
1634
1713
|
const mounted = useStore(store, selectors.mounted);
|
|
1635
1714
|
const selectedValue = useStore(store, selectors.selectedValue);
|
|
1636
|
-
const popupSideValue = useStore(store, selectors.popupSide);
|
|
1637
|
-
const positionerElement = useStore(store, selectors.positionerElement);
|
|
1638
1715
|
const rootId = useStore(store, selectors.id);
|
|
1639
1716
|
const inline = useStore(store, selectors.inline);
|
|
1640
1717
|
const modal = useStore(store, selectors.modal);
|
|
1641
1718
|
const autoHighlightEnabled = Boolean(autoHighlightMode);
|
|
1642
|
-
const popupSide =
|
|
1719
|
+
const popupSide = usePopupSide(store);
|
|
1643
1720
|
const disabled = fieldDisabled || comboboxDisabled || disabledProp;
|
|
1644
|
-
const listEmpty =
|
|
1721
|
+
const listEmpty = useListEmpty();
|
|
1645
1722
|
const isInsidePopup = hasPositionerParent || inline;
|
|
1646
1723
|
const focusManagerModal = !isInsidePopup || modal;
|
|
1647
1724
|
const id = useBaseUiId(idProp ?? (!isInsidePopup ? rootId : undefined));
|
|
1648
|
-
const ariaLabelledBy = resolveAriaLabelledBy(fieldLabelId, undefined);
|
|
1649
1725
|
const fieldStateForInput = hasPositionerParent ? DEFAULT_FIELD_STATE_ATTRIBUTES : fieldState;
|
|
1650
1726
|
const [composingValue, setComposingValue] = React.useState(null);
|
|
1651
1727
|
const isComposingRef = React.useRef(false);
|
|
@@ -1663,7 +1739,17 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1663
1739
|
inputOwnsFormValue
|
|
1664
1740
|
});
|
|
1665
1741
|
});
|
|
1666
|
-
const validationProps = hasPositionerParent
|
|
1742
|
+
const validationProps = hasPositionerParent ? elementProps : validation.getValidationProps(disabled, elementProps);
|
|
1743
|
+
function clearHighlight() {
|
|
1744
|
+
store.state.setIndices({
|
|
1745
|
+
activeIndex: null,
|
|
1746
|
+
selectedIndex: null,
|
|
1747
|
+
type: store.state.keyboardActiveRef.current ? keyboard : pointer
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
function markPointerActive() {
|
|
1751
|
+
store.state.keyboardActiveRef.current = false;
|
|
1752
|
+
}
|
|
1667
1753
|
const state = {
|
|
1668
1754
|
...fieldStateForInput,
|
|
1669
1755
|
open,
|
|
@@ -1681,9 +1767,7 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1681
1767
|
highlightedChipIndex
|
|
1682
1768
|
} = comboboxChipsContext;
|
|
1683
1769
|
const renderedChipsCount = comboboxChipsContext.chipsRef.current.length;
|
|
1684
|
-
const
|
|
1685
|
-
const previousChipKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
|
|
1686
|
-
const nextChipKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
|
|
1770
|
+
const [previousChipKey, nextChipKey] = getChipNavigationKeys(direction);
|
|
1687
1771
|
if (highlightedChipIndex !== undefined) {
|
|
1688
1772
|
if (event.key === previousChipKey) {
|
|
1689
1773
|
event.preventDefault();
|
|
@@ -1702,14 +1786,8 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1702
1786
|
} else if (event.key === 'Backspace' || event.key === 'Delete') {
|
|
1703
1787
|
event.preventDefault();
|
|
1704
1788
|
// Move highlight appropriately after removal.
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
nextIndex = computedNextIndex >= 0 ? computedNextIndex : undefined;
|
|
1708
|
-
store.state.setIndices({
|
|
1709
|
-
activeIndex: null,
|
|
1710
|
-
selectedIndex: null,
|
|
1711
|
-
type: 'keyboard'
|
|
1712
|
-
});
|
|
1789
|
+
nextIndex = getIndexAfterChipRemoval(highlightedChipIndex, selectedValue.length);
|
|
1790
|
+
clearHighlight();
|
|
1713
1791
|
}
|
|
1714
1792
|
return nextIndex;
|
|
1715
1793
|
}
|
|
@@ -1718,13 +1796,6 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1718
1796
|
if (event.key === previousChipKey && (event.currentTarget.selectionStart ?? 0) === 0 && selectedValue.length > 0) {
|
|
1719
1797
|
event.preventDefault();
|
|
1720
1798
|
nextIndex = renderedChipsCount > 0 ? renderedChipsCount - 1 : undefined;
|
|
1721
|
-
} else if (event.key === 'Backspace' && event.currentTarget.value === '' && selectedValue.length > 0) {
|
|
1722
|
-
store.state.setIndices({
|
|
1723
|
-
activeIndex: null,
|
|
1724
|
-
selectedIndex: null,
|
|
1725
|
-
type: 'keyboard'
|
|
1726
|
-
});
|
|
1727
|
-
event.preventDefault();
|
|
1728
1799
|
}
|
|
1729
1800
|
return nextIndex;
|
|
1730
1801
|
}
|
|
@@ -1732,11 +1803,10 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1732
1803
|
state,
|
|
1733
1804
|
ref: [forwardedRef, store.state.inputRef, setInputElement],
|
|
1734
1805
|
props: [inputProps, triggerProps, {
|
|
1735
|
-
|
|
1736
|
-
value: componentProps.value ?? composingValue ?? inputValue,
|
|
1806
|
+
value: composingValue ?? inputValue,
|
|
1737
1807
|
'aria-readonly': readOnly || undefined,
|
|
1738
1808
|
'aria-required': required || undefined,
|
|
1739
|
-
'aria-labelledby':
|
|
1809
|
+
'aria-labelledby': fieldLabelId,
|
|
1740
1810
|
disabled,
|
|
1741
1811
|
readOnly,
|
|
1742
1812
|
required: selectionMode === 'none' ? required : undefined,
|
|
@@ -1791,11 +1861,23 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1791
1861
|
store.state.setInputValue(next, createChangeEventDetails(inputChange, event.nativeEvent));
|
|
1792
1862
|
},
|
|
1793
1863
|
onChange(event) {
|
|
1864
|
+
const nativeEvent = event.nativeEvent;
|
|
1794
1865
|
// Autofill may not provide `inputType` (Chrome) or may report
|
|
1795
1866
|
// `insertReplacementText` (Firefox).
|
|
1796
|
-
const inputType =
|
|
1867
|
+
const inputType = nativeEvent.inputType;
|
|
1797
1868
|
const autofillLikeInput = !inputType || inputType === 'insertReplacementText';
|
|
1869
|
+
// During composition the input is always considered typed into.
|
|
1798
1870
|
const shouldOpenOnInput = isComposingRef.current || !autofillLikeInput;
|
|
1871
|
+
function maybeOpenOnInput(trimmed) {
|
|
1872
|
+
if (readOnly || disabled || !trimmed || !shouldOpenOnInput) {
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
store.state.setOpen(true, createChangeEventDetails(inputChange, nativeEvent));
|
|
1876
|
+
// When autoHighlight is enabled, keep the highlight (will be set to 0 in root).
|
|
1877
|
+
if (!autoHighlightEnabled) {
|
|
1878
|
+
clearHighlight();
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1799
1881
|
|
|
1800
1882
|
// During IME composition, avoid propagating controlled updates to prevent
|
|
1801
1883
|
// filtering the options prematurely so `Empty` won't show incorrectly.
|
|
@@ -1807,38 +1889,23 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1807
1889
|
const nextVal = event.currentTarget.value;
|
|
1808
1890
|
setComposingValue(nextVal);
|
|
1809
1891
|
if (nextVal === '' && !store.state.openOnInputClick && !store.state.inputInsidePopup) {
|
|
1810
|
-
store.state.setOpen(false, createChangeEventDetails(inputClear,
|
|
1892
|
+
store.state.setOpen(false, createChangeEventDetails(inputClear, nativeEvent));
|
|
1811
1893
|
}
|
|
1812
1894
|
const trimmed = nextVal.trim();
|
|
1813
1895
|
const shouldMaintainHighlight = autoHighlightEnabled && trimmed !== '';
|
|
1814
|
-
|
|
1815
|
-
if (shouldOpenOnInput) {
|
|
1816
|
-
store.state.setOpen(true, createChangeEventDetails(inputChange, event.nativeEvent));
|
|
1817
|
-
if (!autoHighlightEnabled) {
|
|
1818
|
-
store.state.setIndices({
|
|
1819
|
-
activeIndex: null,
|
|
1820
|
-
selectedIndex: null,
|
|
1821
|
-
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
|
|
1822
|
-
});
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1896
|
+
maybeOpenOnInput(trimmed);
|
|
1826
1897
|
if (open && store.state.activeIndex !== null && !shouldMaintainHighlight) {
|
|
1827
|
-
|
|
1828
|
-
activeIndex: null,
|
|
1829
|
-
selectedIndex: null,
|
|
1830
|
-
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
|
|
1831
|
-
});
|
|
1898
|
+
clearHighlight();
|
|
1832
1899
|
}
|
|
1833
1900
|
return;
|
|
1834
1901
|
}
|
|
1835
|
-
const inputChangeDetails = createChangeEventDetails(inputChange,
|
|
1902
|
+
const inputChangeDetails = createChangeEventDetails(inputChange, nativeEvent);
|
|
1836
1903
|
store.state.setInputValue(event.currentTarget.value, inputChangeDetails);
|
|
1837
1904
|
if (inputChangeDetails.isCanceled) {
|
|
1838
1905
|
return;
|
|
1839
1906
|
}
|
|
1840
1907
|
const empty = event.currentTarget.value === '';
|
|
1841
|
-
const clearDetails = createChangeEventDetails(inputClear,
|
|
1908
|
+
const clearDetails = createChangeEventDetails(inputClear, nativeEvent);
|
|
1842
1909
|
if (empty && !store.state.inputInsidePopup) {
|
|
1843
1910
|
if (selectionMode === 'single') {
|
|
1844
1911
|
store.state.setSelectedValue(null, clearDetails);
|
|
@@ -1847,30 +1914,13 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1847
1914
|
store.state.setOpen(false, clearDetails);
|
|
1848
1915
|
}
|
|
1849
1916
|
}
|
|
1850
|
-
|
|
1851
|
-
if (!readOnly && !disabled && trimmed) {
|
|
1852
|
-
if (shouldOpenOnInput) {
|
|
1853
|
-
store.state.setOpen(true, createChangeEventDetails(inputChange, event.nativeEvent));
|
|
1854
|
-
// When autoHighlight is enabled, keep the highlight (will be set to 0 in root).
|
|
1855
|
-
if (!autoHighlightEnabled) {
|
|
1856
|
-
store.state.setIndices({
|
|
1857
|
-
activeIndex: null,
|
|
1858
|
-
selectedIndex: null,
|
|
1859
|
-
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
|
|
1860
|
-
});
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1917
|
+
maybeOpenOnInput(event.currentTarget.value.trim());
|
|
1864
1918
|
|
|
1865
1919
|
// When the user types, ensure the list resets its highlight so that
|
|
1866
1920
|
// virtual focus returns to the input (aria-activedescendant is
|
|
1867
1921
|
// cleared).
|
|
1868
1922
|
if (open && store.state.activeIndex !== null && !autoHighlightEnabled) {
|
|
1869
|
-
|
|
1870
|
-
activeIndex: null,
|
|
1871
|
-
selectedIndex: null,
|
|
1872
|
-
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
|
|
1873
|
-
});
|
|
1923
|
+
clearHighlight();
|
|
1874
1924
|
}
|
|
1875
1925
|
},
|
|
1876
1926
|
onKeyDown(event) {
|
|
@@ -1916,11 +1966,7 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1916
1966
|
const removalIndex = renderedChipsCount > 0 ? renderedChipsCount - 1 : selectedValue.length - 1;
|
|
1917
1967
|
const newValue = selectedValue.filter((_, index) => index !== removalIndex);
|
|
1918
1968
|
// If the removed item was also the active (highlighted) item, clear highlight
|
|
1919
|
-
|
|
1920
|
-
activeIndex: null,
|
|
1921
|
-
selectedIndex: null,
|
|
1922
|
-
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
|
|
1923
|
-
});
|
|
1969
|
+
clearHighlight();
|
|
1924
1970
|
store.state.setSelectedValue(newValue, createChangeEventDetails(none, event.nativeEvent));
|
|
1925
1971
|
return;
|
|
1926
1972
|
}
|
|
@@ -1950,20 +1996,11 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
|
|
|
1950
1996
|
return;
|
|
1951
1997
|
}
|
|
1952
1998
|
stopEvent(event);
|
|
1953
|
-
|
|
1954
|
-
if (listItem) {
|
|
1955
|
-
store.state.selectionEventRef.current = nativeEvent;
|
|
1956
|
-
listItem.click();
|
|
1957
|
-
store.state.selectionEventRef.current = null;
|
|
1958
|
-
}
|
|
1999
|
+
clickHighlightedItem(store, activeIndex, nativeEvent);
|
|
1959
2000
|
}
|
|
1960
2001
|
},
|
|
1961
|
-
onPointerMove
|
|
1962
|
-
|
|
1963
|
-
},
|
|
1964
|
-
onPointerDown() {
|
|
1965
|
-
store.state.keyboardActiveRef.current = false;
|
|
1966
|
-
}
|
|
2002
|
+
onPointerMove: markPointerActive,
|
|
2003
|
+
onPointerDown: markPointerActive
|
|
1967
2004
|
}, validationProps],
|
|
1968
2005
|
stateAttributesMapping: triggerStateAttributesMapping
|
|
1969
2006
|
});
|
|
@@ -2015,20 +2052,14 @@ const ComboboxInputGroup = /*#__PURE__*/React.forwardRef(function ComboboxInputG
|
|
|
2015
2052
|
state: fieldState
|
|
2016
2053
|
} = useFieldRootContext();
|
|
2017
2054
|
const store = useComboboxRootContext();
|
|
2018
|
-
const {
|
|
2019
|
-
filteredItems
|
|
2020
|
-
} = useComboboxDerivedItemsContext();
|
|
2021
2055
|
const open = useStore(store, selectors.open);
|
|
2022
|
-
const mounted = useStore(store, selectors.mounted);
|
|
2023
|
-
const popupSideValue = useStore(store, selectors.popupSide);
|
|
2024
|
-
const positionerElement = useStore(store, selectors.positionerElement);
|
|
2025
2056
|
const comboboxDisabled = useStore(store, selectors.disabled);
|
|
2026
2057
|
const readOnly = useStore(store, selectors.readOnly);
|
|
2027
2058
|
const hasSelectedValue = useStore(store, selectors.hasSelectedValue);
|
|
2028
2059
|
const selectionMode = useStore(store, selectors.selectionMode);
|
|
2029
|
-
const popupSide =
|
|
2060
|
+
const popupSide = usePopupSide(store);
|
|
2030
2061
|
const disabled = comboboxDisabled;
|
|
2031
|
-
const listEmpty =
|
|
2062
|
+
const listEmpty = useListEmpty();
|
|
2032
2063
|
const placeholder = selectionMode === 'none' ? false : !hasSelectedValue;
|
|
2033
2064
|
const state = {
|
|
2034
2065
|
...fieldState,
|
|
@@ -2164,19 +2195,21 @@ const ComboboxClear = /*#__PURE__*/React.forwardRef(function ComboboxClear(compo
|
|
|
2164
2195
|
if (disabled || readOnly) {
|
|
2165
2196
|
return;
|
|
2166
2197
|
}
|
|
2167
|
-
const
|
|
2198
|
+
const type = store.state.keyboardActiveRef.current ? keyboard : pointer;
|
|
2168
2199
|
store.state.setInputValue('', createChangeEventDetails(clearPress, event.nativeEvent));
|
|
2169
2200
|
if (selectionMode !== 'none') {
|
|
2170
2201
|
store.state.setSelectedValue(Array.isArray(selectedValue) ? [] : null, createChangeEventDetails(clearPress, event.nativeEvent));
|
|
2202
|
+
// A distinct object shape: `Store.update` iterates own keys, so passing an explicit
|
|
2203
|
+
// `selectedIndex: undefined` would overwrite the state instead of leaving it alone.
|
|
2171
2204
|
store.state.setIndices({
|
|
2172
2205
|
activeIndex: null,
|
|
2173
2206
|
selectedIndex: null,
|
|
2174
|
-
type
|
|
2207
|
+
type
|
|
2175
2208
|
});
|
|
2176
2209
|
} else {
|
|
2177
2210
|
store.state.setIndices({
|
|
2178
2211
|
activeIndex: null,
|
|
2179
|
-
type
|
|
2212
|
+
type
|
|
2180
2213
|
});
|
|
2181
2214
|
}
|
|
2182
2215
|
store.state.inputRef.current?.focus();
|
|
@@ -2220,9 +2253,6 @@ function ComboboxCollection(props) {
|
|
|
2220
2253
|
} = useComboboxDerivedItemsContext();
|
|
2221
2254
|
const groupContext = useGroupCollectionContext();
|
|
2222
2255
|
const itemsToRender = groupContext ? groupContext.items : filteredItems;
|
|
2223
|
-
if (!itemsToRender) {
|
|
2224
|
-
return null;
|
|
2225
|
-
}
|
|
2226
2256
|
return /*#__PURE__*/jsx(React.Fragment, {
|
|
2227
2257
|
children: itemsToRender.map(children)
|
|
2228
2258
|
});
|
|
@@ -2246,7 +2276,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
|
|
|
2246
2276
|
} = useComboboxDerivedItemsContext();
|
|
2247
2277
|
const selectionMode = useStore(store, selectors.selectionMode);
|
|
2248
2278
|
const grid = useStore(store, selectors.grid);
|
|
2249
|
-
const
|
|
2279
|
+
const listProps = useStore(store, selectors.listProps);
|
|
2250
2280
|
const virtualized = useStore(store, selectors.virtualized);
|
|
2251
2281
|
const forceMounted = useStore(store, selectors.forceMounted);
|
|
2252
2282
|
const multiple = selectionMode === 'multiple';
|
|
@@ -2260,7 +2290,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
|
|
|
2260
2290
|
|
|
2261
2291
|
// Support "closed template" API: if children is a function, implicitly wrap it
|
|
2262
2292
|
// with a Combobox.Collection that reads items from context/root.
|
|
2263
|
-
// Ensures this component's `
|
|
2293
|
+
// Ensures this component's `listProps` subscription does not cause <Combobox.Item>
|
|
2264
2294
|
// to re-render on every active index change.
|
|
2265
2295
|
const resolvedChildren = React.useMemo(() => {
|
|
2266
2296
|
if (typeof children === 'function') {
|
|
@@ -2277,7 +2307,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
|
|
|
2277
2307
|
const element = useRenderElement('div', componentProps, {
|
|
2278
2308
|
state,
|
|
2279
2309
|
ref: [forwardedRef, setListElement, hasPositionerContext ? null : setPositionerElement],
|
|
2280
|
-
props: [
|
|
2310
|
+
props: [listProps, {
|
|
2281
2311
|
children: resolvedChildren,
|
|
2282
2312
|
tabIndex: -1,
|
|
2283
2313
|
id: floatingId,
|
|
@@ -2294,13 +2324,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
|
|
|
2294
2324
|
return;
|
|
2295
2325
|
}
|
|
2296
2326
|
stopEvent(event);
|
|
2297
|
-
|
|
2298
|
-
const listItem = store.state.listRef.current[activeIndex];
|
|
2299
|
-
if (listItem) {
|
|
2300
|
-
store.state.selectionEventRef.current = nativeEvent;
|
|
2301
|
-
listItem.click();
|
|
2302
|
-
store.state.selectionEventRef.current = null;
|
|
2303
|
-
}
|
|
2327
|
+
clickHighlightedItem(store, activeIndex, event.nativeEvent);
|
|
2304
2328
|
}
|
|
2305
2329
|
},
|
|
2306
2330
|
onKeyDownCapture() {
|
|
@@ -2359,7 +2383,7 @@ function useInitialLiveRegionTextMutation() {
|
|
|
2359
2383
|
if (textNode == null) {
|
|
2360
2384
|
return undefined;
|
|
2361
2385
|
}
|
|
2362
|
-
const originalValue = textNode.
|
|
2386
|
+
const originalValue = textNode.data;
|
|
2363
2387
|
const markedValue = `${originalValue}${LIVE_REGION_MARKER}`;
|
|
2364
2388
|
textNode.nodeValue = markedValue;
|
|
2365
2389
|
timeout.start(INITIAL_LIVE_REGION_TEXT_MUTATION_RESET_DELAY, () => {
|
|
@@ -2488,24 +2512,23 @@ const ComboboxPositioner = /*#__PURE__*/React.forwardRef(function ComboboxPositi
|
|
|
2488
2512
|
render,
|
|
2489
2513
|
className,
|
|
2490
2514
|
anchor,
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2515
|
+
// `useAnchorPositioning` applies the same defaults to the undefined values; the names
|
|
2516
|
+
// remain destructured to exclude the props from `elementProps`.
|
|
2517
|
+
positionMethod,
|
|
2518
|
+
side,
|
|
2519
|
+
align,
|
|
2520
|
+
sideOffset,
|
|
2521
|
+
alignOffset,
|
|
2496
2522
|
collisionBoundary = 'clipping-ancestors',
|
|
2497
|
-
collisionPadding
|
|
2498
|
-
arrowPadding
|
|
2499
|
-
sticky
|
|
2523
|
+
collisionPadding,
|
|
2524
|
+
arrowPadding,
|
|
2525
|
+
sticky,
|
|
2500
2526
|
disableAnchorTracking = false,
|
|
2501
2527
|
collisionAvoidance = DROPDOWN_COLLISION_AVOIDANCE,
|
|
2502
2528
|
style: styleProp,
|
|
2503
2529
|
...elementProps
|
|
2504
2530
|
} = componentProps;
|
|
2505
2531
|
const store = useComboboxRootContext();
|
|
2506
|
-
const {
|
|
2507
|
-
filteredItems
|
|
2508
|
-
} = useComboboxDerivedItemsContext();
|
|
2509
2532
|
const floatingRootContext = useComboboxFloatingContext();
|
|
2510
2533
|
const keepMounted = useComboboxPortalContext();
|
|
2511
2534
|
const modal = useStore(store, selectors.modal);
|
|
@@ -2518,7 +2541,7 @@ const ComboboxPositioner = /*#__PURE__*/React.forwardRef(function ComboboxPositi
|
|
|
2518
2541
|
const inputGroupElement = useStore(store, selectors.inputGroupElement);
|
|
2519
2542
|
const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
|
|
2520
2543
|
const transitionStatus = useStore(store, selectors.transitionStatus);
|
|
2521
|
-
const empty =
|
|
2544
|
+
const empty = useListEmpty();
|
|
2522
2545
|
const resolvedAnchor = anchor ?? (inputInsidePopup ? triggerElement : inputGroupElement ?? inputElement);
|
|
2523
2546
|
const positioning = useAnchorPositioning({
|
|
2524
2547
|
anchor: resolvedAnchor,
|
|
@@ -2593,18 +2616,16 @@ const ComboboxPopup = /*#__PURE__*/React.forwardRef(function ComboboxPopup(compo
|
|
|
2593
2616
|
const store = useComboboxRootContext();
|
|
2594
2617
|
const positioning = useComboboxPositionerContext();
|
|
2595
2618
|
const floatingRootContext = useComboboxFloatingContext();
|
|
2596
|
-
const {
|
|
2597
|
-
filteredItems
|
|
2598
|
-
} = useComboboxDerivedItemsContext();
|
|
2599
2619
|
const mounted = useStore(store, selectors.mounted);
|
|
2600
2620
|
const open = useStore(store, selectors.open);
|
|
2601
2621
|
const openMethod = useStore(store, selectors.openMethod);
|
|
2622
|
+
const popupProps = useStore(store, selectors.popupProps);
|
|
2602
2623
|
const transitionStatus = useStore(store, selectors.transitionStatus);
|
|
2603
2624
|
const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
|
|
2604
2625
|
const inputElement = useStore(store, selectors.inputElement);
|
|
2605
2626
|
const modal = useStore(store, selectors.modal);
|
|
2606
2627
|
const rootId = useStore(store, selectors.id);
|
|
2607
|
-
const empty =
|
|
2628
|
+
const empty = useListEmpty();
|
|
2608
2629
|
const popupId = elementProps.id ?? (inputInsidePopup ? getComboboxPopupId(rootId) : undefined);
|
|
2609
2630
|
useIsoLayoutEffect(() => {
|
|
2610
2631
|
// Prefer the rendered DOM id, which a `render` prop element or function may override.
|
|
@@ -2633,10 +2654,9 @@ const ComboboxPopup = /*#__PURE__*/React.forwardRef(function ComboboxPopup(compo
|
|
|
2633
2654
|
const element = useRenderElement('div', componentProps, {
|
|
2634
2655
|
state,
|
|
2635
2656
|
ref: [forwardedRef, store.state.popupRef],
|
|
2636
|
-
props: [{
|
|
2657
|
+
props: [popupProps, {
|
|
2637
2658
|
id: popupId,
|
|
2638
2659
|
role: inputInsidePopup ? 'dialog' : 'presentation',
|
|
2639
|
-
tabIndex: -1,
|
|
2640
2660
|
onFocus(event) {
|
|
2641
2661
|
const target = getTarget(event.nativeEvent);
|
|
2642
2662
|
if (openMethod !== 'touch' && (contains(store.state.listElement, target) || target === event.currentTarget)) {
|
|
@@ -2783,7 +2803,7 @@ const ComboboxGroupLabel = /*#__PURE__*/React.forwardRef(function ComboboxGroupL
|
|
|
2783
2803
|
useIsoLayoutEffect(() => {
|
|
2784
2804
|
setLabelId(id);
|
|
2785
2805
|
return () => {
|
|
2786
|
-
setLabelId(undefined);
|
|
2806
|
+
setLabelId(currentId => currentId === id ? undefined : currentId);
|
|
2787
2807
|
};
|
|
2788
2808
|
}, [id, setLabelId]);
|
|
2789
2809
|
const element = useRenderElement('div', componentProps, {
|
|
@@ -2825,27 +2845,27 @@ function ComboboxItemInner(props) {
|
|
|
2825
2845
|
style,
|
|
2826
2846
|
value: itemValue = null,
|
|
2827
2847
|
index: indexProp,
|
|
2828
|
-
disabled = false,
|
|
2848
|
+
disabled: disabledProp = false,
|
|
2829
2849
|
nativeButton = false,
|
|
2830
2850
|
...elementProps
|
|
2831
2851
|
} = componentProps;
|
|
2832
|
-
const didPointerDownRef = React.useRef(false);
|
|
2833
2852
|
const textRef = React.useRef(null);
|
|
2834
2853
|
const listItem = useCompositeListItem({
|
|
2854
|
+
guess: true,
|
|
2835
2855
|
index: indexProp,
|
|
2836
|
-
textRef
|
|
2837
|
-
indexGuessBehavior: IndexGuessBehavior.GuessFromOrder
|
|
2856
|
+
textRef
|
|
2838
2857
|
});
|
|
2839
2858
|
const store = useComboboxRootContext();
|
|
2840
2859
|
const isRow = useComboboxRowContext();
|
|
2841
2860
|
const hasItems = useComboboxHasItemsContext();
|
|
2842
|
-
const open = useStore(store, selectors.open);
|
|
2843
2861
|
const selectionMode = useStore(store, selectors.selectionMode);
|
|
2862
|
+
const rootDisabled = useStore(store, selectors.disabled);
|
|
2844
2863
|
const readOnly = useStore(store, selectors.readOnly);
|
|
2845
2864
|
const isItemEqualToValue = useStore(store, selectors.isItemEqualToValue);
|
|
2865
|
+
const disabled = rootDisabled || disabledProp;
|
|
2846
2866
|
const selectable = selectionMode !== 'none';
|
|
2847
|
-
const index = indexProp ??
|
|
2848
|
-
const hasRegistered =
|
|
2867
|
+
const index = indexProp ?? indexFromFilter ?? listItem.index;
|
|
2868
|
+
const hasRegistered = index !== -1;
|
|
2849
2869
|
const rootId = useStore(store, selectors.id);
|
|
2850
2870
|
const highlighted = useStore(store, selectors.isActive, index);
|
|
2851
2871
|
const matchesSelectedValue = useStore(store, selectors.isSelected, itemValue);
|
|
@@ -2870,31 +2890,24 @@ function ComboboxItemInner(props) {
|
|
|
2870
2890
|
}
|
|
2871
2891
|
const visibleMap = store.state.valuesRef.current;
|
|
2872
2892
|
visibleMap[index] = itemValue;
|
|
2873
|
-
|
|
2874
|
-
// Stable registry that doesn't depend on filtering. Assume that no
|
|
2875
|
-
// filtering had occurred at this point; otherwise, an `items` prop is
|
|
2876
|
-
// required.
|
|
2877
|
-
if (selectionMode !== 'none') {
|
|
2878
|
-
store.state.allValuesRef.current.push(itemValue);
|
|
2879
|
-
}
|
|
2880
2893
|
return () => {
|
|
2881
2894
|
delete visibleMap[index];
|
|
2882
2895
|
};
|
|
2883
|
-
}, [hasRegistered, hasItems, index, itemValue, store
|
|
2896
|
+
}, [hasRegistered, hasItems, index, itemValue, store]);
|
|
2884
2897
|
useIsoLayoutEffect(() => {
|
|
2885
|
-
if (!open) {
|
|
2886
|
-
didPointerDownRef.current = false;
|
|
2887
|
-
return;
|
|
2888
|
-
}
|
|
2889
2898
|
if (!hasRegistered || hasItems) {
|
|
2890
2899
|
return;
|
|
2891
2900
|
}
|
|
2901
|
+
|
|
2902
|
+
// Runs while closed as well (the list can stay mounted via `keepMounted` or a
|
|
2903
|
+
// force-mount) so the index tracks the item's composite position, keeping features
|
|
2904
|
+
// like closed-trigger typeahead in sync when the rendered order changes.
|
|
2892
2905
|
const selectedValue = store.state.selectedValue;
|
|
2893
2906
|
const lastSelectedValue = Array.isArray(selectedValue) ? selectedValue[selectedValue.length - 1] : selectedValue;
|
|
2894
2907
|
if (compareItemEquality(itemValue, lastSelectedValue, isItemEqualToValue)) {
|
|
2895
2908
|
store.set('selectedIndex', index);
|
|
2896
2909
|
}
|
|
2897
|
-
}, [hasRegistered, hasItems,
|
|
2910
|
+
}, [hasRegistered, hasItems, store, index, itemValue, isItemEqualToValue]);
|
|
2898
2911
|
const {
|
|
2899
2912
|
getButtonProps,
|
|
2900
2913
|
buttonRef
|
|
@@ -2929,7 +2942,12 @@ function ComboboxItemInner(props) {
|
|
|
2929
2942
|
// as it should be a `<div>` instead.
|
|
2930
2943
|
tabIndex: undefined,
|
|
2931
2944
|
onPointerDownCapture(event) {
|
|
2932
|
-
|
|
2945
|
+
// The compat `mouseup` only fires for the primary pointer, so a non-primary
|
|
2946
|
+
// touch must not overwrite the shared ref — a mismatch would make the primary
|
|
2947
|
+
// pointer's release read as a drag-select and commit a second time after `click`.
|
|
2948
|
+
if (event.isPrimary) {
|
|
2949
|
+
store.state.pointerDownItemRef.current = event.currentTarget;
|
|
2950
|
+
}
|
|
2933
2951
|
event.preventDefault();
|
|
2934
2952
|
},
|
|
2935
2953
|
onMouseDown(event) {
|
|
@@ -2944,8 +2962,8 @@ function ComboboxItemInner(props) {
|
|
|
2944
2962
|
commitSelection(event.nativeEvent);
|
|
2945
2963
|
},
|
|
2946
2964
|
onMouseUp(event) {
|
|
2947
|
-
const pointerStartedOnItem =
|
|
2948
|
-
|
|
2965
|
+
const pointerStartedOnItem = store.state.pointerDownItemRef.current === event.currentTarget;
|
|
2966
|
+
store.state.pointerDownItemRef.current = null;
|
|
2949
2967
|
if (disabled || readOnly || event.button !== 0 || pointerStartedOnItem || !highlighted) {
|
|
2950
2968
|
return;
|
|
2951
2969
|
}
|
|
@@ -3081,6 +3099,33 @@ const ComboboxEmpty = /*#__PURE__*/React.forwardRef(function ComboboxEmpty(compo
|
|
|
3081
3099
|
});
|
|
3082
3100
|
if (process.env.NODE_ENV !== "production") ComboboxEmpty.displayName = "ComboboxEmpty";
|
|
3083
3101
|
|
|
3102
|
+
/**
|
|
3103
|
+
* A visual separator between items.
|
|
3104
|
+
* Renders a `<div>` element.
|
|
3105
|
+
*
|
|
3106
|
+
* @internal
|
|
3107
|
+
*/
|
|
3108
|
+
const ListboxSeparator = /*#__PURE__*/React.forwardRef(function ListboxSeparator(componentProps, forwardedRef) {
|
|
3109
|
+
const {
|
|
3110
|
+
className,
|
|
3111
|
+
render,
|
|
3112
|
+
orientation = 'horizontal',
|
|
3113
|
+
style,
|
|
3114
|
+
...elementProps
|
|
3115
|
+
} = componentProps;
|
|
3116
|
+
const state = {
|
|
3117
|
+
orientation
|
|
3118
|
+
};
|
|
3119
|
+
return useRenderElement('div', componentProps, {
|
|
3120
|
+
state,
|
|
3121
|
+
ref: forwardedRef,
|
|
3122
|
+
props: [{
|
|
3123
|
+
role: 'presentation'
|
|
3124
|
+
}, elementProps]
|
|
3125
|
+
});
|
|
3126
|
+
});
|
|
3127
|
+
if (process.env.NODE_ENV !== "production") ListboxSeparator.displayName = "ListboxSeparator";
|
|
3128
|
+
|
|
3084
3129
|
/**
|
|
3085
3130
|
* Returns the internally filtered items.
|
|
3086
3131
|
*/
|
|
@@ -3147,7 +3192,8 @@ const ComboboxLabel = /*#__PURE__*/React.forwardRef(function ComboboxLabel(compo
|
|
|
3147
3192
|
id: defaultLabelId,
|
|
3148
3193
|
fallbackControlId: localControlId,
|
|
3149
3194
|
setLabelId(nextLabelId) {
|
|
3150
|
-
store.
|
|
3195
|
+
const resolvedLabelId = typeof nextLabelId === 'function' ? nextLabelId(store.state.labelId) : nextLabelId;
|
|
3196
|
+
store.set('labelId', resolvedLabelId);
|
|
3151
3197
|
}
|
|
3152
3198
|
});
|
|
3153
3199
|
return useRenderElement('div', componentProps, {
|
|
@@ -3190,11 +3236,10 @@ function ComboboxValue(props) {
|
|
|
3190
3236
|
}
|
|
3191
3237
|
|
|
3192
3238
|
const ComboboxItemIndicator = /*#__PURE__*/React.forwardRef(function ComboboxItemIndicator(componentProps, forwardedRef) {
|
|
3193
|
-
const keepMounted = componentProps.keepMounted ?? false;
|
|
3194
3239
|
const {
|
|
3195
3240
|
selected
|
|
3196
3241
|
} = useComboboxItemContext();
|
|
3197
|
-
const shouldRender = keepMounted || selected;
|
|
3242
|
+
const shouldRender = componentProps.keepMounted || selected;
|
|
3198
3243
|
if (!shouldRender) {
|
|
3199
3244
|
return null;
|
|
3200
3245
|
}
|
|
@@ -3324,9 +3369,7 @@ const ComboboxChip = /*#__PURE__*/React.forwardRef(function ComboboxChip(compone
|
|
|
3324
3369
|
} = useCompositeListItem();
|
|
3325
3370
|
function handleKeyDown(event) {
|
|
3326
3371
|
let nextIndex = index;
|
|
3327
|
-
const
|
|
3328
|
-
const previousChipKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
|
|
3329
|
-
const nextChipKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
|
|
3372
|
+
const [previousChipKey, nextChipKey] = getChipNavigationKeys(direction);
|
|
3330
3373
|
if (event.key === previousChipKey) {
|
|
3331
3374
|
event.preventDefault();
|
|
3332
3375
|
if (index > 0) {
|
|
@@ -3342,13 +3385,12 @@ const ComboboxChip = /*#__PURE__*/React.forwardRef(function ComboboxChip(compone
|
|
|
3342
3385
|
nextIndex = undefined;
|
|
3343
3386
|
}
|
|
3344
3387
|
} else if (event.key === 'Backspace' || event.key === 'Delete') {
|
|
3345
|
-
|
|
3346
|
-
nextIndex = computedNextIndex >= 0 ? computedNextIndex : undefined;
|
|
3388
|
+
nextIndex = getIndexAfterChipRemoval(index, selectedValue.length);
|
|
3347
3389
|
stopEvent(event);
|
|
3348
3390
|
store.state.setIndices({
|
|
3349
3391
|
activeIndex: null,
|
|
3350
3392
|
selectedIndex: null,
|
|
3351
|
-
type:
|
|
3393
|
+
type: keyboard
|
|
3352
3394
|
});
|
|
3353
3395
|
store.state.setSelectedValue(selectedValue.filter((_, i) => i !== index), createChangeEventDetails(none, event.nativeEvent));
|
|
3354
3396
|
} else if (event.key === 'Enter' || event.key === ' ') {
|
|
@@ -3448,7 +3490,7 @@ const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRe
|
|
|
3448
3490
|
if (removedIndex !== -1 && activeIndex === removedIndex) {
|
|
3449
3491
|
store.state.setIndices({
|
|
3450
3492
|
activeIndex: null,
|
|
3451
|
-
type: store.state.keyboardActiveRef.current ?
|
|
3493
|
+
type: store.state.keyboardActiveRef.current ? keyboard : pointer
|
|
3452
3494
|
});
|
|
3453
3495
|
}
|
|
3454
3496
|
}
|
|
@@ -3469,18 +3511,12 @@ const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRe
|
|
|
3469
3511
|
event.preventDefault();
|
|
3470
3512
|
},
|
|
3471
3513
|
onClick(event) {
|
|
3472
|
-
if (disabled || readOnly) {
|
|
3473
|
-
return;
|
|
3474
|
-
}
|
|
3475
3514
|
const eventDetails = removeChip(event);
|
|
3476
3515
|
if (!eventDetails.isPropagationAllowed) {
|
|
3477
3516
|
event.stopPropagation();
|
|
3478
3517
|
}
|
|
3479
3518
|
},
|
|
3480
3519
|
onKeyDown(event) {
|
|
3481
|
-
if (disabled || readOnly) {
|
|
3482
|
-
return;
|
|
3483
|
-
}
|
|
3484
3520
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
3485
3521
|
const eventDetails = removeChip(event);
|
|
3486
3522
|
if (!eventDetails.isPropagationAllowed) {
|
|
@@ -3494,4 +3530,12 @@ const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRe
|
|
|
3494
3530
|
});
|
|
3495
3531
|
if (process.env.NODE_ENV !== "production") ComboboxChipRemove.displayName = "ComboboxChipRemove";
|
|
3496
3532
|
|
|
3497
|
-
|
|
3533
|
+
/**
|
|
3534
|
+
* A visual separator between items or groups.
|
|
3535
|
+
* Renders a `<div>` element.
|
|
3536
|
+
*
|
|
3537
|
+
* Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
|
|
3538
|
+
*/
|
|
3539
|
+
const ComboboxSeparator = ListboxSeparator;
|
|
3540
|
+
|
|
3541
|
+
export { useFilteredItems as A, useCoreFilter as B, ComboboxRoot as C, AriaCombobox as D, useComboboxInputValueContext as E, ListboxSeparator as L, ComboboxLabel as a, ComboboxValue as b, ComboboxInput as c, ComboboxInputGroup as d, ComboboxTrigger as e, ComboboxList as f, ComboboxStatus as g, ComboboxPortal as h, ComboboxBackdrop as i, ComboboxPositioner as j, ComboboxPopup as k, ComboboxArrow as l, ComboboxIcon as m, ComboboxGroup as n, ComboboxGroupLabel as o, ComboboxItem as p, ComboboxItemIndicator as q, ComboboxChips as r, ComboboxChip as s, ComboboxChipRemove as t, ComboboxRow as u, ComboboxCollection as v, ComboboxEmpty as w, ComboboxClear as x, ComboboxSeparator as y, useComboboxFilter as z };
|