@pixum/combobox 5.10.3 → 5.10.4-alpha.18
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/lib/ComboBox.module.css +1 -0
- package/lib/hooks/useComboBox.d.ts +60 -0
- package/lib/hooks/useComboBox.js +225 -0
- package/lib/hooks/useComboBox.js.map +1 -0
- package/lib/hooks/useComboBoxFocus.d.ts +35 -0
- package/lib/hooks/useComboBoxFocus.js +48 -0
- package/lib/hooks/useComboBoxFocus.js.map +1 -0
- package/lib/partials/ComboBoxNoResults.d.ts +14 -0
- package/lib/partials/ComboBoxNoResults.js +18 -0
- package/lib/partials/ComboBoxNoResults.js.map +1 -0
- package/lib/partials/ComboBoxOptionsGroup.d.ts +15 -0
- package/lib/partials/ComboBoxOptionsGroup.js +31 -0
- package/lib/partials/ComboBoxOptionsGroup.js.map +1 -0
- package/lib/utils/options.d.ts +40 -0
- package/lib/utils/options.js +85 -0
- package/lib/utils/options.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.root{position:relative;width:100%}.root .headline[class*=text--]+*{margin-top:var(--spacing)}:is(.root,.sheet) :is(.panel,.optionsSlot,.header){position:relative;width:100%}:is(.root,.sheet) .panel{display:flex;flex-direction:column;max-height:inherit;overflow:hidden}:is(.root,.sheet) :is(.options,.optionsSlot){display:flex;flex:1 1 auto;flex-direction:column;min-height:0;overflow:auto;scroll-padding-block-start:var(--combobox-sticky-offset,2.75rem)}.sheet .header{align-items:flex-start;height:auto;margin:var(--spacing-5) auto var(--spacing-2);padding:0 var(--spacing-2)}:is(.root,.sheet) ul.optionsSlot{list-style:none;padding:0}.root .optionsSlot{margin-top:var(--spacing)}:is(.root,.sheet) .option{cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;user-select:none}:is(.root,.sheet) .optionCheckbox[data-selected=true] :global(.menu-item){background-color:initial}:is(.root,.sheet) .option.optionActive :global(.menu-item),:is(.root,.sheet) .optionCheckbox.optionActive :global(.menu-item){background-color:var(--color-canvas--hover)}:is(.root,.sheet) .option[data-selected=true]:not(.optionCheckbox) :global(.menu-item){background-color:var(--color-accent);color:var(--color-surface)}:is(.root,.sheet) .option[aria-disabled=true]{cursor:not-allowed;opacity:.5}@media (forced-colors:active){:is(.root,.sheet) .option.optionActive :global(.menu-item){outline:2px solid Highlight;outline-offset:-2px}}:is(.root,.sheet) .stickyHeader{background-color:var(--color-surface);border-bottom:1px solid var(--color-dim);padding:var(--spacing-1_25) var(--spacing-2);position:-webkit-sticky;position:sticky;top:0}:is(.root,.sheet) .options :global(.text--caption){color:var(--color-on-secondary)}:is(.root,.sheet) .option[data-selected=true]:not(.optionCheckbox) :global(.menu-item .text--caption){color:var(--color-surface)}:is(.root,.sheet) .options :global(.item-header){display:flex;justify-content:space-between}:is(.root,.sheet) .optionDetails{background-color:var(--color-surface);border-top:1px solid var(--color-dim);flex:0 0 auto;padding:var(--spacing-1_25) var(--spacing)}:is(.root,.sheet) .noResults{padding:var(--spacing-2) var(--spacing);text-align:center}@media screen and (min-width:960px){.root :is(.panel,.optionsSlot){border-radius:12px;margin-top:var(--spacing);max-height:330px;position:absolute}}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ComboBoxAriaProps, ComboBoxOption, ListboxAriaProps } from '../types';
|
|
2
|
+
export interface UseComboBoxOptions {
|
|
3
|
+
/** Normalised options in render order. */
|
|
4
|
+
options: ComboBoxOption[];
|
|
5
|
+
/** Whether the list is currently open. */
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
/** Opens the list. */
|
|
8
|
+
open: () => void;
|
|
9
|
+
/**
|
|
10
|
+
* Closes the list. `restoreFocus` is `false` when the focus must stay where
|
|
11
|
+
* the user put it (Tab).
|
|
12
|
+
*/
|
|
13
|
+
close: (restoreFocus?: boolean) => void;
|
|
14
|
+
/** Values that are currently selected. */
|
|
15
|
+
selectedValues: string[];
|
|
16
|
+
/** Called with the value of the option that was activated by the keyboard. */
|
|
17
|
+
onSelect: (value: string) => void;
|
|
18
|
+
/** Id of the listbox element. */
|
|
19
|
+
listboxId: string;
|
|
20
|
+
/** Whether the open list offers a freely typeable search field. */
|
|
21
|
+
searchable?: boolean;
|
|
22
|
+
/** Multi select keeps the list open after a selection. */
|
|
23
|
+
multiple?: boolean;
|
|
24
|
+
/** Enter opens the closed list (APG select-only). */
|
|
25
|
+
openOnEnter?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface UseComboBoxResult {
|
|
28
|
+
/** ARIA props for the element that owns the `combobox` role. */
|
|
29
|
+
comboboxAriaProps: ComboBoxAriaProps;
|
|
30
|
+
/** ARIA props for the element that owns the `listbox` role. */
|
|
31
|
+
listboxAriaProps: ListboxAriaProps;
|
|
32
|
+
/** Keyboard handler for the combobox element. */
|
|
33
|
+
onKeyDown: React.KeyboardEventHandler<HTMLInputElement>;
|
|
34
|
+
/** DOM id of the active option, `undefined` while closed. */
|
|
35
|
+
activeOptionId: string | undefined;
|
|
36
|
+
/** Index of the active option, `null` while closed or empty. */
|
|
37
|
+
activeIndex: number | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Headless controller for the WAI-ARIA combobox pattern.
|
|
41
|
+
*
|
|
42
|
+
* The focus never moves into the list: the active option is communicated
|
|
43
|
+
* exclusively via `aria-activedescendant`, which keeps the search field
|
|
44
|
+
* typeable while the user navigates.
|
|
45
|
+
*
|
|
46
|
+
* Keyboard (APG combobox with listbox popup):
|
|
47
|
+
* - closed: ArrowUp/ArrowDown/Alt+ArrowDown, Enter, Space open the list
|
|
48
|
+
* - open: ArrowUp/ArrowDown navigate (skipping disabled options),
|
|
49
|
+
* Home/End jump to the first/last option, Enter selects,
|
|
50
|
+
* Escape and Alt+ArrowUp close, Tab closes and lets the focus move on
|
|
51
|
+
* - Space selects while the field is not typeable; as soon as a search field
|
|
52
|
+
* is open it types a space character instead, as APG requires
|
|
53
|
+
*
|
|
54
|
+
* On open the starting point is always the selected option, so ArrowDown moves
|
|
55
|
+
* to the option right after it.
|
|
56
|
+
*
|
|
57
|
+
* @param options - Configuration of the combobox
|
|
58
|
+
* @returns ARIA props, the keyboard handler and the active option
|
|
59
|
+
*/
|
|
60
|
+
export declare function useComboBox({ options, isOpen, open, close, selectedValues, onSelect, listboxId, searchable, multiple, openOnEnter, }: UseComboBoxOptions): UseComboBoxResult;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { findEnabledIndex } from '../utils';
|
|
3
|
+
/**
|
|
4
|
+
* Headless controller for the WAI-ARIA combobox pattern.
|
|
5
|
+
*
|
|
6
|
+
* The focus never moves into the list: the active option is communicated
|
|
7
|
+
* exclusively via `aria-activedescendant`, which keeps the search field
|
|
8
|
+
* typeable while the user navigates.
|
|
9
|
+
*
|
|
10
|
+
* Keyboard (APG combobox with listbox popup):
|
|
11
|
+
* - closed: ArrowUp/ArrowDown/Alt+ArrowDown, Enter, Space open the list
|
|
12
|
+
* - open: ArrowUp/ArrowDown navigate (skipping disabled options),
|
|
13
|
+
* Home/End jump to the first/last option, Enter selects,
|
|
14
|
+
* Escape and Alt+ArrowUp close, Tab closes and lets the focus move on
|
|
15
|
+
* - Space selects while the field is not typeable; as soon as a search field
|
|
16
|
+
* is open it types a space character instead, as APG requires
|
|
17
|
+
*
|
|
18
|
+
* On open the starting point is always the selected option, so ArrowDown moves
|
|
19
|
+
* to the option right after it.
|
|
20
|
+
*
|
|
21
|
+
* @param options - Configuration of the combobox
|
|
22
|
+
* @returns ARIA props, the keyboard handler and the active option
|
|
23
|
+
*/
|
|
24
|
+
export function useComboBox({ options, isOpen, open, close, selectedValues, onSelect, listboxId, searchable = true, multiple = false, openOnEnter = true, }) {
|
|
25
|
+
var _a;
|
|
26
|
+
const [activeIndex, setActiveIndex] = useState(null);
|
|
27
|
+
/**
|
|
28
|
+
* Read inside effects only. Keeping the selection in a ref avoids re-running
|
|
29
|
+
* the "open" effect when a consumer passes a new array instance per render.
|
|
30
|
+
*/
|
|
31
|
+
const selectedValuesRef = useRef(selectedValues);
|
|
32
|
+
selectedValuesRef.current = selectedValues;
|
|
33
|
+
/** Tracks the closed -> open edge so navigation is not reset while open. */
|
|
34
|
+
const wasOpen = useRef(false);
|
|
35
|
+
/**
|
|
36
|
+
* Active option requested while the list was still closed (Home/End). The
|
|
37
|
+
* open effect consumes it instead of falling back to the selected option.
|
|
38
|
+
*/
|
|
39
|
+
const pendingActiveIndex = useRef(null);
|
|
40
|
+
/** Typing is only possible while an open search field is rendered. */
|
|
41
|
+
const canType = isOpen && searchable;
|
|
42
|
+
/**
|
|
43
|
+
* Sets the starting point when the list opens: the selected option, or the
|
|
44
|
+
* first enabled one as a fallback.
|
|
45
|
+
*/
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!isOpen) {
|
|
48
|
+
wasOpen.current = false;
|
|
49
|
+
setActiveIndex(null);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (wasOpen.current)
|
|
53
|
+
return;
|
|
54
|
+
wasOpen.current = true;
|
|
55
|
+
const pending = pendingActiveIndex.current;
|
|
56
|
+
pendingActiveIndex.current = null;
|
|
57
|
+
if (pending !== null) {
|
|
58
|
+
setActiveIndex(pending);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const selectedIndex = options.findIndex(option => !option.disabled && selectedValuesRef.current.includes(option.value));
|
|
62
|
+
setActiveIndex(selectedIndex !== -1 ? selectedIndex : findEnabledIndex(options, -1, 1));
|
|
63
|
+
}, [isOpen, options]);
|
|
64
|
+
/**
|
|
65
|
+
* Keeps the active index valid while the list is filtered down.
|
|
66
|
+
*/
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!isOpen)
|
|
69
|
+
return;
|
|
70
|
+
setActiveIndex(previous => {
|
|
71
|
+
const isStillValid = previous !== null &&
|
|
72
|
+
previous < options.length &&
|
|
73
|
+
!options[previous].disabled;
|
|
74
|
+
if (isStillValid)
|
|
75
|
+
return previous;
|
|
76
|
+
return findEnabledIndex(options, -1, 1);
|
|
77
|
+
});
|
|
78
|
+
}, [isOpen, options]);
|
|
79
|
+
/**
|
|
80
|
+
* Moves the active option by `delta` positions, wrapping around and skipping
|
|
81
|
+
* disabled options.
|
|
82
|
+
*
|
|
83
|
+
* @param delta - Number of positions to move; negative moves backwards
|
|
84
|
+
*/
|
|
85
|
+
const move = useCallback((delta) => setActiveIndex(previous => findEnabledIndex(options, previous !== null && previous !== void 0 ? previous : (delta > 0 ? -1 : 0), delta)), [options]);
|
|
86
|
+
/**
|
|
87
|
+
* Selects the active option. In single select the list closes afterwards,
|
|
88
|
+
* in multi select it stays open so further options can be toggled.
|
|
89
|
+
*/
|
|
90
|
+
const selectActiveOption = useCallback(() => {
|
|
91
|
+
if (activeIndex === null)
|
|
92
|
+
return;
|
|
93
|
+
const option = options[activeIndex];
|
|
94
|
+
if (!option || option.disabled)
|
|
95
|
+
return;
|
|
96
|
+
onSelect(option.value);
|
|
97
|
+
}, [activeIndex, options, onSelect]);
|
|
98
|
+
const onKeyDown = useCallback(event => {
|
|
99
|
+
switch (event.key) {
|
|
100
|
+
case 'ArrowDown':
|
|
101
|
+
case 'ArrowUp': {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
if (!isOpen) {
|
|
104
|
+
open();
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
// Alt+ArrowUp closes and keeps the current selection.
|
|
108
|
+
if (event.key === 'ArrowUp' && event.altKey) {
|
|
109
|
+
close();
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
// Alt+ArrowDown only opens, it never moves the active option.
|
|
113
|
+
if (event.altKey)
|
|
114
|
+
break;
|
|
115
|
+
move(event.key === 'ArrowDown' ? 1 : -1);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'Enter': {
|
|
119
|
+
if (!isOpen) {
|
|
120
|
+
if (!openOnEnter)
|
|
121
|
+
break;
|
|
122
|
+
// preventDefault stops an implicit form submit.
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
open();
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
if (activeIndex === null)
|
|
128
|
+
break;
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
selectActiveOption();
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case ' ': {
|
|
134
|
+
// An open search field must receive a real space character.
|
|
135
|
+
if (canType)
|
|
136
|
+
break;
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
if (isOpen && activeIndex !== null) {
|
|
139
|
+
selectActiveOption();
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
open();
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case 'Escape': {
|
|
146
|
+
if (!isOpen)
|
|
147
|
+
break;
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
close();
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 'Tab': {
|
|
153
|
+
// No preventDefault: the focus moves on, the popup just closes.
|
|
154
|
+
if (isOpen)
|
|
155
|
+
close(false);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'Home':
|
|
159
|
+
case 'End': {
|
|
160
|
+
if (options.length === 0)
|
|
161
|
+
break;
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
const targetIndex = event.key === 'Home'
|
|
164
|
+
? findEnabledIndex(options, -1, 1)
|
|
165
|
+
: findEnabledIndex(options, options.length, -1);
|
|
166
|
+
// Closed: open first, the effect picks the index up from the ref.
|
|
167
|
+
if (!isOpen) {
|
|
168
|
+
pendingActiveIndex.current = targetIndex;
|
|
169
|
+
open();
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
setActiveIndex(targetIndex);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
default:
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}, [
|
|
179
|
+
isOpen,
|
|
180
|
+
open,
|
|
181
|
+
close,
|
|
182
|
+
move,
|
|
183
|
+
activeIndex,
|
|
184
|
+
selectActiveOption,
|
|
185
|
+
canType,
|
|
186
|
+
openOnEnter,
|
|
187
|
+
options,
|
|
188
|
+
]);
|
|
189
|
+
const activeOptionId = isOpen && activeIndex !== null ? (_a = options[activeIndex]) === null || _a === void 0 ? void 0 : _a.id : undefined;
|
|
190
|
+
/**
|
|
191
|
+
* Scrolls the active option into view. Purely DOM based, so it works for the
|
|
192
|
+
* desktop popover and the bottom sheet alike.
|
|
193
|
+
*/
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!activeOptionId)
|
|
196
|
+
return;
|
|
197
|
+
const frameId = requestAnimationFrame(() => {
|
|
198
|
+
var _a;
|
|
199
|
+
(_a = document
|
|
200
|
+
.getElementById(activeOptionId)) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ block: 'nearest' });
|
|
201
|
+
});
|
|
202
|
+
return () => cancelAnimationFrame(frameId);
|
|
203
|
+
}, [activeOptionId]);
|
|
204
|
+
const comboboxAriaProps = useMemo(() => ({
|
|
205
|
+
role: 'combobox',
|
|
206
|
+
'aria-expanded': isOpen,
|
|
207
|
+
'aria-haspopup': 'listbox',
|
|
208
|
+
'aria-controls': isOpen ? listboxId : undefined,
|
|
209
|
+
'aria-activedescendant': activeOptionId,
|
|
210
|
+
'aria-autocomplete': searchable ? 'list' : 'none',
|
|
211
|
+
}), [isOpen, listboxId, activeOptionId, searchable]);
|
|
212
|
+
const listboxAriaProps = useMemo(() => ({
|
|
213
|
+
role: 'listbox',
|
|
214
|
+
id: listboxId,
|
|
215
|
+
'aria-multiselectable': multiple || undefined,
|
|
216
|
+
}), [listboxId, multiple]);
|
|
217
|
+
return {
|
|
218
|
+
comboboxAriaProps,
|
|
219
|
+
listboxAriaProps,
|
|
220
|
+
onKeyDown,
|
|
221
|
+
activeOptionId,
|
|
222
|
+
activeIndex,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=useComboBox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useComboBox.js","sourceRoot":"","sources":["../../src/hooks/useComboBox.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface UseComboBoxFocusResult {
|
|
2
|
+
/**
|
|
3
|
+
* Ref for the trigger field (SelectInput).
|
|
4
|
+
*
|
|
5
|
+
* `| null` is part of the type parameter: since React 19 `useRef<T>(null)`
|
|
6
|
+
* returns `RefObject<T | null>` instead of the old `RefObject<T>`.
|
|
7
|
+
*/
|
|
8
|
+
triggerRef: React.RefObject<HTMLInputElement | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Callback ref for the search field. Focuses it the moment it is mounted -
|
|
11
|
+
* in the desktop popover as well as in the bottom sheet.
|
|
12
|
+
*/
|
|
13
|
+
searchInputRef: (input: HTMLInputElement | null) => void;
|
|
14
|
+
/** Moves the focus back to the trigger without re-opening the list. */
|
|
15
|
+
returnFocusToTrigger: () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the current focus event on the trigger was caused by
|
|
18
|
+
* `returnFocusToTrigger` and must therefore not open the list.
|
|
19
|
+
*/
|
|
20
|
+
isReturningFocus: () => boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Bundles the focus choreography of the ComboBox:
|
|
24
|
+
*
|
|
25
|
+
* - opening moves the focus into the search field as soon as it mounts,
|
|
26
|
+
* - closing moves it back to the trigger,
|
|
27
|
+
* - the focus handler of the trigger can tell a returning focus from a real
|
|
28
|
+
* user focus and therefore does not re-open the list it just closed.
|
|
29
|
+
*
|
|
30
|
+
* The search field is wired up with a callback ref, so both the popover and the
|
|
31
|
+
* bottom sheet share one implementation and no polling is needed.
|
|
32
|
+
*
|
|
33
|
+
* @returns Refs and helpers for the focus handling
|
|
34
|
+
*/
|
|
35
|
+
export declare function useComboBoxFocus(): UseComboBoxFocusResult;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Bundles the focus choreography of the ComboBox:
|
|
4
|
+
*
|
|
5
|
+
* - opening moves the focus into the search field as soon as it mounts,
|
|
6
|
+
* - closing moves it back to the trigger,
|
|
7
|
+
* - the focus handler of the trigger can tell a returning focus from a real
|
|
8
|
+
* user focus and therefore does not re-open the list it just closed.
|
|
9
|
+
*
|
|
10
|
+
* The search field is wired up with a callback ref, so both the popover and the
|
|
11
|
+
* bottom sheet share one implementation and no polling is needed.
|
|
12
|
+
*
|
|
13
|
+
* @returns Refs and helpers for the focus handling
|
|
14
|
+
*/
|
|
15
|
+
export function useComboBoxFocus() {
|
|
16
|
+
const triggerRef = useRef(null);
|
|
17
|
+
const returningFocus = useRef(false);
|
|
18
|
+
const frameRef = useRef(null);
|
|
19
|
+
useEffect(() => () => {
|
|
20
|
+
if (frameRef.current !== null)
|
|
21
|
+
cancelAnimationFrame(frameRef.current);
|
|
22
|
+
}, []);
|
|
23
|
+
const searchInputRef = useCallback((input) => {
|
|
24
|
+
if (!input)
|
|
25
|
+
return;
|
|
26
|
+
input.focus();
|
|
27
|
+
// One frame later, so Safari does not drop the selection again when
|
|
28
|
+
// `readOnly` changes within the same render.
|
|
29
|
+
requestAnimationFrame(() => {
|
|
30
|
+
var _a;
|
|
31
|
+
(_a = input.setSelectionRange) === null || _a === void 0 ? void 0 : _a.call(input, 0, input.value.length);
|
|
32
|
+
});
|
|
33
|
+
}, []);
|
|
34
|
+
const returnFocusToTrigger = useCallback(() => {
|
|
35
|
+
returningFocus.current = true;
|
|
36
|
+
// Wait for the commit that unmounts the popover / bottom sheet, otherwise
|
|
37
|
+
// the focus would end up on `<body>`.
|
|
38
|
+
frameRef.current = requestAnimationFrame(() => {
|
|
39
|
+
var _a;
|
|
40
|
+
(_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
41
|
+
// `focus()` dispatches synchronously, so the flag has served its purpose.
|
|
42
|
+
returningFocus.current = false;
|
|
43
|
+
});
|
|
44
|
+
}, []);
|
|
45
|
+
const isReturningFocus = useCallback(() => returningFocus.current, []);
|
|
46
|
+
return { triggerRef, searchInputRef, returnFocusToTrigger, isReturningFocus };
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=useComboBoxFocus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useComboBoxFocus.js","sourceRoot":"","sources":["../../src/hooks/useComboBoxFocus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAwBtD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,UAAU,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAA;IACjD,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IACpC,MAAM,QAAQ,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAA;IAE5C,SAAS,CACP,GAAG,EAAE,CAAC,GAAG,EAAE;QACT,IAAI,QAAQ,CAAC,OAAO,KAAK,IAAI;YAAE,oBAAoB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IACvE,CAAC,EACD,EAAE,CACH,CAAA;IAED,MAAM,cAAc,GAAG,WAAW,CAAC,CAAC,KAA8B,EAAE,EAAE;QACpE,IAAI,CAAC,KAAK;YAAE,OAAM;QAElB,KAAK,CAAC,KAAK,EAAE,CAAA;QAEb,oEAAoE;QACpE,6CAA6C;QAC7C,qBAAqB,CAAC,GAAG,EAAE;;YACzB,MAAA,KAAK,CAAC,iBAAiB,sDAAG,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QAClD,CAAC,CAAC,CAAA;IACJ,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,oBAAoB,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5C,cAAc,CAAC,OAAO,GAAG,IAAI,CAAA;QAE7B,0EAA0E;QAC1E,sCAAsC;QACtC,QAAQ,CAAC,OAAO,GAAG,qBAAqB,CAAC,GAAG,EAAE;;YAC5C,MAAA,UAAU,CAAC,OAAO,0CAAE,KAAK,EAAE,CAAA;YAC3B,0EAA0E;YAC1E,cAAc,CAAC,OAAO,GAAG,KAAK,CAAA;QAChC,CAAC,CAAC,CAAA;IACJ,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,gBAAgB,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IAEtE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,CAAA;AAC/E,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ComboBoxNoResultsProps {
|
|
2
|
+
/** Message shown when no option matches the search. */
|
|
3
|
+
text?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Empty state of the options list.
|
|
7
|
+
*
|
|
8
|
+
* `role="status"` makes screen readers announce the message when the list is
|
|
9
|
+
* filtered down to nothing.
|
|
10
|
+
*
|
|
11
|
+
* @param props - The props for the ComboBoxNoResults component
|
|
12
|
+
* @returns The no-results message
|
|
13
|
+
*/
|
|
14
|
+
export declare function ComboBoxNoResults({ text }: ComboBoxNoResultsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import Text, { TEXT_VARIANT } from '@pixum/text';
|
|
3
|
+
import styles from '../ComboBox.module.css';
|
|
4
|
+
/**
|
|
5
|
+
* Empty state of the options list.
|
|
6
|
+
*
|
|
7
|
+
* `role="status"` makes screen readers announce the message when the list is
|
|
8
|
+
* filtered down to nothing.
|
|
9
|
+
*
|
|
10
|
+
* @param props - The props for the ComboBoxNoResults component
|
|
11
|
+
* @returns The no-results message
|
|
12
|
+
*/
|
|
13
|
+
export function ComboBoxNoResults({ text }) {
|
|
14
|
+
if (!text)
|
|
15
|
+
return null;
|
|
16
|
+
return (_jsx(Text, { variant: TEXT_VARIANT.SUBHEADLINE, className: styles.noResults, children: text }));
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=ComboBoxNoResults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComboBoxNoResults.js","sourceRoot":"","sources":["../../src/partials/ComboBoxNoResults.tsx"],"names":[],"mappings":";AAAA,OAAO,IAAI,EAAE,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,MAAM,MAAM,wBAAwB,CAAA;AAO3C;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAE,IAAI,EAA0B;IAChE,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IAEtB,OAAO,CACL,KAAC,IAAI,IAAC,OAAO,EAAE,YAAY,CAAC,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,YACjE,IAAI,GACA,CACR,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ComboBoxOptionsItemsProps } from './ComboBoxOptionsItems';
|
|
2
|
+
export interface ComboBoxOptionsGroupProps extends ComboBoxOptionsItemsProps {
|
|
3
|
+
/** Group headline. Without it the options are rendered without a header. */
|
|
4
|
+
group?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Renders one group of options with an optional sticky headline.
|
|
8
|
+
*
|
|
9
|
+
* Flat lists use the same component with `group` left undefined, so there is
|
|
10
|
+
* only one place that renders options.
|
|
11
|
+
*
|
|
12
|
+
* @param props - The props for the ComboBoxOptionsGroup component
|
|
13
|
+
* @returns A group container with an optional header and its options
|
|
14
|
+
*/
|
|
15
|
+
export declare function ComboBoxOptionsGroup({ group, ...itemProps }: ComboBoxOptionsGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import Text, { TEXT_VARIANT } from '@pixum/text';
|
|
14
|
+
import { ComboBoxOptionsItems } from './ComboBoxOptionsItems';
|
|
15
|
+
import styles from '../ComboBox.module.css';
|
|
16
|
+
/**
|
|
17
|
+
* Renders one group of options with an optional sticky headline.
|
|
18
|
+
*
|
|
19
|
+
* Flat lists use the same component with `group` left undefined, so there is
|
|
20
|
+
* only one place that renders options.
|
|
21
|
+
*
|
|
22
|
+
* @param props - The props for the ComboBoxOptionsGroup component
|
|
23
|
+
* @returns A group container with an optional header and its options
|
|
24
|
+
*/
|
|
25
|
+
export function ComboBoxOptionsGroup(_a) {
|
|
26
|
+
var { group } = _a, itemProps = __rest(_a, ["group"]);
|
|
27
|
+
if (!group)
|
|
28
|
+
return _jsx(ComboBoxOptionsItems, Object.assign({}, itemProps));
|
|
29
|
+
return (_jsxs("div", { role: "group", "aria-label": group, children: [_jsx(Text, { variant: TEXT_VARIANT.HEADLINE, className: styles.stickyHeader, children: group }), _jsx(ComboBoxOptionsItems, Object.assign({}, itemProps))] }));
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=ComboBoxOptionsGroup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComboBoxOptionsGroup.js","sourceRoot":"","sources":["../../src/partials/ComboBoxOptionsGroup.tsx"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,IAAI,EAAE,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,MAAM,MAAM,wBAAwB,CAAA;AAQ3C;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAGT;QAHS,EACnC,KAAK,OAEqB,EADvB,SAAS,cAFuB,SAGpC,CADa;IAEZ,IAAI,CAAC,KAAK;QAAE,OAAO,KAAC,oBAAoB,oBAAK,SAAS,EAAI,CAAA;IAE1D,OAAO,CACL,eAAK,IAAI,EAAC,OAAO,gBAAa,KAAK,aACjC,KAAC,IAAI,IAAC,OAAO,EAAE,YAAY,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,YAAY,YACjE,KAAK,GACD,EACP,KAAC,oBAAoB,oBAAK,SAAS,EAAI,IACnC,CACP,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ComboBoxOption, ComboBoxOptionGroup, ComboBoxOptionsInput } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Normalises flat and grouped options into one list in render order.
|
|
4
|
+
*
|
|
5
|
+
* The index of an entry is identical for both input shapes and is therefore
|
|
6
|
+
* used as the option DOM id that `aria-activedescendant` points at. Doing this
|
|
7
|
+
* once removes the offset arithmetic that grouped rendering used to need.
|
|
8
|
+
*
|
|
9
|
+
* @param options - Flat or grouped options as passed in by the consumer
|
|
10
|
+
* @param listboxId - Id of the listbox, used as prefix for the option ids
|
|
11
|
+
* @returns The normalised options in render order
|
|
12
|
+
*/
|
|
13
|
+
export declare function normaliseOptions(options: ComboBoxOptionsInput, listboxId: string): ComboBoxOption[];
|
|
14
|
+
/**
|
|
15
|
+
* Splits normalised options back into their render groups while keeping the
|
|
16
|
+
* global indices intact, so that the list can be rendered with a single code
|
|
17
|
+
* path for flat and grouped options.
|
|
18
|
+
*
|
|
19
|
+
* @param options - Normalised options in render order
|
|
20
|
+
* @returns One entry per consecutive group
|
|
21
|
+
*/
|
|
22
|
+
export declare function groupOptions(options: ComboBoxOption[]): ComboBoxOptionGroup[];
|
|
23
|
+
/**
|
|
24
|
+
* Normalises the `value` prop to an array, so that single and multi select
|
|
25
|
+
* share the same selection logic.
|
|
26
|
+
*
|
|
27
|
+
* @param value - Selected value(s) or `undefined`
|
|
28
|
+
* @returns The selected values as an array
|
|
29
|
+
*/
|
|
30
|
+
export declare function toValueArray(value: string | string[] | undefined): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Finds the first option that can be activated, starting at `start` and moving
|
|
33
|
+
* by `delta` with wrap-around. Disabled options are skipped.
|
|
34
|
+
*
|
|
35
|
+
* @param options - Normalised options
|
|
36
|
+
* @param start - Index to start from; use `-1` to start before the first option
|
|
37
|
+
* @param delta - Step width, negative values move backwards
|
|
38
|
+
* @returns The index of the next enabled option, or `null` when there is none
|
|
39
|
+
*/
|
|
40
|
+
export declare function findEnabledIndex(options: ComboBoxOption[], start: number, delta: number): number | null;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalises flat and grouped options into one list in render order.
|
|
3
|
+
*
|
|
4
|
+
* The index of an entry is identical for both input shapes and is therefore
|
|
5
|
+
* used as the option DOM id that `aria-activedescendant` points at. Doing this
|
|
6
|
+
* once removes the offset arithmetic that grouped rendering used to need.
|
|
7
|
+
*
|
|
8
|
+
* @param options - Flat or grouped options as passed in by the consumer
|
|
9
|
+
* @param listboxId - Id of the listbox, used as prefix for the option ids
|
|
10
|
+
* @returns The normalised options in render order
|
|
11
|
+
*/
|
|
12
|
+
export function normaliseOptions(options, listboxId) {
|
|
13
|
+
const groups = Array.isArray(options)
|
|
14
|
+
? [['', options]]
|
|
15
|
+
: Object.entries(options);
|
|
16
|
+
const normalised = [];
|
|
17
|
+
groups.forEach(([group, items]) => {
|
|
18
|
+
items.forEach(item => {
|
|
19
|
+
var _a;
|
|
20
|
+
const index = normalised.length;
|
|
21
|
+
normalised.push({
|
|
22
|
+
item,
|
|
23
|
+
group: group || undefined,
|
|
24
|
+
value: (_a = item.value) !== null && _a !== void 0 ? _a : item.text,
|
|
25
|
+
index,
|
|
26
|
+
id: `${listboxId}-option-${index}`,
|
|
27
|
+
disabled: item.disabled === true,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
return normalised;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Splits normalised options back into their render groups while keeping the
|
|
35
|
+
* global indices intact, so that the list can be rendered with a single code
|
|
36
|
+
* path for flat and grouped options.
|
|
37
|
+
*
|
|
38
|
+
* @param options - Normalised options in render order
|
|
39
|
+
* @returns One entry per consecutive group
|
|
40
|
+
*/
|
|
41
|
+
export function groupOptions(options) {
|
|
42
|
+
return options.reduce((groups, option) => {
|
|
43
|
+
const current = groups[groups.length - 1];
|
|
44
|
+
if (current && current.group === option.group) {
|
|
45
|
+
current.options.push(option);
|
|
46
|
+
return groups;
|
|
47
|
+
}
|
|
48
|
+
groups.push({ group: option.group, options: [option] });
|
|
49
|
+
return groups;
|
|
50
|
+
}, []);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Normalises the `value` prop to an array, so that single and multi select
|
|
54
|
+
* share the same selection logic.
|
|
55
|
+
*
|
|
56
|
+
* @param value - Selected value(s) or `undefined`
|
|
57
|
+
* @returns The selected values as an array
|
|
58
|
+
*/
|
|
59
|
+
export function toValueArray(value) {
|
|
60
|
+
if (Array.isArray(value))
|
|
61
|
+
return value;
|
|
62
|
+
return value ? [value] : [];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Finds the first option that can be activated, starting at `start` and moving
|
|
66
|
+
* by `delta` with wrap-around. Disabled options are skipped.
|
|
67
|
+
*
|
|
68
|
+
* @param options - Normalised options
|
|
69
|
+
* @param start - Index to start from; use `-1` to start before the first option
|
|
70
|
+
* @param delta - Step width, negative values move backwards
|
|
71
|
+
* @returns The index of the next enabled option, or `null` when there is none
|
|
72
|
+
*/
|
|
73
|
+
export function findEnabledIndex(options, start, delta) {
|
|
74
|
+
const { length } = options;
|
|
75
|
+
if (length === 0)
|
|
76
|
+
return null;
|
|
77
|
+
let index = start;
|
|
78
|
+
for (let step = 0; step < length; step += 1) {
|
|
79
|
+
index = (index + delta + length) % length;
|
|
80
|
+
if (!options[index].disabled)
|
|
81
|
+
return index;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/utils/options.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAA6B,EAC7B,SAAiB;IAEjB,MAAM,MAAM,GAA0C,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAC1E,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QACjB,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IAE3B,MAAM,UAAU,GAAqB,EAAE,CAAA;IAEvC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE;QAChC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;;YACnB,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAA;YAE/B,UAAU,CAAC,IAAI,CAAC;gBACd,IAAI;gBACJ,KAAK,EAAE,KAAK,IAAI,SAAS;gBACzB,KAAK,EAAE,MAAA,IAAI,CAAC,KAAK,mCAAI,IAAI,CAAC,IAAI;gBAC9B,KAAK;gBACL,EAAE,EAAE,GAAG,SAAS,WAAW,KAAK,EAAE;gBAClC,QAAQ,EAAE,IAAI,CAAC,QAAQ,KAAK,IAAI;aACjC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,OAAO,UAAU,CAAA;AACnB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,OAAyB;IACpD,OAAO,OAAO,CAAC,MAAM,CAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE;QAC9D,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QAEzC,IAAI,OAAO,IAAI,OAAO,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;YAC9C,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAE5B,OAAO,MAAM,CAAA;QACf,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QAEvD,OAAO,MAAM,CAAA;IACf,CAAC,EAAE,EAAE,CAAC,CAAA;AACR,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,KAAoC;IAC/D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAEtC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;AAC7B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAyB,EACzB,KAAa,EACb,KAAa;IAEb,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAC1B,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAE7B,IAAI,KAAK,GAAG,KAAK,CAAA;IAEjB,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,MAAM,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;QAC5C,KAAK,GAAG,CAAC,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC,GAAG,MAAM,CAAA;QAEzC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAA;IAC5C,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixum/combobox",
|
|
3
|
-
"version": "5.10.
|
|
3
|
+
"version": "5.10.4-alpha.18+d1e52db7f",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"module": "lib/index.js",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"@pixum/bottom-sheet": "3.0.18",
|
|
9
9
|
"@pixum/menu": "3.0.14",
|
|
10
10
|
"@pixum/menu-item": "2.0.14",
|
|
11
|
-
"@pixum/text": "
|
|
11
|
+
"@pixum/text": ">=3.7.0",
|
|
12
12
|
"@pixum/theme-provider": "4.2.20",
|
|
13
13
|
"@pixum/web-input": "2.1.0"
|
|
14
14
|
},
|
|
@@ -18,5 +18,5 @@
|
|
|
18
18
|
"sideEffects": [
|
|
19
19
|
"*.css"
|
|
20
20
|
],
|
|
21
|
-
"gitHead": "
|
|
21
|
+
"gitHead": "d1e52db7fc60033d1e40fedde10ee6e52216e657"
|
|
22
22
|
}
|