@react-stately/combobox 3.0.0-beta.0 → 3.0.0-nightly-641446f65-240905
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/import.mjs +17 -0
- package/dist/main.js +15 -269
- package/dist/main.js.map +1 -1
- package/dist/module.js +16 -253
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +14 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/useComboBoxState.main.js +305 -0
- package/dist/useComboBoxState.main.js.map +1 -0
- package/dist/useComboBoxState.mjs +300 -0
- package/dist/useComboBoxState.module.js +300 -0
- package/dist/useComboBoxState.module.js.map +1 -0
- package/package.json +18 -11
- package/src/index.ts +3 -1
- package/src/useComboBoxState.ts +222 -117
- package/LICENSE +0 -201
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-stately/combobox",
|
|
3
|
-
"version": "3.0.0-
|
|
3
|
+
"version": "3.0.0-nightly-641446f65-240905",
|
|
4
4
|
"description": "Spectrum UI components in React",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
7
7
|
"module": "dist/module.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
"types": "./dist/types.d.ts",
|
|
10
|
+
"import": "./dist/import.mjs",
|
|
11
|
+
"require": "./dist/main.js"
|
|
12
|
+
},
|
|
8
13
|
"types": "dist/types.d.ts",
|
|
9
14
|
"source": "src/index.ts",
|
|
10
15
|
"files": [
|
|
@@ -17,19 +22,21 @@
|
|
|
17
22
|
"url": "https://github.com/adobe/react-spectrum"
|
|
18
23
|
},
|
|
19
24
|
"dependencies": {
|
|
20
|
-
"@
|
|
21
|
-
"@react-stately/
|
|
22
|
-
"@react-stately/
|
|
23
|
-
"@react-stately/
|
|
24
|
-
"@react-stately/
|
|
25
|
-
"@react-
|
|
26
|
-
"@react-types/
|
|
25
|
+
"@react-stately/collections": "^3.0.0-nightly-641446f65-240905",
|
|
26
|
+
"@react-stately/form": "^3.0.0-nightly-641446f65-240905",
|
|
27
|
+
"@react-stately/list": "^3.0.0-nightly-641446f65-240905",
|
|
28
|
+
"@react-stately/overlays": "^3.0.0-nightly-641446f65-240905",
|
|
29
|
+
"@react-stately/select": "^3.0.0-nightly-641446f65-240905",
|
|
30
|
+
"@react-stately/utils": "^3.0.0-nightly-641446f65-240905",
|
|
31
|
+
"@react-types/combobox": "^3.0.0-nightly-641446f65-240905",
|
|
32
|
+
"@react-types/shared": "^3.0.0-nightly-641446f65-240905",
|
|
33
|
+
"@swc/helpers": "^0.5.0"
|
|
27
34
|
},
|
|
28
35
|
"peerDependencies": {
|
|
29
|
-
"react": "^16.8.0 || ^17.0.0-rc.1"
|
|
36
|
+
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
|
|
30
37
|
},
|
|
31
38
|
"publishConfig": {
|
|
32
39
|
"access": "public"
|
|
33
40
|
},
|
|
34
|
-
"
|
|
35
|
-
}
|
|
41
|
+
"stableVersion": "3.9.2"
|
|
42
|
+
}
|
package/src/index.ts
CHANGED
package/src/useComboBoxState.ts
CHANGED
|
@@ -10,25 +10,36 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {Collection, FocusStrategy, Node} from '@react-types/shared';
|
|
14
|
-
import {ComboBoxProps} from '@react-types/combobox';
|
|
13
|
+
import {Collection, CollectionStateBase, FocusStrategy, Node} from '@react-types/shared';
|
|
14
|
+
import {ComboBoxProps, MenuTriggerAction} from '@react-types/combobox';
|
|
15
|
+
import {FormValidationState, useFormValidationState} from '@react-stately/form';
|
|
16
|
+
import {getChildNodes} from '@react-stately/collections';
|
|
15
17
|
import {ListCollection, useSingleSelectListState} from '@react-stately/list';
|
|
16
18
|
import {SelectState} from '@react-stately/select';
|
|
19
|
+
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
|
17
20
|
import {useControlledState} from '@react-stately/utils';
|
|
18
|
-
import {
|
|
19
|
-
import {useMenuTriggerState} from '@react-stately/menu';
|
|
21
|
+
import {useOverlayTriggerState} from '@react-stately/overlays';
|
|
20
22
|
|
|
21
|
-
export interface ComboBoxState<T> extends SelectState<T
|
|
23
|
+
export interface ComboBoxState<T> extends SelectState<T>, FormValidationState{
|
|
22
24
|
/** The current value of the combo box input. */
|
|
23
25
|
inputValue: string,
|
|
24
26
|
/** Sets the value of the combo box input. */
|
|
25
27
|
setInputValue(value: string): void,
|
|
26
28
|
/** Selects the currently focused item and updates the input value. */
|
|
27
|
-
commit(): void
|
|
29
|
+
commit(): void,
|
|
30
|
+
/** Controls which item will be auto focused when the menu opens. */
|
|
31
|
+
readonly focusStrategy: FocusStrategy | null,
|
|
32
|
+
/** Opens the menu. */
|
|
33
|
+
open(focusStrategy?: FocusStrategy | null, trigger?: MenuTriggerAction): void,
|
|
34
|
+
/** Toggles the menu. */
|
|
35
|
+
toggle(focusStrategy?: FocusStrategy | null, trigger?: MenuTriggerAction): void,
|
|
36
|
+
/** Resets the input value to the previously selected item's text if any and closes the menu. */
|
|
37
|
+
revert(): void
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
type FilterFn = (textValue: string, inputValue: string) => boolean;
|
|
31
|
-
|
|
41
|
+
|
|
42
|
+
export interface ComboBoxStateOptions<T> extends Omit<ComboBoxProps<T>, 'children'>, CollectionStateBase<T> {
|
|
32
43
|
/** The filter function used to determine if a option should be included in the combo box list. */
|
|
33
44
|
defaultFilter?: FilterFn,
|
|
34
45
|
/** Whether the combo box allows the menu to be open when the collection is empty. */
|
|
@@ -42,7 +53,7 @@ interface ComboBoxStateProps<T> extends ComboBoxProps<T> {
|
|
|
42
53
|
* of items from props and manages the option selection state of the combo box. In addition, it tracks the input value,
|
|
43
54
|
* focus state, and other properties of the combo box.
|
|
44
55
|
*/
|
|
45
|
-
export function useComboBoxState<T extends object>(props:
|
|
56
|
+
export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T>): ComboBoxState<T> {
|
|
46
57
|
let {
|
|
47
58
|
defaultFilter,
|
|
48
59
|
menuTrigger = 'input',
|
|
@@ -51,103 +62,181 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateProps<T>)
|
|
|
51
62
|
shouldCloseOnBlur = true
|
|
52
63
|
} = props;
|
|
53
64
|
|
|
65
|
+
let [showAllItems, setShowAllItems] = useState(false);
|
|
54
66
|
let [isFocused, setFocusedState] = useState(false);
|
|
55
|
-
let [
|
|
56
|
-
props.inputValue,
|
|
57
|
-
props.defaultInputValue ?? '',
|
|
58
|
-
props.onInputChange
|
|
59
|
-
);
|
|
67
|
+
let [focusStrategy, setFocusStrategy] = useState<FocusStrategy | null>(null);
|
|
60
68
|
|
|
61
69
|
let onSelectionChange = (key) => {
|
|
62
70
|
if (props.onSelectionChange) {
|
|
63
71
|
props.onSelectionChange(key);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
// If
|
|
74
|
+
// If key is the same, reset the inputValue and close the menu
|
|
67
75
|
// (scenario: user clicks on already selected option)
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
triggerState.close();
|
|
72
|
-
}
|
|
76
|
+
if (key === selectedKey) {
|
|
77
|
+
resetInputValue();
|
|
78
|
+
closeMenu();
|
|
73
79
|
}
|
|
74
80
|
};
|
|
75
81
|
|
|
76
|
-
let {collection,
|
|
82
|
+
let {collection,
|
|
83
|
+
selectionManager,
|
|
84
|
+
selectedKey,
|
|
85
|
+
setSelectedKey,
|
|
86
|
+
selectedItem,
|
|
87
|
+
disabledKeys
|
|
88
|
+
} = useSingleSelectListState({
|
|
77
89
|
...props,
|
|
78
90
|
onSelectionChange,
|
|
79
91
|
items: props.items ?? props.defaultItems
|
|
80
92
|
});
|
|
93
|
+
let defaultInputValue: string | null | undefined = props.defaultInputValue;
|
|
94
|
+
if (defaultInputValue == null) {
|
|
95
|
+
if (selectedKey == null) {
|
|
96
|
+
defaultInputValue = '';
|
|
97
|
+
} else {
|
|
98
|
+
defaultInputValue = collection.getItem(selectedKey)?.textValue ?? '';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
81
101
|
|
|
102
|
+
let [inputValue, setInputValue] = useControlledState(
|
|
103
|
+
props.inputValue,
|
|
104
|
+
defaultInputValue!,
|
|
105
|
+
props.onInputChange
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Preserve original collection so we can show all items on demand
|
|
109
|
+
let originalCollection = collection;
|
|
82
110
|
let filteredCollection = useMemo(() => (
|
|
83
111
|
// No default filter if items are controlled.
|
|
84
112
|
props.items != null || !defaultFilter
|
|
85
113
|
? collection
|
|
86
114
|
: filterCollection(collection, inputValue, defaultFilter)
|
|
87
115
|
), [collection, inputValue, defaultFilter, props.items]);
|
|
116
|
+
let [lastCollection, setLastCollection] = useState(filteredCollection);
|
|
117
|
+
|
|
118
|
+
// Track what action is attempting to open the menu
|
|
119
|
+
let menuOpenTrigger = useRef<MenuTriggerAction | undefined>('focus');
|
|
120
|
+
let onOpenChange = (open: boolean) => {
|
|
121
|
+
if (props.onOpenChange) {
|
|
122
|
+
props.onOpenChange(open, open ? menuOpenTrigger.current : undefined);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
selectionManager.setFocused(open);
|
|
126
|
+
if (!open) {
|
|
127
|
+
selectionManager.setFocusedKey(null);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
88
130
|
|
|
89
|
-
let triggerState =
|
|
90
|
-
let open = (focusStrategy?:
|
|
131
|
+
let triggerState = useOverlayTriggerState({...props, onOpenChange, isOpen: undefined, defaultOpen: undefined});
|
|
132
|
+
let open = (focusStrategy: FocusStrategy | null = null, trigger?: MenuTriggerAction) => {
|
|
133
|
+
let displayAllItems = (trigger === 'manual' || (trigger === 'focus' && menuTrigger === 'focus'));
|
|
91
134
|
// Prevent open operations from triggering if there is nothing to display
|
|
92
|
-
if
|
|
93
|
-
|
|
135
|
+
// Also prevent open operations from triggering if items are uncontrolled but defaultItems is empty, even if displayAllItems is true.
|
|
136
|
+
// This is to prevent comboboxes with empty defaultItems from opening but allow controlled items comboboxes to open even if the inital list is empty (assumption is user will provide swap the empty list with a base list via onOpenChange returning `menuTrigger` manual)
|
|
137
|
+
if (allowsEmptyCollection || filteredCollection.size > 0 || (displayAllItems && originalCollection.size > 0) || props.items) {
|
|
138
|
+
if (displayAllItems && !triggerState.isOpen && props.items === undefined) {
|
|
139
|
+
// Show all items if menu is manually opened. Only care about this if items are undefined
|
|
140
|
+
setShowAllItems(true);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
menuOpenTrigger.current = trigger;
|
|
144
|
+
setFocusStrategy(focusStrategy);
|
|
145
|
+
triggerState.open();
|
|
94
146
|
}
|
|
95
147
|
};
|
|
96
148
|
|
|
97
|
-
let toggle = (focusStrategy?:
|
|
149
|
+
let toggle = (focusStrategy: FocusStrategy | null = null, trigger?: MenuTriggerAction) => {
|
|
150
|
+
let displayAllItems = (trigger === 'manual' || (trigger === 'focus' && menuTrigger === 'focus'));
|
|
98
151
|
// If the menu is closed and there is nothing to display, early return so toggle isn't called to prevent extraneous onOpenChange
|
|
99
|
-
if (!(allowsEmptyCollection || filteredCollection.size > 0) && !triggerState.isOpen) {
|
|
152
|
+
if (!(allowsEmptyCollection || filteredCollection.size > 0 || (displayAllItems && originalCollection.size > 0) || props.items) && !triggerState.isOpen) {
|
|
100
153
|
return;
|
|
101
154
|
}
|
|
102
155
|
|
|
103
|
-
triggerState.
|
|
156
|
+
if (displayAllItems && !triggerState.isOpen && props.items === undefined) {
|
|
157
|
+
// Show all items if menu is toggled open. Only care about this if items are undefined
|
|
158
|
+
setShowAllItems(true);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Only update the menuOpenTrigger if menu is currently closed
|
|
162
|
+
if (!triggerState.isOpen) {
|
|
163
|
+
menuOpenTrigger.current = trigger;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
toggleMenu(focusStrategy);
|
|
104
167
|
};
|
|
105
168
|
|
|
106
|
-
let
|
|
169
|
+
let updateLastCollection = useCallback(() => {
|
|
170
|
+
setLastCollection(showAllItems ? originalCollection : filteredCollection);
|
|
171
|
+
}, [showAllItems, originalCollection, filteredCollection]);
|
|
172
|
+
|
|
173
|
+
// If menu is going to close, save the current collection so we can freeze the displayed collection when the
|
|
174
|
+
// user clicks outside the popover to close the menu. Prevents the menu contents from updating as the menu closes.
|
|
175
|
+
let toggleMenu = useCallback((focusStrategy: FocusStrategy | null = null) => {
|
|
176
|
+
if (triggerState.isOpen) {
|
|
177
|
+
updateLastCollection();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setFocusStrategy(focusStrategy);
|
|
181
|
+
triggerState.toggle();
|
|
182
|
+
}, [triggerState, updateLastCollection]);
|
|
183
|
+
|
|
184
|
+
let closeMenu = useCallback(() => {
|
|
185
|
+
if (triggerState.isOpen) {
|
|
186
|
+
updateLastCollection();
|
|
187
|
+
triggerState.close();
|
|
188
|
+
}
|
|
189
|
+
}, [triggerState, updateLastCollection]);
|
|
190
|
+
|
|
191
|
+
let [lastValue, setLastValue] = useState(inputValue);
|
|
107
192
|
let resetInputValue = () => {
|
|
108
|
-
let itemText = collection.getItem(selectedKey)?.textValue ?? '';
|
|
109
|
-
|
|
193
|
+
let itemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : '';
|
|
194
|
+
setLastValue(itemText);
|
|
110
195
|
setInputValue(itemText);
|
|
111
196
|
};
|
|
112
197
|
|
|
113
|
-
let isInitialRender = useRef(true);
|
|
114
198
|
let lastSelectedKey = useRef(props.selectedKey ?? props.defaultSelectedKey ?? null);
|
|
199
|
+
let lastSelectedKeyText = useRef(
|
|
200
|
+
selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : ''
|
|
201
|
+
);
|
|
202
|
+
// intentional omit dependency array, want this to happen on every render
|
|
203
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
115
204
|
useEffect(() => {
|
|
116
|
-
//
|
|
117
|
-
//
|
|
205
|
+
// Open and close menu automatically when the input value changes if the input is focused,
|
|
206
|
+
// and there are items in the collection or allowEmptyCollection is true.
|
|
118
207
|
if (
|
|
119
208
|
isFocused &&
|
|
120
209
|
(filteredCollection.size > 0 || allowsEmptyCollection) &&
|
|
121
210
|
!triggerState.isOpen &&
|
|
122
|
-
inputValue !== lastValue
|
|
123
|
-
menuTrigger !== 'manual'
|
|
124
|
-
(props.isOpen === undefined || props.inputValue === undefined)
|
|
211
|
+
inputValue !== lastValue &&
|
|
212
|
+
menuTrigger !== 'manual'
|
|
125
213
|
) {
|
|
126
|
-
open();
|
|
214
|
+
open(null, 'input');
|
|
127
215
|
}
|
|
128
216
|
|
|
129
|
-
// Close the menu if the collection is empty
|
|
217
|
+
// Close the menu if the collection is empty. Don't close menu if filtered collection size is 0
|
|
218
|
+
// but we are currently showing all items via button press
|
|
130
219
|
if (
|
|
220
|
+
!showAllItems &&
|
|
131
221
|
!allowsEmptyCollection &&
|
|
132
222
|
triggerState.isOpen &&
|
|
133
|
-
filteredCollection.size === 0
|
|
134
|
-
(props.isOpen === undefined || props.items === undefined)
|
|
223
|
+
filteredCollection.size === 0
|
|
135
224
|
) {
|
|
136
|
-
|
|
225
|
+
closeMenu();
|
|
137
226
|
}
|
|
138
227
|
|
|
139
|
-
// Close when an item is selected
|
|
228
|
+
// Close when an item is selected.
|
|
140
229
|
if (
|
|
141
230
|
selectedKey != null &&
|
|
142
|
-
selectedKey !== lastSelectedKey.current
|
|
143
|
-
(props.isOpen === undefined || props.selectedKey === undefined)
|
|
231
|
+
selectedKey !== lastSelectedKey.current
|
|
144
232
|
) {
|
|
145
|
-
|
|
233
|
+
closeMenu();
|
|
146
234
|
}
|
|
147
235
|
|
|
148
|
-
// Clear focused key when input value changes.
|
|
149
|
-
if (inputValue !== lastValue
|
|
236
|
+
// Clear focused key when input value changes and display filtered collection again.
|
|
237
|
+
if (inputValue !== lastValue) {
|
|
150
238
|
selectionManager.setFocusedKey(null);
|
|
239
|
+
setShowAllItems(false);
|
|
151
240
|
|
|
152
241
|
// Set selectedKey to null when the user clears the input.
|
|
153
242
|
// If controlled, this is the application developer's responsibility.
|
|
@@ -156,82 +245,77 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateProps<T>)
|
|
|
156
245
|
}
|
|
157
246
|
}
|
|
158
247
|
|
|
159
|
-
// If it is the intial render and inputValue isn't controlled nor has an intial value, set input to match current selected key if any
|
|
160
|
-
if (isInitialRender.current && (props.inputValue === undefined && props.defaultInputValue === undefined)) {
|
|
161
|
-
resetInputValue();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
248
|
// If the selectedKey changed, update the input value.
|
|
165
249
|
// Do nothing if both inputValue and selectedKey are controlled.
|
|
166
|
-
// In this case, it's the user's responsibility to update inputValue in onSelectionChange.
|
|
167
|
-
// on initial render, even if it doesn't match the selected item's text.
|
|
250
|
+
// In this case, it's the user's responsibility to update inputValue in onSelectionChange.
|
|
168
251
|
if (
|
|
169
252
|
selectedKey !== lastSelectedKey.current &&
|
|
170
253
|
(props.inputValue === undefined || props.selectedKey === undefined)
|
|
171
254
|
) {
|
|
172
255
|
resetInputValue();
|
|
173
|
-
} else {
|
|
174
|
-
|
|
256
|
+
} else if (lastValue !== inputValue) {
|
|
257
|
+
setLastValue(inputValue);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Update the inputValue if the selected item's text changes from its last tracked value.
|
|
261
|
+
// This is to handle cases where a selectedKey is specified but the items aren't available (async loading) or the selected item's text value updates.
|
|
262
|
+
// Only reset if the user isn't currently within the field so we don't erroneously modify user input.
|
|
263
|
+
// If inputValue is controlled, it is the user's responsibility to update the inputValue when items change.
|
|
264
|
+
let selectedItemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : '';
|
|
265
|
+
if (!isFocused && selectedKey != null && props.inputValue === undefined && selectedKey === lastSelectedKey.current) {
|
|
266
|
+
if (lastSelectedKeyText.current !== selectedItemText) {
|
|
267
|
+
setLastValue(selectedItemText);
|
|
268
|
+
setInputValue(selectedItemText);
|
|
269
|
+
}
|
|
175
270
|
}
|
|
176
271
|
|
|
177
|
-
isInitialRender.current = false;
|
|
178
272
|
lastSelectedKey.current = selectedKey;
|
|
273
|
+
lastSelectedKeyText.current = selectedItemText;
|
|
179
274
|
});
|
|
180
275
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
276
|
+
let validation = useFormValidationState({
|
|
277
|
+
...props,
|
|
278
|
+
value: useMemo(() => ({inputValue, selectedKey}), [inputValue, selectedKey])
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Revert input value and close menu
|
|
282
|
+
let revert = () => {
|
|
283
|
+
if (allowsCustomValue && selectedKey == null) {
|
|
284
|
+
commitCustomValue();
|
|
285
|
+
} else {
|
|
286
|
+
commitSelection();
|
|
185
287
|
}
|
|
186
|
-
}
|
|
288
|
+
};
|
|
187
289
|
|
|
188
290
|
let commitCustomValue = () => {
|
|
189
|
-
let shouldClose = false;
|
|
190
291
|
lastSelectedKey.current = null;
|
|
191
292
|
setSelectedKey(null);
|
|
192
|
-
|
|
193
|
-
// If previous key was already null, need to manually call onSelectionChange since it won't be triggered by a setSelectedKey call
|
|
194
|
-
// This allows the application to control whether or not to close the menu on custom value commit
|
|
195
|
-
if (selectedKey === null && props.onSelectionChange) {
|
|
196
|
-
props.onSelectionChange(null);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Should close menu ourselves if component open state or selected key is uncontrolled and therefore won't be closed by a user defined event handler
|
|
200
|
-
shouldClose = props.isOpen == null || props.selectedKey === undefined;
|
|
201
|
-
|
|
202
|
-
// Close if no other event will be fired. Otherwise, allow the
|
|
203
|
-
// application to control this based on that event.
|
|
204
|
-
if (shouldClose) {
|
|
205
|
-
triggerState.close();
|
|
206
|
-
}
|
|
293
|
+
closeMenu();
|
|
207
294
|
};
|
|
208
295
|
|
|
209
296
|
let commitSelection = () => {
|
|
210
297
|
// If multiple things are controlled, call onSelectionChange
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
(props.selectedKey !== undefined && props.inputValue !== undefined) ||
|
|
214
|
-
(props.inputValue !== undefined && props.isOpen !== undefined) ||
|
|
215
|
-
(props.selectedKey !== undefined && props.isOpen !== undefined)
|
|
216
|
-
) {
|
|
217
|
-
props.onSelectionChange(selectedKey);
|
|
218
|
-
|
|
219
|
-
// If multiple things are controlled but inputValue isn't, reset the input value for the user
|
|
220
|
-
if (props.inputValue === undefined) {
|
|
221
|
-
resetInputValue();
|
|
222
|
-
}
|
|
298
|
+
if (props.selectedKey !== undefined && props.inputValue !== undefined) {
|
|
299
|
+
props.onSelectionChange?.(selectedKey);
|
|
223
300
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
lastValue.current = itemText;
|
|
229
|
-
triggerState.close();
|
|
230
|
-
}
|
|
301
|
+
// Stop menu from reopening from useEffect
|
|
302
|
+
let itemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : '';
|
|
303
|
+
setLastValue(itemText);
|
|
304
|
+
closeMenu();
|
|
231
305
|
} else {
|
|
232
306
|
// If only a single aspect of combobox is controlled, reset input value and close menu for the user
|
|
233
307
|
resetInputValue();
|
|
234
|
-
|
|
308
|
+
closeMenu();
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const commitValue = () => {
|
|
313
|
+
if (allowsCustomValue) {
|
|
314
|
+
const itemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : '';
|
|
315
|
+
(inputValue === itemText) ? commitSelection() : commitCustomValue();
|
|
316
|
+
} else {
|
|
317
|
+
// Reset inputValue and close menu
|
|
318
|
+
commitSelection();
|
|
235
319
|
}
|
|
236
320
|
};
|
|
237
321
|
|
|
@@ -244,32 +328,50 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateProps<T>)
|
|
|
244
328
|
} else {
|
|
245
329
|
setSelectedKey(selectionManager.focusedKey);
|
|
246
330
|
}
|
|
247
|
-
} else
|
|
248
|
-
|
|
331
|
+
} else {
|
|
332
|
+
commitValue();
|
|
249
333
|
}
|
|
250
334
|
};
|
|
251
335
|
|
|
336
|
+
let valueOnFocus = useRef(inputValue);
|
|
252
337
|
let setFocused = (isFocused: boolean) => {
|
|
253
338
|
if (isFocused) {
|
|
254
|
-
|
|
255
|
-
|
|
339
|
+
valueOnFocus.current = inputValue;
|
|
340
|
+
if (menuTrigger === 'focus' && !props.isReadOnly) {
|
|
341
|
+
open(null, 'focus');
|
|
256
342
|
}
|
|
257
|
-
} else
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
343
|
+
} else {
|
|
344
|
+
if (shouldCloseOnBlur) {
|
|
345
|
+
commitValue();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (inputValue !== valueOnFocus.current) {
|
|
349
|
+
validation.commitValidation();
|
|
263
350
|
}
|
|
264
351
|
}
|
|
265
352
|
|
|
266
353
|
setFocusedState(isFocused);
|
|
267
354
|
};
|
|
268
355
|
|
|
356
|
+
let displayedCollection = useMemo(() => {
|
|
357
|
+
if (triggerState.isOpen) {
|
|
358
|
+
if (showAllItems) {
|
|
359
|
+
return originalCollection;
|
|
360
|
+
} else {
|
|
361
|
+
return filteredCollection;
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
return lastCollection;
|
|
365
|
+
}
|
|
366
|
+
}, [triggerState.isOpen, originalCollection, filteredCollection, showAllItems, lastCollection]);
|
|
367
|
+
|
|
269
368
|
return {
|
|
369
|
+
...validation,
|
|
270
370
|
...triggerState,
|
|
371
|
+
focusStrategy,
|
|
271
372
|
toggle,
|
|
272
373
|
open,
|
|
374
|
+
close: commitValue,
|
|
273
375
|
selectionManager,
|
|
274
376
|
selectedKey,
|
|
275
377
|
setSelectedKey,
|
|
@@ -277,27 +379,30 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateProps<T>)
|
|
|
277
379
|
isFocused,
|
|
278
380
|
setFocused,
|
|
279
381
|
selectedItem,
|
|
280
|
-
collection:
|
|
382
|
+
collection: displayedCollection,
|
|
281
383
|
inputValue,
|
|
282
384
|
setInputValue,
|
|
283
|
-
commit
|
|
385
|
+
commit,
|
|
386
|
+
revert
|
|
284
387
|
};
|
|
285
388
|
}
|
|
286
389
|
|
|
287
390
|
function filterCollection<T extends object>(collection: Collection<Node<T>>, inputValue: string, filter: FilterFn): Collection<Node<T>> {
|
|
288
|
-
return new ListCollection(filterNodes(collection, inputValue, filter));
|
|
391
|
+
return new ListCollection(filterNodes(collection, collection, inputValue, filter));
|
|
289
392
|
}
|
|
290
393
|
|
|
291
|
-
function filterNodes<T>(nodes: Iterable<Node<T>>, inputValue: string, filter: FilterFn): Iterable<Node<T>> {
|
|
292
|
-
let filteredNode = [];
|
|
394
|
+
function filterNodes<T>(collection: Collection<Node<T>>, nodes: Iterable<Node<T>>, inputValue: string, filter: FilterFn): Iterable<Node<T>> {
|
|
395
|
+
let filteredNode: Node<T>[] = [];
|
|
293
396
|
for (let node of nodes) {
|
|
294
397
|
if (node.type === 'section' && node.hasChildNodes) {
|
|
295
|
-
let filtered = filterNodes(node
|
|
296
|
-
if ([...filtered].
|
|
398
|
+
let filtered = filterNodes(collection, getChildNodes(node, collection), inputValue, filter);
|
|
399
|
+
if ([...filtered].some(node => node.type === 'item')) {
|
|
297
400
|
filteredNode.push({...node, childNodes: filtered});
|
|
298
401
|
}
|
|
299
|
-
} else if (node.type
|
|
300
|
-
filteredNode.push(node);
|
|
402
|
+
} else if (node.type === 'item' && filter(node.textValue, inputValue)) {
|
|
403
|
+
filteredNode.push({...node});
|
|
404
|
+
} else if (node.type !== 'item') {
|
|
405
|
+
filteredNode.push({...node});
|
|
301
406
|
}
|
|
302
407
|
}
|
|
303
408
|
return filteredNode;
|