@dbcdk/react-components 0.0.44 → 0.0.46

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.
@@ -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 [activeIndex, setActiveIndex] = useState(() => {
26
- const selectedIndex = filteredOptions.findIndex(option => selectedSet.has(option.value));
27
- return selectedIndex >= 0 ? selectedIndex : 0;
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 clearTypeahead = () => {
71
- typeaheadRef.current = '';
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 'ArrowDown':
119
- e.preventDefault();
120
- if (filteredOptions.length > 0)
121
- (_a = optionRefs.current[activeIndex]) === null || _a === void 0 ? void 0 : _a.focus();
55
+ case 'Enter':
56
+ if (filteredOptions[activeIndex]) {
57
+ e.preventDefault();
58
+ toggleValue(filteredOptions[activeIndex].value);
59
+ }
122
60
  return;
123
- case 'Escape':
124
- e.preventDefault();
125
- (_b = popoverRef.current) === null || _b === void 0 ? void 0 : _b.close();
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
- if (filteredOptions[activeIndex])
179
- toggleValue(filteredOptions[activeIndex].value);
180
- break;
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
- (_d = popoverRef.current) === null || _d === void 0 ? void 0 : _d.close();
186
- break;
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
- else
100
+ }
101
+ else {
194
102
  setSearchQuery('');
195
- }, dataCy: dataCy, fullWidth: fullWidth, autoFocusContent: false, returnFocus: true, trigger: (onClick, icon, isOpen) => (_jsx(Button, { variant: variant, onClick: onClick, onKeyDown: handleKeyDown, 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 }), icon] })] }) })), children: _jsxs(Menu, { onKeyDown: handleKeyDown, children: [searchable ? (_jsx(Menu.Item, { children: _jsx(Input, { ref: searchInputRef, value: searchQuery, onChange: e => setSearchQuery(e.target.value), onKeyDown: handleKeyDown, placeholder: searchPlaceholder, icon: _jsx(Search, { size: 16 }), fullWidth: true }) })) : null, filteredOptions.map((option, index) => (_jsx(Menu.Item, { active: index === activeIndex, children: _jsxs("button", { ref: el => (optionRefs.current[index] = el), type: "button", role: "menuitemcheckbox", "aria-checked": selectedSet.has(option.value), tabIndex: index === activeIndex ? 0 : -1, disabled: disabled, onFocus: () => setActiveIndex(index), onClick: () => {
196
- toggleValue(option.value);
197
- }, children: [_jsx("span", { style: {
198
- pointerEvents: 'none',
199
- display: 'flex',
200
- alignItems: 'center',
201
- color: !selectedSet.has(option.value) ? 'var(--color-border-strong)' : 'inherit',
202
- }, children: selectedSet.has(option.value) ? _jsx(Check, {}) : _jsx(Square, {}) }), _jsx("span", { children: option.label })] }) }, option.value))), filteredOptions.length === 0 ? _jsx(Menu.Item, { disabled: true, children: emptyMessage }) : null] }) }));
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
  }
@@ -7,6 +7,8 @@
7
7
  border: var(--border-width-thin) solid var(--color-border-default);
8
8
  border-radius: var(--border-radius-sm);
9
9
  font-family: var(--font-family);
10
+ font-size: var(--font-size-sm);
11
+ line-height: var(--line-height-normal);
10
12
  }
11
13
 
12
14
  .container textarea:disabled {
@@ -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.FC<MenuProps> & {
37
- Item: React.FC<MenuItemProps>;
38
- CheckItem: React.FC<MenuCheckItemProps>;
39
- RadioItem: React.FC<MenuRadioItemProps>;
40
- Separator: React.FC<MenuSeparatorProps>;
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
  };