@equinor/eds-core-react 2.2.1-beta.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/eds-core-react.cjs +754 -499
- package/dist/esm/components/Autocomplete/AddNewOption.js +31 -14
- package/dist/esm/components/Autocomplete/Autocomplete.js +54 -874
- package/dist/esm/components/Autocomplete/AutocompleteContext.js +12 -0
- package/dist/esm/components/Autocomplete/EmptyOption.js +21 -0
- package/dist/esm/components/Autocomplete/MultipleInput.js +85 -0
- package/dist/esm/components/Autocomplete/Option.js +42 -23
- package/dist/esm/components/Autocomplete/OptionList.js +124 -0
- package/dist/esm/components/Autocomplete/RightAdornments.js +48 -0
- package/dist/esm/components/Autocomplete/SelectAllOption.js +63 -0
- package/dist/esm/components/Autocomplete/SingleInput.js +28 -0
- package/dist/esm/components/Autocomplete/useAutocomplete.js +605 -0
- package/dist/esm/components/Autocomplete/utils.js +93 -0
- package/dist/esm/components/Datepicker/fields/FieldWrapper.js +10 -0
- package/dist/esm/components/Dialog/Dialog.js +6 -4
- package/dist/esm/components/next/Icon/Icon.js +57 -0
- package/dist/esm/components/next/Icon/icon.css.js +6 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.next.js +1 -0
- package/dist/esm/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.js +26 -0
- package/dist/index.next.cjs +82 -0
- package/dist/types/components/Autocomplete/AddNewOption.d.ts +4 -12
- package/dist/types/components/Autocomplete/Autocomplete.d.ts +22 -7
- package/dist/types/components/Autocomplete/AutocompleteContext.d.ts +228 -0
- package/dist/types/components/Autocomplete/EmptyOption.d.ts +1 -0
- package/dist/types/components/Autocomplete/MultipleInput.d.ts +1 -0
- package/dist/types/components/Autocomplete/Option.d.ts +7 -15
- package/dist/types/components/Autocomplete/OptionList.d.ts +2 -0
- package/dist/types/components/Autocomplete/RightAdornments.d.ts +1 -0
- package/dist/types/components/Autocomplete/SelectAllOption.d.ts +6 -0
- package/dist/types/components/Autocomplete/SingleInput.d.ts +1 -0
- package/dist/types/components/Autocomplete/useAutocomplete.d.ts +122 -0
- package/dist/types/components/Autocomplete/utils.d.ts +13 -0
- package/dist/types/components/Datepicker/DateRangePicker.d.ts +1 -1
- package/dist/types/components/SideBar/SideBarButton/index.d.ts +1 -1
- package/dist/types/components/next/Icon/Icon.d.ts +29 -0
- package/dist/types/components/next/Icon/Icon.types.d.ts +19 -0
- package/dist/types/components/next/Icon/index.d.ts +2 -0
- package/dist/types/components/next/Placeholder/Placeholder.figma.d.ts +16 -0
- package/dist/types/components/next/index.d.ts +2 -0
- package/package.json +5 -4
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import { useToken, useIsomorphicLayoutEffect } from '@equinor/eds-utils';
|
|
2
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
3
|
+
import { useMultipleSelection, useCombobox } from 'downshift';
|
|
4
|
+
import { useState, useMemo, useCallback, useRef, useImperativeHandle, useEffect } from 'react';
|
|
5
|
+
import { AddSymbol, AllSymbol, defaultOptionDisabled } from './Autocomplete.js';
|
|
6
|
+
import { multiSelect, selectTokens } from './Autocomplete.tokens.js';
|
|
7
|
+
import { findPrevIndex, findNextIndex, mergeEventsFromRight } from './utils.js';
|
|
8
|
+
import { useEds } from '../EdsProvider/eds.context.js';
|
|
9
|
+
|
|
10
|
+
const useAutocomplete = ({
|
|
11
|
+
options = [],
|
|
12
|
+
totalOptions,
|
|
13
|
+
label,
|
|
14
|
+
meta,
|
|
15
|
+
className,
|
|
16
|
+
style,
|
|
17
|
+
disabled = false,
|
|
18
|
+
readOnly = false,
|
|
19
|
+
loading = false,
|
|
20
|
+
hideClearButton = false,
|
|
21
|
+
onOptionsChange,
|
|
22
|
+
onAddNewOption,
|
|
23
|
+
onInputChange,
|
|
24
|
+
selectedOptions: _selectedOptions,
|
|
25
|
+
selectionDisplay = 'summary',
|
|
26
|
+
multiple,
|
|
27
|
+
itemToKey: _itemToKey,
|
|
28
|
+
itemCompare: _itemCompare,
|
|
29
|
+
allowSelectAll,
|
|
30
|
+
initialSelectedOptions: _initialSelectedOptions = [],
|
|
31
|
+
optionDisabled = defaultOptionDisabled,
|
|
32
|
+
optionsFilter,
|
|
33
|
+
autoWidth,
|
|
34
|
+
placeholder,
|
|
35
|
+
optionLabel,
|
|
36
|
+
clearSearchOnChange = true,
|
|
37
|
+
multiline = false,
|
|
38
|
+
dropdownHeight = 300,
|
|
39
|
+
optionComponent,
|
|
40
|
+
helperText,
|
|
41
|
+
helperIcon,
|
|
42
|
+
noOptionsText = 'No options',
|
|
43
|
+
variant,
|
|
44
|
+
onClear,
|
|
45
|
+
ref,
|
|
46
|
+
...other
|
|
47
|
+
}) => {
|
|
48
|
+
const [lastScrollOffset, setLastScrollOffset] = useState(0);
|
|
49
|
+
const [controlledHighlightedIndex, setControlledHighlightedIndex] = useState(0);
|
|
50
|
+
const itemCompare = useMemo(() => {
|
|
51
|
+
if (_itemCompare && _itemToKey) {
|
|
52
|
+
console.error('Error: Specifying both itemCompare and itemToKey. itemCompare is deprecated, while itemToKey should be used instead of it. Please only use one.');
|
|
53
|
+
return _itemCompare;
|
|
54
|
+
}
|
|
55
|
+
if (_itemToKey) {
|
|
56
|
+
return (o1, o2) => _itemToKey(o1) === _itemToKey(o2);
|
|
57
|
+
}
|
|
58
|
+
return _itemCompare;
|
|
59
|
+
}, [_itemCompare, _itemToKey]);
|
|
60
|
+
const itemToKey = useCallback(item => {
|
|
61
|
+
return _itemToKey ? _itemToKey(item) : item;
|
|
62
|
+
}, [_itemToKey]);
|
|
63
|
+
|
|
64
|
+
// MARK: initializing data/setup
|
|
65
|
+
const selectedOptions = _selectedOptions ? itemCompare ? options.filter(item => _selectedOptions.some(compare => itemCompare(item, compare))) : _selectedOptions : undefined;
|
|
66
|
+
const initialSelectedOptions = _initialSelectedOptions ? itemCompare ? options.filter(item => _initialSelectedOptions.some(compare => itemCompare(item, compare))) : _initialSelectedOptions : undefined;
|
|
67
|
+
const isControlled = Boolean(selectedOptions);
|
|
68
|
+
const [inputOptions, setInputOptions] = useState(options);
|
|
69
|
+
const [_availableItems, setAvailableItems] = useState(inputOptions);
|
|
70
|
+
const [typedInputValue, setTypedInputValue] = useState('');
|
|
71
|
+
const inputRef = useRef(null);
|
|
72
|
+
useImperativeHandle(ref, () => inputRef.current);
|
|
73
|
+
const showSelectAll = useMemo(() => {
|
|
74
|
+
if (!multiple && allowSelectAll) {
|
|
75
|
+
throw new Error(`allowSelectAll can only be used with multiple`);
|
|
76
|
+
}
|
|
77
|
+
return allowSelectAll && !typedInputValue;
|
|
78
|
+
}, [allowSelectAll, multiple, typedInputValue]);
|
|
79
|
+
const availableItems = useMemo(() => {
|
|
80
|
+
if (showSelectAll && onAddNewOption) return [AddSymbol, AllSymbol, ..._availableItems];
|
|
81
|
+
if (showSelectAll) return [AllSymbol, ..._availableItems];
|
|
82
|
+
if (onAddNewOption) return [AddSymbol, ..._availableItems];
|
|
83
|
+
return _availableItems;
|
|
84
|
+
}, [_availableItems, showSelectAll, onAddNewOption]);
|
|
85
|
+
const getSelectedIndex = useCallback(selectedItem => availableItems.findIndex(item => itemCompare ? itemCompare(item, selectedItem) : item === selectedItem), [availableItems, itemCompare]);
|
|
86
|
+
|
|
87
|
+
//issue 2304, update dataset when options are added dynamically
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const availableHash = JSON.stringify(inputOptions);
|
|
90
|
+
const optionsHash = JSON.stringify(options);
|
|
91
|
+
if (availableHash !== optionsHash) {
|
|
92
|
+
setInputOptions(options);
|
|
93
|
+
}
|
|
94
|
+
}, [options, inputOptions]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
setAvailableItems(inputOptions);
|
|
97
|
+
}, [inputOptions]);
|
|
98
|
+
const {
|
|
99
|
+
density
|
|
100
|
+
} = useEds();
|
|
101
|
+
const token = useToken({
|
|
102
|
+
density
|
|
103
|
+
}, multiple ? multiSelect : selectTokens);
|
|
104
|
+
const tokens = token();
|
|
105
|
+
let placeholderText = placeholder;
|
|
106
|
+
let multipleSelectionProps = {
|
|
107
|
+
itemToKey,
|
|
108
|
+
initialSelectedItems: multiple ? initialSelectedOptions : initialSelectedOptions[0] ? [initialSelectedOptions[0]] : []
|
|
109
|
+
};
|
|
110
|
+
if (multiple) {
|
|
111
|
+
multipleSelectionProps = {
|
|
112
|
+
...multipleSelectionProps,
|
|
113
|
+
onSelectedItemsChange: changes => {
|
|
114
|
+
if (onOptionsChange) {
|
|
115
|
+
let selectedItems = changes.selectedItems.filter(item => item !== AllSymbol || item !== AddSymbol);
|
|
116
|
+
if (itemCompare) {
|
|
117
|
+
selectedItems = inputOptions.filter(item => selectedItems.some(compare => itemCompare(item, compare)));
|
|
118
|
+
}
|
|
119
|
+
onOptionsChange({
|
|
120
|
+
selectedItems
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
if (isControlled) {
|
|
126
|
+
multipleSelectionProps = {
|
|
127
|
+
...multipleSelectionProps,
|
|
128
|
+
selectedItems: selectedOptions
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const {
|
|
133
|
+
getDropdownProps,
|
|
134
|
+
addSelectedItem,
|
|
135
|
+
removeSelectedItem,
|
|
136
|
+
selectedItems,
|
|
137
|
+
setSelectedItems
|
|
138
|
+
} = useMultipleSelection(multipleSelectionProps);
|
|
139
|
+
// MARK: select all logic
|
|
140
|
+
const enabledItems = useMemo(() => {
|
|
141
|
+
const disabledItemsSet = new Set(inputOptions.filter(optionDisabled));
|
|
142
|
+
return inputOptions.filter(x => !disabledItemsSet.has(x));
|
|
143
|
+
}, [inputOptions, optionDisabled]);
|
|
144
|
+
const allDisabled = enabledItems.length === 0;
|
|
145
|
+
const selectedDisabledItemsSet = useMemo(() => new Set(selectedItems.filter(x => x !== null && optionDisabled(x))), [selectedItems, optionDisabled]);
|
|
146
|
+
const selectedEnabledItems = useMemo(() => selectedItems.filter(x => !selectedDisabledItemsSet.has(x)), [selectedItems, selectedDisabledItemsSet]);
|
|
147
|
+
const allSelectedState = useMemo(() => {
|
|
148
|
+
if (!enabledItems || !selectedEnabledItems) return 'NONE';
|
|
149
|
+
if (enabledItems.length === selectedEnabledItems.length) return 'ALL';
|
|
150
|
+
if (enabledItems.length != selectedEnabledItems.length && selectedEnabledItems.length > 0) return 'SOME';
|
|
151
|
+
return 'NONE';
|
|
152
|
+
}, [enabledItems, selectedEnabledItems]);
|
|
153
|
+
const toggleAllSelected = () => {
|
|
154
|
+
if (selectedEnabledItems.length === enabledItems.length) {
|
|
155
|
+
setSelectedItems([...selectedDisabledItemsSet]);
|
|
156
|
+
} else {
|
|
157
|
+
setSelectedItems([...enabledItems, ...selectedDisabledItemsSet]);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// MARK: getLabel
|
|
162
|
+
const getLabel = useCallback(item => {
|
|
163
|
+
//note: non strict check for null or undefined to allow 0
|
|
164
|
+
if (item == null) {
|
|
165
|
+
return '';
|
|
166
|
+
}
|
|
167
|
+
if (optionLabel) {
|
|
168
|
+
return optionLabel(item);
|
|
169
|
+
} else if (typeof item === 'object') {
|
|
170
|
+
throw new Error('Missing label. When using objects for options make sure to define the `optionLabel` property');
|
|
171
|
+
}
|
|
172
|
+
if (typeof item === 'string') {
|
|
173
|
+
return item;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
177
|
+
return item?.toString();
|
|
178
|
+
} catch {
|
|
179
|
+
throw new Error('Unable to find label, make sure your are using options as documented');
|
|
180
|
+
}
|
|
181
|
+
}, [optionLabel]);
|
|
182
|
+
|
|
183
|
+
// MARK: setup virtualizer
|
|
184
|
+
const scrollContainer = useRef(null);
|
|
185
|
+
const rowVirtualizer = useVirtualizer({
|
|
186
|
+
count: availableItems.length,
|
|
187
|
+
getScrollElement: () => scrollContainer.current,
|
|
188
|
+
estimateSize: useCallback(() => {
|
|
189
|
+
return parseInt(token().entities.label.minHeight);
|
|
190
|
+
}, [token]),
|
|
191
|
+
overscan: 25
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
//https://github.com/TanStack/virtual/discussions/379#discussioncomment-3501037
|
|
195
|
+
useIsomorphicLayoutEffect(() => {
|
|
196
|
+
rowVirtualizer?.measure?.();
|
|
197
|
+
}, [rowVirtualizer, density]);
|
|
198
|
+
|
|
199
|
+
// MARK: downshift state
|
|
200
|
+
let comboBoxProps = {
|
|
201
|
+
items: availableItems,
|
|
202
|
+
//can not pass readonly type to downshift so we cast it to regular T[]
|
|
203
|
+
initialSelectedItem: initialSelectedOptions[0],
|
|
204
|
+
isItemDisabled(item) {
|
|
205
|
+
if (item === AddSymbol) return !typedInputValue.trim();
|
|
206
|
+
return optionDisabled(item);
|
|
207
|
+
},
|
|
208
|
+
itemToKey,
|
|
209
|
+
itemToString: getLabel,
|
|
210
|
+
onInputValueChange: ({
|
|
211
|
+
inputValue
|
|
212
|
+
}) => {
|
|
213
|
+
onInputChange && onInputChange(inputValue);
|
|
214
|
+
setAvailableItems(options.filter(item => {
|
|
215
|
+
if (optionsFilter) {
|
|
216
|
+
return optionsFilter(item, inputValue);
|
|
217
|
+
}
|
|
218
|
+
return getLabel(item).toLowerCase().includes(inputValue.toLowerCase());
|
|
219
|
+
}));
|
|
220
|
+
},
|
|
221
|
+
onHighlightedIndexChange({
|
|
222
|
+
highlightedIndex
|
|
223
|
+
}) {
|
|
224
|
+
if (highlightedIndex >= 0 && rowVirtualizer.getVirtualItems) {
|
|
225
|
+
const visibleIndexes = rowVirtualizer.getVirtualItems().map(v => v.index);
|
|
226
|
+
if (!visibleIndexes.includes(highlightedIndex)) {
|
|
227
|
+
rowVirtualizer.scrollToIndex(highlightedIndex, {
|
|
228
|
+
align: allowSelectAll ? 'center' : 'auto'
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (typeof rowVirtualizer.scrollOffset === 'number') {
|
|
233
|
+
setLastScrollOffset(rowVirtualizer.scrollOffset);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
onIsOpenChange: ({
|
|
237
|
+
selectedItem
|
|
238
|
+
}) => {
|
|
239
|
+
if (!multiple && selectedItem !== null) {
|
|
240
|
+
setAvailableItems(options);
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
if (controlledHighlightedIndex === 0) {
|
|
243
|
+
rowVirtualizer.scrollToOffset?.(0);
|
|
244
|
+
} else if (rowVirtualizer.scrollToOffset && lastScrollOffset > 0) {
|
|
245
|
+
rowVirtualizer.scrollToOffset(lastScrollOffset);
|
|
246
|
+
}
|
|
247
|
+
const visibleIndexes = rowVirtualizer.getVirtualItems?.().map(v => v.index) || [];
|
|
248
|
+
if (!visibleIndexes.includes(controlledHighlightedIndex)) {
|
|
249
|
+
rowVirtualizer.scrollToIndex(controlledHighlightedIndex, {
|
|
250
|
+
align: allowSelectAll ? 'center' : 'auto'
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}, 10);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
onStateChange: ({
|
|
257
|
+
type,
|
|
258
|
+
selectedItem
|
|
259
|
+
}) => {
|
|
260
|
+
switch (type) {
|
|
261
|
+
case useCombobox.stateChangeTypes.InputChange:
|
|
262
|
+
case useCombobox.stateChangeTypes.InputBlur:
|
|
263
|
+
break;
|
|
264
|
+
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
|
265
|
+
case useCombobox.stateChangeTypes.ItemClick:
|
|
266
|
+
//note: non strict check for null or undefined to allow 0
|
|
267
|
+
if (selectedItem != null && !optionDisabled(selectedItem)) {
|
|
268
|
+
if (selectedItem === AllSymbol) {
|
|
269
|
+
toggleAllSelected();
|
|
270
|
+
} else if (selectedItem === AddSymbol && typedInputValue.trim()) {
|
|
271
|
+
onAddNewOption?.(typedInputValue);
|
|
272
|
+
} else if (multiple) {
|
|
273
|
+
const shouldRemove = itemCompare ? selectedItems.some(i => itemCompare(selectedItem, i)) : selectedItems.includes(selectedItem);
|
|
274
|
+
if (shouldRemove) {
|
|
275
|
+
removeSelectedItem(selectedItem);
|
|
276
|
+
} else {
|
|
277
|
+
addSelectedItem(selectedItem);
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
setSelectedItems([selectedItem]);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
// MARK: singleselect specific
|
|
288
|
+
if (!multiple) {
|
|
289
|
+
comboBoxProps = {
|
|
290
|
+
...comboBoxProps,
|
|
291
|
+
onSelectedItemChange: changes => {
|
|
292
|
+
if (changes.selectedItem === AddSymbol) return;
|
|
293
|
+
const idx = getSelectedIndex(changes.selectedItem);
|
|
294
|
+
setControlledHighlightedIndex(idx >= 0 ? idx : 0);
|
|
295
|
+
if (onOptionsChange) {
|
|
296
|
+
let {
|
|
297
|
+
selectedItem
|
|
298
|
+
} = changes;
|
|
299
|
+
if (itemCompare) {
|
|
300
|
+
selectedItem = inputOptions.find(item => itemCompare(item, selectedItem));
|
|
301
|
+
}
|
|
302
|
+
onOptionsChange({
|
|
303
|
+
selectedItems: selectedItem ? [selectedItem] : []
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
stateReducer: (state, actionAndChanges) => {
|
|
308
|
+
const {
|
|
309
|
+
changes,
|
|
310
|
+
type
|
|
311
|
+
} = actionAndChanges;
|
|
312
|
+
switch (type) {
|
|
313
|
+
case useCombobox.stateChangeTypes.InputClick:
|
|
314
|
+
return {
|
|
315
|
+
...changes,
|
|
316
|
+
isOpen: !(disabled || readOnly),
|
|
317
|
+
highlightedIndex: controlledHighlightedIndex
|
|
318
|
+
};
|
|
319
|
+
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
|
320
|
+
case useCombobox.stateChangeTypes.ItemClick:
|
|
321
|
+
{
|
|
322
|
+
if (changes.selectedItem === AddSymbol) {
|
|
323
|
+
return {
|
|
324
|
+
...changes,
|
|
325
|
+
inputValue: ''
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const idx = getSelectedIndex(changes.selectedItem);
|
|
329
|
+
setControlledHighlightedIndex(idx >= 0 ? idx : 0);
|
|
330
|
+
return {
|
|
331
|
+
...changes,
|
|
332
|
+
highlightedIndex: idx >= 0 ? idx : 0
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
case useCombobox.stateChangeTypes.InputBlur:
|
|
336
|
+
return {
|
|
337
|
+
...changes,
|
|
338
|
+
inputValue: changes.selectedItem ? getLabel(changes.selectedItem) : ''
|
|
339
|
+
};
|
|
340
|
+
case useCombobox.stateChangeTypes.InputChange:
|
|
341
|
+
setTypedInputValue(changes.inputValue);
|
|
342
|
+
return {
|
|
343
|
+
...changes
|
|
344
|
+
};
|
|
345
|
+
case useCombobox.stateChangeTypes.InputKeyDownArrowDown:
|
|
346
|
+
if (readOnly) {
|
|
347
|
+
return {
|
|
348
|
+
...changes,
|
|
349
|
+
isOpen: false
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (state.isOpen === false) {
|
|
353
|
+
return {
|
|
354
|
+
...changes,
|
|
355
|
+
isOpen: true,
|
|
356
|
+
highlightedIndex: controlledHighlightedIndex
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
...changes,
|
|
361
|
+
highlightedIndex: findNextIndex({
|
|
362
|
+
index: changes.highlightedIndex,
|
|
363
|
+
availableItems,
|
|
364
|
+
optionDisabled,
|
|
365
|
+
allDisabled
|
|
366
|
+
})
|
|
367
|
+
};
|
|
368
|
+
case useCombobox.stateChangeTypes.InputKeyDownHome:
|
|
369
|
+
if (readOnly) {
|
|
370
|
+
return {
|
|
371
|
+
...changes,
|
|
372
|
+
isOpen: false
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
...changes,
|
|
377
|
+
highlightedIndex: findNextIndex({
|
|
378
|
+
index: 0,
|
|
379
|
+
availableItems,
|
|
380
|
+
optionDisabled,
|
|
381
|
+
allDisabled
|
|
382
|
+
})
|
|
383
|
+
};
|
|
384
|
+
case useCombobox.stateChangeTypes.InputKeyDownArrowUp:
|
|
385
|
+
if (readOnly) {
|
|
386
|
+
return {
|
|
387
|
+
...changes,
|
|
388
|
+
isOpen: false
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (state.isOpen === false) {
|
|
392
|
+
return {
|
|
393
|
+
...changes,
|
|
394
|
+
isOpen: true,
|
|
395
|
+
highlightedIndex: controlledHighlightedIndex
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
...changes,
|
|
400
|
+
highlightedIndex: findPrevIndex({
|
|
401
|
+
index: changes.highlightedIndex,
|
|
402
|
+
availableItems,
|
|
403
|
+
optionDisabled,
|
|
404
|
+
allDisabled
|
|
405
|
+
})
|
|
406
|
+
};
|
|
407
|
+
case useCombobox.stateChangeTypes.InputKeyDownEnd:
|
|
408
|
+
if (readOnly) {
|
|
409
|
+
return {
|
|
410
|
+
...changes,
|
|
411
|
+
isOpen: false
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
...changes,
|
|
416
|
+
highlightedIndex: findPrevIndex({
|
|
417
|
+
index: availableItems.length - 1,
|
|
418
|
+
availableItems,
|
|
419
|
+
optionDisabled,
|
|
420
|
+
allDisabled
|
|
421
|
+
})
|
|
422
|
+
};
|
|
423
|
+
case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem:
|
|
424
|
+
setSelectedItems([changes.selectedItem]);
|
|
425
|
+
return {
|
|
426
|
+
...changes,
|
|
427
|
+
highlightedIndex: controlledHighlightedIndex
|
|
428
|
+
};
|
|
429
|
+
default:
|
|
430
|
+
return changes;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
if (isControlled) {
|
|
435
|
+
comboBoxProps = {
|
|
436
|
+
...comboBoxProps,
|
|
437
|
+
selectedItem: selectedOptions[0] || null
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// MARK: multiselect specific
|
|
442
|
+
if (multiple) {
|
|
443
|
+
const showPlaceholder = placeholderText && selectedItems.length === 0;
|
|
444
|
+
const optionCount = totalOptions || inputOptions.length;
|
|
445
|
+
placeholderText = showPlaceholder ? placeholderText : `${selectedItems.length}/${optionCount} selected`;
|
|
446
|
+
if (selectionDisplay === 'chips') placeholderText = placeholder;
|
|
447
|
+
comboBoxProps = {
|
|
448
|
+
...comboBoxProps,
|
|
449
|
+
selectedItem: null,
|
|
450
|
+
stateReducer: (state, actionAndChanges) => {
|
|
451
|
+
const {
|
|
452
|
+
changes,
|
|
453
|
+
type
|
|
454
|
+
} = actionAndChanges;
|
|
455
|
+
switch (type) {
|
|
456
|
+
case useCombobox.stateChangeTypes.InputClick:
|
|
457
|
+
return {
|
|
458
|
+
...changes,
|
|
459
|
+
isOpen: !(disabled || readOnly)
|
|
460
|
+
};
|
|
461
|
+
case useCombobox.stateChangeTypes.InputKeyDownArrowDown:
|
|
462
|
+
case useCombobox.stateChangeTypes.InputKeyDownHome:
|
|
463
|
+
if (readOnly) {
|
|
464
|
+
return {
|
|
465
|
+
...changes,
|
|
466
|
+
isOpen: false
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
...changes,
|
|
471
|
+
highlightedIndex: findNextIndex({
|
|
472
|
+
index: changes.highlightedIndex,
|
|
473
|
+
availableItems,
|
|
474
|
+
optionDisabled,
|
|
475
|
+
allDisabled
|
|
476
|
+
})
|
|
477
|
+
};
|
|
478
|
+
case useCombobox.stateChangeTypes.InputKeyDownArrowUp:
|
|
479
|
+
case useCombobox.stateChangeTypes.InputKeyDownEnd:
|
|
480
|
+
if (readOnly) {
|
|
481
|
+
return {
|
|
482
|
+
...changes,
|
|
483
|
+
isOpen: false
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
...changes,
|
|
488
|
+
highlightedIndex: findPrevIndex({
|
|
489
|
+
index: changes.highlightedIndex,
|
|
490
|
+
availableItems,
|
|
491
|
+
optionDisabled,
|
|
492
|
+
allDisabled
|
|
493
|
+
})
|
|
494
|
+
};
|
|
495
|
+
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
|
496
|
+
case useCombobox.stateChangeTypes.ItemClick:
|
|
497
|
+
if (clearSearchOnChange) {
|
|
498
|
+
setTypedInputValue('');
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
...changes,
|
|
502
|
+
isOpen: true,
|
|
503
|
+
// keep menu open after selection.
|
|
504
|
+
highlightedIndex: state.highlightedIndex,
|
|
505
|
+
inputValue: !clearSearchOnChange ? typedInputValue : ''
|
|
506
|
+
};
|
|
507
|
+
case useCombobox.stateChangeTypes.InputChange:
|
|
508
|
+
setTypedInputValue(changes.inputValue);
|
|
509
|
+
return {
|
|
510
|
+
...changes
|
|
511
|
+
};
|
|
512
|
+
case useCombobox.stateChangeTypes.InputBlur:
|
|
513
|
+
setTypedInputValue('');
|
|
514
|
+
return {
|
|
515
|
+
...changes,
|
|
516
|
+
inputValue: ''
|
|
517
|
+
};
|
|
518
|
+
case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem:
|
|
519
|
+
return {
|
|
520
|
+
...changes,
|
|
521
|
+
inputValue: !clearSearchOnChange ? typedInputValue : changes.inputValue
|
|
522
|
+
};
|
|
523
|
+
default:
|
|
524
|
+
return changes;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const _comboBoxProps = useCombobox(comboBoxProps);
|
|
530
|
+
const clear = () => {
|
|
531
|
+
if (onClear) onClear();
|
|
532
|
+
_comboBoxProps.reset();
|
|
533
|
+
//dont clear items if they are selected and disabled
|
|
534
|
+
setSelectedItems([...selectedDisabledItemsSet]);
|
|
535
|
+
setTypedInputValue('');
|
|
536
|
+
inputRef.current?.focus();
|
|
537
|
+
};
|
|
538
|
+
const inputProps = _comboBoxProps.getInputProps(getDropdownProps({
|
|
539
|
+
preventKeyAction: multiple ? _comboBoxProps.isOpen : undefined,
|
|
540
|
+
disabled,
|
|
541
|
+
ref: inputRef
|
|
542
|
+
}));
|
|
543
|
+
const consolidatedEvents = mergeEventsFromRight(other, inputProps);
|
|
544
|
+
const selectedItemsLabels = useMemo(() => selectedItems.map(getLabel), [selectedItems, getLabel]);
|
|
545
|
+
return {
|
|
546
|
+
..._comboBoxProps,
|
|
547
|
+
getDropdownProps,
|
|
548
|
+
addSelectedItem,
|
|
549
|
+
removeSelectedItem,
|
|
550
|
+
selectedItems,
|
|
551
|
+
setSelectedItems,
|
|
552
|
+
clear,
|
|
553
|
+
availableItems,
|
|
554
|
+
getLabel,
|
|
555
|
+
scrollContainer,
|
|
556
|
+
rowVirtualizer,
|
|
557
|
+
allSelectedState,
|
|
558
|
+
toggleAllSelected,
|
|
559
|
+
typedInputValue,
|
|
560
|
+
inputRef,
|
|
561
|
+
token,
|
|
562
|
+
tokens,
|
|
563
|
+
placeholderText,
|
|
564
|
+
readOnly,
|
|
565
|
+
inputProps,
|
|
566
|
+
consolidatedEvents,
|
|
567
|
+
multiple,
|
|
568
|
+
disabled,
|
|
569
|
+
optionDisabled,
|
|
570
|
+
onAddNewOption,
|
|
571
|
+
options,
|
|
572
|
+
totalOptions,
|
|
573
|
+
label,
|
|
574
|
+
meta,
|
|
575
|
+
className,
|
|
576
|
+
style,
|
|
577
|
+
loading,
|
|
578
|
+
hideClearButton,
|
|
579
|
+
onOptionsChange,
|
|
580
|
+
onInputChange,
|
|
581
|
+
selectedOptions,
|
|
582
|
+
selectionDisplay,
|
|
583
|
+
itemToKey,
|
|
584
|
+
itemCompare,
|
|
585
|
+
allowSelectAll,
|
|
586
|
+
initialSelectedOptions,
|
|
587
|
+
optionsFilter,
|
|
588
|
+
autoWidth,
|
|
589
|
+
placeholder,
|
|
590
|
+
optionLabel,
|
|
591
|
+
clearSearchOnChange,
|
|
592
|
+
multiline,
|
|
593
|
+
dropdownHeight,
|
|
594
|
+
optionComponent,
|
|
595
|
+
helperText,
|
|
596
|
+
helperIcon,
|
|
597
|
+
noOptionsText,
|
|
598
|
+
variant,
|
|
599
|
+
onClear,
|
|
600
|
+
selectedItemsLabels,
|
|
601
|
+
restHtmlProps: other
|
|
602
|
+
};
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
export { useAutocomplete };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import pickBy from '../../node_modules/.pnpm/ramda@0.32.0/node_modules/ramda/es/pickBy.js';
|
|
2
|
+
import mergeWith from '../../node_modules/.pnpm/ramda@0.32.0/node_modules/ramda/es/mergeWith.js';
|
|
3
|
+
|
|
4
|
+
const findIndex = ({
|
|
5
|
+
calc,
|
|
6
|
+
index,
|
|
7
|
+
optionDisabled,
|
|
8
|
+
availableItems
|
|
9
|
+
}) => {
|
|
10
|
+
const nextItem = availableItems[index];
|
|
11
|
+
if (optionDisabled(nextItem) && index >= 0 && index < availableItems.length) {
|
|
12
|
+
const nextIndex = calc(index);
|
|
13
|
+
return findIndex({
|
|
14
|
+
calc,
|
|
15
|
+
index: nextIndex,
|
|
16
|
+
availableItems,
|
|
17
|
+
optionDisabled
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return index;
|
|
21
|
+
};
|
|
22
|
+
const findNextIndex = ({
|
|
23
|
+
index,
|
|
24
|
+
optionDisabled,
|
|
25
|
+
availableItems,
|
|
26
|
+
allDisabled
|
|
27
|
+
}) => {
|
|
28
|
+
if (allDisabled) return 0;
|
|
29
|
+
const options = {
|
|
30
|
+
index,
|
|
31
|
+
optionDisabled,
|
|
32
|
+
availableItems,
|
|
33
|
+
calc: num => num + 1
|
|
34
|
+
};
|
|
35
|
+
const nextIndex = findIndex(options);
|
|
36
|
+
if (nextIndex > availableItems.length - 1) {
|
|
37
|
+
// jump to start of list
|
|
38
|
+
return findIndex({
|
|
39
|
+
...options,
|
|
40
|
+
index: 0
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return nextIndex;
|
|
44
|
+
};
|
|
45
|
+
const findPrevIndex = ({
|
|
46
|
+
index,
|
|
47
|
+
optionDisabled,
|
|
48
|
+
availableItems,
|
|
49
|
+
allDisabled
|
|
50
|
+
}) => {
|
|
51
|
+
if (allDisabled) return 0;
|
|
52
|
+
const options = {
|
|
53
|
+
index,
|
|
54
|
+
optionDisabled,
|
|
55
|
+
availableItems,
|
|
56
|
+
calc: num => num - 1
|
|
57
|
+
};
|
|
58
|
+
const prevIndex = findIndex(options);
|
|
59
|
+
if (prevIndex < 0) {
|
|
60
|
+
// jump to end of list
|
|
61
|
+
return findIndex({
|
|
62
|
+
...options,
|
|
63
|
+
index: availableItems.length - 1
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return prevIndex;
|
|
67
|
+
};
|
|
68
|
+
const isEvent = (val, key) => /^on[A-Z](.*)/.test(key) && typeof val === 'function';
|
|
69
|
+
function mergeEventsFromRight(props1, props2) {
|
|
70
|
+
const events1 = pickBy(isEvent, props1);
|
|
71
|
+
const events2 = pickBy(isEvent, props2);
|
|
72
|
+
return mergeWith((event1, event2) => {
|
|
73
|
+
return (...args) => {
|
|
74
|
+
event1(...args);
|
|
75
|
+
event2(...args);
|
|
76
|
+
};
|
|
77
|
+
}, events1, events2);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/*When a user clicks the StyledList scrollbar, the input loses focus which breaks downshift
|
|
81
|
+
* keyboard navigation in the list. This code returns focus to the input on mouseUp
|
|
82
|
+
*/
|
|
83
|
+
const handleListFocus = e => {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
window?.addEventListener('mouseup', () => {
|
|
87
|
+
e.relatedTarget?.focus();
|
|
88
|
+
}, {
|
|
89
|
+
once: true
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export { findNextIndex, findPrevIndex, handleListFocus, mergeEventsFromRight };
|