@tangible/ui 0.0.1 → 0.0.2

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.
Files changed (129) hide show
  1. package/components/Card/Card.d.ts +1 -0
  2. package/components/Card/Card.js +17 -20
  3. package/components/Checkbox/Checkbox.d.ts +9 -0
  4. package/components/Checkbox/Checkbox.js +92 -0
  5. package/components/Checkbox/index.d.ts +2 -0
  6. package/components/Checkbox/index.js +1 -0
  7. package/components/Checkbox/types.d.ts +10 -0
  8. package/components/Checkbox/types.js +1 -0
  9. package/components/Chip/Chip.d.ts +4 -1
  10. package/components/Chip/Chip.js +32 -7
  11. package/components/ChipGroup/ChipGroup.d.ts +5 -0
  12. package/components/ChipGroup/ChipGroup.js +68 -0
  13. package/components/ChipGroup/ChipGroupContext.d.ts +3 -0
  14. package/components/ChipGroup/ChipGroupContext.js +5 -0
  15. package/components/ChipGroup/index.d.ts +3 -0
  16. package/components/ChipGroup/index.js +2 -0
  17. package/components/ChipGroup/types.d.ts +36 -0
  18. package/components/ChipGroup/types.js +1 -0
  19. package/components/Chips/Chips.d.ts +2 -0
  20. package/components/Chips/Chips.js +1 -1
  21. package/components/Combobox/Combobox.d.ts +33 -0
  22. package/components/Combobox/Combobox.js +466 -0
  23. package/components/Combobox/ComboboxContext.d.ts +8 -0
  24. package/components/Combobox/ComboboxContext.js +36 -0
  25. package/components/Combobox/index.d.ts +2 -0
  26. package/components/Combobox/index.js +1 -0
  27. package/components/Combobox/types.d.ts +204 -0
  28. package/components/Combobox/types.js +1 -0
  29. package/components/Dropdown/Dropdown.js +2 -1
  30. package/components/Field/Field.d.ts +39 -0
  31. package/components/Field/Field.js +92 -0
  32. package/components/Field/FieldContext.d.ts +16 -0
  33. package/components/Field/FieldContext.js +10 -0
  34. package/components/Field/index.d.ts +2 -0
  35. package/components/Field/index.js +1 -0
  36. package/components/Modal/Modal.d.ts +4 -4
  37. package/components/Modal/Modal.js +14 -12
  38. package/components/MultiSelect/MultiSelect.d.ts +39 -0
  39. package/components/MultiSelect/MultiSelect.js +623 -0
  40. package/components/MultiSelect/MultiSelectContext.d.ts +20 -0
  41. package/components/MultiSelect/MultiSelectContext.js +56 -0
  42. package/components/MultiSelect/index.d.ts +2 -0
  43. package/components/MultiSelect/index.js +1 -0
  44. package/components/MultiSelect/types.d.ts +218 -0
  45. package/components/MultiSelect/types.js +3 -0
  46. package/components/Notice/Notice.d.ts +1 -1
  47. package/components/Notice/Notice.js +1 -1
  48. package/components/Progress/Progress.js +1 -1
  49. package/components/Progress/types.d.ts +7 -7
  50. package/components/Radio/Radio.d.ts +2 -0
  51. package/components/Radio/Radio.js +50 -0
  52. package/components/Radio/RadioGroup.d.ts +2 -0
  53. package/components/Radio/RadioGroup.js +54 -0
  54. package/components/Radio/RadioGroupContext.d.ts +3 -0
  55. package/components/Radio/RadioGroupContext.js +9 -0
  56. package/components/Radio/index.d.ts +8 -0
  57. package/components/Radio/index.js +6 -0
  58. package/components/Radio/types.d.ts +32 -0
  59. package/components/Radio/types.js +1 -0
  60. package/components/Rating/Rating.d.ts +5 -5
  61. package/components/Rating/Rating.js +2 -2
  62. package/components/SegmentedControl/SegmentedControl.js +20 -104
  63. package/components/SegmentedControl/types.d.ts +4 -8
  64. package/components/Select/Select.d.ts +39 -0
  65. package/components/Select/Select.js +497 -0
  66. package/components/Select/SelectContext.d.ts +20 -0
  67. package/components/Select/SelectContext.js +56 -0
  68. package/components/Select/index.d.ts +3 -0
  69. package/components/Select/index.js +1 -0
  70. package/components/Select/types.d.ts +216 -0
  71. package/components/Select/types.js +11 -0
  72. package/components/Sidebar/Sidebar.js +12 -12
  73. package/components/Sidebar/types.d.ts +5 -5
  74. package/components/StepIndicator/StepIndicator.js +1 -1
  75. package/components/StepList/StepList.js +2 -2
  76. package/components/StepList/types.d.ts +4 -4
  77. package/components/Switch/Switch.d.ts +9 -0
  78. package/components/Switch/Switch.js +91 -0
  79. package/components/Switch/index.d.ts +2 -0
  80. package/components/Switch/index.js +1 -0
  81. package/components/Switch/types.d.ts +11 -0
  82. package/components/Switch/types.js +1 -0
  83. package/components/TextInput/TextInput.d.ts +8 -0
  84. package/components/TextInput/TextInput.js +25 -0
  85. package/components/TextInput/index.d.ts +2 -0
  86. package/components/TextInput/index.js +1 -0
  87. package/components/TextInput/types.d.ts +32 -0
  88. package/components/TextInput/types.js +1 -0
  89. package/components/Textarea/Textarea.d.ts +6 -0
  90. package/components/Textarea/Textarea.js +49 -0
  91. package/components/Textarea/index.d.ts +2 -0
  92. package/components/Textarea/index.js +1 -0
  93. package/components/Textarea/types.d.ts +25 -0
  94. package/components/Textarea/types.js +1 -0
  95. package/components/index.d.ts +20 -0
  96. package/components/index.js +10 -0
  97. package/icons/icons.svg +1 -0
  98. package/icons/manifest.json +8 -0
  99. package/icons/registry.d.ts +2 -0
  100. package/icons/registry.js +1 -0
  101. package/icons/system/index.d.ts +2 -0
  102. package/icons/system/index.js +11 -0
  103. package/package.json +1 -1
  104. package/styles/all.css +1 -1
  105. package/styles/all.expanded.css +1187 -96
  106. package/styles/all.expanded.unlayered.css +1187 -96
  107. package/styles/all.unlayered.css +1 -1
  108. package/styles/components/_bundle.scss +20 -0
  109. package/styles/components/input/index.scss +5 -20
  110. package/styles/index.scss +16 -0
  111. package/styles/system/_control.scss +34 -0
  112. package/styles/system/_tokens.scss +8 -0
  113. package/styles/system/index.scss +2 -1
  114. package/styles/utilities/_index.scss +50 -0
  115. package/tui-manifest.json +632 -61
  116. package/utils/compose-events.d.ts +15 -0
  117. package/utils/compose-events.js +27 -0
  118. package/utils/hash.d.ts +15 -0
  119. package/utils/hash.js +32 -0
  120. package/utils/index.d.ts +3 -0
  121. package/utils/index.js +6 -0
  122. package/utils/is-dev.d.ts +5 -0
  123. package/utils/is-dev.js +7 -0
  124. package/utils/use-controllable-state.d.ts +19 -0
  125. package/utils/use-controllable-state.js +59 -0
  126. package/utils/use-roving-group.d.ts +33 -0
  127. package/utils/use-roving-group.js +123 -0
  128. package/utils/value-key.d.ts +16 -0
  129. package/utils/value-key.js +14 -0
@@ -0,0 +1,466 @@
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, } from 'react';
3
+ import { useFloating, offset, flip, shift, size as sizeMiddleware, autoUpdate, FloatingPortal, useDismiss, useInteractions, useListNavigation, useRole, } from '@floating-ui/react';
4
+ import { cx } from '../../utils/cx.js';
5
+ import { toKey } from '../../utils/value-key.js';
6
+ import { getPortalRootFor } from '../../utils/portal.js';
7
+ import { hashForId } from '../../utils/hash.js';
8
+ import { composeEventHandlers } from '../../utils/compose-events.js';
9
+ import { Icon } from '../Icon/index.js';
10
+ import { ComboboxActionsContext, ComboboxStateContext, ComboboxContentContext, useComboboxContext, useComboboxContentContext, } from './ComboboxContext.js';
11
+ // =============================================================================
12
+ // Combobox Root
13
+ // =============================================================================
14
+ function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inputValue: controlledInputValue, onInputChange, open: controlledOpen, defaultOpen, onOpenChange, disabled = false, placeholder = '', size = 'md', openOnFocus = true, filterMode = 'always', onQueryChange, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, inputClassName, children, }) {
15
+ // Controlled/uncontrolled value (initialize from defaultValue)
16
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
17
+ const isValueControlled = controlledValue !== undefined;
18
+ const value = isValueControlled ? controlledValue : uncontrolledValue;
19
+ // Controlled/uncontrolled inputValue
20
+ const [uncontrolledInputValue, setUncontrolledInputValue] = useState('');
21
+ const isInputControlled = controlledInputValue !== undefined;
22
+ const inputValue = isInputControlled ? controlledInputValue : uncontrolledInputValue;
23
+ // Controlled/uncontrolled open
24
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen ?? false);
25
+ const isOpenControlled = controlledOpen !== undefined;
26
+ const open = isOpenControlled ? controlledOpen : uncontrolledOpen;
27
+ // Internal searching state (private - not exposed)
28
+ // True only when user is actively typing, false after selection/clear/escape
29
+ const [searching, setSearching] = useState(false);
30
+ // Compute effective query for filtering
31
+ const query = filterMode === 'when-searching' && !searching ? '' : inputValue;
32
+ // Track previous query to fire onQueryChange
33
+ const prevQueryRef = useRef(query);
34
+ useEffect(() => {
35
+ if (prevQueryRef.current !== query) {
36
+ prevQueryRef.current = query;
37
+ onQueryChange?.(query);
38
+ }
39
+ }, [query, onQueryChange]);
40
+ // Option registration
41
+ const optionsRef = useRef(new Map());
42
+ const [registryVersion, setRegistryVersion] = useState(0);
43
+ // IDs
44
+ const baseId = useId();
45
+ const inputId = `${baseId}-input`;
46
+ const listboxId = `${baseId}-listbox`;
47
+ // Refs
48
+ const inputRef = useRef(null);
49
+ const listRef = useRef([]);
50
+ // Active index for navigation (-1 means no active option)
51
+ const [activeIndex, setActiveIndex] = useState(-1);
52
+ // Track previous highlighted value to maintain selection across filter changes
53
+ const prevHighlightedValueRef = useRef(null);
54
+ // Set open state
55
+ const setOpen = useCallback((nextOpen) => {
56
+ if (disabled)
57
+ return;
58
+ if (!isOpenControlled) {
59
+ setUncontrolledOpen(nextOpen);
60
+ }
61
+ onOpenChange?.(nextOpen);
62
+ }, [disabled, isOpenControlled, onOpenChange]);
63
+ // Set input value
64
+ const setInputValue = useCallback((nextValue) => {
65
+ if (!isInputControlled) {
66
+ setUncontrolledInputValue(nextValue);
67
+ }
68
+ onInputChange?.(nextValue);
69
+ }, [isInputControlled, onInputChange]);
70
+ // Select an option
71
+ const selectOption = useCallback((optionValue) => {
72
+ if (!isValueControlled) {
73
+ setUncontrolledValue(optionValue);
74
+ }
75
+ onValueChange?.(optionValue);
76
+ setSearching(false); // End searching mode on selection
77
+ setOpen(false);
78
+ }, [isValueControlled, onValueChange, setOpen]);
79
+ // Clear the selected value
80
+ // Always clear uncontrolled state - we can't predict what state we'll be in
81
+ // after the callback runs (controlled value might become undefined)
82
+ const clearValue = useCallback(() => {
83
+ setUncontrolledValue(undefined);
84
+ onValueChange?.(undefined);
85
+ setSearching(false); // End searching mode on clear
86
+ }, [onValueChange]);
87
+ // Get ordered options
88
+ const getOrderedOptions = useCallback(() => {
89
+ const options = Array.from(optionsRef.current.values());
90
+ return options.sort((a, b) => {
91
+ if (!a.ref.current || !b.ref.current)
92
+ return 0;
93
+ const position = a.ref.current.compareDocumentPosition(b.ref.current);
94
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING)
95
+ return -1;
96
+ if (position & Node.DOCUMENT_POSITION_PRECEDING)
97
+ return 1;
98
+ return 0;
99
+ });
100
+ }, []);
101
+ // Memoized ordered options
102
+ const orderedOptions = useMemo(() => {
103
+ return getOrderedOptions();
104
+ // eslint-disable-next-line react-hooks/exhaustive-deps
105
+ }, [registryVersion]);
106
+ // Compute disabled indices for keyboard navigation
107
+ const disabledIndices = useMemo(() => orderedOptions.reduce((acc, opt, i) => {
108
+ if (opt.disabled)
109
+ acc.push(i);
110
+ return acc;
111
+ }, []), [orderedOptions]);
112
+ // Floating UI setup
113
+ const { refs, floatingStyles, context } = useFloating({
114
+ placement: 'bottom-start',
115
+ open,
116
+ onOpenChange: setOpen,
117
+ middleware: [
118
+ offset(4),
119
+ flip({ padding: 8 }),
120
+ shift({ padding: 8 }),
121
+ sizeMiddleware({
122
+ apply({ availableHeight, rects, elements }) {
123
+ Object.assign(elements.floating.style, {
124
+ maxHeight: `${Math.min(availableHeight - 16, 300)}px`,
125
+ minWidth: `${rects.reference.width}px`,
126
+ });
127
+ },
128
+ padding: 8,
129
+ }),
130
+ ],
131
+ whileElementsMounted: autoUpdate,
132
+ });
133
+ // Floating UI interactions
134
+ const dismiss = useDismiss(context);
135
+ const role = useRole(context, { role: 'listbox' });
136
+ const listNavigation = useListNavigation(context, {
137
+ listRef,
138
+ activeIndex: activeIndex >= 0 ? activeIndex : null,
139
+ onNavigate: (index) => setActiveIndex(index ?? -1),
140
+ loop: true,
141
+ virtual: true,
142
+ disabledIndices,
143
+ });
144
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
145
+ dismiss,
146
+ role,
147
+ listNavigation,
148
+ ]);
149
+ // Register option
150
+ const registerOption = useCallback((option) => {
151
+ optionsRef.current.set(toKey(option.value), option);
152
+ setRegistryVersion((v) => v + 1);
153
+ }, []);
154
+ const unregisterOption = useCallback((optionValue) => {
155
+ optionsRef.current.delete(toKey(optionValue));
156
+ setRegistryVersion((v) => v + 1);
157
+ }, []);
158
+ // Active option ID for aria-activedescendant (hash-based for stability during filtering)
159
+ const activeOptionId = activeIndex >= 0 && orderedOptions[activeIndex]
160
+ ? `${listboxId}-opt-${hashForId(toKey(orderedOptions[activeIndex].value))}`
161
+ : undefined;
162
+ // Update activeIndex when options change (filtering)
163
+ // Stable logic: keep highlighted if still present, else first enabled, else -1
164
+ useEffect(() => {
165
+ if (!open)
166
+ return;
167
+ const prevValue = prevHighlightedValueRef.current;
168
+ const enabledOptions = orderedOptions.filter((opt) => !opt.disabled);
169
+ if (enabledOptions.length === 0) {
170
+ setActiveIndex(-1);
171
+ prevHighlightedValueRef.current = null;
172
+ return;
173
+ }
174
+ // If previously highlighted value still exists and is enabled, keep it
175
+ if (prevValue !== null) {
176
+ const prevIndex = orderedOptions.findIndex((opt) => toKey(opt.value) === toKey(prevValue) && !opt.disabled);
177
+ if (prevIndex >= 0) {
178
+ setActiveIndex(prevIndex);
179
+ return;
180
+ }
181
+ }
182
+ // Otherwise, set to first enabled option
183
+ const firstEnabledIndex = orderedOptions.findIndex((opt) => !opt.disabled);
184
+ setActiveIndex(firstEnabledIndex >= 0 ? firstEnabledIndex : -1);
185
+ if (firstEnabledIndex >= 0) {
186
+ prevHighlightedValueRef.current = orderedOptions[firstEnabledIndex].value;
187
+ }
188
+ }, [open, orderedOptions]);
189
+ // Track highlighted value when activeIndex changes
190
+ useEffect(() => {
191
+ if (activeIndex >= 0 && orderedOptions[activeIndex]) {
192
+ prevHighlightedValueRef.current = orderedOptions[activeIndex].value;
193
+ }
194
+ }, [activeIndex, orderedOptions]);
195
+ // Reset activeIndex when closing
196
+ useEffect(() => {
197
+ if (!open) {
198
+ setActiveIndex(-1);
199
+ prevHighlightedValueRef.current = null;
200
+ }
201
+ }, [open]);
202
+ // Scroll active option into view
203
+ useEffect(() => {
204
+ if (open && activeIndex >= 0 && listRef.current[activeIndex]) {
205
+ listRef.current[activeIndex]?.scrollIntoView({ block: 'nearest' });
206
+ }
207
+ }, [open, activeIndex]);
208
+ // Handle input change (user typing)
209
+ const handleInputChange = useCallback((e) => {
210
+ const newValue = e.target.value;
211
+ setInputValue(newValue);
212
+ // Clear selection when input is emptied (clearValue sets searching=false)
213
+ if (newValue === '' && value !== undefined) {
214
+ clearValue();
215
+ }
216
+ else {
217
+ // User is actively typing - enter searching mode
218
+ setSearching(true);
219
+ }
220
+ if (!open) {
221
+ setOpen(true);
222
+ }
223
+ }, [setInputValue, open, setOpen, value, clearValue]);
224
+ // Handle input focus
225
+ const handleInputFocus = useCallback(() => {
226
+ if (openOnFocus && !disabled && !open) {
227
+ setOpen(true);
228
+ }
229
+ }, [openOnFocus, disabled, open, setOpen]);
230
+ // Handle input keydown
231
+ const handleInputKeyDown = useCallback((e) => {
232
+ if (disabled)
233
+ return;
234
+ switch (e.key) {
235
+ case 'ArrowDown':
236
+ e.preventDefault();
237
+ if (!open) {
238
+ setOpen(true);
239
+ }
240
+ break;
241
+ case 'ArrowUp':
242
+ e.preventDefault();
243
+ if (!open) {
244
+ setOpen(true);
245
+ }
246
+ break;
247
+ case 'Enter':
248
+ if (open && activeIndex >= 0) {
249
+ e.preventDefault();
250
+ const option = orderedOptions[activeIndex];
251
+ if (option && !option.disabled) {
252
+ selectOption(option.value);
253
+ }
254
+ }
255
+ break;
256
+ case 'Escape':
257
+ if (open) {
258
+ e.preventDefault();
259
+ setSearching(false); // End searching mode on escape
260
+ setOpen(false);
261
+ }
262
+ break;
263
+ case 'Tab':
264
+ if (open) {
265
+ setSearching(false); // End searching mode on tab away
266
+ setOpen(false);
267
+ }
268
+ break;
269
+ }
270
+ }, [disabled, open, setOpen, activeIndex, orderedOptions, selectOption]);
271
+ // Handle input blur with blur guard
272
+ const handleInputBlur = useCallback((e) => {
273
+ if (!open)
274
+ return;
275
+ const relatedTarget = e.relatedTarget;
276
+ // Focus left document
277
+ if (relatedTarget === null) {
278
+ setSearching(false); // End searching mode on blur
279
+ setOpen(false);
280
+ return;
281
+ }
282
+ // Check if focus moved to floating content
283
+ const floating = refs.floating.current;
284
+ if (floating && relatedTarget && floating.contains(relatedTarget)) {
285
+ return;
286
+ }
287
+ setSearching(false); // End searching mode on blur
288
+ setOpen(false);
289
+ }, [open, setOpen, refs.floating]);
290
+ // Handle clear button - clears both input text and selected value
291
+ const handleClear = useCallback((e) => {
292
+ e.preventDefault(); // Keep focus in input
293
+ setInputValue('');
294
+ clearValue();
295
+ inputRef.current?.focus();
296
+ }, [setInputValue, clearValue]);
297
+ // ==========================================================================
298
+ // Split Context Values
299
+ // ==========================================================================
300
+ const actionsValue = useMemo(() => ({
301
+ disabled,
302
+ placeholder,
303
+ openOnFocus,
304
+ inputId,
305
+ listboxId,
306
+ ariaLabel,
307
+ ariaLabelledBy,
308
+ setOpen,
309
+ setInputValue,
310
+ selectOption,
311
+ registerOption,
312
+ unregisterOption,
313
+ refs,
314
+ inputRef,
315
+ listRef,
316
+ getReferenceProps,
317
+ getFloatingProps,
318
+ getItemProps,
319
+ }), [
320
+ disabled,
321
+ placeholder,
322
+ openOnFocus,
323
+ inputId,
324
+ listboxId,
325
+ ariaLabel,
326
+ ariaLabelledBy,
327
+ setOpen,
328
+ setInputValue,
329
+ selectOption,
330
+ registerOption,
331
+ unregisterOption,
332
+ refs,
333
+ getReferenceProps,
334
+ getFloatingProps,
335
+ getItemProps,
336
+ ]);
337
+ const stateValue = useMemo(() => ({
338
+ open,
339
+ value,
340
+ inputValue,
341
+ query,
342
+ activeIndex,
343
+ activeOptionId,
344
+ orderedOptions,
345
+ floatingStyles,
346
+ floatingContext: context,
347
+ }), [
348
+ open,
349
+ value,
350
+ inputValue,
351
+ query,
352
+ activeIndex,
353
+ activeOptionId,
354
+ orderedOptions,
355
+ floatingStyles,
356
+ context,
357
+ ]);
358
+ // Get reference props for input (onChange handled separately to ensure it runs)
359
+ const referenceProps = getReferenceProps({
360
+ onFocus: handleInputFocus,
361
+ onBlur: handleInputBlur,
362
+ onKeyDown: handleInputKeyDown,
363
+ });
364
+ return (_jsx(ComboboxActionsContext.Provider, { value: actionsValue, children: _jsx(ComboboxStateContext.Provider, { value: stateValue, children: _jsxs("div", { className: "tui-combobox", children: [_jsxs("div", { className: "tui-combobox__input-wrapper", children: [_jsx("input", { ref: (node) => {
365
+ inputRef.current = node;
366
+ refs.setReference(node);
367
+ }, type: "text", id: inputId, className: cx('tui-combobox__input', size !== 'md' && `is-size-${size}`, inputClassName), role: "combobox", "aria-expanded": open, "aria-controls": listboxId, "aria-autocomplete": "list", "aria-activedescendant": activeOptionId, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, disabled: disabled, placeholder: placeholder, value: inputValue, autoComplete: "off", ...referenceProps, onChange: handleInputChange }), inputValue && !disabled && (_jsx("span", { className: "tui-combobox__clear", onPointerDown: handleClear, "aria-hidden": "true", children: _jsx(Icon, { name: "system/close", size: "sm" }) })), _jsx("span", { className: "tui-combobox__icon", "aria-hidden": "true", children: _jsx(Icon, { name: "system/chevron-down", size: "sm" }) })] }), children] }) }) }));
368
+ }
369
+ ComboboxRoot.displayName = 'Combobox';
370
+ // =============================================================================
371
+ // Combobox.Content
372
+ // =============================================================================
373
+ function ComboboxContentComponent({ className, children }) {
374
+ const { open, listboxId, inputId, refs, floatingStyles, getFloatingProps, listRef, activeIndex, orderedOptions, } = useComboboxContext();
375
+ const portalRoot = getPortalRootFor(refs.reference.current);
376
+ const contentContext = useMemo(() => ({
377
+ listRef,
378
+ activeIndex,
379
+ orderedOptions,
380
+ }), [listRef, activeIndex, orderedOptions]);
381
+ // Always render for option registration
382
+ return (_jsxs(_Fragment, { children: [!open && (_jsx("div", { style: { display: 'none' }, "aria-hidden": "true", children: _jsx(ComboboxContentContext.Provider, { value: contentContext, children: children }) })), open && (_jsx(FloatingPortal, { root: portalRoot, children: _jsx("div", { ref: refs.setFloating, id: listboxId, role: "listbox", "aria-labelledby": inputId, className: cx('tui-combobox__content', className), style: {
383
+ ...floatingStyles,
384
+ minWidth: refs.reference.current?.offsetWidth,
385
+ pointerEvents: 'auto',
386
+ }, tabIndex: -1, ...getFloatingProps(), children: _jsx(ComboboxContentContext.Provider, { value: contentContext, children: children }) }) }))] }));
387
+ }
388
+ ComboboxContentComponent.displayName = 'Combobox.Content';
389
+ // =============================================================================
390
+ // Combobox.Option
391
+ // =============================================================================
392
+ function ComboboxOptionComponent({ value: optionValue, disabled = false, textValue: explicitTextValue, className, children, }) {
393
+ const { value: selectedValue, selectOption, listboxId, registerOption, unregisterOption, getItemProps, } = useComboboxContext();
394
+ const { listRef, activeIndex, orderedOptions } = useComboboxContentContext();
395
+ const ref = useRef(null);
396
+ const textValue = explicitTextValue ?? (typeof children === 'string' ? children : '');
397
+ // Warn in dev if textValue couldn't be derived
398
+ useEffect(() => {
399
+ if (import.meta.env.DEV && !textValue) {
400
+ console.warn(`Combobox.Option with value="${optionValue}" has no textValue. Provide textValue prop when children is not a string.`);
401
+ }
402
+ }, [textValue, optionValue]);
403
+ // Register option
404
+ useLayoutEffect(() => {
405
+ registerOption({ value: optionValue, ref, disabled, textValue });
406
+ return () => unregisterOption(optionValue);
407
+ }, [optionValue, disabled, textValue, registerOption, unregisterOption]);
408
+ // Find this option's index
409
+ const index = orderedOptions.findIndex((opt) => toKey(opt.value) === toKey(optionValue));
410
+ // Assign ref to listRef for navigation
411
+ useEffect(() => {
412
+ if (index >= 0) {
413
+ listRef.current[index] = ref.current;
414
+ }
415
+ }, [index, listRef]);
416
+ const isSelected = selectedValue !== undefined && toKey(selectedValue) === toKey(optionValue);
417
+ const isHighlighted = activeIndex === index;
418
+ const handleClick = useCallback(() => {
419
+ if (disabled)
420
+ return;
421
+ selectOption(optionValue);
422
+ }, [disabled, optionValue, selectOption]);
423
+ // Prevent blur on input when clicking option (must run before any Floating UI handler)
424
+ const handlePointerDown = useCallback((e) => {
425
+ e.preventDefault();
426
+ }, []);
427
+ // Hash-based ID for stability during filtering (aria-activedescendant references this)
428
+ // Use toKey() before hashing to differentiate number 1 from string "1"
429
+ const optionId = `${listboxId}-opt-${hashForId(toKey(optionValue))}`;
430
+ // Get item props and compose our pointerdown handler with any from Floating UI
431
+ const itemProps = getItemProps({
432
+ onClick: handleClick,
433
+ });
434
+ return (_jsxs("div", { ref: ref, id: optionId, role: "option", className: cx('tui-combobox__option', className), "aria-selected": isSelected, "aria-disabled": disabled || undefined, "data-highlighted": isHighlighted || undefined, "data-disabled": disabled || undefined, ...itemProps, onPointerDown: composeEventHandlers(handlePointerDown, itemProps.onPointerDown), children: [_jsx("span", { className: "tui-combobox__option-content", children: children }), isSelected && (_jsx(Icon, { name: "system/check", size: "sm", className: "tui-combobox__option-check", "aria-hidden": "true" }))] }));
435
+ }
436
+ ComboboxOptionComponent.displayName = 'Combobox.Option';
437
+ // =============================================================================
438
+ // Combobox.Group
439
+ // =============================================================================
440
+ function ComboboxGroupComponent({ className, children }) {
441
+ const groupId = useId();
442
+ return (_jsx("div", { role: "group", "aria-labelledby": `${groupId}-label`, className: cx('tui-combobox__group', className), children: _jsx(ComboboxGroupContext.Provider, { value: { groupId }, children: children }) }));
443
+ }
444
+ ComboboxGroupComponent.displayName = 'Combobox.Group';
445
+ const ComboboxGroupContext = React.createContext(null);
446
+ // =============================================================================
447
+ // Combobox.Label
448
+ // =============================================================================
449
+ function ComboboxLabelComponent({ className, children }) {
450
+ const groupContext = React.useContext(ComboboxGroupContext);
451
+ return (_jsx("div", { id: groupContext ? `${groupContext.groupId}-label` : undefined, className: cx('tui-combobox__label', className), children: children }));
452
+ }
453
+ ComboboxLabelComponent.displayName = 'Combobox.Label';
454
+ export const Combobox = ComboboxRoot;
455
+ Combobox.Content = ComboboxContentComponent;
456
+ Combobox.Option = ComboboxOptionComponent;
457
+ Combobox.Group = ComboboxGroupComponent;
458
+ Combobox.Label = ComboboxLabelComponent;
459
+ // Named exports
460
+ export const ComboboxContent = ComboboxContentComponent;
461
+ export const ComboboxOption = ComboboxOptionComponent;
462
+ export const ComboboxGroup = ComboboxGroupComponent;
463
+ export const ComboboxLabel = ComboboxLabelComponent;
464
+ // Hook for advanced use cases
465
+ // eslint-disable-next-line react-refresh/only-export-components
466
+ export { useComboboxContext as useCombobox } from './ComboboxContext.js';
@@ -0,0 +1,8 @@
1
+ import type { ComboboxActionsContextValue, ComboboxStateContextValue, ComboboxContentContextValue } from './types';
2
+ export declare const ComboboxActionsContext: import("react").Context<ComboboxActionsContextValue | null>;
3
+ export declare const ComboboxStateContext: import("react").Context<ComboboxStateContextValue | null>;
4
+ export declare function useComboboxActions(): ComboboxActionsContextValue;
5
+ export declare function useComboboxState(): ComboboxStateContextValue;
6
+ export declare function useComboboxContext(): ComboboxActionsContextValue & ComboboxStateContextValue;
7
+ export declare const ComboboxContentContext: import("react").Context<ComboboxContentContextValue | null>;
8
+ export declare function useComboboxContentContext(): ComboboxContentContextValue;
@@ -0,0 +1,36 @@
1
+ import { createContext, useContext } from 'react';
2
+ // =============================================================================
3
+ // Split Contexts for Performance
4
+ // =============================================================================
5
+ export const ComboboxActionsContext = createContext(null);
6
+ export const ComboboxStateContext = createContext(null);
7
+ export function useComboboxActions() {
8
+ const context = useContext(ComboboxActionsContext);
9
+ if (!context) {
10
+ throw new Error('Combobox components must be used within a Combobox');
11
+ }
12
+ return context;
13
+ }
14
+ export function useComboboxState() {
15
+ const context = useContext(ComboboxStateContext);
16
+ if (!context) {
17
+ throw new Error('Combobox components must be used within a Combobox');
18
+ }
19
+ return context;
20
+ }
21
+ export function useComboboxContext() {
22
+ const actions = useComboboxActions();
23
+ const state = useComboboxState();
24
+ return { ...actions, ...state };
25
+ }
26
+ // =============================================================================
27
+ // Content Context (for Option registration)
28
+ // =============================================================================
29
+ export const ComboboxContentContext = createContext(null);
30
+ export function useComboboxContentContext() {
31
+ const context = useContext(ComboboxContentContext);
32
+ if (!context) {
33
+ throw new Error('Combobox.Option must be used within Combobox.Content');
34
+ }
35
+ return context;
36
+ }
@@ -0,0 +1,2 @@
1
+ export { Combobox, ComboboxContent, ComboboxOption, ComboboxGroup, ComboboxLabel, useCombobox, } from './Combobox';
2
+ export type { ComboboxProps, ComboboxContentProps, ComboboxOptionProps, ComboboxGroupProps, ComboboxLabelProps, FilterMode as ComboboxFilterMode, RegisteredOption as ComboboxRegisteredOption, } from './types';
@@ -0,0 +1 @@
1
+ export { Combobox, ComboboxContent, ComboboxOption, ComboboxGroup, ComboboxLabel, useCombobox, } from './Combobox.js';