@proyecto-viviana/solidaria-components 0.2.9 → 0.3.1

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 (222) hide show
  1. package/README.md +39 -272
  2. package/dist/ActionBar.d.ts +21 -13
  3. package/dist/ActionBar.d.ts.map +1 -1
  4. package/dist/ActionGroup.d.ts +8 -8
  5. package/dist/ActionGroup.d.ts.map +1 -1
  6. package/dist/Alert.d.ts +5 -5
  7. package/dist/Alert.d.ts.map +1 -1
  8. package/dist/Autocomplete.d.ts +5 -5
  9. package/dist/Autocomplete.d.ts.map +1 -1
  10. package/dist/Breadcrumbs.d.ts +18 -7
  11. package/dist/Breadcrumbs.d.ts.map +1 -1
  12. package/dist/Button.d.ts +24 -5
  13. package/dist/Button.d.ts.map +1 -1
  14. package/dist/Calendar.d.ts +38 -7
  15. package/dist/Calendar.d.ts.map +1 -1
  16. package/dist/Checkbox.d.ts +32 -7
  17. package/dist/Checkbox.d.ts.map +1 -1
  18. package/dist/Collection.d.ts +19 -14
  19. package/dist/Collection.d.ts.map +1 -1
  20. package/dist/Color.d.ts +103 -14
  21. package/dist/Color.d.ts.map +1 -1
  22. package/dist/ColorEditor.d.ts +6 -6
  23. package/dist/ColorEditor.d.ts.map +1 -1
  24. package/dist/ComboBox.d.ts +85 -19
  25. package/dist/ComboBox.d.ts.map +1 -1
  26. package/dist/ContextualHelpTrigger.d.ts +2 -2
  27. package/dist/ContextualHelpTrigger.d.ts.map +1 -1
  28. package/dist/DateField.d.ts +8 -6
  29. package/dist/DateField.d.ts.map +1 -1
  30. package/dist/DatePicker.d.ts +53 -22
  31. package/dist/DatePicker.d.ts.map +1 -1
  32. package/dist/DateRangePickerContext.d.ts +30 -0
  33. package/dist/DateRangePickerContext.d.ts.map +1 -0
  34. package/dist/Dialog.d.ts +5 -5
  35. package/dist/Dialog.d.ts.map +1 -1
  36. package/dist/Disclosure.d.ts +23 -5
  37. package/dist/Disclosure.d.ts.map +1 -1
  38. package/dist/DragAndDrop.d.ts +6 -6
  39. package/dist/DragAndDrop.d.ts.map +1 -1
  40. package/dist/DragPreview.d.ts +2 -2
  41. package/dist/DragPreview.d.ts.map +1 -1
  42. package/dist/DropZone.d.ts +4 -4
  43. package/dist/DropZone.d.ts.map +1 -1
  44. package/dist/FieldError.d.ts +9 -5
  45. package/dist/FieldError.d.ts.map +1 -1
  46. package/dist/FileTrigger.d.ts +3 -3
  47. package/dist/FileTrigger.d.ts.map +1 -1
  48. package/dist/Focusable.d.ts +2 -2
  49. package/dist/Focusable.d.ts.map +1 -1
  50. package/dist/Form.d.ts +18 -4
  51. package/dist/Form.d.ts.map +1 -1
  52. package/dist/GridList.d.ts +32 -12
  53. package/dist/GridList.d.ts.map +1 -1
  54. package/dist/HiddenDateInput.d.ts +26 -0
  55. package/dist/HiddenDateInput.d.ts.map +1 -0
  56. package/dist/HiddenTimeInput.d.ts +25 -0
  57. package/dist/HiddenTimeInput.d.ts.map +1 -0
  58. package/dist/Icon.d.ts +5 -5
  59. package/dist/Icon.d.ts.map +1 -1
  60. package/dist/Keyboard.d.ts +1 -1
  61. package/dist/Landmark.d.ts +3 -3
  62. package/dist/Landmark.d.ts.map +1 -1
  63. package/dist/Link.d.ts +10 -4
  64. package/dist/Link.d.ts.map +1 -1
  65. package/dist/ListBox.d.ts +32 -12
  66. package/dist/ListBox.d.ts.map +1 -1
  67. package/dist/ListDropTargetDelegate.d.ts +6 -6
  68. package/dist/ListDropTargetDelegate.d.ts.map +1 -1
  69. package/dist/Menu.d.ts +65 -14
  70. package/dist/Menu.d.ts.map +1 -1
  71. package/dist/Meter.d.ts +3 -3
  72. package/dist/Meter.d.ts.map +1 -1
  73. package/dist/Modal.d.ts +5 -5
  74. package/dist/Modal.d.ts.map +1 -1
  75. package/dist/NumberField.d.ts +8 -12
  76. package/dist/NumberField.d.ts.map +1 -1
  77. package/dist/Popover.d.ts +28 -5
  78. package/dist/Popover.d.ts.map +1 -1
  79. package/dist/Pressable.d.ts +2 -2
  80. package/dist/Pressable.d.ts.map +1 -1
  81. package/dist/ProgressBar.d.ts +5 -3
  82. package/dist/ProgressBar.d.ts.map +1 -1
  83. package/dist/RadioGroup.d.ts +43 -9
  84. package/dist/RadioGroup.d.ts.map +1 -1
  85. package/dist/RangeCalendar.d.ts +34 -7
  86. package/dist/RangeCalendar.d.ts.map +1 -1
  87. package/dist/RouterProvider.d.ts +2 -2
  88. package/dist/RouterProvider.d.ts.map +1 -1
  89. package/dist/SearchField.d.ts +23 -20
  90. package/dist/SearchField.d.ts.map +1 -1
  91. package/dist/Select.d.ts +41 -11
  92. package/dist/Select.d.ts.map +1 -1
  93. package/dist/SelectionIndicator.d.ts +3 -3
  94. package/dist/SelectionIndicator.d.ts.map +1 -1
  95. package/dist/Separator.d.ts +9 -3
  96. package/dist/Separator.d.ts.map +1 -1
  97. package/dist/SharedElementTransition.d.ts +6 -4
  98. package/dist/SharedElementTransition.d.ts.map +1 -1
  99. package/dist/Slider.d.ts +12 -8
  100. package/dist/Slider.d.ts.map +1 -1
  101. package/dist/StepList.d.ts +90 -0
  102. package/dist/StepList.d.ts.map +1 -0
  103. package/dist/Switch.d.ts +11 -5
  104. package/dist/Switch.d.ts.map +1 -1
  105. package/dist/Table.d.ts +187 -23
  106. package/dist/Table.d.ts.map +1 -1
  107. package/dist/Tabs.d.ts +45 -9
  108. package/dist/Tabs.d.ts.map +1 -1
  109. package/dist/TagGroup.d.ts +12 -10
  110. package/dist/TagGroup.d.ts.map +1 -1
  111. package/dist/Text.d.ts +2 -2
  112. package/dist/TextField.d.ts +15 -11
  113. package/dist/TextField.d.ts.map +1 -1
  114. package/dist/TimeField.d.ts +6 -6
  115. package/dist/TimeField.d.ts.map +1 -1
  116. package/dist/Toast.d.ts +29 -14
  117. package/dist/Toast.d.ts.map +1 -1
  118. package/dist/ToggleButton.d.ts +11 -5
  119. package/dist/ToggleButton.d.ts.map +1 -1
  120. package/dist/ToggleButtonGroup.d.ts +7 -7
  121. package/dist/ToggleButtonGroup.d.ts.map +1 -1
  122. package/dist/Toolbar.d.ts +7 -3
  123. package/dist/Toolbar.d.ts.map +1 -1
  124. package/dist/Tooltip.d.ts +50 -8
  125. package/dist/Tooltip.d.ts.map +1 -1
  126. package/dist/Tree.d.ts +66 -17
  127. package/dist/Tree.d.ts.map +1 -1
  128. package/dist/Virtualizer.d.ts +12 -12
  129. package/dist/Virtualizer.d.ts.map +1 -1
  130. package/dist/VirtualizerLayouts.d.ts +2 -2
  131. package/dist/VirtualizerLayouts.d.ts.map +1 -1
  132. package/dist/VisuallyHidden.d.ts +1 -1
  133. package/dist/VisuallyHidden.d.ts.map +1 -1
  134. package/dist/contexts.d.ts +5 -1
  135. package/dist/contexts.d.ts.map +1 -1
  136. package/dist/index.d.ts +73 -71
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +23253 -18564
  139. package/dist/index.js.map +1 -1
  140. package/dist/index.jsx +18116 -0
  141. package/dist/index.jsx.map +1 -0
  142. package/dist/useDragAndDrop.d.ts +13 -13
  143. package/dist/useDragAndDrop.d.ts.map +1 -1
  144. package/dist/utils.d.ts +2 -2
  145. package/dist/utils.d.ts.map +1 -1
  146. package/dist/virtualizer/Layout.d.ts +1 -1
  147. package/dist/virtualizer/Layout.d.ts.map +1 -1
  148. package/package.json +31 -32
  149. package/src/ActionBar.tsx +75 -72
  150. package/src/ActionGroup.tsx +53 -61
  151. package/src/Alert.tsx +17 -42
  152. package/src/Autocomplete.tsx +39 -44
  153. package/src/Breadcrumbs.tsx +149 -80
  154. package/src/Button.tsx +267 -70
  155. package/src/Calendar.tsx +218 -138
  156. package/src/Checkbox.tsx +413 -121
  157. package/src/Collection.tsx +67 -58
  158. package/src/Color.tsx +803 -380
  159. package/src/ColorEditor.tsx +131 -149
  160. package/src/ComboBox.tsx +414 -249
  161. package/src/ContextualHelpTrigger.tsx +86 -74
  162. package/src/DateField.tsx +185 -91
  163. package/src/DatePicker.tsx +524 -213
  164. package/src/DateRangePickerContext.tsx +44 -0
  165. package/src/Dialog.tsx +156 -118
  166. package/src/Disclosure.tsx +127 -80
  167. package/src/DragAndDrop.tsx +60 -54
  168. package/src/DragPreview.tsx +13 -11
  169. package/src/DropZone.tsx +42 -22
  170. package/src/FieldError.tsx +45 -23
  171. package/src/FileTrigger.tsx +19 -19
  172. package/src/Focusable.tsx +21 -24
  173. package/src/Form.tsx +71 -16
  174. package/src/GridList.tsx +273 -197
  175. package/src/HiddenDateInput.tsx +153 -0
  176. package/src/HiddenTimeInput.tsx +133 -0
  177. package/src/Icon.tsx +22 -43
  178. package/src/Keyboard.tsx +3 -3
  179. package/src/Landmark.tsx +37 -63
  180. package/src/Link.tsx +125 -75
  181. package/src/ListBox.tsx +332 -233
  182. package/src/ListDropTargetDelegate.ts +81 -80
  183. package/src/Menu.tsx +1023 -274
  184. package/src/Meter.tsx +38 -56
  185. package/src/Modal.tsx +251 -176
  186. package/src/NumberField.tsx +139 -143
  187. package/src/Popover.tsx +396 -234
  188. package/src/Pressable.tsx +21 -21
  189. package/src/ProgressBar.tsx +48 -57
  190. package/src/RadioGroup.tsx +524 -122
  191. package/src/RangeCalendar.tsx +157 -90
  192. package/src/RouterProvider.tsx +30 -47
  193. package/src/SearchField.tsx +362 -143
  194. package/src/Select.tsx +656 -233
  195. package/src/SelectionIndicator.tsx +18 -15
  196. package/src/Separator.tsx +47 -49
  197. package/src/SharedElementTransition.tsx +103 -97
  198. package/src/Slider.tsx +138 -98
  199. package/src/StepList.tsx +272 -0
  200. package/src/Switch.tsx +93 -46
  201. package/src/Table.tsx +1308 -342
  202. package/src/Tabs.tsx +324 -103
  203. package/src/TagGroup.tsx +139 -126
  204. package/src/Text.tsx +3 -3
  205. package/src/TextField.tsx +389 -79
  206. package/src/TimeField.tsx +136 -76
  207. package/src/Toast.tsx +216 -158
  208. package/src/ToggleButton.tsx +47 -37
  209. package/src/ToggleButtonGroup.tsx +39 -34
  210. package/src/Toolbar.tsx +54 -69
  211. package/src/Tooltip.tsx +387 -119
  212. package/src/Tree.tsx +651 -368
  213. package/src/Virtualizer.tsx +208 -180
  214. package/src/VirtualizerLayouts.ts +45 -30
  215. package/src/VisuallyHidden.tsx +19 -19
  216. package/src/contexts.ts +29 -37
  217. package/src/index.ts +110 -195
  218. package/src/useDragAndDrop.ts +87 -71
  219. package/src/utils.tsx +49 -60
  220. package/src/virtualizer/Layout.ts +14 -22
  221. package/dist/index.ssr.js +0 -16996
  222. package/dist/index.ssr.js.map +0 -1
package/src/ComboBox.tsx CHANGED
@@ -15,17 +15,19 @@ import {
15
15
  useContext,
16
16
  For,
17
17
  Show,
18
- } from 'solid-js';
18
+ } from "solid-js";
19
19
  import {
20
20
  createComboBox,
21
21
  createListBox,
22
22
  createOption,
23
+ getComboBoxData,
23
24
  createHover,
24
25
  createInteractOutside,
26
+ mergeProps,
25
27
  type AriaComboBoxProps,
26
28
  type AriaListBoxProps,
27
29
  type AriaOptionProps,
28
- } from '@proyecto-viviana/solidaria';
30
+ } from "@proyecto-viviana/solidaria";
29
31
  import {
30
32
  createComboBoxState,
31
33
  defaultContainsFilter,
@@ -34,7 +36,7 @@ import {
34
36
  type Key,
35
37
  type FilterFn,
36
38
  type MenuTriggerAction,
37
- } from '@proyecto-viviana/solid-stately';
39
+ } from "@proyecto-viviana/solid-stately";
38
40
  import {
39
41
  type RenderChildren,
40
42
  type ClassNameOrFunction,
@@ -42,15 +44,22 @@ import {
42
44
  type SlotProps,
43
45
  useRenderProps,
44
46
  filterDOMProps,
45
- } from './utils';
47
+ } from "./utils";
46
48
  import {
47
49
  SelectionIndicatorContext,
48
50
  type SelectionIndicatorContextValue,
49
- } from './SelectionIndicator';
51
+ } from "./SelectionIndicator";
50
52
 
51
- // ============================================
52
- // TYPES
53
- // ============================================
53
+ type RefLike<T> = ((el: T) => void) | { current?: T | null } | undefined;
54
+
55
+ function assignRef<T>(ref: RefLike<T>, el: T): void {
56
+ if (!ref) return;
57
+ if (typeof ref === "function") {
58
+ ref(el);
59
+ } else {
60
+ ref.current = el;
61
+ }
62
+ }
54
63
 
55
64
  export interface ComboBoxRenderProps {
56
65
  /** Whether the combobox listbox is open. */
@@ -65,17 +74,19 @@ export interface ComboBoxRenderProps {
65
74
  isRequired: boolean;
66
75
  /** Whether the combobox is invalid. */
67
76
  isInvalid: boolean;
77
+ /** Whether the combobox is read only. */
78
+ isReadOnly: boolean;
68
79
  /** Whether a value is selected. */
69
80
  isSelected: boolean;
70
81
  /** The current input value. */
71
82
  inputValue: string;
72
83
  }
73
84
 
74
- export interface ComboBoxProps<T>
75
- extends Omit<AriaComboBoxProps, 'children'>,
76
- SlotProps {
85
+ export interface ComboBoxProps<T> extends Omit<AriaComboBoxProps, "children">, SlotProps {
77
86
  /** The items to render in the combobox. */
78
- items: T[];
87
+ items?: T[];
88
+ /** The default items to render in the combobox when uncontrolled. */
89
+ defaultItems?: T[];
79
90
  /** Function to get the key from an item. */
80
91
  getKey?: (item: T) => Key;
81
92
  /** Function to get the text value from an item. */
@@ -84,12 +95,20 @@ export interface ComboBoxProps<T>
84
95
  getDisabled?: (item: T) => boolean;
85
96
  /** Keys of disabled items. */
86
97
  disabledKeys?: Iterable<Key>;
87
- /** The currently selected key (controlled). */
98
+ /** The selection mode for the combobox. */
99
+ selectionMode?: "single" | "multiple";
100
+ /** The currently selected key (controlled, single mode). */
88
101
  selectedKey?: Key | null;
89
- /** The default selected key (uncontrolled). */
102
+ /** The default selected key (uncontrolled, single mode). */
90
103
  defaultSelectedKey?: Key | null;
91
- /** Handler called when selection changes. */
104
+ /** The currently selected keys (controlled, multiple mode). */
105
+ selectedKeys?: Iterable<Key>;
106
+ /** The default selected keys (uncontrolled, multiple mode). */
107
+ defaultSelectedKeys?: Iterable<Key>;
108
+ /** Handler called when selection changes (single mode). */
92
109
  onSelectionChange?: (key: Key | null) => void;
110
+ /** Handler called when selection changes (multiple mode). */
111
+ onSelectionChangeMultiple?: (keys: Set<Key>) => void;
93
112
  /** The current input value (controlled). */
94
113
  inputValue?: string;
95
114
  /** The default input value (uncontrolled). */
@@ -109,7 +128,7 @@ export interface ComboBoxProps<T>
109
128
  /** Whether to allow an empty collection (show listbox even with no matches). */
110
129
  allowsEmptyCollection?: boolean;
111
130
  /** The trigger mechanism for the combobox menu. */
112
- menuTrigger?: 'focus' | 'input' | 'manual';
131
+ menuTrigger?: "focus" | "input" | "manual";
113
132
  /** The name of the combobox, used when submitting an HTML form. */
114
133
  name?: string;
115
134
  /**
@@ -119,13 +138,19 @@ export interface ComboBoxProps<T>
119
138
  *
120
139
  * When allowsCustomValue is true, formValue is forced to 'text'.
121
140
  */
122
- formValue?: 'key' | 'text';
141
+ formValue?: "key" | "text";
123
142
  /** The children of the component (compound components: ComboBoxInput, ComboBoxButton, ComboBoxListBox). */
124
- children: JSX.Element;
143
+ children: RenderChildren<ComboBoxRenderProps>;
125
144
  /** The CSS className for the element. */
126
145
  class?: ClassNameOrFunction<ComboBoxRenderProps>;
127
146
  /** The inline style for the element. */
128
147
  style?: StyleOrFunction<ComboBoxRenderProps>;
148
+ /** Ref for the root combobox element. */
149
+ ref?: RefLike<HTMLDivElement>;
150
+ /** Internal alias for libraries that wrap ComboBox and need a root ref. */
151
+ rootRef?: RefLike<HTMLDivElement>;
152
+ /** Slot definitions provided through ComboBoxContext. */
153
+ slots?: Record<string, Partial<ComboBoxProps<T>>>;
129
154
  }
130
155
 
131
156
  export interface ComboBoxInputRenderProps {
@@ -195,12 +220,16 @@ export interface ComboBoxButtonRenderProps {
195
220
  export interface ComboBoxValueRenderProps {
196
221
  textValue: string;
197
222
  isPlaceholder: boolean;
223
+ selectedItems: unknown[];
224
+ selectedText: string;
225
+ state: ComboBoxState<unknown>;
198
226
  }
199
227
 
200
228
  export interface ComboBoxValueProps extends SlotProps {
201
229
  children?: RenderChildren<ComboBoxValueRenderProps>;
202
230
  class?: ClassNameOrFunction<ComboBoxValueRenderProps>;
203
231
  style?: StyleOrFunction<ComboBoxValueRenderProps>;
232
+ placeholder?: JSX.Element;
204
233
  }
205
234
 
206
235
  export interface ComboBoxButtonProps extends SlotProps {
@@ -242,8 +271,7 @@ export interface ComboBoxOptionRenderProps {
242
271
  }
243
272
 
244
273
  export interface ComboBoxOptionProps<T>
245
- extends Omit<AriaOptionProps, 'children' | 'key'>,
246
- SlotProps {
274
+ extends Omit<AriaOptionProps, "children" | "key">, SlotProps {
247
275
  /** The unique key for the option. */
248
276
  id: Key;
249
277
  /** The item value. */
@@ -256,20 +284,18 @@ export interface ComboBoxOptionProps<T>
256
284
  style?: StyleOrFunction<ComboBoxOptionRenderProps>;
257
285
  /** The text value of the option (for typeahead). */
258
286
  textValue?: string;
287
+ /** Handler called when the option is activated. */
288
+ onAction?: () => void;
259
289
  }
260
290
 
261
- // ============================================
262
- // CONTEXT
263
- // ============================================
264
-
265
291
  interface ComboBoxContextValue<T> {
266
292
  state: ComboBoxState<T>;
267
- inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
268
- buttonProps: JSX.HTMLAttributes<HTMLElement>;
269
- listBoxProps: JSX.HTMLAttributes<HTMLElement>;
270
- labelProps: JSX.HTMLAttributes<HTMLElement>;
271
- descriptionProps: JSX.HTMLAttributes<HTMLElement>;
272
- errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
293
+ inputProps: () => JSX.InputHTMLAttributes<HTMLInputElement>;
294
+ buttonProps: () => JSX.HTMLAttributes<HTMLElement>;
295
+ listBoxProps: () => JSX.HTMLAttributes<HTMLElement>;
296
+ labelProps: () => JSX.HTMLAttributes<HTMLElement>;
297
+ descriptionProps: () => JSX.HTMLAttributes<HTMLElement>;
298
+ errorMessageProps: () => JSX.HTMLAttributes<HTMLElement>;
273
299
  isOpen: Accessor<boolean>;
274
300
  isFocused: Accessor<boolean>;
275
301
  isFocusVisible: Accessor<boolean>;
@@ -278,59 +304,70 @@ interface ComboBoxContextValue<T> {
278
304
  setInputRef: (el: HTMLInputElement | null) => void;
279
305
  buttonRef: () => HTMLElement | null;
280
306
  setButtonRef: (el: HTMLElement | null) => void;
307
+ triggerRef: () => HTMLElement | null;
308
+ setTriggerRef: (el: HTMLElement | null) => void;
281
309
  listBoxRef: () => HTMLElement | null;
282
310
  setListBoxRef: (el: HTMLElement | null) => void;
311
+ slots?: Record<string, Partial<ComboBoxProps<T>>>;
283
312
  }
284
313
 
285
314
  export const ComboBoxContext = createContext<ComboBoxContextValue<unknown> | null>(null);
286
315
  export const ComboBoxStateContext = createContext<ComboBoxState<unknown> | null>(null);
287
316
  export const ComboBoxValueContext = ComboBoxContext;
288
317
 
289
- // ============================================
290
- // COMPONENTS
291
- // ============================================
292
-
293
318
  /**
294
319
  * A combobox combines a text input with a listbox, allowing users to filter a list of options.
295
320
  */
296
321
  export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
322
+ const parentContext = useContext(ComboBoxContext) as ComboBoxContextValue<T> | null;
323
+ const contextSlotProps = parentContext?.slots?.[props.slot ?? "default"];
324
+ const mergedComboBoxProps = contextSlotProps
325
+ ? (mergeProps(contextSlotProps, props) as ComboBoxProps<T>)
326
+ : props;
297
327
  const [local, stateProps, ariaProps] = splitProps(
298
- props,
299
- ['class', 'style', 'slot', 'children'],
328
+ mergedComboBoxProps,
329
+ ["class", "style", "slot", "children", "ref", "rootRef", "slots"],
300
330
  [
301
- 'items',
302
- 'getKey',
303
- 'getTextValue',
304
- 'getDisabled',
305
- 'disabledKeys',
306
- 'selectedKey',
307
- 'defaultSelectedKey',
308
- 'onSelectionChange',
309
- 'inputValue',
310
- 'defaultInputValue',
311
- 'onInputChange',
312
- 'isOpen',
313
- 'defaultOpen',
314
- 'onOpenChange',
315
- 'defaultFilter',
316
- 'allowsCustomValue',
317
- 'allowsEmptyCollection',
318
- 'menuTrigger',
319
- 'name',
320
- 'formValue',
321
- ]
331
+ "items",
332
+ "defaultItems",
333
+ "getKey",
334
+ "getTextValue",
335
+ "getDisabled",
336
+ "disabledKeys",
337
+ "selectionMode",
338
+ "selectedKey",
339
+ "defaultSelectedKey",
340
+ "selectedKeys",
341
+ "defaultSelectedKeys",
342
+ "onSelectionChange",
343
+ "onSelectionChangeMultiple",
344
+ "inputValue",
345
+ "defaultInputValue",
346
+ "onInputChange",
347
+ "isOpen",
348
+ "defaultOpen",
349
+ "onOpenChange",
350
+ "defaultFilter",
351
+ "allowsCustomValue",
352
+ "allowsEmptyCollection",
353
+ "menuTrigger",
354
+ "name",
355
+ "formValue",
356
+ ],
322
357
  );
323
358
 
324
- // Refs
325
359
  let inputRef: HTMLInputElement | null = null;
326
360
  let buttonRef: HTMLElement | null = null;
361
+ let triggerRef: HTMLElement | null = null;
327
362
  let listBoxRef: HTMLElement | null = null;
328
363
 
329
- // Create combobox state
330
364
  const state = createComboBoxState<T>({
331
365
  get items() {
332
366
  return stateProps.items;
333
367
  },
368
+ get defaultItems() {
369
+ return stateProps.defaultItems;
370
+ },
334
371
  get getKey() {
335
372
  return stateProps.getKey;
336
373
  },
@@ -343,15 +380,27 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
343
380
  get disabledKeys() {
344
381
  return stateProps.disabledKeys;
345
382
  },
383
+ get selectionMode() {
384
+ return stateProps.selectionMode;
385
+ },
346
386
  get selectedKey() {
347
387
  return stateProps.selectedKey;
348
388
  },
349
389
  get defaultSelectedKey() {
350
390
  return stateProps.defaultSelectedKey;
351
391
  },
392
+ get selectedKeys() {
393
+ return stateProps.selectedKeys;
394
+ },
395
+ get defaultSelectedKeys() {
396
+ return stateProps.defaultSelectedKeys;
397
+ },
352
398
  get onSelectionChange() {
353
399
  return stateProps.onSelectionChange;
354
400
  },
401
+ get onSelectionChangeMultiple() {
402
+ return stateProps.onSelectionChangeMultiple;
403
+ },
355
404
  get inputValue() {
356
405
  return stateProps.inputValue;
357
406
  },
@@ -393,35 +442,42 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
393
442
  },
394
443
  });
395
444
 
396
- const effectiveFormValue = createMemo<'key' | 'text'>(() => {
445
+ const effectiveFormValue = createMemo<"key" | "text">(() => {
397
446
  if (stateProps.allowsCustomValue) {
398
- return 'text';
447
+ return "text";
399
448
  }
400
- return stateProps.formValue ?? 'key';
449
+ return stateProps.formValue ?? "key";
450
+ });
451
+
452
+ const comboBoxAriaProps = createMemo(() => {
453
+ const cleanProps: Record<string, unknown> = {};
454
+ for (const key in ariaProps) {
455
+ if (!key.startsWith("data-")) {
456
+ cleanProps[key] = (ariaProps as Record<string, unknown>)[key];
457
+ }
458
+ }
459
+ return cleanProps as AriaComboBoxProps;
401
460
  });
402
461
 
403
- // Create combobox aria props
404
462
  const comboBoxAria = createComboBox<T>(
405
- {
406
- ...ariaProps,
463
+ () => ({
464
+ ...comboBoxAriaProps(),
407
465
  get name() {
408
- return effectiveFormValue() === 'text' ? stateProps.name : undefined;
466
+ return effectiveFormValue() === "text" ? stateProps.name : undefined;
409
467
  },
410
- },
468
+ }),
411
469
  state,
412
470
  () => inputRef,
413
471
  () => buttonRef,
414
- () => listBoxRef
472
+ () => listBoxRef,
415
473
  );
416
474
 
417
- // Create hover for wrapper
418
475
  const { isHovered, hoverProps } = createHover({
419
476
  get isDisabled() {
420
477
  return ariaProps.isDisabled;
421
478
  },
422
479
  });
423
480
 
424
- // Render props values
425
481
  const renderValues = createMemo<ComboBoxRenderProps>(() => ({
426
482
  isOpen: comboBoxAria.isOpen(),
427
483
  isFocused: comboBoxAria.isFocused(),
@@ -429,58 +485,78 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
429
485
  isDisabled: !!ariaProps.isDisabled,
430
486
  isRequired: !!ariaProps.isRequired,
431
487
  isInvalid: !!ariaProps.isInvalid,
488
+ isReadOnly: !!ariaProps.isReadOnly,
432
489
  isSelected: state.selectedKey() != null,
433
490
  inputValue: state.inputValue(),
434
491
  }));
435
492
 
436
- // Resolve render props
437
493
  const renderProps = useRenderProps(
438
494
  {
439
495
  class: local.class,
440
496
  style: local.style,
441
- defaultClassName: 'solidaria-ComboBox',
497
+ defaultClassName: "solidaria-ComboBox",
442
498
  },
443
- renderValues
499
+ renderValues,
444
500
  );
445
501
 
446
- // Filter DOM props
447
502
  const domProps = createMemo(() => {
448
503
  const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
449
504
  return filtered;
450
505
  });
451
506
 
452
- // Remove ref from hover props
453
507
  const cleanHoverProps = () => {
454
508
  const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
455
509
  return rest;
456
510
  };
457
511
 
512
+ const ComboBoxChildren = () =>
513
+ typeof local.children === "function"
514
+ ? (local.children as (values: ComboBoxRenderProps) => JSX.Element)(renderValues())
515
+ : local.children;
516
+
458
517
  return (
459
518
  <ComboBoxContext.Provider
460
- value={{
461
- state,
462
- inputProps: comboBoxAria.inputProps,
463
- buttonProps: comboBoxAria.buttonProps,
464
- listBoxProps: comboBoxAria.listBoxProps,
465
- labelProps: comboBoxAria.labelProps,
466
- descriptionProps: comboBoxAria.descriptionProps,
467
- errorMessageProps: comboBoxAria.errorMessageProps,
468
- isOpen: comboBoxAria.isOpen,
469
- isFocused: comboBoxAria.isFocused,
470
- isFocusVisible: comboBoxAria.isFocusVisible,
471
- items: stateProps.items,
472
- inputRef: () => inputRef,
473
- setInputRef: (el) => { inputRef = el; },
474
- buttonRef: () => buttonRef,
475
- setButtonRef: (el) => { buttonRef = el; },
476
- listBoxRef: () => listBoxRef,
477
- setListBoxRef: (el) => { listBoxRef = el; },
478
- }}
519
+ value={
520
+ {
521
+ state,
522
+ inputProps: () => comboBoxAria.inputProps,
523
+ buttonProps: () => comboBoxAria.buttonProps,
524
+ listBoxProps: () => comboBoxAria.listBoxProps,
525
+ labelProps: () => comboBoxAria.labelProps,
526
+ descriptionProps: () => comboBoxAria.descriptionProps,
527
+ errorMessageProps: () => comboBoxAria.errorMessageProps,
528
+ isOpen: comboBoxAria.isOpen,
529
+ isFocused: comboBoxAria.isFocused,
530
+ isFocusVisible: comboBoxAria.isFocusVisible,
531
+ items: stateProps.items ?? stateProps.defaultItems ?? [],
532
+ inputRef: () => inputRef,
533
+ setInputRef: (el) => {
534
+ inputRef = el;
535
+ },
536
+ buttonRef: () => buttonRef,
537
+ setButtonRef: (el) => {
538
+ buttonRef = el;
539
+ },
540
+ triggerRef: () => triggerRef,
541
+ setTriggerRef: (el) => {
542
+ triggerRef = el;
543
+ },
544
+ listBoxRef: () => listBoxRef,
545
+ setListBoxRef: (el) => {
546
+ listBoxRef = el;
547
+ },
548
+ slots: local.slots,
549
+ } as ComboBoxContextValue<unknown>
550
+ }
479
551
  >
480
552
  <ComboBoxStateContext.Provider value={state}>
481
553
  <div
482
554
  {...domProps()}
483
555
  {...cleanHoverProps()}
556
+ ref={(el) => {
557
+ assignRef(local.ref, el);
558
+ assignRef(local.rootRef, el);
559
+ }}
484
560
  class={renderProps.class()}
485
561
  style={renderProps.style()}
486
562
  data-open={comboBoxAria.isOpen() || undefined}
@@ -489,17 +565,20 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
489
565
  data-disabled={ariaProps.isDisabled || undefined}
490
566
  data-required={ariaProps.isRequired || undefined}
491
567
  data-invalid={ariaProps.isInvalid || undefined}
568
+ data-readonly={ariaProps.isReadOnly || undefined}
492
569
  data-hovered={isHovered() || undefined}
570
+ slot={local.slot}
493
571
  >
494
572
  {/* Hidden input for key-based form submission parity */}
495
- <Show when={stateProps.name && effectiveFormValue() === 'key'}>
573
+ <Show when={stateProps.name && effectiveFormValue() === "key"}>
496
574
  <input
497
575
  type="hidden"
498
576
  name={stateProps.name}
499
- value={state.selectedKey()?.toString() ?? ''}
577
+ form={ariaProps.form}
578
+ value={state.selectedKey()?.toString() ?? ""}
500
579
  />
501
580
  </Show>
502
- {local.children}
581
+ <ComboBoxChildren />
503
582
  </div>
504
583
  </ComboBoxStateContext.Provider>
505
584
  </ComboBoxContext.Provider>
@@ -510,25 +589,20 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
510
589
  * Label element for a combobox.
511
590
  */
512
591
  export function ComboBoxLabel(props: ComboBoxLabelProps): JSX.Element {
513
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
592
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
514
593
 
515
594
  const context = useContext(ComboBoxContext);
516
595
  if (!context) {
517
- throw new Error('ComboBoxLabel must be used within a ComboBox');
596
+ throw new Error("ComboBoxLabel must be used within a ComboBox");
518
597
  }
519
598
 
520
599
  const cleanLabelProps = () => {
521
- const { ref: _ref, ...rest } = context.labelProps as Record<string, unknown>;
600
+ const { ref: _ref, ...rest } = context.labelProps() as Record<string, unknown>;
522
601
  return rest;
523
602
  };
524
603
 
525
604
  return (
526
- <label
527
- {...domProps}
528
- {...cleanLabelProps()}
529
- class={local.class}
530
- style={local.style}
531
- >
605
+ <label {...domProps} {...cleanLabelProps()} class={local.class} style={local.style}>
532
606
  {local.children}
533
607
  </label>
534
608
  );
@@ -538,25 +612,20 @@ export function ComboBoxLabel(props: ComboBoxLabelProps): JSX.Element {
538
612
  * Description element for a combobox.
539
613
  */
540
614
  export function ComboBoxDescription(props: ComboBoxDescriptionProps): JSX.Element {
541
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
615
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
542
616
 
543
617
  const context = useContext(ComboBoxContext);
544
618
  if (!context) {
545
- throw new Error('ComboBoxDescription must be used within a ComboBox');
619
+ throw new Error("ComboBoxDescription must be used within a ComboBox");
546
620
  }
547
621
 
548
622
  const cleanDescriptionProps = () => {
549
- const { ref: _ref, ...rest } = context.descriptionProps as Record<string, unknown>;
623
+ const { ref: _ref, ...rest } = context.descriptionProps() as Record<string, unknown>;
550
624
  return rest;
551
625
  };
552
626
 
553
627
  return (
554
- <div
555
- {...domProps}
556
- {...cleanDescriptionProps()}
557
- class={local.class}
558
- style={local.style}
559
- >
628
+ <div {...domProps} {...cleanDescriptionProps()} class={local.class} style={local.style}>
560
629
  {local.children}
561
630
  </div>
562
631
  );
@@ -566,25 +635,20 @@ export function ComboBoxDescription(props: ComboBoxDescriptionProps): JSX.Elemen
566
635
  * Error message element for a combobox.
567
636
  */
568
637
  export function ComboBoxErrorMessage(props: ComboBoxErrorMessageProps): JSX.Element {
569
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
638
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
570
639
 
571
640
  const context = useContext(ComboBoxContext);
572
641
  if (!context) {
573
- throw new Error('ComboBoxErrorMessage must be used within a ComboBox');
642
+ throw new Error("ComboBoxErrorMessage must be used within a ComboBox");
574
643
  }
575
644
 
576
645
  const cleanErrorMessageProps = () => {
577
- const { ref: _ref, ...rest } = context.errorMessageProps as Record<string, unknown>;
646
+ const { ref: _ref, ...rest } = context.errorMessageProps() as Record<string, unknown>;
578
647
  return rest;
579
648
  };
580
649
 
581
650
  return (
582
- <div
583
- {...domProps}
584
- {...cleanErrorMessageProps()}
585
- class={local.class}
586
- style={local.style}
587
- >
651
+ <div {...domProps} {...cleanErrorMessageProps()} class={local.class} style={local.style}>
588
652
  {local.children}
589
653
  </div>
590
654
  );
@@ -594,23 +658,20 @@ export function ComboBoxErrorMessage(props: ComboBoxErrorMessageProps): JSX.Elem
594
658
  * The text input for a combobox.
595
659
  */
596
660
  export function ComboBoxInput(props: ComboBoxInputProps): JSX.Element {
597
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
661
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
598
662
 
599
- // Get context
600
663
  const context = useContext(ComboBoxContext);
601
664
  if (!context) {
602
- throw new Error('ComboBoxInput must be used within a ComboBox');
665
+ throw new Error("ComboBoxInput must be used within a ComboBox");
603
666
  }
604
- const { inputProps, isOpen, isFocused, isFocusVisible, state, setInputRef } = context;
667
+ const { isOpen, isFocused, isFocusVisible, state, setInputRef } = context;
605
668
 
606
- // Create hover
607
669
  const { isHovered, hoverProps } = createHover({
608
670
  get isDisabled() {
609
671
  return state.isDisabled;
610
672
  },
611
673
  });
612
674
 
613
- // Render props values
614
675
  const renderValues = createMemo<ComboBoxInputRenderProps>(() => ({
615
676
  isOpen: isOpen(),
616
677
  isFocused: isFocused(),
@@ -620,20 +681,18 @@ export function ComboBoxInput(props: ComboBoxInputProps): JSX.Element {
620
681
  inputValue: state.inputValue(),
621
682
  }));
622
683
 
623
- // Resolve render props
624
684
  const renderProps = useRenderProps(
625
685
  {
626
686
  children: local.children,
627
687
  class: local.class,
628
688
  style: local.style,
629
- defaultClassName: 'solidaria-ComboBox-input',
689
+ defaultClassName: "solidaria-ComboBox-input",
630
690
  },
631
- renderValues
691
+ renderValues,
632
692
  );
633
693
 
634
- // Remove ref from spread props
635
694
  const cleanInputProps = () => {
636
- const { ref: _ref1, value: _value, ...rest } = inputProps as Record<string, unknown>;
695
+ const { ref: _ref1, value: _value, ...rest } = context.inputProps() as Record<string, unknown>;
637
696
  return rest;
638
697
  };
639
698
  const cleanHoverProps = () => {
@@ -662,11 +721,27 @@ export function ComboBoxInput(props: ComboBoxInputProps): JSX.Element {
662
721
  export function ComboBoxValue(props: ComboBoxValueProps): JSX.Element {
663
722
  const context = useContext(ComboBoxContext);
664
723
  if (!context) {
665
- throw new Error('ComboBoxValue must be used within a ComboBox');
724
+ throw new Error("ComboBoxValue must be used within a ComboBox");
666
725
  }
667
726
 
668
727
  const state = context.state;
669
- const textValue = createMemo(() => state.inputValue() ?? '');
728
+ const isMulti = createMemo(() => state.selectionMode() === "multiple");
729
+ const selectedItem = createMemo(() => state.selectedItem());
730
+ const selectedItems = createMemo(() => {
731
+ if (isMulti()) {
732
+ return state.selectedItems().map((node) => node.value ?? null);
733
+ }
734
+ const item = selectedItem();
735
+ return item ? [item.value ?? null] : [];
736
+ });
737
+ const selectedText = createMemo(() => {
738
+ if (isMulti()) {
739
+ const items = state.selectedItems();
740
+ return items.map((n) => n.textValue).join(", ");
741
+ }
742
+ return selectedItem()?.textValue ?? "";
743
+ });
744
+ const textValue = createMemo(() => selectedText() || state.inputValue() || "");
670
745
  const isPlaceholder = createMemo(() => textValue().length === 0);
671
746
 
672
747
  const renderProps = useRenderProps(
@@ -674,12 +749,15 @@ export function ComboBoxValue(props: ComboBoxValueProps): JSX.Element {
674
749
  children: props.children,
675
750
  class: props.class,
676
751
  style: props.style,
677
- defaultClassName: 'solidaria-ComboBox-value',
752
+ defaultClassName: "solidaria-ComboBox-value",
678
753
  },
679
754
  () => ({
680
755
  textValue: textValue(),
681
756
  isPlaceholder: isPlaceholder(),
682
- })
757
+ selectedItems: selectedItems(),
758
+ selectedText: selectedText(),
759
+ state: state as ComboBoxState<unknown>,
760
+ }),
683
761
  );
684
762
 
685
763
  return (
@@ -688,7 +766,11 @@ export function ComboBoxValue(props: ComboBoxValueProps): JSX.Element {
688
766
  style={renderProps.style()}
689
767
  data-placeholder={isPlaceholder() || undefined}
690
768
  >
691
- {props.children ? renderProps.renderChildren() : textValue()}
769
+ {props.children
770
+ ? renderProps.renderChildren()
771
+ : isPlaceholder()
772
+ ? props.placeholder
773
+ : textValue()}
692
774
  </span>
693
775
  );
694
776
  }
@@ -697,23 +779,20 @@ export function ComboBoxValue(props: ComboBoxValueProps): JSX.Element {
697
779
  * The trigger button for a combobox.
698
780
  */
699
781
  export function ComboBoxButton(props: ComboBoxButtonProps): JSX.Element {
700
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
782
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
701
783
 
702
- // Get context
703
784
  const context = useContext(ComboBoxContext);
704
785
  if (!context) {
705
- throw new Error('ComboBoxButton must be used within a ComboBox');
786
+ throw new Error("ComboBoxButton must be used within a ComboBox");
706
787
  }
707
- const { buttonProps, isOpen, isFocused, state, setButtonRef } = context;
788
+ const { isOpen, isFocused, state, setButtonRef } = context;
708
789
 
709
- // Create hover
710
790
  const { isHovered, hoverProps } = createHover({
711
791
  get isDisabled() {
712
792
  return state.isDisabled;
713
793
  },
714
794
  });
715
795
 
716
- // Render props values
717
796
  const renderValues = createMemo<ComboBoxButtonRenderProps>(() => ({
718
797
  isOpen: isOpen(),
719
798
  isFocused: isFocused(),
@@ -722,20 +801,18 @@ export function ComboBoxButton(props: ComboBoxButtonProps): JSX.Element {
722
801
  isDisabled: state.isDisabled,
723
802
  }));
724
803
 
725
- // Resolve render props
726
804
  const renderProps = useRenderProps(
727
805
  {
728
806
  children: local.children,
729
807
  class: local.class,
730
808
  style: local.style,
731
- defaultClassName: 'solidaria-ComboBox-button',
809
+ defaultClassName: "solidaria-ComboBox-button",
732
810
  },
733
- renderValues
811
+ renderValues,
734
812
  );
735
813
 
736
- // Remove ref from spread props
737
814
  const cleanButtonProps = () => {
738
- const { ref: _ref1, ...rest } = buttonProps as Record<string, unknown>;
815
+ const { ref: _ref1, ...rest } = context.buttonProps() as Record<string, unknown>;
739
816
  return rest;
740
817
  };
741
818
  const cleanHoverProps = () => {
@@ -752,6 +829,7 @@ export function ComboBoxButton(props: ComboBoxButtonProps): JSX.Element {
752
829
  class={renderProps.class()}
753
830
  style={renderProps.style()}
754
831
  data-open={isOpen() || undefined}
832
+ data-pressed={isOpen() || undefined}
755
833
  data-focused={isFocused() || undefined}
756
834
  data-hovered={isHovered() || undefined}
757
835
  data-disabled={state.isDisabled || undefined}
@@ -765,27 +843,18 @@ export function ComboBoxButton(props: ComboBoxButtonProps): JSX.Element {
765
843
  * The listbox popup for a combobox.
766
844
  */
767
845
  export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element {
768
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
846
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
769
847
 
770
- // Get context
771
- const context = useContext(ComboBoxContext);
772
- if (!context) {
773
- throw new Error('ComboBoxListBox must be used within a ComboBox');
848
+ const rawContext = useContext(ComboBoxContext);
849
+ if (!rawContext) {
850
+ throw new Error("ComboBoxListBox must be used within a ComboBox");
774
851
  }
775
- const {
776
- listBoxProps: contextListBoxProps,
777
- state: comboBoxState,
778
- isOpen,
779
- inputRef,
780
- buttonRef,
781
- setListBoxRef,
782
- } = context;
783
- const state = comboBoxState as ComboBoxState<T>;
784
-
785
- // Ref for the listbox element (for click outside detection)
852
+ const context = rawContext as ComboBoxContextValue<T>;
853
+ const { state: comboBoxState, isOpen, inputRef, buttonRef, setListBoxRef } = context;
854
+ const state = comboBoxState;
855
+
786
856
  let listBoxRef: HTMLUListElement | undefined;
787
857
 
788
- // Handle click outside to close combobox
789
858
  createInteractOutside({
790
859
  ref: () => listBoxRef ?? null,
791
860
  onInteractOutside: (e) => {
@@ -810,28 +879,25 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
810
879
 
811
880
  // Create listbox aria props using ComboBoxState's ListState-compatible interface
812
881
  const { listBoxProps } = createListBox(
813
- contextListBoxProps as unknown as AriaListBoxProps,
814
- createComboBoxListStateAdapter(state)
882
+ context.listBoxProps as unknown as AriaListBoxProps,
883
+ createComboBoxListStateAdapter(state),
815
884
  );
816
885
 
817
- // Render props values
818
886
  const renderValues = createMemo<ComboBoxListBoxRenderProps>(() => ({
819
887
  isFocused: state.isFocused(),
820
888
  }));
821
889
 
822
- // Resolve render props
823
890
  const renderProps = useRenderProps(
824
891
  {
825
892
  class: local.class,
826
893
  style: local.style,
827
- defaultClassName: 'solidaria-ComboBox-listbox',
894
+ defaultClassName: "solidaria-ComboBox-listbox",
828
895
  },
829
- renderValues
896
+ renderValues,
830
897
  );
831
898
 
832
- // Remove ref from spread props
833
899
  const cleanContextProps = () => {
834
- const { ref: _ref1, ...rest } = contextListBoxProps as Record<string, unknown>;
900
+ const { ref: _ref1, ...rest } = context.listBoxProps() as Record<string, unknown>;
835
901
  return rest;
836
902
  };
837
903
  const cleanListBoxProps = () => {
@@ -840,37 +906,26 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
840
906
  };
841
907
 
842
908
  const items = () => Array.from(state.collection());
909
+ const getNodeValue = (node: { key: Key; value?: T | null; index?: number }): T | null => {
910
+ if (node.value != null) {
911
+ return node.value;
912
+ }
843
913
 
844
- // Prevent focus from being lost when clicking in the listbox
845
- // This is critical - if we don't prevent default, the input loses focus
846
- // and the blur handler closes the menu before the click can be processed
847
- // We need to attach this in the ref callback to use capture phase
848
- let cleanupFocusGuard: (() => void) | undefined;
849
-
850
- const setupMouseDownHandler = (el: HTMLUListElement) => {
851
- cleanupFocusGuard?.();
852
- cleanupFocusGuard = undefined;
914
+ return (
915
+ context.items.find((item, index) => {
916
+ const candidate = item as { key?: Key; id?: Key };
917
+ const key = candidate.key ?? candidate.id ?? index;
918
+ return key === node.key;
919
+ }) ?? null
920
+ );
921
+ };
853
922
 
923
+ const setListBoxElement = (el: HTMLUListElement) => {
854
924
  listBoxRef = el;
855
925
  setListBoxRef(el);
856
- if (el) {
857
- const mouseHandler = (e: MouseEvent) => {
858
- e.preventDefault();
859
- };
860
- const pointerHandler = (e: PointerEvent) => {
861
- e.preventDefault();
862
- };
863
- el.addEventListener('mousedown', mouseHandler, true); // capture phase
864
- el.addEventListener('pointerdown', pointerHandler, true); // capture phase
865
- cleanupFocusGuard = () => {
866
- el.removeEventListener('mousedown', mouseHandler, true);
867
- el.removeEventListener('pointerdown', pointerHandler, true);
868
- };
869
- }
870
926
  };
871
927
 
872
928
  onCleanup(() => {
873
- cleanupFocusGuard?.();
874
929
  setListBoxRef(null);
875
930
  });
876
931
 
@@ -878,24 +933,26 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
878
933
  <Show when={isOpen()}>
879
934
  <ul
880
935
  {...domProps}
881
- ref={setupMouseDownHandler}
936
+ ref={setListBoxElement}
882
937
  {...cleanContextProps()}
883
938
  {...cleanListBoxProps()}
884
939
  class={renderProps.class()}
885
940
  style={renderProps.style()}
886
941
  data-focused={state.isFocused() || undefined}
887
942
  >
888
- <Show when={local.children} fallback={
889
- <For each={items()}>
890
- {(node) => (
891
- <ComboBoxOption id={node.key}>
892
- {node.textValue}
893
- </ComboBoxOption>
894
- )}
895
- </For>
896
- }>
943
+ <Show
944
+ when={local.children}
945
+ fallback={
946
+ <For each={items()}>
947
+ {(node) => <ComboBoxOption id={node.key}>{node.textValue}</ComboBoxOption>}
948
+ </For>
949
+ }
950
+ >
897
951
  <For each={items()}>
898
- {(node) => node.value != null ? (local.children as Function)!(node.value) : null}
952
+ {(node) => {
953
+ const value = getNodeValue(node);
954
+ return value != null ? (local.children as Function)!(value) : null;
955
+ }}
899
956
  </For>
900
957
  </Show>
901
958
  </ul>
@@ -908,89 +965,102 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
908
965
  */
909
966
  export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
910
967
  const [local, ariaProps] = splitProps(props, [
911
- 'class',
912
- 'style',
913
- 'slot',
914
- 'id',
915
- 'item',
916
- 'textValue',
968
+ "class",
969
+ "style",
970
+ "slot",
971
+ "id",
972
+ "item",
973
+ "textValue",
974
+ "onAction",
917
975
  ]);
918
976
 
919
- // Get state from context
920
- const context = useContext(ComboBoxStateContext);
921
- if (!context) {
922
- throw new Error('ComboBoxOption must be used within a ComboBox');
977
+ const stateContext = useContext(ComboBoxStateContext);
978
+ const comboBoxContext = useContext(ComboBoxContext);
979
+ if (!stateContext) {
980
+ throw new Error("ComboBoxOption must be used within a ComboBox");
923
981
  }
924
- const state = context as ComboBoxState<T>;
982
+ const state = stateContext as ComboBoxState<T>;
983
+ const optionId = () => {
984
+ const listBoxId = getComboBoxData(state as ComboBoxState<unknown>)?.listBoxId;
985
+ return listBoxId ? `${listBoxId}-option-${local.id}` : String(local.id);
986
+ };
925
987
 
926
988
  // Create option aria props using ComboBoxState's ListState-compatible interface
927
989
  const optionAria = createOption<T>(
928
990
  {
929
991
  key: local.id,
992
+ get optionId() {
993
+ return optionId();
994
+ },
930
995
  get isDisabled() {
931
996
  return ariaProps.isDisabled;
932
997
  },
933
- get 'aria-label'() {
934
- return ariaProps['aria-label'];
998
+ get "aria-label"() {
999
+ return ariaProps["aria-label"];
1000
+ },
1001
+ get onAction() {
1002
+ return local.onAction;
1003
+ },
1004
+ shouldSelectOnPressUp: true,
1005
+ shouldFocusOnHover: true,
1006
+ shouldUseVirtualFocus: true,
1007
+ allowsDifferentPressOrigin: true,
1008
+ get onHoverStart() {
1009
+ return ariaProps.onHoverStart;
1010
+ },
1011
+ get onHoverEnd() {
1012
+ return ariaProps.onHoverEnd;
1013
+ },
1014
+ get onHoverChange() {
1015
+ return ariaProps.onHoverChange;
935
1016
  },
936
1017
  },
937
- createComboBoxListStateAdapter(state)
1018
+ createComboBoxListStateAdapter(state),
938
1019
  );
939
1020
 
940
- // Create hover
941
- const { isHovered, hoverProps } = createHover({
942
- get isDisabled() {
943
- return optionAria.isDisabled();
944
- },
945
- });
1021
+ const isOptionFocusVisible = () =>
1022
+ optionAria.isFocusVisible() ||
1023
+ (optionAria.isFocused() && (comboBoxContext?.isFocusVisible() ?? false));
946
1024
 
947
- // Render props values
948
1025
  const renderValues = createMemo<ComboBoxOptionRenderProps>(() => ({
949
1026
  isSelected: optionAria.isSelected(),
950
1027
  isFocused: optionAria.isFocused(),
951
- isFocusVisible: optionAria.isFocusVisible(),
1028
+ isFocusVisible: isOptionFocusVisible(),
952
1029
  isPressed: optionAria.isPressed(),
953
- isHovered: isHovered(),
1030
+ isHovered: optionAria.isHovered(),
954
1031
  isDisabled: optionAria.isDisabled(),
955
1032
  }));
956
1033
 
957
- // Resolve render props
958
1034
  const renderProps = useRenderProps(
959
1035
  {
960
1036
  children: props.children,
961
1037
  class: local.class,
962
1038
  style: local.style,
963
- defaultClassName: 'solidaria-ComboBox-option',
1039
+ defaultClassName: "solidaria-ComboBox-option",
964
1040
  },
965
- renderValues
1041
+ renderValues,
966
1042
  );
967
1043
 
968
1044
  const selectionIndicatorContext = createMemo<SelectionIndicatorContextValue>(() => ({
969
1045
  isSelected: optionAria.isSelected,
970
1046
  }));
971
1047
 
972
- // Remove ref from spread props
973
1048
  const cleanOptionProps = () => {
974
1049
  const { ref: _ref1, ...rest } = optionAria.optionProps as Record<string, unknown>;
975
1050
  return rest;
976
1051
  };
977
- const cleanHoverProps = () => {
978
- const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
979
- return rest;
980
- };
981
1052
 
982
1053
  return (
983
1054
  <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
984
1055
  <li
985
1056
  {...cleanOptionProps()}
986
- {...cleanHoverProps()}
987
1057
  class={renderProps.class()}
988
1058
  style={renderProps.style()}
989
1059
  data-selected={optionAria.isSelected() || undefined}
990
1060
  data-focused={optionAria.isFocused() || undefined}
991
- data-focus-visible={optionAria.isFocusVisible() || undefined}
1061
+ data-focus-visible={isOptionFocusVisible() || undefined}
992
1062
  data-pressed={optionAria.isPressed() || undefined}
993
- data-hovered={isHovered() || undefined}
1063
+ data-hovered={optionAria.isHovered() || undefined}
994
1064
  data-disabled={optionAria.isDisabled() || undefined}
995
1065
  >
996
1066
  {renderProps.renderChildren()}
@@ -999,7 +1069,98 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
999
1069
  );
1000
1070
  }
1001
1071
 
1002
- // Attach sub-components
1072
+ export interface ComboBoxTagGroupProps {
1073
+ /** Render function for each selected item. */
1074
+ children: (item: { key: Key; label: string }) => JSX.Element;
1075
+ /** The CSS className for the container. */
1076
+ class?: string;
1077
+ /** The inline style for the container. */
1078
+ style?: JSX.CSSProperties;
1079
+ }
1080
+
1081
+ /**
1082
+ * Renders selected items as tags in multi-select mode.
1083
+ */
1084
+ export function ComboBoxTagGroup(props: ComboBoxTagGroupProps): JSX.Element {
1085
+ const context = useContext(ComboBoxContext);
1086
+ if (!context) {
1087
+ throw new Error("ComboBoxTagGroup must be used within a ComboBox");
1088
+ }
1089
+
1090
+ const state = context.state;
1091
+ const items = createMemo(() =>
1092
+ state.selectedItems().map((node) => ({
1093
+ key: node.key,
1094
+ label: node.textValue,
1095
+ })),
1096
+ );
1097
+
1098
+ return (
1099
+ <Show when={items().length > 0}>
1100
+ <div
1101
+ class={props.class ?? "solidaria-ComboBox-tagGroup"}
1102
+ style={props.style}
1103
+ role="group"
1104
+ aria-label="Selected items"
1105
+ >
1106
+ <For each={items()}>{(item) => props.children(item)}</For>
1107
+ </div>
1108
+ </Show>
1109
+ );
1110
+ }
1111
+
1112
+ export interface ComboBoxTagProps {
1113
+ /** The item data. */
1114
+ item: { key: Key; label: string };
1115
+ /** Handler called when the tag remove button is clicked. */
1116
+ onRemove?: () => void;
1117
+ /** The children to render inside the tag. */
1118
+ children?: JSX.Element;
1119
+ /** The CSS className for the tag. */
1120
+ class?: string;
1121
+ /** The inline style for the tag. */
1122
+ style?: JSX.CSSProperties;
1123
+ }
1124
+
1125
+ /**
1126
+ * A tag representing a selected item in a multi-select combobox.
1127
+ */
1128
+ export function ComboBoxTag(props: ComboBoxTagProps): JSX.Element {
1129
+ const context = useContext(ComboBoxContext);
1130
+ if (!context) {
1131
+ throw new Error("ComboBoxTag must be used within a ComboBox");
1132
+ }
1133
+
1134
+ const state = context.state;
1135
+
1136
+ const handleRemove = () => {
1137
+ if (props.onRemove) {
1138
+ props.onRemove();
1139
+ } else {
1140
+ state.removeSelectedKey(props.item.key);
1141
+ }
1142
+ };
1143
+
1144
+ return (
1145
+ <span
1146
+ class={props.class ?? "solidaria-ComboBox-tag"}
1147
+ style={props.style}
1148
+ data-key={String(props.item.key)}
1149
+ >
1150
+ {props.children ?? props.item.label}
1151
+ <button
1152
+ type="button"
1153
+ aria-label={`Remove ${props.item.label}`}
1154
+ onClick={handleRemove}
1155
+ class="solidaria-ComboBox-tag-remove"
1156
+ tabIndex={-1}
1157
+ >
1158
+ &#215;
1159
+ </button>
1160
+ </span>
1161
+ );
1162
+ }
1163
+
1003
1164
  ComboBox.Input = ComboBoxInput;
1004
1165
  ComboBox.Button = ComboBoxButton;
1005
1166
  ComboBox.ListBox = ComboBoxListBox;
@@ -1007,12 +1168,16 @@ ComboBox.Option = ComboBoxOption;
1007
1168
  ComboBox.Label = ComboBoxLabel;
1008
1169
  ComboBox.Description = ComboBoxDescription;
1009
1170
  ComboBox.ErrorMessage = ComboBoxErrorMessage;
1171
+ ComboBox.TagGroup = ComboBoxTagGroup;
1172
+ ComboBox.Tag = ComboBoxTag;
1010
1173
 
1011
- // Re-export filter function for convenience
1012
1174
  export { defaultContainsFilter };
1013
1175
 
1014
1176
  function createComboBoxListStateAdapter<T>(state: ComboBoxState<T>): ListState<T> {
1015
1177
  const selectedKeys = createMemo(() => {
1178
+ if (state.selectionMode() === "multiple") {
1179
+ return state.selectedKeys();
1180
+ }
1016
1181
  const key = state.selectedKey();
1017
1182
  return key != null ? new Set<Key>([key]) : new Set<Key>();
1018
1183
  });
@@ -1033,11 +1198,11 @@ function createComboBoxListStateAdapter<T>(state: ComboBoxState<T>): ListState<T
1033
1198
  setFocusedKey: (key) => state.setFocusedKey(key ?? null),
1034
1199
  childFocusStrategy: () => null,
1035
1200
  selectionMode: state.selectionMode,
1036
- selectionBehavior: () => 'replace',
1201
+ selectionBehavior: () => "replace",
1037
1202
  disallowEmptySelection: () => true,
1038
1203
  selectedKeys,
1039
1204
  disabledKeys,
1040
- disabledBehavior: () => 'all',
1205
+ disabledBehavior: () => "all",
1041
1206
  isEmpty: () => selectedKeys().size === 0,
1042
1207
  isSelectAll: () => false,
1043
1208
  isSelected: state.isSelected,