@dbcdk/react-components 0.0.43 → 0.0.45
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/components/datetime-picker/DateTimePicker.js +1 -1
- package/dist/components/datetime-picker/DateTimePicker.module.css +9 -0
- package/dist/components/filter-field/FilterField.d.ts +2 -1
- package/dist/components/filter-field/FilterField.js +8 -7
- package/dist/components/filter-field/FilterField.module.css +30 -10
- package/dist/components/forms/input/Input.d.ts +11 -0
- package/dist/components/forms/input/Input.js +12 -9
- package/dist/components/forms/input/Input.module.css +182 -30
- package/dist/components/forms/input-container/InputContainer.js +2 -2
- package/dist/components/forms/input-container/InputContainer.module.css +8 -51
- package/dist/components/forms/multi-select/MultiSelect.js +59 -140
- package/dist/components/forms/typeahead/Typeahead.d.ts +34 -0
- package/dist/components/forms/typeahead/Typeahead.js +340 -0
- package/dist/components/menu/Menu.d.ts +6 -5
- package/dist/components/menu/Menu.js +14 -7
- package/dist/components/menu/Menu.module.css +15 -9
- package/dist/components/overlay/modal/Modal.js +9 -4
- package/dist/components/table/Table.js +1 -1
- package/dist/components/table/components/TableLoadingBody.js +18 -2
- package/dist/hooks/useListNavigation.d.ts +24 -0
- package/dist/hooks/useListNavigation.js +234 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -7,13 +7,11 @@ import { ClearButton } from '../../../components/clear-button/ClearButton';
|
|
|
7
7
|
import { Input } from '../../../components/forms/input/Input';
|
|
8
8
|
import { Menu } from '../../../components/menu/Menu';
|
|
9
9
|
import { Popover } from '../../../components/popover/Popover';
|
|
10
|
+
import { useListNavigation } from '../../../hooks/useListNavigation';
|
|
10
11
|
export function MultiSelect({ options, selectedValues = [], onChange, placeholder = 'Vælg', children, variant = 'outlined', size = 'md', onClear, dataCy, fullWidth = false, disabled = false, searchable = false, searchPlaceholder = 'Søg', emptyMessage = 'Ingen resultater', }) {
|
|
11
12
|
const selectedSet = useMemo(() => new Set(selectedValues), [selectedValues]);
|
|
12
13
|
const popoverRef = useRef(null);
|
|
13
|
-
const optionRefs = useRef([]);
|
|
14
14
|
const searchInputRef = useRef(null);
|
|
15
|
-
const typeaheadRef = useRef('');
|
|
16
|
-
const typeaheadTimeoutRef = useRef(null);
|
|
17
15
|
const [open, setOpen] = useState(false);
|
|
18
16
|
const [searchQuery, setSearchQuery] = useState('');
|
|
19
17
|
const filteredOptions = useMemo(() => {
|
|
@@ -22,35 +20,18 @@ export function MultiSelect({ options, selectedValues = [], onChange, placeholde
|
|
|
22
20
|
return options;
|
|
23
21
|
return options.filter(option => option.label.toLocaleLowerCase().includes(normalizedQuery));
|
|
24
22
|
}, [options, searchQuery]);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const { activeIndex, setActiveIndex, optionRefs, resetActiveIndex, clearTypeahead, handleKeyDown, } = useListNavigation({
|
|
24
|
+
options: filteredOptions,
|
|
25
|
+
getLabel: option => option.label,
|
|
26
|
+
isOpen: open,
|
|
27
|
+
onOpenChange: setOpen,
|
|
28
|
+
searchable,
|
|
29
|
+
searchInputRef,
|
|
30
|
+
getInitialActiveIndex: items => {
|
|
31
|
+
const selectedIndex = items.findIndex(option => selectedSet.has(option.value));
|
|
32
|
+
return selectedIndex >= 0 ? selectedIndex : 0;
|
|
33
|
+
},
|
|
28
34
|
});
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
var _a;
|
|
31
|
-
if (!open)
|
|
32
|
-
return;
|
|
33
|
-
if (searchable && document.activeElement === searchInputRef.current)
|
|
34
|
-
return;
|
|
35
|
-
(_a = optionRefs.current[activeIndex]) === null || _a === void 0 ? void 0 : _a.focus();
|
|
36
|
-
}, [activeIndex, open, searchable]);
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
return () => {
|
|
39
|
-
if (typeaheadTimeoutRef.current)
|
|
40
|
-
clearTimeout(typeaheadTimeoutRef.current);
|
|
41
|
-
};
|
|
42
|
-
}, []);
|
|
43
|
-
const resetActiveIndex = () => {
|
|
44
|
-
const selectedIndex = filteredOptions.findIndex(option => selectedSet.has(option.value));
|
|
45
|
-
setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
|
|
46
|
-
};
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
setActiveIndex(current => {
|
|
49
|
-
if (filteredOptions.length === 0)
|
|
50
|
-
return 0;
|
|
51
|
-
return Math.min(current, filteredOptions.length - 1);
|
|
52
|
-
});
|
|
53
|
-
}, [filteredOptions]);
|
|
54
35
|
useEffect(() => {
|
|
55
36
|
var _a;
|
|
56
37
|
if (!open || !searchable)
|
|
@@ -67,137 +48,75 @@ export function MultiSelect({ options, selectedValues = [], onChange, placeholde
|
|
|
67
48
|
next.add(value);
|
|
68
49
|
onChange(Array.from(next));
|
|
69
50
|
};
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
if (typeaheadTimeoutRef.current) {
|
|
73
|
-
clearTimeout(typeaheadTimeoutRef.current);
|
|
74
|
-
typeaheadTimeoutRef.current = null;
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
const findTypeaheadMatch = (query, startIndex) => {
|
|
78
|
-
var _a, _b;
|
|
79
|
-
if (!query || filteredOptions.length === 0)
|
|
80
|
-
return -1;
|
|
81
|
-
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
82
|
-
if (!normalizedQuery)
|
|
83
|
-
return -1;
|
|
84
|
-
for (let step = 1; step <= filteredOptions.length; step += 1) {
|
|
85
|
-
const index = (startIndex + step + filteredOptions.length) % filteredOptions.length;
|
|
86
|
-
const label = (_b = (_a = filteredOptions[index]) === null || _a === void 0 ? void 0 : _a.label) === null || _b === void 0 ? void 0 : _b.trim().toLocaleLowerCase();
|
|
87
|
-
if (label === null || label === void 0 ? void 0 : label.startsWith(normalizedQuery))
|
|
88
|
-
return index;
|
|
89
|
-
}
|
|
90
|
-
return -1;
|
|
91
|
-
};
|
|
92
|
-
const handleTypeahead = (key) => {
|
|
93
|
-
const nextBuffer = `${typeaheadRef.current}${key.toLocaleLowerCase()}`;
|
|
94
|
-
const repeatedChar = new Set(nextBuffer).size === 1;
|
|
95
|
-
let nextIndex = findTypeaheadMatch(nextBuffer, activeIndex);
|
|
96
|
-
let appliedBuffer = nextBuffer;
|
|
97
|
-
if (nextIndex < 0 && repeatedChar) {
|
|
98
|
-
appliedBuffer = key.toLocaleLowerCase();
|
|
99
|
-
nextIndex = findTypeaheadMatch(appliedBuffer, activeIndex);
|
|
100
|
-
}
|
|
101
|
-
if (typeaheadTimeoutRef.current)
|
|
102
|
-
clearTimeout(typeaheadTimeoutRef.current);
|
|
103
|
-
typeaheadRef.current = appliedBuffer;
|
|
104
|
-
typeaheadTimeoutRef.current = setTimeout(() => {
|
|
105
|
-
typeaheadRef.current = '';
|
|
106
|
-
typeaheadTimeoutRef.current = null;
|
|
107
|
-
}, 500);
|
|
108
|
-
if (nextIndex < 0)
|
|
109
|
-
return;
|
|
110
|
-
setActiveIndex(nextIndex);
|
|
111
|
-
if (!open)
|
|
112
|
-
setOpen(true);
|
|
113
|
-
};
|
|
114
|
-
const handleKeyDown = (e) => {
|
|
115
|
-
var _a, _b, _c, _d;
|
|
51
|
+
const handleCombinedKeyDown = (e) => {
|
|
52
|
+
var _a;
|
|
116
53
|
if (searchable && e.target === searchInputRef.current) {
|
|
117
54
|
switch (e.key) {
|
|
118
|
-
case '
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
(
|
|
55
|
+
case 'Enter':
|
|
56
|
+
if (filteredOptions[activeIndex]) {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
toggleValue(filteredOptions[activeIndex].value);
|
|
59
|
+
}
|
|
122
60
|
return;
|
|
123
|
-
case '
|
|
124
|
-
|
|
125
|
-
|
|
61
|
+
case ' ':
|
|
62
|
+
if (filteredOptions[activeIndex]) {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
toggleValue(filteredOptions[activeIndex].value);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
default:
|
|
68
|
+
handleKeyDown(e);
|
|
126
69
|
return;
|
|
127
70
|
}
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (e.key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey && !/\s/.test(e.key)) {
|
|
131
|
-
e.preventDefault();
|
|
132
|
-
handleTypeahead(e.key);
|
|
133
|
-
return;
|
|
134
71
|
}
|
|
135
72
|
switch (e.key) {
|
|
136
|
-
case 'ArrowDown':
|
|
137
|
-
e.preventDefault();
|
|
138
|
-
if (!open) {
|
|
139
|
-
setOpen(true);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
setActiveIndex(i => Math.min(i + 1, filteredOptions.length - 1));
|
|
143
|
-
break;
|
|
144
|
-
case 'ArrowUp':
|
|
145
|
-
e.preventDefault();
|
|
146
|
-
if (!open) {
|
|
147
|
-
setOpen(true);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
if (searchable && optionRefs.current[activeIndex] === document.activeElement) {
|
|
151
|
-
if (activeIndex === 0) {
|
|
152
|
-
(_c = searchInputRef.current) === null || _c === void 0 ? void 0 : _c.focus();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
setActiveIndex(i => Math.max(i - 1, 0));
|
|
157
|
-
break;
|
|
158
|
-
case 'Home':
|
|
159
|
-
if (!open)
|
|
160
|
-
return;
|
|
161
|
-
e.preventDefault();
|
|
162
|
-
setActiveIndex(0);
|
|
163
|
-
break;
|
|
164
|
-
case 'End':
|
|
165
|
-
if (!open)
|
|
166
|
-
return;
|
|
167
|
-
e.preventDefault();
|
|
168
|
-
setActiveIndex(filteredOptions.length - 1);
|
|
169
|
-
break;
|
|
170
73
|
case 'Enter':
|
|
171
|
-
case ' ':
|
|
74
|
+
case ' ': {
|
|
172
75
|
e.preventDefault();
|
|
173
76
|
clearTypeahead();
|
|
174
77
|
if (!open) {
|
|
175
78
|
setOpen(true);
|
|
176
79
|
return;
|
|
177
80
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
81
|
+
const activeOption = filteredOptions[activeIndex];
|
|
82
|
+
if (activeOption)
|
|
83
|
+
toggleValue(activeOption.value);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
181
86
|
case 'Escape':
|
|
182
87
|
if (!open)
|
|
183
88
|
return;
|
|
184
89
|
e.preventDefault();
|
|
185
|
-
(
|
|
186
|
-
|
|
90
|
+
(_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
91
|
+
return;
|
|
92
|
+
default:
|
|
93
|
+
handleKeyDown(e);
|
|
187
94
|
}
|
|
188
95
|
};
|
|
189
96
|
return (_jsx(Popover, { ref: popoverRef, open: open, onOpenChange: next => {
|
|
190
97
|
setOpen(next);
|
|
191
|
-
if (next)
|
|
98
|
+
if (next) {
|
|
192
99
|
resetActiveIndex();
|
|
193
|
-
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
194
102
|
setSearchQuery('');
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
103
|
+
}
|
|
104
|
+
}, dataCy: dataCy, fullWidth: fullWidth, autoFocusContent: false, returnFocus: true, trigger: (onClick, icon, isOpen) => (_jsx(Button, { variant: variant, onClick: onClick, onKeyDown: handleCombinedKeyDown, size: size, fullWidth: fullWidth, disabled: disabled, "aria-haspopup": "menu", "aria-expanded": !!isOpen, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [_jsx("span", { children: children !== null && children !== void 0 ? children : placeholder }), selectedValues.length > 0 ? _jsx(Chip, { size: "sm", children: selectedValues.length }) : null] }), _jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [onClear && selectedValues.length > 0 ? _jsx(ClearButton, { onClick: onClear }) : null, icon] })] }) })), children: _jsxs(Menu, { onKeyDown: handleCombinedKeyDown, children: [searchable ? (_jsx(Menu.Item, { children: _jsx(Input, { ref: searchInputRef, value: searchQuery, onChange: e => setSearchQuery(e.target.value), onKeyDown: handleCombinedKeyDown, placeholder: searchPlaceholder, icon: _jsx(Search, { size: 16 }), fullWidth: true }) })) : null, filteredOptions.map((option, index) => {
|
|
105
|
+
const isSelected = selectedSet.has(option.value);
|
|
106
|
+
const isActive = index === activeIndex;
|
|
107
|
+
return (_jsx(Menu.Item, { active: isActive, children: _jsxs("button", { ref: el => {
|
|
108
|
+
if (optionRefs.current) {
|
|
109
|
+
optionRefs.current[index] = el;
|
|
110
|
+
}
|
|
111
|
+
}, type: "button", role: "menuitemcheckbox", "aria-checked": isSelected, tabIndex: isActive ? 0 : -1, disabled: disabled, onFocus: () => setActiveIndex(index), onClick: () => toggleValue(option.value), children: [_jsx("span", { style: {
|
|
112
|
+
pointerEvents: 'none',
|
|
113
|
+
display: 'flex',
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
color: !isSelected ? 'var(--color-border-strong)' : 'inherit',
|
|
116
|
+
}, children: isSelected ? _jsx(Check, {}) : _jsx(Square, {}) }), option.icon ? (_jsx("span", { style: {
|
|
117
|
+
pointerEvents: 'none',
|
|
118
|
+
display: 'flex',
|
|
119
|
+
alignItems: 'center',
|
|
120
|
+
}, children: option.icon })) : null, _jsx("span", { children: option.label })] }) }, option.value));
|
|
121
|
+
}), filteredOptions.length === 0 ? _jsx(Menu.Item, { disabled: true, children: emptyMessage }) : null] }) }));
|
|
203
122
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { InputProps, InputVariant } from '../../../components/forms/input/Input';
|
|
3
|
+
type TypeaheadOption<T> = {
|
|
4
|
+
label: string;
|
|
5
|
+
value: T;
|
|
6
|
+
};
|
|
7
|
+
type MultiValueDisplayMode = 'chips' | 'count';
|
|
8
|
+
type MultiSelectedValuesDisplayMode = 'hidden' | 'below-input';
|
|
9
|
+
type MultiSelectedValueChipContent = 'label' | 'value' | 'value-label';
|
|
10
|
+
interface TypeaheadProps<T> {
|
|
11
|
+
options: TypeaheadOption<T>[];
|
|
12
|
+
mode?: 'single' | 'multi';
|
|
13
|
+
multiValueDisplayMode?: MultiValueDisplayMode;
|
|
14
|
+
multiSelectedValuesDisplayMode?: MultiSelectedValuesDisplayMode;
|
|
15
|
+
multiSelectedValueChipContent?: MultiSelectedValueChipContent;
|
|
16
|
+
selectedValue?: T | T[] | null;
|
|
17
|
+
onChange: (value: T | T[] | null) => void;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
variant?: InputVariant;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
fullWidth?: boolean;
|
|
22
|
+
onClear?: () => void;
|
|
23
|
+
emptyMessage?: string;
|
|
24
|
+
filterOptions?: (options: TypeaheadOption<T>[], query: string) => TypeaheadOption<T>[];
|
|
25
|
+
inputProps?: Omit<InputProps, 'onChange' | 'value'>;
|
|
26
|
+
inputSize?: InputProps['inputSize'];
|
|
27
|
+
width?: InputProps['width'];
|
|
28
|
+
autoComplete?: InputProps['autoComplete'];
|
|
29
|
+
autoCorrect?: InputProps['autoCorrect'];
|
|
30
|
+
autoCapitalize?: InputProps['autoCapitalize'];
|
|
31
|
+
spellCheck?: InputProps['spellCheck'];
|
|
32
|
+
}
|
|
33
|
+
export declare function Typeahead<T extends string | number>({ options, mode, multiValueDisplayMode, multiSelectedValuesDisplayMode, multiSelectedValueChipContent, selectedValue, onChange, placeholder, variant, disabled, fullWidth, onClear, emptyMessage, filterOptions, inputProps, inputSize, width, autoComplete, autoCorrect, autoCapitalize, spellCheck, }: TypeaheadProps<T>): React.ReactElement;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { Chip } from '../../../components/chip/Chip';
|
|
6
|
+
import { Input } from '../../../components/forms/input/Input';
|
|
7
|
+
import { Menu } from '../../../components/menu/Menu';
|
|
8
|
+
import { Popover } from '../../../components/popover/Popover';
|
|
9
|
+
export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'chips', multiSelectedValuesDisplayMode = 'hidden', multiSelectedValueChipContent = 'label', selectedValue = null, onChange, placeholder, variant = 'outlined', disabled = false, fullWidth = false, onClear, emptyMessage = 'No results', filterOptions, inputProps, inputSize, width, autoComplete, autoCorrect, autoCapitalize, spellCheck, }) {
|
|
10
|
+
var _a;
|
|
11
|
+
const inputRef = useRef(null);
|
|
12
|
+
const listboxRef = useRef(null);
|
|
13
|
+
const optionRefs = useRef([]);
|
|
14
|
+
const listboxId = useId();
|
|
15
|
+
const { onFocus: inputPropsOnFocus, onBlur: inputPropsOnBlur, onKeyDown: inputPropsOnKeyDown, onMouseDown: inputPropsOnMouseDown, onClear: inputPropsOnClear, startAdornment: inputPropsStartAdornment, ...passthroughInputProps } = inputProps !== null && inputProps !== void 0 ? inputProps : {};
|
|
16
|
+
const selectedOption = useMemo(() => {
|
|
17
|
+
var _a;
|
|
18
|
+
if (mode === 'multi')
|
|
19
|
+
return null;
|
|
20
|
+
return (_a = options.find(option => option.value === selectedValue)) !== null && _a !== void 0 ? _a : null;
|
|
21
|
+
}, [options, selectedValue, mode]);
|
|
22
|
+
const selectedOptions = useMemo(() => {
|
|
23
|
+
if (mode !== 'multi' || !Array.isArray(selectedValue))
|
|
24
|
+
return [];
|
|
25
|
+
return options.filter(option => selectedValue.includes(option.value));
|
|
26
|
+
}, [options, selectedValue, mode]);
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
const [inputValue, setInputValue] = useState(mode === 'multi' ? '' : ((_a = selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.label) !== null && _a !== void 0 ? _a : ''));
|
|
29
|
+
const [query, setQuery] = useState('');
|
|
30
|
+
const [activeIndex, setActiveIndex] = useState(-1);
|
|
31
|
+
const getSelectedValueChipLabel = React.useCallback((option) => {
|
|
32
|
+
switch (multiSelectedValueChipContent) {
|
|
33
|
+
case 'value':
|
|
34
|
+
return String(option.value);
|
|
35
|
+
case 'value-label':
|
|
36
|
+
return `${String(option.value)} - ${option.label}`;
|
|
37
|
+
case 'label':
|
|
38
|
+
default:
|
|
39
|
+
return option.label;
|
|
40
|
+
}
|
|
41
|
+
}, [multiSelectedValueChipContent]);
|
|
42
|
+
const multiSelectionAdornment = mode === 'multi' && selectedOptions.length > 0 ? (multiValueDisplayMode === 'count' ? (_jsxs("span", { className: "dbc-muted-text dbc-sm-text", style: { whiteSpace: 'nowrap', flexShrink: 0, marginRight: 6 }, children: ["(", selectedOptions.length, ")"] })) : ((() => {
|
|
43
|
+
const MAX_CHIPS = 2;
|
|
44
|
+
const chipsToShow = selectedOptions.slice(0, MAX_CHIPS);
|
|
45
|
+
const extraCount = selectedOptions.length - MAX_CHIPS;
|
|
46
|
+
return (_jsxs("div", { style: {
|
|
47
|
+
display: 'flex',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
gap: 4,
|
|
50
|
+
flexWrap: 'nowrap',
|
|
51
|
+
overflow: 'hidden',
|
|
52
|
+
}, children: [chipsToShow.map(option => (_jsx(Chip, { size: "sm", type: "rounded", onClose: () => commitSelection(option), children: option.label }, option.value))), extraCount > 0 && (_jsxs("span", { className: "dbc-muted-text dbc-sm-text dbc-px-xxs", children: ["+", extraCount] }))] }));
|
|
53
|
+
})())) : undefined;
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
var _a;
|
|
56
|
+
if (mode === 'multi') {
|
|
57
|
+
setInputValue('');
|
|
58
|
+
setQuery('');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const nextLabel = (_a = selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.label) !== null && _a !== void 0 ? _a : '';
|
|
62
|
+
setInputValue(nextLabel);
|
|
63
|
+
}
|
|
64
|
+
}, [selectedOption, mode]);
|
|
65
|
+
const filteredOptions = useMemo(() => {
|
|
66
|
+
if (filterOptions)
|
|
67
|
+
return filterOptions(options, query);
|
|
68
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
69
|
+
if (!normalizedQuery)
|
|
70
|
+
return options;
|
|
71
|
+
return options.filter(option => option.label.toLocaleLowerCase().includes(normalizedQuery));
|
|
72
|
+
}, [filterOptions, query, options]);
|
|
73
|
+
const getSelectedIndex = React.useCallback((items) => {
|
|
74
|
+
if (items.length === 0)
|
|
75
|
+
return -1;
|
|
76
|
+
if (mode === 'multi') {
|
|
77
|
+
if (!Array.isArray(selectedValue) || selectedValue.length === 0)
|
|
78
|
+
return 0;
|
|
79
|
+
const firstSelectedIndex = items.findIndex(option => selectedValue.includes(option.value));
|
|
80
|
+
return firstSelectedIndex >= 0 ? firstSelectedIndex : 0;
|
|
81
|
+
}
|
|
82
|
+
const singleSelectedIndex = items.findIndex(option => option.value === selectedValue);
|
|
83
|
+
return singleSelectedIndex >= 0 ? singleSelectedIndex : 0;
|
|
84
|
+
}, [mode, selectedValue]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
setActiveIndex(current => {
|
|
87
|
+
if (filteredOptions.length === 0)
|
|
88
|
+
return -1;
|
|
89
|
+
if (current < 0)
|
|
90
|
+
return getSelectedIndex(filteredOptions);
|
|
91
|
+
return Math.min(current, filteredOptions.length - 1);
|
|
92
|
+
});
|
|
93
|
+
}, [filteredOptions, getSelectedIndex]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
var _a;
|
|
96
|
+
if (!open || activeIndex < 0)
|
|
97
|
+
return;
|
|
98
|
+
const activeEl = optionRefs.current[activeIndex];
|
|
99
|
+
(_a = activeEl === null || activeEl === void 0 ? void 0 : activeEl.scrollIntoView) === null || _a === void 0 ? void 0 : _a.call(activeEl, { block: 'nearest' });
|
|
100
|
+
}, [open, activeIndex, filteredOptions]);
|
|
101
|
+
const commitSelection = (option) => {
|
|
102
|
+
var _a, _b;
|
|
103
|
+
if (mode === 'multi') {
|
|
104
|
+
if (!option)
|
|
105
|
+
return;
|
|
106
|
+
const nextValues = Array.isArray(selectedValue) ? [...selectedValue] : [];
|
|
107
|
+
const idx = nextValues.indexOf(option.value);
|
|
108
|
+
if (idx > -1) {
|
|
109
|
+
nextValues.splice(idx, 1);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
nextValues.push(option.value);
|
|
113
|
+
}
|
|
114
|
+
onChange(nextValues);
|
|
115
|
+
setInputValue('');
|
|
116
|
+
setQuery('');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
onChange((_a = option === null || option === void 0 ? void 0 : option.value) !== null && _a !== void 0 ? _a : null);
|
|
120
|
+
setInputValue((_b = option === null || option === void 0 ? void 0 : option.label) !== null && _b !== void 0 ? _b : '');
|
|
121
|
+
setQuery('');
|
|
122
|
+
setOpen(false);
|
|
123
|
+
setActiveIndex(-1);
|
|
124
|
+
};
|
|
125
|
+
const handleInputChange = (nextValue) => {
|
|
126
|
+
setInputValue(nextValue);
|
|
127
|
+
setQuery(nextValue);
|
|
128
|
+
if (!open)
|
|
129
|
+
setOpen(true);
|
|
130
|
+
if (mode === 'multi')
|
|
131
|
+
return;
|
|
132
|
+
const exactMatch = options.find(option => option.label === nextValue);
|
|
133
|
+
if (!exactMatch && selectedValue !== null) {
|
|
134
|
+
onChange(null);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const handleBlur = () => {
|
|
138
|
+
if (mode === 'multi') {
|
|
139
|
+
setInputValue('');
|
|
140
|
+
setQuery('');
|
|
141
|
+
setOpen(false);
|
|
142
|
+
setActiveIndex(-1);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const exactLabelMatch = options.find(option => option.label === inputValue);
|
|
146
|
+
if (exactLabelMatch) {
|
|
147
|
+
commitSelection(exactLabelMatch);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (selectedOption) {
|
|
151
|
+
setInputValue(selectedOption.label);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
setInputValue('');
|
|
155
|
+
}
|
|
156
|
+
setQuery('');
|
|
157
|
+
setOpen(false);
|
|
158
|
+
setActiveIndex(-1);
|
|
159
|
+
};
|
|
160
|
+
const openWithAllOptions = React.useCallback(() => {
|
|
161
|
+
setQuery('');
|
|
162
|
+
setOpen(true);
|
|
163
|
+
setActiveIndex(getSelectedIndex(options));
|
|
164
|
+
}, [getSelectedIndex, options]);
|
|
165
|
+
const openWithCurrentFilter = React.useCallback(() => {
|
|
166
|
+
setOpen(true);
|
|
167
|
+
setActiveIndex(getSelectedIndex(filteredOptions));
|
|
168
|
+
}, [getSelectedIndex, filteredOptions]);
|
|
169
|
+
const handleOpen = React.useCallback(() => {
|
|
170
|
+
if (mode === 'single' && selectedOption) {
|
|
171
|
+
openWithAllOptions();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
openWithCurrentFilter();
|
|
175
|
+
}, [mode, selectedOption, openWithAllOptions, openWithCurrentFilter]);
|
|
176
|
+
const handleKeyDown = (e) => {
|
|
177
|
+
switch (e.key) {
|
|
178
|
+
case 'ArrowDown':
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
if (!open) {
|
|
181
|
+
handleOpen();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
setActiveIndex(index => {
|
|
185
|
+
if (filteredOptions.length === 0)
|
|
186
|
+
return -1;
|
|
187
|
+
if (index < 0)
|
|
188
|
+
return getSelectedIndex(filteredOptions);
|
|
189
|
+
return Math.min(index + 1, filteredOptions.length - 1);
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
case 'ArrowUp':
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
if (!open) {
|
|
195
|
+
handleOpen();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
setActiveIndex(index => {
|
|
199
|
+
if (filteredOptions.length === 0)
|
|
200
|
+
return -1;
|
|
201
|
+
if (index < 0)
|
|
202
|
+
return filteredOptions.length - 1;
|
|
203
|
+
return Math.max(index - 1, 0);
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
case 'Home':
|
|
207
|
+
if (!open || filteredOptions.length === 0)
|
|
208
|
+
return;
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
setActiveIndex(0);
|
|
211
|
+
return;
|
|
212
|
+
case 'End':
|
|
213
|
+
if (!open || filteredOptions.length === 0)
|
|
214
|
+
return;
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
setActiveIndex(filteredOptions.length - 1);
|
|
217
|
+
return;
|
|
218
|
+
case 'Enter':
|
|
219
|
+
if (!open)
|
|
220
|
+
return;
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
if (activeIndex >= 0 && filteredOptions[activeIndex]) {
|
|
223
|
+
commitSelection(filteredOptions[activeIndex]);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
case 'Escape':
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
e.stopPropagation();
|
|
229
|
+
setOpen(false);
|
|
230
|
+
setActiveIndex(-1);
|
|
231
|
+
setQuery('');
|
|
232
|
+
if (mode === 'single' && selectedOption) {
|
|
233
|
+
setInputValue(selectedOption.label);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
setInputValue('');
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
return (_jsxs("div", { style: {
|
|
242
|
+
display: 'flex',
|
|
243
|
+
flexDirection: 'column',
|
|
244
|
+
gap: mode === 'multi' &&
|
|
245
|
+
multiSelectedValuesDisplayMode === 'below-input' &&
|
|
246
|
+
selectedOptions.length > 0
|
|
247
|
+
? 8
|
|
248
|
+
: 0,
|
|
249
|
+
width: fullWidth ? '100%' : undefined,
|
|
250
|
+
}, children: [_jsx(Popover, { open: open, onOpenChange: nextOpen => {
|
|
251
|
+
setOpen(nextOpen);
|
|
252
|
+
if (nextOpen) {
|
|
253
|
+
if (mode === 'single' && selectedOption) {
|
|
254
|
+
setQuery('');
|
|
255
|
+
setActiveIndex(getSelectedIndex(options));
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
setActiveIndex(getSelectedIndex(filteredOptions));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
setActiveIndex(-1);
|
|
263
|
+
}
|
|
264
|
+
}, fullWidth: fullWidth, autoFocusContent: false, returnFocus: false, trigger: openPopover => {
|
|
265
|
+
var _a, _b, _c, _d, _e;
|
|
266
|
+
return (_jsx(Input, { ...passthroughInputProps, ref: inputRef, value: inputValue, startAdornment: multiSelectionAdornment || inputPropsStartAdornment ? (_jsxs(_Fragment, { children: [multiSelectionAdornment, inputPropsStartAdornment] })) : undefined, onFocus: e => {
|
|
267
|
+
inputPropsOnFocus === null || inputPropsOnFocus === void 0 ? void 0 : inputPropsOnFocus(e);
|
|
268
|
+
if (e.defaultPrevented)
|
|
269
|
+
return;
|
|
270
|
+
handleOpen();
|
|
271
|
+
openPopover(e);
|
|
272
|
+
}, onMouseDown: e => {
|
|
273
|
+
var _a;
|
|
274
|
+
inputPropsOnMouseDown === null || inputPropsOnMouseDown === void 0 ? void 0 : inputPropsOnMouseDown(e);
|
|
275
|
+
if (e.defaultPrevented)
|
|
276
|
+
return;
|
|
277
|
+
const isAlreadyFocused = document.activeElement === inputRef.current;
|
|
278
|
+
if (isAlreadyFocused && open) {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
setOpen(false);
|
|
281
|
+
setActiveIndex(-1);
|
|
282
|
+
if (mode === 'single') {
|
|
283
|
+
setQuery('');
|
|
284
|
+
setInputValue((_a = selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.label) !== null && _a !== void 0 ? _a : '');
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
setQuery('');
|
|
288
|
+
setInputValue('');
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (isAlreadyFocused && mode === 'single' && selectedOption) {
|
|
293
|
+
setQuery('');
|
|
294
|
+
setOpen(true);
|
|
295
|
+
setActiveIndex(getSelectedIndex(options));
|
|
296
|
+
}
|
|
297
|
+
else if (isAlreadyFocused && !open) {
|
|
298
|
+
setOpen(true);
|
|
299
|
+
setActiveIndex(getSelectedIndex(filteredOptions));
|
|
300
|
+
}
|
|
301
|
+
}, onChange: e => handleInputChange(e.currentTarget.value), onBlur: e => {
|
|
302
|
+
inputPropsOnBlur === null || inputPropsOnBlur === void 0 ? void 0 : inputPropsOnBlur(e);
|
|
303
|
+
if (e.defaultPrevented)
|
|
304
|
+
return;
|
|
305
|
+
handleBlur();
|
|
306
|
+
}, onKeyDown: e => {
|
|
307
|
+
inputPropsOnKeyDown === null || inputPropsOnKeyDown === void 0 ? void 0 : inputPropsOnKeyDown(e);
|
|
308
|
+
if (e.defaultPrevented)
|
|
309
|
+
return;
|
|
310
|
+
handleKeyDown(e);
|
|
311
|
+
}, placeholder: placeholder, variant: variant, inputSize: (_a = inputSize !== null && inputSize !== void 0 ? inputSize : inputProps === null || inputProps === void 0 ? void 0 : inputProps.inputSize) !== null && _a !== void 0 ? _a : 'md', width: width !== null && width !== void 0 ? width : inputProps === null || inputProps === void 0 ? void 0 : inputProps.width, autoComplete: (_b = autoComplete !== null && autoComplete !== void 0 ? autoComplete : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoComplete) !== null && _b !== void 0 ? _b : 'off', autoCorrect: (_c = autoCorrect !== null && autoCorrect !== void 0 ? autoCorrect : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoCorrect) !== null && _c !== void 0 ? _c : 'off', autoCapitalize: (_d = autoCapitalize !== null && autoCapitalize !== void 0 ? autoCapitalize : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoCapitalize) !== null && _d !== void 0 ? _d : 'off', spellCheck: (_e = spellCheck !== null && spellCheck !== void 0 ? spellCheck : inputProps === null || inputProps === void 0 ? void 0 : inputProps.spellCheck) !== null && _e !== void 0 ? _e : false, disabled: disabled, fullWidth: fullWidth, onClear: () => {
|
|
312
|
+
setInputValue('');
|
|
313
|
+
setQuery('');
|
|
314
|
+
setActiveIndex(-1);
|
|
315
|
+
onChange(mode === 'multi' ? [] : null);
|
|
316
|
+
inputPropsOnClear === null || inputPropsOnClear === void 0 ? void 0 : inputPropsOnClear();
|
|
317
|
+
onClear === null || onClear === void 0 ? void 0 : onClear();
|
|
318
|
+
if (open) {
|
|
319
|
+
setOpen(false);
|
|
320
|
+
}
|
|
321
|
+
}, role: "combobox", "aria-expanded": open, "aria-controls": listboxId, "aria-autocomplete": "list", "aria-activedescendant": open && activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined }));
|
|
322
|
+
}, children: _jsx(Menu, { role: "listbox", id: listboxId, ref: listboxRef, children: filteredOptions.length > 0 ? (filteredOptions.map((option, index) => {
|
|
323
|
+
const isActive = index === activeIndex;
|
|
324
|
+
const isSelected = mode === 'multi'
|
|
325
|
+
? Array.isArray(selectedValue) && selectedValue.includes(option.value)
|
|
326
|
+
: option.value === selectedValue;
|
|
327
|
+
return (_jsx(Menu.Item, { active: isActive, selected: isSelected, children: _jsx("button", { ref: node => {
|
|
328
|
+
optionRefs.current[index] = node;
|
|
329
|
+
}, id: `${listboxId}-option-${index}`, type: "button", role: "option", "aria-selected": isSelected, onMouseEnter: () => setActiveIndex(index), onMouseDown: e => {
|
|
330
|
+
e.preventDefault();
|
|
331
|
+
}, onClick: () => commitSelection(option), children: _jsx("span", { children: option.label }) }) }, option.value));
|
|
332
|
+
})) : (_jsx(Menu.Item, { disabled: true, children: emptyMessage })) }) }), mode === 'multi' &&
|
|
333
|
+
multiSelectedValuesDisplayMode === 'below-input' &&
|
|
334
|
+
selectedOptions.length > 0 && (_jsx("div", { style: {
|
|
335
|
+
display: 'flex',
|
|
336
|
+
flexWrap: 'wrap',
|
|
337
|
+
gap: 8,
|
|
338
|
+
alignItems: 'flex-start',
|
|
339
|
+
}, children: selectedOptions.map(option => (_jsx(Chip, { size: "sm", type: "default", severity: "neutral", onClose: () => commitSelection(option), children: getSelectedValueChipLabel(option) }, option.value))) }))] }));
|
|
340
|
+
}
|
|
@@ -12,6 +12,7 @@ export type MenuSeparatorProps = React.LiHTMLAttributes<HTMLLIElement>;
|
|
|
12
12
|
export interface MenuItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
|
|
13
13
|
children: React.ReactNode;
|
|
14
14
|
active?: boolean;
|
|
15
|
+
selected?: boolean;
|
|
15
16
|
disabled?: boolean;
|
|
16
17
|
/**
|
|
17
18
|
* Override the role applied to the interactive element for this item only.
|
|
@@ -33,9 +34,9 @@ export interface MenuRadioItemProps extends Omit<React.LiHTMLAttributes<HTMLLIEl
|
|
|
33
34
|
label: string;
|
|
34
35
|
onValueChange?: (value: string) => void;
|
|
35
36
|
}
|
|
36
|
-
export declare const Menu: React.
|
|
37
|
-
Item: React.
|
|
38
|
-
CheckItem: React.
|
|
39
|
-
RadioItem: React.
|
|
40
|
-
Separator: React.
|
|
37
|
+
export declare const Menu: React.ForwardRefExoticComponent<MenuProps & React.RefAttributes<HTMLUListElement>> & {
|
|
38
|
+
Item: React.ForwardRefExoticComponent<MenuItemProps & React.RefAttributes<HTMLLIElement>>;
|
|
39
|
+
CheckItem: React.ForwardRefExoticComponent<MenuCheckItemProps & React.RefAttributes<HTMLLIElement>>;
|
|
40
|
+
RadioItem: React.ForwardRefExoticComponent<MenuRadioItemProps & React.RefAttributes<HTMLLIElement>>;
|
|
41
|
+
Separator: React.ForwardRefExoticComponent<MenuSeparatorProps & React.RefAttributes<HTMLLIElement>>;
|
|
41
42
|
};
|