@proyecto-viviana/solidaria-components 0.2.5 → 0.3.0

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