@proyecto-viviana/solidaria-components 0.2.5 → 0.2.9

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 (194) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ActionBar.d.ts +71 -0
  3. package/dist/ActionBar.d.ts.map +1 -0
  4. package/dist/ActionGroup.d.ts +74 -0
  5. package/dist/ActionGroup.d.ts.map +1 -0
  6. package/dist/Alert.d.ts +70 -0
  7. package/dist/Alert.d.ts.map +1 -0
  8. package/dist/Breadcrumbs.d.ts +10 -2
  9. package/dist/Breadcrumbs.d.ts.map +1 -1
  10. package/dist/Button.d.ts +4 -0
  11. package/dist/Button.d.ts.map +1 -1
  12. package/dist/Calendar.d.ts +13 -0
  13. package/dist/Calendar.d.ts.map +1 -1
  14. package/dist/Checkbox.d.ts +2 -2
  15. package/dist/Checkbox.d.ts.map +1 -1
  16. package/dist/Collection.d.ts +125 -0
  17. package/dist/Collection.d.ts.map +1 -0
  18. package/dist/Color.d.ts +114 -2
  19. package/dist/Color.d.ts.map +1 -1
  20. package/dist/ColorEditor.d.ts +42 -0
  21. package/dist/ColorEditor.d.ts.map +1 -0
  22. package/dist/ComboBox.d.ts +64 -0
  23. package/dist/ComboBox.d.ts.map +1 -1
  24. package/dist/ContextualHelpTrigger.d.ts +40 -0
  25. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  26. package/dist/DateField.d.ts +27 -2
  27. package/dist/DateField.d.ts.map +1 -1
  28. package/dist/DatePicker.d.ts +67 -2
  29. package/dist/DatePicker.d.ts.map +1 -1
  30. package/dist/Dialog.d.ts.map +1 -1
  31. package/dist/Disclosure.d.ts +2 -0
  32. package/dist/Disclosure.d.ts.map +1 -1
  33. package/dist/DragAndDrop.d.ts +80 -0
  34. package/dist/DragAndDrop.d.ts.map +1 -0
  35. package/dist/DragPreview.d.ts +14 -0
  36. package/dist/DragPreview.d.ts.map +1 -0
  37. package/dist/DropZone.d.ts +27 -0
  38. package/dist/DropZone.d.ts.map +1 -0
  39. package/dist/FieldError.d.ts +23 -0
  40. package/dist/FieldError.d.ts.map +1 -0
  41. package/dist/FileTrigger.d.ts +26 -0
  42. package/dist/FileTrigger.d.ts.map +1 -0
  43. package/dist/Focusable.d.ts +27 -0
  44. package/dist/Focusable.d.ts.map +1 -0
  45. package/dist/Form.d.ts +27 -0
  46. package/dist/Form.d.ts.map +1 -0
  47. package/dist/GridList.d.ts +40 -1
  48. package/dist/GridList.d.ts.map +1 -1
  49. package/dist/Icon.d.ts +57 -0
  50. package/dist/Icon.d.ts.map +1 -0
  51. package/dist/Keyboard.d.ts +13 -0
  52. package/dist/Keyboard.d.ts.map +1 -0
  53. package/dist/Link.d.ts.map +1 -1
  54. package/dist/ListBox.d.ts +43 -1
  55. package/dist/ListBox.d.ts.map +1 -1
  56. package/dist/ListDropTargetDelegate.d.ts +38 -0
  57. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  58. package/dist/Menu.d.ts +20 -2
  59. package/dist/Menu.d.ts.map +1 -1
  60. package/dist/Meter.d.ts +2 -2
  61. package/dist/Meter.d.ts.map +1 -1
  62. package/dist/Modal.d.ts +2 -0
  63. package/dist/Modal.d.ts.map +1 -1
  64. package/dist/NumberField.d.ts +2 -0
  65. package/dist/NumberField.d.ts.map +1 -1
  66. package/dist/Popover.d.ts +4 -2
  67. package/dist/Popover.d.ts.map +1 -1
  68. package/dist/Pressable.d.ts +27 -0
  69. package/dist/Pressable.d.ts.map +1 -0
  70. package/dist/ProgressBar.d.ts +2 -2
  71. package/dist/ProgressBar.d.ts.map +1 -1
  72. package/dist/RadioGroup.d.ts.map +1 -1
  73. package/dist/RangeCalendar.d.ts +5 -0
  74. package/dist/RangeCalendar.d.ts.map +1 -1
  75. package/dist/RouterProvider.d.ts +75 -0
  76. package/dist/RouterProvider.d.ts.map +1 -0
  77. package/dist/SearchField.d.ts +2 -3
  78. package/dist/SearchField.d.ts.map +1 -1
  79. package/dist/Select.d.ts +11 -0
  80. package/dist/Select.d.ts.map +1 -1
  81. package/dist/SelectionIndicator.d.ts +30 -0
  82. package/dist/SelectionIndicator.d.ts.map +1 -0
  83. package/dist/SharedElementTransition.d.ts +39 -0
  84. package/dist/SharedElementTransition.d.ts.map +1 -0
  85. package/dist/Slider.d.ts +6 -3
  86. package/dist/Slider.d.ts.map +1 -1
  87. package/dist/Table.d.ts +39 -0
  88. package/dist/Table.d.ts.map +1 -1
  89. package/dist/Tabs.d.ts +4 -3
  90. package/dist/Tabs.d.ts.map +1 -1
  91. package/dist/TagGroup.d.ts +12 -2
  92. package/dist/TagGroup.d.ts.map +1 -1
  93. package/dist/Text.d.ts +10 -0
  94. package/dist/Text.d.ts.map +1 -0
  95. package/dist/TextField.d.ts +4 -0
  96. package/dist/TextField.d.ts.map +1 -1
  97. package/dist/TimeField.d.ts +26 -1
  98. package/dist/TimeField.d.ts.map +1 -1
  99. package/dist/Toast.d.ts.map +1 -1
  100. package/dist/ToggleButton.d.ts +30 -0
  101. package/dist/ToggleButton.d.ts.map +1 -0
  102. package/dist/ToggleButtonGroup.d.ts +33 -0
  103. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  104. package/dist/Toolbar.d.ts.map +1 -1
  105. package/dist/Tooltip.d.ts +9 -0
  106. package/dist/Tooltip.d.ts.map +1 -1
  107. package/dist/Tree.d.ts +44 -2
  108. package/dist/Tree.d.ts.map +1 -1
  109. package/dist/Virtualizer.d.ts +61 -0
  110. package/dist/Virtualizer.d.ts.map +1 -0
  111. package/dist/VirtualizerLayouts.d.ts +82 -0
  112. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  113. package/dist/VisuallyHidden.d.ts +3 -1
  114. package/dist/VisuallyHidden.d.ts.map +1 -1
  115. package/dist/contexts.d.ts +1 -0
  116. package/dist/contexts.d.ts.map +1 -1
  117. package/dist/index.d.ts +57 -25
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +13961 -5946
  120. package/dist/index.js.map +1 -7
  121. package/dist/index.ssr.js +9612 -2401
  122. package/dist/index.ssr.js.map +1 -7
  123. package/dist/useDragAndDrop.d.ts +93 -0
  124. package/dist/useDragAndDrop.d.ts.map +1 -0
  125. package/dist/utils.d.ts +7 -1
  126. package/dist/utils.d.ts.map +1 -1
  127. package/dist/virtualizer/Layout.d.ts +79 -0
  128. package/dist/virtualizer/Layout.d.ts.map +1 -0
  129. package/package.json +8 -6
  130. package/src/ActionBar.tsx +248 -0
  131. package/src/ActionGroup.tsx +285 -0
  132. package/src/Alert.tsx +177 -0
  133. package/src/Autocomplete.tsx +1 -1
  134. package/src/Breadcrumbs.tsx +103 -17
  135. package/src/Button.tsx +65 -21
  136. package/src/Calendar.tsx +179 -53
  137. package/src/Checkbox.tsx +1 -2
  138. package/src/Collection.tsx +341 -0
  139. package/src/Color.tsx +652 -34
  140. package/src/ColorEditor.tsx +231 -0
  141. package/src/ComboBox.tsx +315 -81
  142. package/src/ContextualHelpTrigger.tsx +183 -0
  143. package/src/DateField.tsx +93 -19
  144. package/src/DatePicker.tsx +495 -25
  145. package/src/Dialog.tsx +40 -9
  146. package/src/Disclosure.tsx +33 -27
  147. package/src/DragAndDrop.tsx +334 -0
  148. package/src/DragPreview.tsx +45 -0
  149. package/src/DropZone.tsx +213 -0
  150. package/src/FieldError.tsx +67 -0
  151. package/src/FileTrigger.tsx +83 -0
  152. package/src/Focusable.tsx +106 -0
  153. package/src/Form.tsx +85 -0
  154. package/src/GridList.tsx +379 -41
  155. package/src/Icon.tsx +154 -0
  156. package/src/Keyboard.tsx +26 -0
  157. package/src/Link.tsx +14 -1
  158. package/src/ListBox.tsx +484 -33
  159. package/src/ListDropTargetDelegate.ts +282 -0
  160. package/src/Menu.tsx +388 -35
  161. package/src/Meter.tsx +7 -3
  162. package/src/Modal.tsx +32 -4
  163. package/src/NumberField.tsx +163 -43
  164. package/src/Popover.tsx +136 -180
  165. package/src/Pressable.tsx +108 -0
  166. package/src/ProgressBar.tsx +7 -3
  167. package/src/RadioGroup.tsx +35 -25
  168. package/src/RangeCalendar.tsx +100 -68
  169. package/src/RouterProvider.tsx +240 -0
  170. package/src/SearchField.tsx +142 -34
  171. package/src/Select.tsx +221 -73
  172. package/src/SelectionIndicator.tsx +105 -0
  173. package/src/SharedElementTransition.tsx +258 -0
  174. package/src/Slider.tsx +16 -6
  175. package/src/Table.tsx +417 -57
  176. package/src/Tabs.tsx +68 -35
  177. package/src/TagGroup.tsx +121 -36
  178. package/src/Text.tsx +18 -0
  179. package/src/TextField.tsx +25 -8
  180. package/src/TimeField.tsx +101 -151
  181. package/src/Toast.tsx +108 -14
  182. package/src/ToggleButton.tsx +159 -0
  183. package/src/ToggleButtonGroup.tsx +136 -0
  184. package/src/Toolbar.tsx +14 -8
  185. package/src/Tooltip.tsx +108 -19
  186. package/src/Tree.tsx +1143 -87
  187. package/src/Virtualizer.tsx +702 -0
  188. package/src/VirtualizerLayouts.ts +265 -0
  189. package/src/VisuallyHidden.tsx +15 -21
  190. package/src/contexts.ts +1 -0
  191. package/src/index.ts +1057 -620
  192. package/src/useDragAndDrop.ts +351 -0
  193. package/src/utils.tsx +37 -3
  194. package/src/virtualizer/Layout.ts +200 -0
package/src/ComboBox.tsx CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  type Accessor,
11
11
  createContext,
12
12
  createMemo,
13
+ onCleanup,
13
14
  splitProps,
14
15
  useContext,
15
16
  For,
@@ -22,12 +23,14 @@ import {
22
23
  createHover,
23
24
  createInteractOutside,
24
25
  type AriaComboBoxProps,
26
+ type AriaListBoxProps,
25
27
  type AriaOptionProps,
26
28
  } from '@proyecto-viviana/solidaria';
27
29
  import {
28
30
  createComboBoxState,
29
31
  defaultContainsFilter,
30
32
  type ComboBoxState,
33
+ type ListState,
31
34
  type Key,
32
35
  type FilterFn,
33
36
  type MenuTriggerAction,
@@ -40,6 +43,10 @@ import {
40
43
  useRenderProps,
41
44
  filterDOMProps,
42
45
  } from './utils';
46
+ import {
47
+ SelectionIndicatorContext,
48
+ type SelectionIndicatorContextValue,
49
+ } from './SelectionIndicator';
43
50
 
44
51
  // ============================================
45
52
  // TYPES
@@ -56,6 +63,8 @@ export interface ComboBoxRenderProps {
56
63
  isDisabled: boolean;
57
64
  /** Whether the combobox is required. */
58
65
  isRequired: boolean;
66
+ /** Whether the combobox is invalid. */
67
+ isInvalid: boolean;
59
68
  /** Whether a value is selected. */
60
69
  isSelected: boolean;
61
70
  /** The current input value. */
@@ -103,6 +112,14 @@ export interface ComboBoxProps<T>
103
112
  menuTrigger?: 'focus' | 'input' | 'manual';
104
113
  /** The name of the combobox, used when submitting an HTML form. */
105
114
  name?: string;
115
+ /**
116
+ * Controls what value is submitted in forms.
117
+ * - 'key': submit the selected key via hidden input (default)
118
+ * - 'text': submit the text input value
119
+ *
120
+ * When allowsCustomValue is true, formValue is forced to 'text'.
121
+ */
122
+ formValue?: 'key' | 'text';
106
123
  /** The children of the component (compound components: ComboBoxInput, ComboBoxButton, ComboBoxListBox). */
107
124
  children: JSX.Element;
108
125
  /** The CSS className for the element. */
@@ -135,6 +152,33 @@ export interface ComboBoxInputProps extends SlotProps {
135
152
  style?: StyleOrFunction<ComboBoxInputRenderProps>;
136
153
  }
137
154
 
155
+ export interface ComboBoxLabelProps extends SlotProps {
156
+ /** The children of the label element. */
157
+ children?: JSX.Element;
158
+ /** The CSS className for the element. */
159
+ class?: string;
160
+ /** The inline style for the element. */
161
+ style?: JSX.CSSProperties;
162
+ }
163
+
164
+ export interface ComboBoxDescriptionProps extends SlotProps {
165
+ /** The children of the description element. */
166
+ children?: JSX.Element;
167
+ /** The CSS className for the element. */
168
+ class?: string;
169
+ /** The inline style for the element. */
170
+ style?: JSX.CSSProperties;
171
+ }
172
+
173
+ export interface ComboBoxErrorMessageProps extends SlotProps {
174
+ /** The children of the error message element. */
175
+ children?: JSX.Element;
176
+ /** The CSS className for the element. */
177
+ class?: string;
178
+ /** The inline style for the element. */
179
+ style?: JSX.CSSProperties;
180
+ }
181
+
138
182
  export interface ComboBoxButtonRenderProps {
139
183
  /** Whether the combobox is open. */
140
184
  isOpen: boolean;
@@ -148,6 +192,17 @@ export interface ComboBoxButtonRenderProps {
148
192
  isDisabled: boolean;
149
193
  }
150
194
 
195
+ export interface ComboBoxValueRenderProps {
196
+ textValue: string;
197
+ isPlaceholder: boolean;
198
+ }
199
+
200
+ export interface ComboBoxValueProps extends SlotProps {
201
+ children?: RenderChildren<ComboBoxValueRenderProps>;
202
+ class?: ClassNameOrFunction<ComboBoxValueRenderProps>;
203
+ style?: StyleOrFunction<ComboBoxValueRenderProps>;
204
+ }
205
+
151
206
  export interface ComboBoxButtonProps extends SlotProps {
152
207
  /** The children of the button. */
153
208
  children?: RenderChildren<ComboBoxButtonRenderProps>;
@@ -213,6 +268,8 @@ interface ComboBoxContextValue<T> {
213
268
  buttonProps: JSX.HTMLAttributes<HTMLElement>;
214
269
  listBoxProps: JSX.HTMLAttributes<HTMLElement>;
215
270
  labelProps: JSX.HTMLAttributes<HTMLElement>;
271
+ descriptionProps: JSX.HTMLAttributes<HTMLElement>;
272
+ errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
216
273
  isOpen: Accessor<boolean>;
217
274
  isFocused: Accessor<boolean>;
218
275
  isFocusVisible: Accessor<boolean>;
@@ -221,10 +278,13 @@ interface ComboBoxContextValue<T> {
221
278
  setInputRef: (el: HTMLInputElement | null) => void;
222
279
  buttonRef: () => HTMLElement | null;
223
280
  setButtonRef: (el: HTMLElement | null) => void;
281
+ listBoxRef: () => HTMLElement | null;
282
+ setListBoxRef: (el: HTMLElement | null) => void;
224
283
  }
225
284
 
226
285
  export const ComboBoxContext = createContext<ComboBoxContextValue<unknown> | null>(null);
227
286
  export const ComboBoxStateContext = createContext<ComboBoxState<unknown> | null>(null);
287
+ export const ComboBoxValueContext = ComboBoxContext;
228
288
 
229
289
  // ============================================
230
290
  // COMPONENTS
@@ -236,7 +296,7 @@ export const ComboBoxStateContext = createContext<ComboBoxState<unknown> | null>
236
296
  export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
237
297
  const [local, stateProps, ariaProps] = splitProps(
238
298
  props,
239
- ['class', 'style', 'slot'],
299
+ ['class', 'style', 'slot', 'children'],
240
300
  [
241
301
  'items',
242
302
  'getKey',
@@ -257,12 +317,14 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
257
317
  'allowsEmptyCollection',
258
318
  'menuTrigger',
259
319
  'name',
320
+ 'formValue',
260
321
  ]
261
322
  );
262
323
 
263
324
  // Refs
264
325
  let inputRef: HTMLInputElement | null = null;
265
326
  let buttonRef: HTMLElement | null = null;
327
+ let listBoxRef: HTMLElement | null = null;
266
328
 
267
329
  // Create combobox state
268
330
  const state = createComboBoxState<T>({
@@ -331,12 +393,25 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
331
393
  },
332
394
  });
333
395
 
396
+ const effectiveFormValue = createMemo<'key' | 'text'>(() => {
397
+ if (stateProps.allowsCustomValue) {
398
+ return 'text';
399
+ }
400
+ return stateProps.formValue ?? 'key';
401
+ });
402
+
334
403
  // Create combobox aria props
335
404
  const comboBoxAria = createComboBox<T>(
336
- ariaProps,
405
+ {
406
+ ...ariaProps,
407
+ get name() {
408
+ return effectiveFormValue() === 'text' ? stateProps.name : undefined;
409
+ },
410
+ },
337
411
  state,
338
412
  () => inputRef,
339
- () => buttonRef
413
+ () => buttonRef,
414
+ () => listBoxRef
340
415
  );
341
416
 
342
417
  // Create hover for wrapper
@@ -353,6 +428,7 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
353
428
  isFocusVisible: comboBoxAria.isFocusVisible(),
354
429
  isDisabled: !!ariaProps.isDisabled,
355
430
  isRequired: !!ariaProps.isRequired,
431
+ isInvalid: !!ariaProps.isInvalid,
356
432
  isSelected: state.selectedKey() != null,
357
433
  inputValue: state.inputValue(),
358
434
  }));
@@ -387,6 +463,8 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
387
463
  buttonProps: comboBoxAria.buttonProps,
388
464
  listBoxProps: comboBoxAria.listBoxProps,
389
465
  labelProps: comboBoxAria.labelProps,
466
+ descriptionProps: comboBoxAria.descriptionProps,
467
+ errorMessageProps: comboBoxAria.errorMessageProps,
390
468
  isOpen: comboBoxAria.isOpen,
391
469
  isFocused: comboBoxAria.isFocused,
392
470
  isFocusVisible: comboBoxAria.isFocusVisible,
@@ -395,6 +473,8 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
395
473
  setInputRef: (el) => { inputRef = el; },
396
474
  buttonRef: () => buttonRef,
397
475
  setButtonRef: (el) => { buttonRef = el; },
476
+ listBoxRef: () => listBoxRef,
477
+ setListBoxRef: (el) => { listBoxRef = el; },
398
478
  }}
399
479
  >
400
480
  <ComboBoxStateContext.Provider value={state}>
@@ -408,28 +488,113 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
408
488
  data-focus-visible={comboBoxAria.isFocusVisible() || undefined}
409
489
  data-disabled={ariaProps.isDisabled || undefined}
410
490
  data-required={ariaProps.isRequired || undefined}
491
+ data-invalid={ariaProps.isInvalid || undefined}
411
492
  data-hovered={isHovered() || undefined}
412
493
  >
413
- {/* Hidden input for form submission */}
414
- <Show when={stateProps.name}>
494
+ {/* Hidden input for key-based form submission parity */}
495
+ <Show when={stateProps.name && effectiveFormValue() === 'key'}>
415
496
  <input
416
497
  type="hidden"
417
498
  name={stateProps.name}
418
499
  value={state.selectedKey()?.toString() ?? ''}
419
500
  />
420
501
  </Show>
421
- {props.children}
502
+ {local.children}
422
503
  </div>
423
504
  </ComboBoxStateContext.Provider>
424
505
  </ComboBoxContext.Provider>
425
506
  );
426
507
  }
427
508
 
509
+ /**
510
+ * Label element for a combobox.
511
+ */
512
+ export function ComboBoxLabel(props: ComboBoxLabelProps): JSX.Element {
513
+ const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
514
+
515
+ const context = useContext(ComboBoxContext);
516
+ if (!context) {
517
+ throw new Error('ComboBoxLabel must be used within a ComboBox');
518
+ }
519
+
520
+ const cleanLabelProps = () => {
521
+ const { ref: _ref, ...rest } = context.labelProps as Record<string, unknown>;
522
+ return rest;
523
+ };
524
+
525
+ return (
526
+ <label
527
+ {...domProps}
528
+ {...cleanLabelProps()}
529
+ class={local.class}
530
+ style={local.style}
531
+ >
532
+ {local.children}
533
+ </label>
534
+ );
535
+ }
536
+
537
+ /**
538
+ * Description element for a combobox.
539
+ */
540
+ export function ComboBoxDescription(props: ComboBoxDescriptionProps): JSX.Element {
541
+ const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
542
+
543
+ const context = useContext(ComboBoxContext);
544
+ if (!context) {
545
+ throw new Error('ComboBoxDescription must be used within a ComboBox');
546
+ }
547
+
548
+ const cleanDescriptionProps = () => {
549
+ const { ref: _ref, ...rest } = context.descriptionProps as Record<string, unknown>;
550
+ return rest;
551
+ };
552
+
553
+ return (
554
+ <div
555
+ {...domProps}
556
+ {...cleanDescriptionProps()}
557
+ class={local.class}
558
+ style={local.style}
559
+ >
560
+ {local.children}
561
+ </div>
562
+ );
563
+ }
564
+
565
+ /**
566
+ * Error message element for a combobox.
567
+ */
568
+ export function ComboBoxErrorMessage(props: ComboBoxErrorMessageProps): JSX.Element {
569
+ const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
570
+
571
+ const context = useContext(ComboBoxContext);
572
+ if (!context) {
573
+ throw new Error('ComboBoxErrorMessage must be used within a ComboBox');
574
+ }
575
+
576
+ const cleanErrorMessageProps = () => {
577
+ const { ref: _ref, ...rest } = context.errorMessageProps as Record<string, unknown>;
578
+ return rest;
579
+ };
580
+
581
+ return (
582
+ <div
583
+ {...domProps}
584
+ {...cleanErrorMessageProps()}
585
+ class={local.class}
586
+ style={local.style}
587
+ >
588
+ {local.children}
589
+ </div>
590
+ );
591
+ }
592
+
428
593
  /**
429
594
  * The text input for a combobox.
430
595
  */
431
596
  export function ComboBoxInput(props: ComboBoxInputProps): JSX.Element {
432
- const [local] = splitProps(props, ['class', 'style', 'slot']);
597
+ const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
433
598
 
434
599
  // Get context
435
600
  const context = useContext(ComboBoxContext);
@@ -458,7 +623,7 @@ export function ComboBoxInput(props: ComboBoxInputProps): JSX.Element {
458
623
  // Resolve render props
459
624
  const renderProps = useRenderProps(
460
625
  {
461
- children: props.children,
626
+ children: local.children,
462
627
  class: local.class,
463
628
  style: local.style,
464
629
  defaultClassName: 'solidaria-ComboBox-input',
@@ -478,6 +643,7 @@ export function ComboBoxInput(props: ComboBoxInputProps): JSX.Element {
478
643
 
479
644
  return (
480
645
  <input
646
+ {...domProps}
481
647
  ref={(el) => setInputRef(el)}
482
648
  {...cleanInputProps()}
483
649
  {...cleanHoverProps()}
@@ -493,11 +659,45 @@ export function ComboBoxInput(props: ComboBoxInputProps): JSX.Element {
493
659
  );
494
660
  }
495
661
 
662
+ export function ComboBoxValue(props: ComboBoxValueProps): JSX.Element {
663
+ const context = useContext(ComboBoxContext);
664
+ if (!context) {
665
+ throw new Error('ComboBoxValue must be used within a ComboBox');
666
+ }
667
+
668
+ const state = context.state;
669
+ const textValue = createMemo(() => state.inputValue() ?? '');
670
+ const isPlaceholder = createMemo(() => textValue().length === 0);
671
+
672
+ const renderProps = useRenderProps(
673
+ {
674
+ children: props.children,
675
+ class: props.class,
676
+ style: props.style,
677
+ defaultClassName: 'solidaria-ComboBox-value',
678
+ },
679
+ () => ({
680
+ textValue: textValue(),
681
+ isPlaceholder: isPlaceholder(),
682
+ })
683
+ );
684
+
685
+ return (
686
+ <span
687
+ class={renderProps.class()}
688
+ style={renderProps.style()}
689
+ data-placeholder={isPlaceholder() || undefined}
690
+ >
691
+ {props.children ? renderProps.renderChildren() : textValue()}
692
+ </span>
693
+ );
694
+ }
695
+
496
696
  /**
497
697
  * The trigger button for a combobox.
498
698
  */
499
699
  export function ComboBoxButton(props: ComboBoxButtonProps): JSX.Element {
500
- const [local] = splitProps(props, ['class', 'style', 'slot']);
700
+ const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
501
701
 
502
702
  // Get context
503
703
  const context = useContext(ComboBoxContext);
@@ -513,22 +713,19 @@ export function ComboBoxButton(props: ComboBoxButtonProps): JSX.Element {
513
713
  },
514
714
  });
515
715
 
516
- // Track pressed state
517
- let isPressed = false;
518
-
519
716
  // Render props values
520
717
  const renderValues = createMemo<ComboBoxButtonRenderProps>(() => ({
521
718
  isOpen: isOpen(),
522
719
  isFocused: isFocused(),
523
720
  isHovered: isHovered(),
524
- isPressed,
721
+ isPressed: isOpen(),
525
722
  isDisabled: state.isDisabled,
526
723
  }));
527
724
 
528
725
  // Resolve render props
529
726
  const renderProps = useRenderProps(
530
727
  {
531
- children: props.children,
728
+ children: local.children,
532
729
  class: local.class,
533
730
  style: local.style,
534
731
  defaultClassName: 'solidaria-ComboBox-button',
@@ -548,6 +745,7 @@ export function ComboBoxButton(props: ComboBoxButtonProps): JSX.Element {
548
745
 
549
746
  return (
550
747
  <button
748
+ {...domProps}
551
749
  ref={(el) => setButtonRef(el)}
552
750
  {...cleanButtonProps()}
553
751
  {...cleanHoverProps()}
@@ -567,14 +765,21 @@ export function ComboBoxButton(props: ComboBoxButtonProps): JSX.Element {
567
765
  * The listbox popup for a combobox.
568
766
  */
569
767
  export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element {
570
- const [local] = splitProps(props, ['class', 'style', 'slot']);
768
+ const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
571
769
 
572
770
  // Get context
573
771
  const context = useContext(ComboBoxContext);
574
772
  if (!context) {
575
773
  throw new Error('ComboBoxListBox must be used within a ComboBox');
576
774
  }
577
- const { listBoxProps: contextListBoxProps, state: comboBoxState, isOpen, inputRef } = context;
775
+ const {
776
+ listBoxProps: contextListBoxProps,
777
+ state: comboBoxState,
778
+ isOpen,
779
+ inputRef,
780
+ buttonRef,
781
+ setListBoxRef,
782
+ } = context;
578
783
  const state = comboBoxState as ComboBoxState<T>;
579
784
 
580
785
  // Ref for the listbox element (for click outside detection)
@@ -587,9 +792,13 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
587
792
  // Don't close if clicking the input or button
588
793
  const target = e.target as HTMLElement;
589
794
  const input = inputRef();
795
+ const button = buttonRef();
590
796
  if (input?.contains(target)) {
591
797
  return;
592
798
  }
799
+ if (button?.contains(target)) {
800
+ return;
801
+ }
593
802
  if (isOpen()) {
594
803
  state.close();
595
804
  }
@@ -601,31 +810,8 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
601
810
 
602
811
  // Create listbox aria props using ComboBoxState's ListState-compatible interface
603
812
  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
813
+ contextListBoxProps as unknown as AriaListBoxProps,
814
+ createComboBoxListStateAdapter(state)
629
815
  );
630
816
 
631
817
  // Render props values
@@ -659,8 +845,14 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
659
845
  // This is critical - if we don't prevent default, the input loses focus
660
846
  // and the blur handler closes the menu before the click can be processed
661
847
  // We need to attach this in the ref callback to use capture phase
848
+ let cleanupFocusGuard: (() => void) | undefined;
849
+
662
850
  const setupMouseDownHandler = (el: HTMLUListElement) => {
851
+ cleanupFocusGuard?.();
852
+ cleanupFocusGuard = undefined;
853
+
663
854
  listBoxRef = el;
855
+ setListBoxRef(el);
664
856
  if (el) {
665
857
  const mouseHandler = (e: MouseEvent) => {
666
858
  e.preventDefault();
@@ -670,12 +862,22 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
670
862
  };
671
863
  el.addEventListener('mousedown', mouseHandler, true); // capture phase
672
864
  el.addEventListener('pointerdown', pointerHandler, true); // capture phase
865
+ cleanupFocusGuard = () => {
866
+ el.removeEventListener('mousedown', mouseHandler, true);
867
+ el.removeEventListener('pointerdown', pointerHandler, true);
868
+ };
673
869
  }
674
870
  };
675
871
 
872
+ onCleanup(() => {
873
+ cleanupFocusGuard?.();
874
+ setListBoxRef(null);
875
+ });
876
+
676
877
  return (
677
878
  <Show when={isOpen()}>
678
879
  <ul
880
+ {...domProps}
679
881
  ref={setupMouseDownHandler}
680
882
  {...cleanContextProps()}
681
883
  {...cleanListBoxProps()}
@@ -683,7 +885,7 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
683
885
  style={renderProps.style()}
684
886
  data-focused={state.isFocused() || undefined}
685
887
  >
686
- <Show when={props.children} fallback={
888
+ <Show when={local.children} fallback={
687
889
  <For each={items()}>
688
890
  {(node) => (
689
891
  <ComboBoxOption id={node.key}>
@@ -693,7 +895,7 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
693
895
  </For>
694
896
  }>
695
897
  <For each={items()}>
696
- {(node) => node.value != null ? props.children!(node.value) : null}
898
+ {(node) => node.value != null ? (local.children as Function)!(node.value) : null}
697
899
  </For>
698
900
  </Show>
699
901
  </ul>
@@ -732,30 +934,7 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
732
934
  return ariaProps['aria-label'];
733
935
  },
734
936
  },
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();
750
- },
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
937
+ createComboBoxListStateAdapter(state)
759
938
  );
760
939
 
761
940
  // Create hover
@@ -786,6 +965,10 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
786
965
  renderValues
787
966
  );
788
967
 
968
+ const selectionIndicatorContext = createMemo<SelectionIndicatorContextValue>(() => ({
969
+ isSelected: optionAria.isSelected,
970
+ }));
971
+
789
972
  // Remove ref from spread props
790
973
  const cleanOptionProps = () => {
791
974
  const { ref: _ref1, ...rest } = optionAria.optionProps as Record<string, unknown>;
@@ -797,20 +980,22 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
797
980
  };
798
981
 
799
982
  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}
811
- >
812
- {renderProps.renderChildren()}
813
- </li>
983
+ <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
984
+ <li
985
+ {...cleanOptionProps()}
986
+ {...cleanHoverProps()}
987
+ class={renderProps.class()}
988
+ style={renderProps.style()}
989
+ data-selected={optionAria.isSelected() || undefined}
990
+ data-focused={optionAria.isFocused() || undefined}
991
+ data-focus-visible={optionAria.isFocusVisible() || undefined}
992
+ data-pressed={optionAria.isPressed() || undefined}
993
+ data-hovered={isHovered() || undefined}
994
+ data-disabled={optionAria.isDisabled() || undefined}
995
+ >
996
+ {renderProps.renderChildren()}
997
+ </li>
998
+ </SelectionIndicatorContext.Provider>
814
999
  );
815
1000
  }
816
1001
 
@@ -819,6 +1004,55 @@ ComboBox.Input = ComboBoxInput;
819
1004
  ComboBox.Button = ComboBoxButton;
820
1005
  ComboBox.ListBox = ComboBoxListBox;
821
1006
  ComboBox.Option = ComboBoxOption;
1007
+ ComboBox.Label = ComboBoxLabel;
1008
+ ComboBox.Description = ComboBoxDescription;
1009
+ ComboBox.ErrorMessage = ComboBoxErrorMessage;
822
1010
 
823
1011
  // Re-export filter function for convenience
824
1012
  export { defaultContainsFilter };
1013
+
1014
+ function createComboBoxListStateAdapter<T>(state: ComboBoxState<T>): ListState<T> {
1015
+ const selectedKeys = createMemo(() => {
1016
+ const key = state.selectedKey();
1017
+ return key != null ? new Set<Key>([key]) : new Set<Key>();
1018
+ });
1019
+
1020
+ const disabledKeys = createMemo(() => {
1021
+ const keys = new Set<Key>();
1022
+ for (const node of state.collection()) {
1023
+ if (node.isDisabled) keys.add(node.key);
1024
+ }
1025
+ return keys;
1026
+ });
1027
+
1028
+ return {
1029
+ collection: state.collection,
1030
+ isFocused: state.isFocused,
1031
+ setFocused: state.setFocused,
1032
+ focusedKey: state.focusedKey,
1033
+ setFocusedKey: (key) => state.setFocusedKey(key ?? null),
1034
+ childFocusStrategy: () => null,
1035
+ selectionMode: state.selectionMode,
1036
+ selectionBehavior: () => 'replace',
1037
+ disallowEmptySelection: () => true,
1038
+ selectedKeys,
1039
+ disabledKeys,
1040
+ disabledBehavior: () => 'all',
1041
+ isEmpty: () => selectedKeys().size === 0,
1042
+ isSelectAll: () => false,
1043
+ isSelected: state.isSelected,
1044
+ isDisabled: state.isKeyDisabled,
1045
+ setSelectionBehavior: () => {},
1046
+ toggleSelection: (key) => state.select(key),
1047
+ replaceSelection: (key) => state.select(key),
1048
+ setSelectedKeys: (keys) => {
1049
+ const first = keys[Symbol.iterator]().next().value as Key | undefined;
1050
+ state.setSelectedKey(first ?? null);
1051
+ },
1052
+ selectAll: () => {},
1053
+ clearSelection: () => state.setSelectedKey(null),
1054
+ toggleSelectAll: () => {},
1055
+ extendSelection: (toKey) => state.select(toKey),
1056
+ select: (key) => state.select(key),
1057
+ };
1058
+ }