@tangible/ui 0.0.1 → 0.0.3
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/components/Card/Card.d.ts +1 -0
- package/components/Card/Card.js +17 -20
- package/components/Checkbox/Checkbox.d.ts +9 -0
- package/components/Checkbox/Checkbox.js +92 -0
- package/components/Checkbox/index.d.ts +2 -0
- package/components/Checkbox/index.js +1 -0
- package/components/Checkbox/types.d.ts +10 -0
- package/components/Checkbox/types.js +1 -0
- package/components/Chip/Chip.d.ts +4 -1
- package/components/Chip/Chip.js +32 -7
- package/components/ChipGroup/ChipGroup.d.ts +5 -0
- package/components/ChipGroup/ChipGroup.js +68 -0
- package/components/ChipGroup/ChipGroupContext.d.ts +3 -0
- package/components/ChipGroup/ChipGroupContext.js +5 -0
- package/components/ChipGroup/index.d.ts +3 -0
- package/components/ChipGroup/index.js +2 -0
- package/components/ChipGroup/types.d.ts +36 -0
- package/components/ChipGroup/types.js +1 -0
- package/components/Chips/Chips.d.ts +2 -0
- package/components/Chips/Chips.js +1 -1
- package/components/Combobox/Combobox.d.ts +33 -0
- package/components/Combobox/Combobox.js +466 -0
- package/components/Combobox/ComboboxContext.d.ts +8 -0
- package/components/Combobox/ComboboxContext.js +36 -0
- package/components/Combobox/index.d.ts +2 -0
- package/components/Combobox/index.js +1 -0
- package/components/Combobox/types.d.ts +204 -0
- package/components/Combobox/types.js +1 -0
- package/components/Dropdown/Dropdown.js +2 -1
- package/components/Field/Field.d.ts +39 -0
- package/components/Field/Field.js +92 -0
- package/components/Field/FieldContext.d.ts +16 -0
- package/components/Field/FieldContext.js +10 -0
- package/components/Field/index.d.ts +2 -0
- package/components/Field/index.js +1 -0
- package/components/Modal/Modal.d.ts +4 -4
- package/components/Modal/Modal.js +14 -12
- package/components/MoveHandle/MoveHandle.d.ts +2 -0
- package/components/MoveHandle/MoveHandle.js +84 -0
- package/components/MoveHandle/index.d.ts +2 -0
- package/components/MoveHandle/index.js +1 -0
- package/components/MoveHandle/types.d.ts +43 -0
- package/components/MoveHandle/types.js +1 -0
- package/components/MultiSelect/MultiSelect.d.ts +39 -0
- package/components/MultiSelect/MultiSelect.js +623 -0
- package/components/MultiSelect/MultiSelectContext.d.ts +20 -0
- package/components/MultiSelect/MultiSelectContext.js +56 -0
- package/components/MultiSelect/index.d.ts +2 -0
- package/components/MultiSelect/index.js +1 -0
- package/components/MultiSelect/types.d.ts +218 -0
- package/components/MultiSelect/types.js +3 -0
- package/components/Notice/Notice.d.ts +1 -1
- package/components/Notice/Notice.js +1 -1
- package/components/Progress/Progress.js +1 -1
- package/components/Progress/types.d.ts +7 -7
- package/components/Radio/Radio.d.ts +2 -0
- package/components/Radio/Radio.js +50 -0
- package/components/Radio/RadioGroup.d.ts +2 -0
- package/components/Radio/RadioGroup.js +54 -0
- package/components/Radio/RadioGroupContext.d.ts +3 -0
- package/components/Radio/RadioGroupContext.js +9 -0
- package/components/Radio/index.d.ts +8 -0
- package/components/Radio/index.js +6 -0
- package/components/Radio/types.d.ts +32 -0
- package/components/Radio/types.js +1 -0
- package/components/Rating/Rating.d.ts +5 -5
- package/components/Rating/Rating.js +2 -2
- package/components/SegmentedControl/SegmentedControl.js +20 -104
- package/components/SegmentedControl/types.d.ts +4 -8
- package/components/Select/Select.d.ts +39 -0
- package/components/Select/Select.js +497 -0
- package/components/Select/SelectContext.d.ts +20 -0
- package/components/Select/SelectContext.js +56 -0
- package/components/Select/index.d.ts +3 -0
- package/components/Select/index.js +1 -0
- package/components/Select/types.d.ts +216 -0
- package/components/Select/types.js +11 -0
- package/components/Sidebar/Sidebar.js +12 -12
- package/components/Sidebar/types.d.ts +5 -5
- package/components/StepIndicator/StepIndicator.js +1 -1
- package/components/StepList/StepList.js +2 -2
- package/components/StepList/types.d.ts +4 -4
- package/components/Switch/Switch.d.ts +9 -0
- package/components/Switch/Switch.js +91 -0
- package/components/Switch/index.d.ts +2 -0
- package/components/Switch/index.js +1 -0
- package/components/Switch/types.d.ts +11 -0
- package/components/Switch/types.js +1 -0
- package/components/TextInput/TextInput.d.ts +8 -0
- package/components/TextInput/TextInput.js +25 -0
- package/components/TextInput/index.d.ts +2 -0
- package/components/TextInput/index.js +1 -0
- package/components/TextInput/types.d.ts +32 -0
- package/components/TextInput/types.js +1 -0
- package/components/Textarea/Textarea.d.ts +6 -0
- package/components/Textarea/Textarea.js +49 -0
- package/components/Textarea/index.d.ts +2 -0
- package/components/Textarea/index.js +1 -0
- package/components/Textarea/types.d.ts +25 -0
- package/components/Textarea/types.js +1 -0
- package/components/index.d.ts +22 -0
- package/components/index.js +11 -0
- package/icons/icons.svg +2 -0
- package/icons/manifest.json +16 -0
- package/icons/registry.d.ts +4 -0
- package/icons/registry.js +2 -0
- package/icons/system/index.d.ts +4 -0
- package/icons/system/index.js +22 -0
- package/package.json +1 -1
- package/styles/all.css +1 -1
- package/styles/all.expanded.css +1838 -136
- package/styles/all.expanded.unlayered.css +1838 -136
- package/styles/all.unlayered.css +1 -1
- package/styles/components/_bundle.scss +22 -0
- package/styles/components/input/index.scss +5 -20
- package/styles/index.scss +21 -0
- package/styles/system/_control.scss +49 -0
- package/styles/system/_tokens.scss +124 -2
- package/styles/system/index.scss +2 -1
- package/styles/utilities/_index.scss +50 -0
- package/tui-manifest.json +907 -112
- package/utils/compose-events.d.ts +15 -0
- package/utils/compose-events.js +27 -0
- package/utils/hash.d.ts +15 -0
- package/utils/hash.js +32 -0
- package/utils/index.d.ts +3 -0
- package/utils/index.js +6 -0
- package/utils/is-dev.d.ts +5 -0
- package/utils/is-dev.js +7 -0
- package/utils/use-controllable-state.d.ts +19 -0
- package/utils/use-controllable-state.js +59 -0
- package/utils/use-roving-group.d.ts +33 -0
- package/utils/use-roving-group.js +123 -0
- package/utils/value-key.d.ts +16 -0
- package/utils/value-key.js +14 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type MultiSelectProps, type MultiSelectTriggerProps, type MultiSelectContentProps, type MultiSelectOptionProps, type MultiSelectGroupProps, type MultiSelectLabelProps } from './types';
|
|
2
|
+
declare function MultiSelectRoot({ id: triggerIdProp, value: controlledValue, defaultValue, onValueChange, open: controlledOpen, defaultOpen, onOpenChange, disabled, placeholder, size, display, maxChips, max, onMaxReached, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, children, }: MultiSelectProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
declare namespace MultiSelectRoot {
|
|
4
|
+
var displayName: string;
|
|
5
|
+
}
|
|
6
|
+
declare function MultiSelectTriggerComponent({ asChild, className, children, }: MultiSelectTriggerProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
declare namespace MultiSelectTriggerComponent {
|
|
8
|
+
var displayName: string;
|
|
9
|
+
}
|
|
10
|
+
declare function MultiSelectContentComponent({ className, children }: MultiSelectContentProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
declare namespace MultiSelectContentComponent {
|
|
12
|
+
var displayName: string;
|
|
13
|
+
}
|
|
14
|
+
declare function MultiSelectOptionComponent({ value: optionValue, disabled, textValue: explicitTextValue, className, children, }: MultiSelectOptionProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
declare namespace MultiSelectOptionComponent {
|
|
16
|
+
var displayName: string;
|
|
17
|
+
}
|
|
18
|
+
declare function MultiSelectGroupComponent({ className, children }: MultiSelectGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
declare namespace MultiSelectGroupComponent {
|
|
20
|
+
var displayName: string;
|
|
21
|
+
}
|
|
22
|
+
declare function MultiSelectLabelComponent({ className, children }: MultiSelectLabelProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
declare namespace MultiSelectLabelComponent {
|
|
24
|
+
var displayName: string;
|
|
25
|
+
}
|
|
26
|
+
type MultiSelectCompound = typeof MultiSelectRoot & {
|
|
27
|
+
Trigger: typeof MultiSelectTriggerComponent;
|
|
28
|
+
Content: typeof MultiSelectContentComponent;
|
|
29
|
+
Option: typeof MultiSelectOptionComponent;
|
|
30
|
+
Group: typeof MultiSelectGroupComponent;
|
|
31
|
+
Label: typeof MultiSelectLabelComponent;
|
|
32
|
+
};
|
|
33
|
+
export declare const MultiSelect: MultiSelectCompound;
|
|
34
|
+
export declare const MultiSelectTrigger: typeof MultiSelectTriggerComponent;
|
|
35
|
+
export declare const MultiSelectContent: typeof MultiSelectContentComponent;
|
|
36
|
+
export declare const MultiSelectOption: typeof MultiSelectOptionComponent;
|
|
37
|
+
export declare const MultiSelectGroup: typeof MultiSelectGroupComponent;
|
|
38
|
+
export declare const MultiSelectLabel: typeof MultiSelectLabelComponent;
|
|
39
|
+
export { useMultiSelectContext as useMultiSelect } from './MultiSelectContext';
|
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, cloneElement, isValidElement, } from 'react';
|
|
3
|
+
import { useFloating, offset, flip, shift, size as sizeMiddleware, autoUpdate, FloatingPortal, useDismiss, useInteractions, useListNavigation, useTypeahead, useRole, useClick, } from '@floating-ui/react';
|
|
4
|
+
import { cx } from '../../utils/cx.js';
|
|
5
|
+
import { getPortalRootFor } from '../../utils/portal.js';
|
|
6
|
+
import { Icon } from '../Icon/index.js';
|
|
7
|
+
import { MultiSelectActionsContext, MultiSelectStateContext, MultiSelectContentContext, useMultiSelectContext, useMultiSelectContentContext, } from './MultiSelectContext.js';
|
|
8
|
+
import { toKey, } from './types.js';
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// MultiSelect Root
|
|
11
|
+
// =============================================================================
|
|
12
|
+
function MultiSelectRoot({ id: triggerIdProp, value: controlledValue, defaultValue, onValueChange, open: controlledOpen, defaultOpen, onOpenChange, disabled = false, placeholder = '', size = 'md', display = 'count', maxChips = 3, max, onMaxReached, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, children, }) {
|
|
13
|
+
// Controlled/uncontrolled value
|
|
14
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? []);
|
|
15
|
+
const isValueControlled = controlledValue !== undefined;
|
|
16
|
+
const value = isValueControlled ? controlledValue : uncontrolledValue;
|
|
17
|
+
// Track selected text values for display when closed
|
|
18
|
+
const selectedTextMapRef = useRef(new Map());
|
|
19
|
+
// Option registration
|
|
20
|
+
const optionsRef = useRef(new Map());
|
|
21
|
+
const [registryVersion, setRegistryVersion] = useState(0);
|
|
22
|
+
// Is selected helper
|
|
23
|
+
const isSelected = useCallback((optionValue) => {
|
|
24
|
+
const key = toKey(optionValue);
|
|
25
|
+
return value.some((v) => toKey(v) === key);
|
|
26
|
+
}, [value]);
|
|
27
|
+
// Check if max reached
|
|
28
|
+
const maxReached = max !== undefined && value.length >= max;
|
|
29
|
+
// Track if we just hit max (for callback)
|
|
30
|
+
const prevMaxReached = useRef(maxReached);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (maxReached && !prevMaxReached.current) {
|
|
33
|
+
onMaxReached?.();
|
|
34
|
+
}
|
|
35
|
+
prevMaxReached.current = maxReached;
|
|
36
|
+
}, [maxReached, onMaxReached]);
|
|
37
|
+
// Toggle option selection
|
|
38
|
+
const toggleOption = useCallback((optionValue, textValue) => {
|
|
39
|
+
const key = toKey(optionValue);
|
|
40
|
+
const currentlySelected = value.some((v) => toKey(v) === key);
|
|
41
|
+
let newValue;
|
|
42
|
+
if (currentlySelected) {
|
|
43
|
+
// Remove from selection
|
|
44
|
+
newValue = value.filter((v) => toKey(v) !== key);
|
|
45
|
+
selectedTextMapRef.current.delete(key);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Add to selection (unless max reached)
|
|
49
|
+
if (max !== undefined && value.length >= max) {
|
|
50
|
+
return; // Don't add more
|
|
51
|
+
}
|
|
52
|
+
newValue = [...value, optionValue];
|
|
53
|
+
selectedTextMapRef.current.set(key, textValue);
|
|
54
|
+
}
|
|
55
|
+
if (!isValueControlled) {
|
|
56
|
+
setUncontrolledValue(newValue);
|
|
57
|
+
}
|
|
58
|
+
onValueChange?.(newValue);
|
|
59
|
+
}, [value, max, isValueControlled, onValueChange]);
|
|
60
|
+
// Clear all selections
|
|
61
|
+
const clearAll = useCallback(() => {
|
|
62
|
+
selectedTextMapRef.current.clear();
|
|
63
|
+
if (!isValueControlled) {
|
|
64
|
+
setUncontrolledValue([]);
|
|
65
|
+
}
|
|
66
|
+
onValueChange?.([]);
|
|
67
|
+
}, [isValueControlled, onValueChange]);
|
|
68
|
+
// Sync selectedTextMap when controlled value changes
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (isValueControlled && controlledValue) {
|
|
71
|
+
// Remove keys that are no longer selected
|
|
72
|
+
const currentKeys = new Set(controlledValue.map(toKey));
|
|
73
|
+
for (const key of selectedTextMapRef.current.keys()) {
|
|
74
|
+
if (!currentKeys.has(key)) {
|
|
75
|
+
selectedTextMapRef.current.delete(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Add any missing keys from registered options
|
|
79
|
+
for (const v of controlledValue) {
|
|
80
|
+
const key = toKey(v);
|
|
81
|
+
if (!selectedTextMapRef.current.has(key)) {
|
|
82
|
+
const option = optionsRef.current.get(key);
|
|
83
|
+
if (option) {
|
|
84
|
+
selectedTextMapRef.current.set(key, option.textValue);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}, [controlledValue, isValueControlled]);
|
|
90
|
+
// Initialize selectedTextMap when options register (handles defaultValue)
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
for (const v of value) {
|
|
93
|
+
const key = toKey(v);
|
|
94
|
+
if (!selectedTextMapRef.current.has(key)) {
|
|
95
|
+
const option = optionsRef.current.get(key);
|
|
96
|
+
if (option) {
|
|
97
|
+
selectedTextMapRef.current.set(key, option.textValue);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, [registryVersion, value]);
|
|
102
|
+
// Controlled/uncontrolled open
|
|
103
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen ?? false);
|
|
104
|
+
const isOpenControlled = controlledOpen !== undefined;
|
|
105
|
+
const open = isOpenControlled ? controlledOpen : uncontrolledOpen;
|
|
106
|
+
const setOpen = useCallback((nextOpen) => {
|
|
107
|
+
if (disabled)
|
|
108
|
+
return;
|
|
109
|
+
if (!isOpenControlled) {
|
|
110
|
+
setUncontrolledOpen(nextOpen);
|
|
111
|
+
}
|
|
112
|
+
onOpenChange?.(nextOpen);
|
|
113
|
+
}, [disabled, isOpenControlled, onOpenChange]);
|
|
114
|
+
// IDs
|
|
115
|
+
const baseId = useId();
|
|
116
|
+
const triggerId = triggerIdProp ?? `${baseId}-trigger`;
|
|
117
|
+
const listboxId = `${baseId}-listbox`;
|
|
118
|
+
// Refs for list navigation
|
|
119
|
+
const listRef = useRef([]);
|
|
120
|
+
const listContentRef = useRef([]);
|
|
121
|
+
// Active index for navigation
|
|
122
|
+
const [activeIndex, setActiveIndex] = useState(null);
|
|
123
|
+
// Get ordered options
|
|
124
|
+
const getOrderedOptions = useCallback(() => {
|
|
125
|
+
const options = Array.from(optionsRef.current.values());
|
|
126
|
+
return options.sort((a, b) => {
|
|
127
|
+
if (!a.ref.current || !b.ref.current)
|
|
128
|
+
return 0;
|
|
129
|
+
const position = a.ref.current.compareDocumentPosition(b.ref.current);
|
|
130
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
131
|
+
return -1;
|
|
132
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING)
|
|
133
|
+
return 1;
|
|
134
|
+
return 0;
|
|
135
|
+
});
|
|
136
|
+
}, []);
|
|
137
|
+
// Memoized ordered options - recompute when registry changes
|
|
138
|
+
// getOrderedOptions is intentionally omitted - we only want to recompute when registryVersion changes
|
|
139
|
+
const orderedOptions = useMemo(() => {
|
|
140
|
+
const options = getOrderedOptions();
|
|
141
|
+
// Update typeahead ref synchronously
|
|
142
|
+
listContentRef.current = options.map((opt) => opt.textValue);
|
|
143
|
+
return options;
|
|
144
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
145
|
+
}, [registryVersion]);
|
|
146
|
+
// Compute disabled indices for keyboard navigation
|
|
147
|
+
// Skip disabled options AND unselected options when max reached
|
|
148
|
+
const disabledIndices = useMemo(() => orderedOptions.reduce((acc, opt, i) => {
|
|
149
|
+
const selected = isSelected(opt.value);
|
|
150
|
+
const isSelectable = !opt.disabled && (!maxReached || selected);
|
|
151
|
+
if (!isSelectable)
|
|
152
|
+
acc.push(i);
|
|
153
|
+
return acc;
|
|
154
|
+
}, []), [orderedOptions, maxReached, isSelected]);
|
|
155
|
+
// Floating UI setup
|
|
156
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
157
|
+
placement: 'bottom-start',
|
|
158
|
+
open,
|
|
159
|
+
onOpenChange: setOpen,
|
|
160
|
+
middleware: [
|
|
161
|
+
offset(4),
|
|
162
|
+
flip({ padding: 8 }),
|
|
163
|
+
shift({ padding: 8 }),
|
|
164
|
+
sizeMiddleware({
|
|
165
|
+
apply({ availableHeight, rects, elements }) {
|
|
166
|
+
Object.assign(elements.floating.style, {
|
|
167
|
+
maxHeight: `${Math.min(availableHeight - 16, 300)}px`,
|
|
168
|
+
minWidth: `${rects.reference.width}px`,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
padding: 8,
|
|
172
|
+
}),
|
|
173
|
+
],
|
|
174
|
+
whileElementsMounted: autoUpdate,
|
|
175
|
+
});
|
|
176
|
+
// Floating UI interactions
|
|
177
|
+
// Use 'click' (not 'mousedown') so button has focus when dropdown opens
|
|
178
|
+
// This ensures keyboard navigation works immediately
|
|
179
|
+
const click = useClick(context);
|
|
180
|
+
const dismiss = useDismiss(context);
|
|
181
|
+
const role = useRole(context, { role: 'listbox' });
|
|
182
|
+
const listNavigation = useListNavigation(context, {
|
|
183
|
+
listRef,
|
|
184
|
+
activeIndex,
|
|
185
|
+
onNavigate: setActiveIndex,
|
|
186
|
+
loop: true,
|
|
187
|
+
virtual: true,
|
|
188
|
+
focusItemOnOpen: true,
|
|
189
|
+
disabledIndices,
|
|
190
|
+
});
|
|
191
|
+
const typeahead = useTypeahead(context, {
|
|
192
|
+
listRef: listContentRef,
|
|
193
|
+
activeIndex,
|
|
194
|
+
onMatch: (index) => {
|
|
195
|
+
// Only match if the option is selectable
|
|
196
|
+
if (open && !disabledIndices.includes(index)) {
|
|
197
|
+
setActiveIndex(index);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
resetMs: 500,
|
|
201
|
+
});
|
|
202
|
+
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
|
|
203
|
+
click,
|
|
204
|
+
dismiss,
|
|
205
|
+
role,
|
|
206
|
+
listNavigation,
|
|
207
|
+
typeahead,
|
|
208
|
+
]);
|
|
209
|
+
// Register option
|
|
210
|
+
const registerOption = useCallback((option) => {
|
|
211
|
+
optionsRef.current.set(toKey(option.value), option);
|
|
212
|
+
setRegistryVersion((v) => v + 1);
|
|
213
|
+
}, []);
|
|
214
|
+
const unregisterOption = useCallback((optionValue) => {
|
|
215
|
+
optionsRef.current.delete(toKey(optionValue));
|
|
216
|
+
setRegistryVersion((v) => v + 1);
|
|
217
|
+
}, []);
|
|
218
|
+
// Get selected options for trigger display
|
|
219
|
+
const getSelectedOptions = useCallback(() => {
|
|
220
|
+
return value
|
|
221
|
+
.map((v) => {
|
|
222
|
+
const key = toKey(v);
|
|
223
|
+
const option = optionsRef.current.get(key);
|
|
224
|
+
if (option)
|
|
225
|
+
return option;
|
|
226
|
+
// Fallback for options not yet registered
|
|
227
|
+
const textValue = selectedTextMapRef.current.get(key) || String(v);
|
|
228
|
+
return { value: v, ref: { current: null }, disabled: false, textValue };
|
|
229
|
+
})
|
|
230
|
+
.filter(Boolean);
|
|
231
|
+
}, [value]);
|
|
232
|
+
// Highlighted value for keyboard navigation
|
|
233
|
+
const highlightedValue = activeIndex !== null ? orderedOptions[activeIndex]?.value ?? null : null;
|
|
234
|
+
// Track previous open state to detect open/close transitions
|
|
235
|
+
const prevOpenRef = useRef(open);
|
|
236
|
+
// Reset active index when closing, set initial when opening
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
const wasOpen = prevOpenRef.current;
|
|
239
|
+
prevOpenRef.current = open;
|
|
240
|
+
if (!open) {
|
|
241
|
+
// Closing: reset active index
|
|
242
|
+
setActiveIndex(null);
|
|
243
|
+
}
|
|
244
|
+
else if (!wasOpen) {
|
|
245
|
+
// Just opened (transition from closed to open): set initial active index
|
|
246
|
+
const firstEnabled = orderedOptions.findIndex((opt) => {
|
|
247
|
+
const selected = value.some((v) => toKey(v) === toKey(opt.value));
|
|
248
|
+
const atMax = max !== undefined && value.length >= max;
|
|
249
|
+
return !opt.disabled && (!atMax || selected);
|
|
250
|
+
});
|
|
251
|
+
setActiveIndex(firstEnabled >= 0 ? firstEnabled : null);
|
|
252
|
+
}
|
|
253
|
+
// Note: Don't reset activeIndex while open - that would jump focus on toggle
|
|
254
|
+
}, [open, orderedOptions, value, max]);
|
|
255
|
+
// Scroll active option into view
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (open && activeIndex !== null && listRef.current[activeIndex]) {
|
|
258
|
+
listRef.current[activeIndex]?.scrollIntoView({ block: 'nearest' });
|
|
259
|
+
}
|
|
260
|
+
}, [open, activeIndex]);
|
|
261
|
+
// ==========================================================================
|
|
262
|
+
// Split Context Values
|
|
263
|
+
// ==========================================================================
|
|
264
|
+
// Actions: stable config, IDs, refs, callbacks — rarely changes
|
|
265
|
+
// State: open, value, activeIndex, etc. — changes on interaction
|
|
266
|
+
// ==========================================================================
|
|
267
|
+
const actionsValue = useMemo(() => ({
|
|
268
|
+
// Config (from props, stable for component lifetime)
|
|
269
|
+
disabled,
|
|
270
|
+
placeholder,
|
|
271
|
+
display,
|
|
272
|
+
maxChips,
|
|
273
|
+
max,
|
|
274
|
+
size,
|
|
275
|
+
// ARIA IDs (stable)
|
|
276
|
+
triggerId,
|
|
277
|
+
listboxId,
|
|
278
|
+
ariaLabel,
|
|
279
|
+
ariaLabelledBy,
|
|
280
|
+
ariaDescribedBy,
|
|
281
|
+
// Stable callbacks
|
|
282
|
+
setOpen,
|
|
283
|
+
toggleOption,
|
|
284
|
+
clearAll,
|
|
285
|
+
registerOption,
|
|
286
|
+
unregisterOption,
|
|
287
|
+
// Refs (stable objects)
|
|
288
|
+
refs,
|
|
289
|
+
listRef,
|
|
290
|
+
// Floating UI interaction props (stable functions from useInteractions)
|
|
291
|
+
getReferenceProps,
|
|
292
|
+
getFloatingProps,
|
|
293
|
+
getItemProps,
|
|
294
|
+
}), [
|
|
295
|
+
// Config props - only change if parent rerenders with new props
|
|
296
|
+
disabled,
|
|
297
|
+
placeholder,
|
|
298
|
+
display,
|
|
299
|
+
maxChips,
|
|
300
|
+
max,
|
|
301
|
+
size,
|
|
302
|
+
// IDs are stable (from useId)
|
|
303
|
+
triggerId,
|
|
304
|
+
listboxId,
|
|
305
|
+
ariaLabel,
|
|
306
|
+
ariaLabelledBy,
|
|
307
|
+
ariaDescribedBy,
|
|
308
|
+
// Callbacks are stable (useCallback with stable deps)
|
|
309
|
+
setOpen,
|
|
310
|
+
toggleOption,
|
|
311
|
+
clearAll,
|
|
312
|
+
registerOption,
|
|
313
|
+
unregisterOption,
|
|
314
|
+
// Refs are stable objects
|
|
315
|
+
refs,
|
|
316
|
+
listRef,
|
|
317
|
+
// Interaction props from useInteractions (stable)
|
|
318
|
+
getReferenceProps,
|
|
319
|
+
getFloatingProps,
|
|
320
|
+
getItemProps,
|
|
321
|
+
]);
|
|
322
|
+
const stateValue = useMemo(() => ({
|
|
323
|
+
// Open state
|
|
324
|
+
open,
|
|
325
|
+
// Selection state
|
|
326
|
+
value,
|
|
327
|
+
maxReached,
|
|
328
|
+
// Selection helpers (semantically tied to value)
|
|
329
|
+
isSelected,
|
|
330
|
+
getSelectedOptions,
|
|
331
|
+
// Navigation state
|
|
332
|
+
activeIndex,
|
|
333
|
+
highlightedValue,
|
|
334
|
+
// Derived (changes when registry/value changes)
|
|
335
|
+
orderedOptions,
|
|
336
|
+
// Floating UI (changes on open/position)
|
|
337
|
+
floatingStyles,
|
|
338
|
+
floatingContext: context,
|
|
339
|
+
}), [
|
|
340
|
+
open,
|
|
341
|
+
value,
|
|
342
|
+
maxReached,
|
|
343
|
+
isSelected,
|
|
344
|
+
getSelectedOptions,
|
|
345
|
+
activeIndex,
|
|
346
|
+
highlightedValue,
|
|
347
|
+
orderedOptions,
|
|
348
|
+
floatingStyles,
|
|
349
|
+
context,
|
|
350
|
+
]);
|
|
351
|
+
return (_jsx(MultiSelectActionsContext.Provider, { value: actionsValue, children: _jsx(MultiSelectStateContext.Provider, { value: stateValue, children: children }) }));
|
|
352
|
+
}
|
|
353
|
+
MultiSelectRoot.displayName = 'MultiSelect';
|
|
354
|
+
// =============================================================================
|
|
355
|
+
// MultiSelect.Trigger
|
|
356
|
+
// =============================================================================
|
|
357
|
+
function MultiSelectTriggerComponent({ asChild = false, className, children, }) {
|
|
358
|
+
const { open, setOpen, disabled, placeholder, size, display, maxChips, max, maxReached, triggerId, listboxId, ariaLabel, ariaLabelledBy, ariaDescribedBy, getSelectedOptions, clearAll, refs, getReferenceProps, activeIndex, orderedOptions, toggleOption, isSelected, } = useMultiSelectContext();
|
|
359
|
+
const sizeClass = size !== 'md' ? `is-size-${size}` : undefined;
|
|
360
|
+
const selectedOptions = getSelectedOptions();
|
|
361
|
+
const hasSelection = selectedOptions.length > 0;
|
|
362
|
+
// Ensure trigger has focus when dropdown opens (Safari doesn't focus buttons on click)
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (open && refs.reference.current) {
|
|
365
|
+
refs.reference.current.focus();
|
|
366
|
+
}
|
|
367
|
+
}, [open, refs.reference]);
|
|
368
|
+
// Handle keyboard interactions (Enter/Space for toggle when open)
|
|
369
|
+
// Note: Backspace/Delete handled directly on button element to avoid typeahead interference
|
|
370
|
+
const handleKeyDown = useCallback((e) => {
|
|
371
|
+
// Toggle option with Enter/Space when open
|
|
372
|
+
if (open && (e.key === 'Enter' || e.key === ' ')) {
|
|
373
|
+
e.preventDefault();
|
|
374
|
+
if (activeIndex !== null) {
|
|
375
|
+
const option = orderedOptions[activeIndex];
|
|
376
|
+
if (option) {
|
|
377
|
+
const selected = isSelected(option.value);
|
|
378
|
+
const canToggle = !option.disabled && (!maxReached || selected);
|
|
379
|
+
if (canToggle) {
|
|
380
|
+
toggleOption(option.value, option.textValue);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}, [open, activeIndex, orderedOptions, isSelected, maxReached, toggleOption]);
|
|
386
|
+
// Close dropdown when focus leaves trigger
|
|
387
|
+
// Uses blur guard pattern: only close if focus moved outside controlled elements
|
|
388
|
+
const handleBlur = useCallback((e) => {
|
|
389
|
+
if (!open)
|
|
390
|
+
return;
|
|
391
|
+
const relatedTarget = e.relatedTarget;
|
|
392
|
+
// Focus left the document entirely (e.g., clicked outside window)
|
|
393
|
+
if (relatedTarget === null) {
|
|
394
|
+
setOpen(false);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
// Check if focus moved to the floating content (clicking an option)
|
|
398
|
+
const floating = refs.floating.current;
|
|
399
|
+
if (floating && relatedTarget && floating.contains(relatedTarget)) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
// Focus moved somewhere outside our control — close
|
|
403
|
+
setOpen(false);
|
|
404
|
+
}, [open, setOpen, refs.floating]);
|
|
405
|
+
// Handle clear button click
|
|
406
|
+
const handleClearClick = useCallback((e) => {
|
|
407
|
+
e.stopPropagation(); // Prevent triggering the dropdown
|
|
408
|
+
if (disabled)
|
|
409
|
+
return;
|
|
410
|
+
clearAll();
|
|
411
|
+
}, [disabled, clearAll]);
|
|
412
|
+
// Get Floating UI's reference props
|
|
413
|
+
const floatingProps = getReferenceProps({
|
|
414
|
+
'aria-activedescendant': activeIndex !== null ? `${listboxId}-option-${activeIndex}` : undefined,
|
|
415
|
+
onKeyDown: handleKeyDown,
|
|
416
|
+
onBlur: handleBlur,
|
|
417
|
+
});
|
|
418
|
+
// Render trigger content based on display mode
|
|
419
|
+
const renderTriggerContent = () => {
|
|
420
|
+
if (!hasSelection) {
|
|
421
|
+
return _jsx("span", { className: "tui-multiselect__placeholder", children: placeholder });
|
|
422
|
+
}
|
|
423
|
+
if (display === 'count') {
|
|
424
|
+
return (_jsxs("span", { className: "tui-multiselect__count", children: [selectedOptions.length, " selected"] }));
|
|
425
|
+
}
|
|
426
|
+
// chips mode
|
|
427
|
+
const visibleChips = selectedOptions.slice(0, maxChips);
|
|
428
|
+
const overflow = selectedOptions.length - maxChips;
|
|
429
|
+
return (_jsxs("span", { className: "tui-multiselect__chips", children: [visibleChips.map((opt) => (_jsx("span", { className: "tui-multiselect__chip", children: opt.textValue }, toKey(opt.value)))), overflow > 0 && (_jsxs("span", { className: "tui-multiselect__more", children: ["+", overflow, " more"] }))] }));
|
|
430
|
+
};
|
|
431
|
+
// Default trigger content (when not using asChild or custom children)
|
|
432
|
+
const defaultTriggerContent = (_jsxs(_Fragment, { children: [_jsx("span", { className: "tui-multiselect__value", children: renderTriggerContent() }), hasSelection && (_jsx("span", { className: "tui-multiselect__clear", onClick: handleClearClick, "aria-hidden": "true", children: _jsx(Icon, { name: "system/close", size: "sm" }) })), _jsx(Icon, { name: "system/chevron-down", size: "sm", className: "tui-multiselect__icon", "aria-hidden": "true" })] }));
|
|
433
|
+
// Generate status message for screen readers
|
|
434
|
+
const statusMessage = hasSelection
|
|
435
|
+
? `${selectedOptions.length} item${selectedOptions.length === 1 ? '' : 's'} selected${maxReached && max ? `. Maximum of ${max} reached` : ''}`
|
|
436
|
+
: '';
|
|
437
|
+
// Live region (rendered outside button, sibling to trigger)
|
|
438
|
+
const liveRegion = (_jsx("span", { className: "tui-visually-hidden", role: "status", "aria-live": "polite", "aria-atomic": "true", children: statusMessage }));
|
|
439
|
+
// Base trigger props
|
|
440
|
+
const triggerProps = {
|
|
441
|
+
id: triggerId,
|
|
442
|
+
'aria-label': ariaLabel,
|
|
443
|
+
'aria-labelledby': ariaLabelledBy,
|
|
444
|
+
'aria-describedby': ariaDescribedBy,
|
|
445
|
+
'aria-haspopup': 'listbox',
|
|
446
|
+
'aria-expanded': open,
|
|
447
|
+
'aria-controls': listboxId,
|
|
448
|
+
'aria-disabled': disabled || undefined,
|
|
449
|
+
'data-state': open ? 'open' : 'closed',
|
|
450
|
+
'data-disabled': disabled || undefined,
|
|
451
|
+
...floatingProps,
|
|
452
|
+
};
|
|
453
|
+
// asChild: merge props onto child element
|
|
454
|
+
if (asChild && isValidElement(children)) {
|
|
455
|
+
const childProps = children.props;
|
|
456
|
+
// Compose event handlers: child runs first, skip ours if defaultPrevented
|
|
457
|
+
const composeHandler = (ours, theirs) => {
|
|
458
|
+
if (!ours && !theirs)
|
|
459
|
+
return undefined;
|
|
460
|
+
if (!ours)
|
|
461
|
+
return theirs;
|
|
462
|
+
if (!theirs)
|
|
463
|
+
return ours;
|
|
464
|
+
return (e) => {
|
|
465
|
+
theirs(e);
|
|
466
|
+
if (!e.defaultPrevented) {
|
|
467
|
+
ours(e);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
return (_jsxs(_Fragment, { children: [cloneElement(children, {
|
|
472
|
+
// Spread order: ours first, then child (child wins), then explicit overrides
|
|
473
|
+
...triggerProps,
|
|
474
|
+
...childProps,
|
|
475
|
+
// Re-assert must-have props that child cannot override
|
|
476
|
+
id: triggerId,
|
|
477
|
+
ref: refs.setReference,
|
|
478
|
+
'aria-haspopup': 'listbox',
|
|
479
|
+
'aria-expanded': open,
|
|
480
|
+
'aria-controls': listboxId,
|
|
481
|
+
'aria-activedescendant': floatingProps['aria-activedescendant'],
|
|
482
|
+
'aria-describedby': ariaDescribedBy,
|
|
483
|
+
'aria-disabled': disabled || undefined,
|
|
484
|
+
'data-state': open ? 'open' : 'closed',
|
|
485
|
+
'data-disabled': disabled || undefined,
|
|
486
|
+
className: cx(childProps.className, 'tui-multiselect__trigger', sizeClass, className),
|
|
487
|
+
// Composed handlers override both spreads
|
|
488
|
+
onClick: composeHandler(triggerProps.onClick, childProps.onClick),
|
|
489
|
+
// Handle Backspace/Delete before composed handlers (typeahead may intercept)
|
|
490
|
+
onKeyDown: (e) => {
|
|
491
|
+
if (!open && hasSelection && (e.key === 'Backspace' || e.key === 'Delete')) {
|
|
492
|
+
e.preventDefault();
|
|
493
|
+
clearAll();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// Then run composed handlers
|
|
497
|
+
const composedHandler = composeHandler(triggerProps.onKeyDown, childProps.onKeyDown);
|
|
498
|
+
composedHandler?.(e);
|
|
499
|
+
},
|
|
500
|
+
onBlur: composeHandler(triggerProps.onBlur, childProps.onBlur),
|
|
501
|
+
onMouseDown: composeHandler(triggerProps.onMouseDown, childProps.onMouseDown),
|
|
502
|
+
onPointerDown: composeHandler(triggerProps.onPointerDown, childProps.onPointerDown),
|
|
503
|
+
}), liveRegion] }));
|
|
504
|
+
}
|
|
505
|
+
// Default: render button with optional custom content
|
|
506
|
+
const triggerContent = children ?? defaultTriggerContent;
|
|
507
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: refs.setReference, type: "button", id: triggerId, className: cx('tui-multiselect__trigger', sizeClass, className), disabled: disabled, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": listboxId, "aria-disabled": disabled || undefined, "data-state": open ? 'open' : 'closed', "data-disabled": disabled || undefined, ...floatingProps,
|
|
508
|
+
// Handle Backspace/Delete AFTER floatingProps to ensure we catch it
|
|
509
|
+
// (typeahead may intercept these for its buffer)
|
|
510
|
+
onKeyDown: (e) => {
|
|
511
|
+
// Clear all with Backspace/Delete when closed and has selection
|
|
512
|
+
if (!open && hasSelection && (e.key === 'Backspace' || e.key === 'Delete')) {
|
|
513
|
+
e.preventDefault();
|
|
514
|
+
clearAll();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// Call the composed handler from floatingProps for everything else
|
|
518
|
+
floatingProps.onKeyDown?.(e);
|
|
519
|
+
}, children: triggerContent }), liveRegion] }));
|
|
520
|
+
}
|
|
521
|
+
MultiSelectTriggerComponent.displayName = 'MultiSelect.Trigger';
|
|
522
|
+
// =============================================================================
|
|
523
|
+
// MultiSelect.Content
|
|
524
|
+
// =============================================================================
|
|
525
|
+
function MultiSelectContentComponent({ className, children }) {
|
|
526
|
+
const { open, listboxId, triggerId, refs, floatingStyles, getFloatingProps, listRef, activeIndex, orderedOptions, } = useMultiSelectContext();
|
|
527
|
+
const portalRoot = getPortalRootFor(refs.reference.current);
|
|
528
|
+
// Memoized context for options
|
|
529
|
+
const contentContext = useMemo(() => ({
|
|
530
|
+
listRef,
|
|
531
|
+
activeIndex,
|
|
532
|
+
orderedOptions,
|
|
533
|
+
}), [listRef, activeIndex, orderedOptions]);
|
|
534
|
+
return (_jsxs(_Fragment, { children: [!open && (_jsx("div", { style: { display: 'none' }, "aria-hidden": "true", children: _jsx(MultiSelectContentContext.Provider, { value: contentContext, children: children }) })), open && (_jsx(FloatingPortal, { root: portalRoot, children: _jsx("div", { ref: refs.setFloating, id: listboxId, role: "listbox", "aria-labelledby": triggerId, "aria-multiselectable": "true", className: cx('tui-multiselect__content', className), style: {
|
|
535
|
+
...floatingStyles,
|
|
536
|
+
minWidth: refs.reference.current?.offsetWidth,
|
|
537
|
+
pointerEvents: 'auto',
|
|
538
|
+
}, ...getFloatingProps(), children: _jsx(MultiSelectContentContext.Provider, { value: contentContext, children: children }) }) }))] }));
|
|
539
|
+
}
|
|
540
|
+
MultiSelectContentComponent.displayName = 'MultiSelect.Content';
|
|
541
|
+
// =============================================================================
|
|
542
|
+
// MultiSelect.Option
|
|
543
|
+
// =============================================================================
|
|
544
|
+
function MultiSelectOptionComponent({ value: optionValue, disabled = false, textValue: explicitTextValue, className, children, }) {
|
|
545
|
+
const { isSelected, toggleOption, listboxId, registerOption, unregisterOption, highlightedValue, getItemProps, maxReached, } = useMultiSelectContext();
|
|
546
|
+
const { listRef, orderedOptions } = useMultiSelectContentContext();
|
|
547
|
+
const ref = useRef(null);
|
|
548
|
+
// Derive textValue from children if not explicitly provided
|
|
549
|
+
const textValue = explicitTextValue ?? (typeof children === 'string' ? children : '');
|
|
550
|
+
// Warn in dev if textValue couldn't be derived
|
|
551
|
+
useEffect(() => {
|
|
552
|
+
if (import.meta.env.DEV && !textValue) {
|
|
553
|
+
console.warn(`MultiSelect.Option with value="${optionValue}" has no textValue. Provide textValue prop when children is not a string.`);
|
|
554
|
+
}
|
|
555
|
+
}, [textValue, optionValue]);
|
|
556
|
+
// Register option
|
|
557
|
+
useLayoutEffect(() => {
|
|
558
|
+
registerOption({ value: optionValue, ref, disabled, textValue });
|
|
559
|
+
return () => unregisterOption(optionValue);
|
|
560
|
+
}, [optionValue, disabled, textValue, registerOption, unregisterOption]);
|
|
561
|
+
// Find this option's index in ordered list
|
|
562
|
+
const index = orderedOptions.findIndex((opt) => toKey(opt.value) === toKey(optionValue));
|
|
563
|
+
// Assign ref to listRef for navigation
|
|
564
|
+
useEffect(() => {
|
|
565
|
+
if (index >= 0) {
|
|
566
|
+
listRef.current[index] = ref.current;
|
|
567
|
+
}
|
|
568
|
+
}, [index, listRef]);
|
|
569
|
+
const selected = isSelected(optionValue);
|
|
570
|
+
const isHighlighted = highlightedValue !== null && toKey(highlightedValue) === toKey(optionValue);
|
|
571
|
+
// Blocked: max reached and not selected (blocked by constraint, not intrinsically disabled)
|
|
572
|
+
const isBlocked = !disabled && maxReached && !selected;
|
|
573
|
+
// Selectable: not disabled AND (not max reached OR already selected)
|
|
574
|
+
const isSelectable = !disabled && (!maxReached || selected);
|
|
575
|
+
const handlePointerDown = useCallback((e) => {
|
|
576
|
+
// Prevent focus shift from trigger
|
|
577
|
+
e.preventDefault();
|
|
578
|
+
}, []);
|
|
579
|
+
const handleClick = useCallback(() => {
|
|
580
|
+
if (!isSelectable)
|
|
581
|
+
return;
|
|
582
|
+
toggleOption(optionValue, textValue);
|
|
583
|
+
}, [isSelectable, optionValue, textValue, toggleOption]);
|
|
584
|
+
return (_jsxs("div", { ref: ref, id: index >= 0 ? `${listboxId}-option-${index}` : undefined, role: "option", className: cx('tui-multiselect__option', className), "aria-selected": selected, "aria-disabled": !isSelectable || undefined, "data-highlighted": isHighlighted || undefined, "data-disabled": disabled || undefined, "data-blocked": isBlocked || undefined, ...getItemProps({
|
|
585
|
+
onClick: isSelectable ? handleClick : undefined,
|
|
586
|
+
onPointerDown: handlePointerDown,
|
|
587
|
+
}), children: [_jsx("span", { className: cx('tui-multiselect__checkbox', selected && 'is-checked'), "aria-hidden": "true", children: selected && _jsx(Icon, { name: "system/check", size: "xs" }) }), _jsx("span", { className: "tui-multiselect__option-content", children: children })] }));
|
|
588
|
+
}
|
|
589
|
+
MultiSelectOptionComponent.displayName = 'MultiSelect.Option';
|
|
590
|
+
// =============================================================================
|
|
591
|
+
// MultiSelect.Group
|
|
592
|
+
// =============================================================================
|
|
593
|
+
function MultiSelectGroupComponent({ className, children }) {
|
|
594
|
+
const groupId = useId();
|
|
595
|
+
return (_jsx("div", { role: "group", "aria-labelledby": `${groupId}-label`, className: cx('tui-multiselect__group', className), children: _jsx(MultiSelectGroupContext.Provider, { value: { groupId }, children: children }) }));
|
|
596
|
+
}
|
|
597
|
+
MultiSelectGroupComponent.displayName = 'MultiSelect.Group';
|
|
598
|
+
const MultiSelectGroupContext = React.createContext(null);
|
|
599
|
+
// =============================================================================
|
|
600
|
+
// MultiSelect.Label
|
|
601
|
+
// =============================================================================
|
|
602
|
+
function MultiSelectLabelComponent({ className, children }) {
|
|
603
|
+
const groupContext = React.useContext(MultiSelectGroupContext);
|
|
604
|
+
// No aria-hidden or role="presentation" — aria-labelledby on Group references this element,
|
|
605
|
+
// so screen readers need access to read the label text.
|
|
606
|
+
return (_jsx("div", { id: groupContext ? `${groupContext.groupId}-label` : undefined, className: cx('tui-multiselect__label', className), children: children }));
|
|
607
|
+
}
|
|
608
|
+
MultiSelectLabelComponent.displayName = 'MultiSelect.Label';
|
|
609
|
+
export const MultiSelect = MultiSelectRoot;
|
|
610
|
+
MultiSelect.Trigger = MultiSelectTriggerComponent;
|
|
611
|
+
MultiSelect.Content = MultiSelectContentComponent;
|
|
612
|
+
MultiSelect.Option = MultiSelectOptionComponent;
|
|
613
|
+
MultiSelect.Group = MultiSelectGroupComponent;
|
|
614
|
+
MultiSelect.Label = MultiSelectLabelComponent;
|
|
615
|
+
// Named exports
|
|
616
|
+
export const MultiSelectTrigger = MultiSelectTriggerComponent;
|
|
617
|
+
export const MultiSelectContent = MultiSelectContentComponent;
|
|
618
|
+
export const MultiSelectOption = MultiSelectOptionComponent;
|
|
619
|
+
export const MultiSelectGroup = MultiSelectGroupComponent;
|
|
620
|
+
export const MultiSelectLabel = MultiSelectLabelComponent;
|
|
621
|
+
// Hook for advanced use cases
|
|
622
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
623
|
+
export { useMultiSelectContext as useMultiSelect } from './MultiSelectContext.js';
|