@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/Select.tsx CHANGED
@@ -9,12 +9,16 @@ import {
9
9
  type JSX,
10
10
  type Accessor,
11
11
  createContext,
12
+ createEffect,
12
13
  createMemo,
14
+ createSignal,
15
+ createUniqueId,
13
16
  splitProps,
14
17
  useContext,
15
18
  For,
16
19
  Show,
17
- } from 'solid-js';
20
+ untrack,
21
+ } from "solid-js";
18
22
  import {
19
23
  createSelect,
20
24
  createHiddenSelect,
@@ -23,15 +27,22 @@ import {
23
27
  createHover,
24
28
  createInteractOutside,
25
29
  FocusScope,
30
+ focusSafely,
31
+ mergeProps,
26
32
  type AriaSelectProps,
33
+ type AriaListBoxProps,
27
34
  type AriaOptionProps,
28
- } from '@proyecto-viviana/solidaria';
35
+ } from "@proyecto-viviana/solidaria";
29
36
  import {
30
37
  createSelectState,
38
+ type ListState,
31
39
  type SelectState,
32
40
  type Key,
33
41
  type CollectionNode,
34
- } from '@proyecto-viviana/solid-stately';
42
+ DEFAULT_VALIDATION_RESULT,
43
+ type ValidationResult,
44
+ } from "@proyecto-viviana/solid-stately";
45
+ import { FieldErrorContext, type FieldErrorContextValue } from "./FieldError";
35
46
  import {
36
47
  type RenderChildren,
37
48
  type ClassNameOrFunction,
@@ -39,11 +50,43 @@ import {
39
50
  type SlotProps,
40
51
  useRenderProps,
41
52
  filterDOMProps,
42
- } from './utils';
53
+ } from "./utils";
54
+ import {
55
+ SelectionIndicatorContext,
56
+ type SelectionIndicatorContextValue,
57
+ } from "./SelectionIndicator";
58
+ import { ListBoxLoadMoreItem } from "./ListBox";
43
59
 
44
- // ============================================
45
- // TYPES
46
- // ============================================
60
+ type RefLike<T> = ((el: T) => void) | { current?: T | null } | undefined;
61
+
62
+ function assignRef<T>(ref: RefLike<T>, el: T): void {
63
+ if (!ref) return;
64
+ if (typeof ref === "function") {
65
+ ref(el);
66
+ } else {
67
+ ref.current = el;
68
+ }
69
+ }
70
+
71
+ function getNativeSelectValidation(select: HTMLSelectElement): ValidationResult {
72
+ return {
73
+ isInvalid: !select.validity.valid,
74
+ validationDetails: {
75
+ badInput: select.validity.badInput,
76
+ customError: select.validity.customError,
77
+ patternMismatch: select.validity.patternMismatch,
78
+ rangeOverflow: select.validity.rangeOverflow,
79
+ rangeUnderflow: select.validity.rangeUnderflow,
80
+ stepMismatch: select.validity.stepMismatch,
81
+ tooLong: select.validity.tooLong,
82
+ tooShort: select.validity.tooShort,
83
+ typeMismatch: select.validity.typeMismatch,
84
+ valueMissing: select.validity.valueMissing,
85
+ valid: select.validity.valid,
86
+ },
87
+ validationErrors: select.validationMessage ? [select.validationMessage] : [],
88
+ };
89
+ }
47
90
 
48
91
  export interface SelectRenderProps {
49
92
  /** Whether the select is open. */
@@ -60,9 +103,7 @@ export interface SelectRenderProps {
60
103
  isSelected: boolean;
61
104
  }
62
105
 
63
- export interface SelectProps<T>
64
- extends Omit<AriaSelectProps, 'children'>,
65
- SlotProps {
106
+ export interface SelectProps<T> extends Omit<AriaSelectProps, "children">, SlotProps {
66
107
  /** The items to render in the select. */
67
108
  items: T[];
68
109
  /** Function to get the key from an item. */
@@ -73,12 +114,20 @@ export interface SelectProps<T>
73
114
  getDisabled?: (item: T) => boolean;
74
115
  /** Keys of disabled items. */
75
116
  disabledKeys?: Iterable<Key>;
117
+ /** Selection mode. */
118
+ selectionMode?: "single" | "multiple";
76
119
  /** The currently selected key (controlled). */
77
120
  selectedKey?: Key | null;
78
121
  /** The default selected key (uncontrolled). */
79
122
  defaultSelectedKey?: Key | null;
123
+ /** Currently selected keys (controlled, for multiple selection). */
124
+ selectedKeys?: "all" | Iterable<Key>;
125
+ /** Default selected keys (uncontrolled, for multiple selection). */
126
+ defaultSelectedKeys?: "all" | Iterable<Key>;
80
127
  /** Handler called when selection changes. */
81
128
  onSelectionChange?: (key: Key | null) => void;
129
+ /** Handler called when selected keys change. */
130
+ onSelectionChangeKeys?: (keys: "all" | Set<Key>) => void;
82
131
  /** Whether the select is open (controlled). */
83
132
  isOpen?: boolean;
84
133
  /** Whether the select is open by default (uncontrolled). */
@@ -90,16 +139,25 @@ export interface SelectProps<T>
90
139
  /** The name of the select, used when submitting an HTML form. */
91
140
  name?: string;
92
141
  /** The children of the component (compound components: SelectTrigger, SelectListBox). */
93
- children: JSX.Element;
142
+ children: RenderChildren<SelectRenderProps>;
94
143
  /** The CSS className for the element. */
95
144
  class?: ClassNameOrFunction<SelectRenderProps>;
96
145
  /** The inline style for the element. */
97
146
  style?: StyleOrFunction<SelectRenderProps>;
147
+ /** Custom renderer for the outer select element. */
148
+ render?: (
149
+ props: JSX.HTMLAttributes<HTMLDivElement>,
150
+ renderProps: SelectRenderProps,
151
+ ) => JSX.Element;
152
+ /** Ref for the outer select element. */
153
+ ref?: RefLike<HTMLDivElement>;
98
154
  }
99
155
 
100
156
  export interface SelectValueRenderProps<T> {
101
157
  /** The selected item. */
102
158
  selectedItem: CollectionNode<T> | null;
159
+ /** The selected items. */
160
+ selectedItems: CollectionNode<T>[];
103
161
  /** The text value of the selected item. */
104
162
  selectedText: string | null;
105
163
  /** Whether a value is selected. */
@@ -149,6 +207,18 @@ export interface SelectListBoxRenderProps {
149
207
  export interface SelectListBoxProps<T> extends SlotProps {
150
208
  /** The children of the listbox. A function may be provided to render each item. */
151
209
  children?: (item: T) => JSX.Element;
210
+ /** Content to display when the listbox has no items. */
211
+ renderEmptyState?: () => JSX.Element;
212
+ /** Called when the load more sentinel becomes visible. */
213
+ onLoadMore?: () => void | Promise<void>;
214
+ /** Whether additional items are currently loading. */
215
+ isLoading?: boolean;
216
+ /** Content to display in the load more sentinel row. */
217
+ renderLoadMore?: () => JSX.Element | undefined;
218
+ /** CSS class for the load more sentinel row. */
219
+ loadMoreClass?: ClassNameOrFunction<{ isLoading: boolean }>;
220
+ /** Whether the listbox is rendered inside an overlay popover. */
221
+ isInPopover?: boolean;
152
222
  /** The CSS className for the element. */
153
223
  class?: ClassNameOrFunction<SelectListBoxRenderProps>;
154
224
  /** The inline style for the element. */
@@ -170,9 +240,7 @@ export interface SelectOptionRenderProps {
170
240
  isDisabled: boolean;
171
241
  }
172
242
 
173
- export interface SelectOptionProps<T>
174
- extends Omit<AriaOptionProps, 'children' | 'key'>,
175
- SlotProps {
243
+ export interface SelectOptionProps<T> extends Omit<AriaOptionProps, "children" | "key">, SlotProps {
176
244
  /** The unique key for the option. */
177
245
  id: Key;
178
246
  /** The item value. */
@@ -187,41 +255,85 @@ export interface SelectOptionProps<T>
187
255
  textValue?: string;
188
256
  }
189
257
 
190
- // ============================================
191
- // CONTEXT
192
- // ============================================
193
-
194
258
  interface SelectContextValue<T> {
195
259
  state: SelectState<T>;
260
+ rootRef: Accessor<HTMLElement | null>;
261
+ triggerRef: Accessor<HTMLElement | null>;
262
+ setTriggerRef: (el: HTMLElement | null) => void;
196
263
  triggerProps: JSX.HTMLAttributes<HTMLElement>;
197
264
  valueProps: JSX.HTMLAttributes<HTMLElement>;
265
+ labelProps: JSX.HTMLAttributes<HTMLElement>;
198
266
  menuProps: JSX.HTMLAttributes<HTMLElement>;
267
+ errorMessageProps?: JSX.HTMLAttributes<HTMLElement>;
268
+ validation?: ValidationResult;
199
269
  isOpen: Accessor<boolean>;
200
270
  isFocused: Accessor<boolean>;
201
271
  isFocusVisible: Accessor<boolean>;
272
+ isDisabled: Accessor<boolean>;
202
273
  placeholder?: string;
203
274
  items: T[];
204
275
  renderItem?: (item: T) => JSX.Element;
276
+ slots?: Record<string, Partial<SelectProps<T>>>;
277
+ autoFocus?: boolean;
205
278
  }
206
279
 
207
280
  export const SelectContext = createContext<SelectContextValue<unknown> | null>(null);
208
281
  export const SelectStateContext = createContext<SelectState<unknown> | null>(null);
282
+ export const SelectValueContext = SelectContext;
209
283
 
210
- // ============================================
211
- // COMPONENTS
212
- // ============================================
284
+ const selectRootLabelProps = new Set([
285
+ "aria-label",
286
+ "aria-labelledby",
287
+ "aria-describedby",
288
+ "aria-details",
289
+ ]);
213
290
 
214
291
  /**
215
292
  * A select displays a collapsible list of options and allows a user to select one of them.
216
293
  */
217
294
  export function Select<T>(props: SelectProps<T>): JSX.Element {
295
+ const parentContext = useContext(SelectContext) as SelectContextValue<T> | null;
296
+ const contextSlotProps = parentContext?.slots?.[props.slot ?? "default"] as
297
+ | Partial<SelectProps<T>>
298
+ | undefined;
299
+ const mergedSelectProps = (
300
+ contextSlotProps ? mergeProps(contextSlotProps, props) : props
301
+ ) as SelectProps<T>;
218
302
  const [local, stateProps, ariaProps] = splitProps(
219
- props,
220
- ['class', 'style', 'slot'],
221
- ['items', 'getKey', 'getTextValue', 'getDisabled', 'disabledKeys', 'selectedKey', 'defaultSelectedKey', 'onSelectionChange', 'isOpen', 'defaultOpen', 'onOpenChange', 'name']
303
+ mergedSelectProps,
304
+ ["class", "style", "render", "ref", "slot", "children"],
305
+ [
306
+ "items",
307
+ "getKey",
308
+ "getTextValue",
309
+ "getDisabled",
310
+ "disabledKeys",
311
+ "selectionMode",
312
+ "selectedKey",
313
+ "defaultSelectedKey",
314
+ "selectedKeys",
315
+ "defaultSelectedKeys",
316
+ "onSelectionChange",
317
+ "onSelectionChangeKeys",
318
+ "isOpen",
319
+ "defaultOpen",
320
+ "onOpenChange",
321
+ "name",
322
+ ],
222
323
  );
324
+ let rootRef: HTMLDivElement | undefined;
325
+ const [selectValidation, setSelectValidation] =
326
+ createSignal<ValidationResult>(DEFAULT_VALIDATION_RESULT);
327
+ const errorMessageId = createUniqueId();
328
+
329
+ const resolveDisabled = (): boolean => {
330
+ const disabled = ariaProps.isDisabled;
331
+ if (typeof disabled === "function") {
332
+ return (disabled as () => boolean)();
333
+ }
334
+ return !!disabled;
335
+ };
223
336
 
224
- // Create select state
225
337
  const state = createSelectState<T>({
226
338
  get items() {
227
339
  return stateProps.items;
@@ -238,15 +350,27 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
238
350
  get disabledKeys() {
239
351
  return stateProps.disabledKeys;
240
352
  },
353
+ get selectionMode() {
354
+ return stateProps.selectionMode;
355
+ },
241
356
  get selectedKey() {
242
357
  return stateProps.selectedKey;
243
358
  },
244
359
  get defaultSelectedKey() {
245
360
  return stateProps.defaultSelectedKey;
246
361
  },
362
+ get selectedKeys() {
363
+ return stateProps.selectedKeys;
364
+ },
365
+ get defaultSelectedKeys() {
366
+ return stateProps.defaultSelectedKeys;
367
+ },
247
368
  get onSelectionChange() {
248
369
  return stateProps.onSelectionChange;
249
370
  },
371
+ get onSelectionChangeKeys() {
372
+ return stateProps.onSelectionChangeKeys;
373
+ },
250
374
  get isOpen() {
251
375
  return stateProps.isOpen;
252
376
  },
@@ -257,113 +381,347 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
257
381
  return stateProps.onOpenChange;
258
382
  },
259
383
  get isDisabled() {
260
- return ariaProps.isDisabled;
384
+ return resolveDisabled();
261
385
  },
262
386
  get isRequired() {
263
387
  return ariaProps.isRequired;
264
388
  },
265
389
  });
266
390
 
267
- // Create select aria props
268
- const { triggerProps, valueProps, menuProps, isFocused, isFocusVisible, isOpen } = createSelect<T>(
269
- ariaProps,
270
- state
271
- );
391
+ const selectAriaProps = createMemo(() => {
392
+ const clean: Record<string, unknown> = {};
393
+ for (const key in ariaProps as Record<string, unknown>) {
394
+ if (!key.startsWith("data-")) {
395
+ clean[key] = (ariaProps as Record<string, unknown>)[key];
396
+ }
397
+ }
398
+ return clean as typeof ariaProps;
399
+ });
400
+
401
+ const { labelProps, triggerProps, valueProps, menuProps, isFocused, isFocusVisible, isOpen } =
402
+ createSelect<T>(selectAriaProps, state);
272
403
 
273
- // Create hover for wrapper
274
404
  const { isHovered, hoverProps } = createHover({
275
405
  get isDisabled() {
276
- return ariaProps.isDisabled;
406
+ return resolveDisabled();
277
407
  },
278
408
  });
279
409
 
280
- // Render props values
281
410
  const renderValues = createMemo<SelectRenderProps>(() => ({
282
411
  isOpen: isOpen(),
283
412
  isFocused: isFocused(),
284
413
  isFocusVisible: isFocusVisible(),
285
- isDisabled: !!ariaProps.isDisabled,
414
+ isDisabled: resolveDisabled(),
286
415
  isRequired: !!ariaProps.isRequired,
287
- isSelected: state.selectedKey() != null,
416
+ isSelected:
417
+ state.selectionMode() === "multiple"
418
+ ? state.selectedKeys() === "all" || (state.selectedKeys() as Set<Key>).size > 0
419
+ : state.selectedKey() != null,
288
420
  }));
421
+ const childRenderValues: SelectRenderProps = {
422
+ get isOpen() {
423
+ return isOpen();
424
+ },
425
+ get isFocused() {
426
+ return isFocused();
427
+ },
428
+ get isFocusVisible() {
429
+ return isFocusVisible();
430
+ },
431
+ get isDisabled() {
432
+ return resolveDisabled();
433
+ },
434
+ get isRequired() {
435
+ return !!ariaProps.isRequired;
436
+ },
437
+ get isSelected() {
438
+ return hasSelection();
439
+ },
440
+ };
289
441
 
290
- // Resolve render props
291
442
  const renderProps = useRenderProps(
292
443
  {
293
444
  class: local.class,
294
445
  style: local.style,
295
- defaultClassName: 'solidaria-Select',
446
+ defaultClassName: "solidaria-Select",
296
447
  },
297
- renderValues
448
+ renderValues,
298
449
  );
299
450
 
300
- // Filter DOM props
301
451
  const domProps = createMemo(() => {
302
452
  const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
453
+ for (const key of selectRootLabelProps) {
454
+ delete filtered[key];
455
+ }
303
456
  return filtered;
304
457
  });
305
458
 
306
- // Remove ref from hover props
307
459
  const cleanHoverProps = () => {
308
460
  const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
309
461
  return rest;
310
462
  };
463
+ const cleanLabelProps = () => {
464
+ const { ref: _ref, ...rest } = labelProps as Record<string, unknown>;
465
+ return rest;
466
+ };
467
+ const setRootRef = (el: HTMLDivElement) => {
468
+ rootRef = el;
469
+ assignRef(local.ref, el);
470
+ };
471
+ const validation = createMemo<ValidationResult>(() => {
472
+ const current = selectValidation();
473
+ if (current.isInvalid || !ariaProps.isInvalid) {
474
+ return current;
475
+ }
476
+
477
+ return {
478
+ ...DEFAULT_VALIDATION_RESULT,
479
+ isInvalid: true,
480
+ };
481
+ });
482
+ const isInvalid = createMemo(() => validation().isInvalid);
483
+ const triggerDescribedBy = () => {
484
+ const ids = [
485
+ (triggerProps as { "aria-describedby"?: string })["aria-describedby"],
486
+ isInvalid() ? errorMessageId : undefined,
487
+ ]
488
+ .filter(Boolean)
489
+ .join(" ")
490
+ .split(" ")
491
+ .filter(Boolean);
492
+ return ids.length ? Array.from(new Set(ids)).join(" ") : undefined;
493
+ };
494
+ const triggerPropsWithValidation = () =>
495
+ ({
496
+ ...triggerProps,
497
+ "aria-describedby": triggerDescribedBy(),
498
+ }) as JSX.HTMLAttributes<HTMLElement>;
499
+ const fieldErrorContext: FieldErrorContextValue = {
500
+ get validation() {
501
+ return validation();
502
+ },
503
+ get errorMessageProps() {
504
+ return { id: errorMessageId };
505
+ },
506
+ };
507
+ const focusTrigger = () => {
508
+ triggerRef?.focus();
509
+ };
510
+ const hasSelection = () =>
511
+ state.selectionMode() === "multiple"
512
+ ? state.selectedKeys() === "all" || (state.selectedKeys() as Set<Key>).size > 0
513
+ : state.selectedKey() != null;
514
+ const hasNativeValidation = () => (ariaProps.validationBehavior ?? "native") === "native";
515
+ const getSelectValidation = (select: HTMLSelectElement): ValidationResult => {
516
+ if (ariaProps.isRequired && !hasSelection()) {
517
+ return {
518
+ isInvalid: true,
519
+ validationDetails: {
520
+ badInput: false,
521
+ customError: false,
522
+ patternMismatch: false,
523
+ rangeOverflow: false,
524
+ rangeUnderflow: false,
525
+ stepMismatch: false,
526
+ tooLong: false,
527
+ tooShort: false,
528
+ typeMismatch: false,
529
+ valueMissing: true,
530
+ valid: false,
531
+ },
532
+ validationErrors: [select.validationMessage || "Constraints not satisfied"],
533
+ };
534
+ }
535
+ return getNativeSelectValidation(select);
536
+ };
311
537
 
312
- // Create hidden select for form submission
313
538
  const { containerProps, selectProps: hiddenSelectProps } = createHiddenSelect({
314
539
  state,
315
540
  name: stateProps.name,
541
+ form: ariaProps.form,
542
+ isRequired: ariaProps.isRequired,
543
+ validationBehavior: ariaProps.validationBehavior ?? "native",
316
544
  get isDisabled() {
317
- return ariaProps.isDisabled;
545
+ return resolveDisabled();
318
546
  },
319
547
  });
548
+ const handleHiddenSelectInvalid: JSX.EventHandler<HTMLSelectElement, Event> = (event) => {
549
+ setSelectValidation(getSelectValidation(event.currentTarget));
550
+ focusTrigger();
551
+ event.preventDefault();
552
+ };
553
+ const handleHiddenSelectChange: JSX.EventHandler<HTMLSelectElement, Event> = (event) => {
554
+ (hiddenSelectProps as { onChange?: JSX.EventHandler<HTMLSelectElement, Event> }).onChange?.(
555
+ event,
556
+ );
557
+ setSelectValidation(
558
+ hasSelection() && event.currentTarget.validity.valid
559
+ ? DEFAULT_VALIDATION_RESULT
560
+ : getSelectValidation(event.currentTarget),
561
+ );
562
+ };
563
+ createEffect(() => {
564
+ if (hasSelection() && selectValidation().isInvalid) {
565
+ setSelectValidation(DEFAULT_VALIDATION_RESULT);
566
+ }
567
+ });
568
+ let triggerRef: HTMLElement | null = null;
569
+ const setTriggerRef = (el: HTMLElement | null) => {
570
+ triggerRef = el;
571
+ };
572
+
573
+ const RootChildren = () => {
574
+ const selectChildren = untrack(() =>
575
+ typeof local.children === "function"
576
+ ? (local.children as (values: SelectRenderProps) => JSX.Element)(childRenderValues)
577
+ : local.children,
578
+ );
579
+
580
+ return (
581
+ <>
582
+ <div {...containerProps}>
583
+ <select
584
+ {...hiddenSelectProps}
585
+ name={hasSelection() ? undefined : stateProps.name}
586
+ required={
587
+ (hasNativeValidation() && ariaProps.isRequired && !hasSelection()) || undefined
588
+ }
589
+ onInvalid={handleHiddenSelectInvalid}
590
+ onChange={handleHiddenSelectChange}
591
+ >
592
+ <Show when={state.selectionMode() !== "multiple"}>
593
+ <option selected={state.selectedKey() == null} />
594
+ </Show>
595
+ <For each={stateProps.items}>
596
+ {(item) => {
597
+ const itemRecord = isObjectRecord(item) ? item : null;
598
+ const fallbackKey =
599
+ itemRecord != null ? (toKey(itemRecord.key) ?? toKey(itemRecord.id)) : undefined;
600
+ const key = stateProps.getKey?.(item) ?? fallbackKey ?? String(item);
601
+ const fallbackTextValue =
602
+ itemRecord != null
603
+ ? (toTextValue(itemRecord.textValue) ?? toTextValue(itemRecord.label))
604
+ : undefined;
605
+ const textValue =
606
+ stateProps.getTextValue?.(item) ?? fallbackTextValue ?? String(item);
607
+ const selectedKeys = state.selectedKeys();
608
+ const isSelected =
609
+ state.selectionMode() === "multiple"
610
+ ? selectedKeys === "all"
611
+ ? true
612
+ : (selectedKeys as Set<Key>).has(key)
613
+ : key === state.selectedKey();
614
+ return (
615
+ <option value={String(key)} selected={isSelected}>
616
+ {textValue}
617
+ </option>
618
+ );
619
+ }}
620
+ </For>
621
+ </select>
622
+ <Show when={state.selectionMode() === "multiple" && stateProps.name}>
623
+ <For
624
+ each={
625
+ state.selectedKeys() === "all"
626
+ ? Array.from(state.collection()).map((item) => item.key)
627
+ : Array.from(state.selectedKeys() as Set<Key>)
628
+ }
629
+ >
630
+ {(key) => (
631
+ <input
632
+ type="hidden"
633
+ name={stateProps.name}
634
+ form={ariaProps.form}
635
+ value={String(key)}
636
+ disabled={resolveDisabled()}
637
+ />
638
+ )}
639
+ </For>
640
+ </Show>
641
+ <Show
642
+ when={
643
+ state.selectionMode() !== "multiple" && stateProps.name && state.selectedKey() != null
644
+ }
645
+ >
646
+ <input
647
+ type="hidden"
648
+ name={stateProps.name}
649
+ form={ariaProps.form}
650
+ value={String(state.selectedKey())}
651
+ disabled={resolveDisabled()}
652
+ />
653
+ </Show>
654
+ </div>
655
+ <Show when={ariaProps.label}>
656
+ <span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
657
+ </Show>
658
+ {selectChildren}
659
+ </>
660
+ );
661
+ };
662
+ const baseRootProps = () =>
663
+ ({
664
+ ...domProps(),
665
+ ...cleanHoverProps(),
666
+ ref: setRootRef,
667
+ class: renderProps.class(),
668
+ style: renderProps.style(),
669
+ slot: local.slot,
670
+ "data-open": isOpen() || undefined,
671
+ "data-disabled": resolveDisabled() || undefined,
672
+ "data-required": ariaProps.isRequired || undefined,
673
+ "data-invalid": isInvalid() || undefined,
674
+ "data-hovered": isHovered() || undefined,
675
+ }) as JSX.HTMLAttributes<HTMLDivElement>;
676
+ const RootContent = () => {
677
+ const renderedRootChildren = <RootChildren />;
678
+ const rootProps = () =>
679
+ ({
680
+ ...baseRootProps(),
681
+ children: renderedRootChildren,
682
+ }) as JSX.HTMLAttributes<HTMLDivElement>;
683
+
684
+ return local.render ? (
685
+ local.render(rootProps(), renderValues())
686
+ ) : (
687
+ <div {...baseRootProps()}>{renderedRootChildren}</div>
688
+ );
689
+ };
320
690
 
321
691
  return (
322
692
  <SelectContext.Provider
323
- value={{
324
- state,
325
- triggerProps,
326
- valueProps,
327
- menuProps,
328
- isOpen,
329
- isFocused,
330
- isFocusVisible,
331
- placeholder: ariaProps.placeholder,
332
- items: stateProps.items,
333
- }}
693
+ value={
694
+ {
695
+ state,
696
+ rootRef: () => rootRef ?? null,
697
+ triggerRef: () => triggerRef,
698
+ setTriggerRef,
699
+ get triggerProps() {
700
+ return triggerPropsWithValidation();
701
+ },
702
+ valueProps,
703
+ labelProps,
704
+ menuProps,
705
+ get errorMessageProps() {
706
+ return { id: errorMessageId };
707
+ },
708
+ get validation() {
709
+ return validation();
710
+ },
711
+ isOpen,
712
+ isFocused,
713
+ isFocusVisible,
714
+ isDisabled: resolveDisabled,
715
+ placeholder: ariaProps.placeholder,
716
+ items: stateProps.items,
717
+ autoFocus: !!ariaProps.autoFocus,
718
+ } as SelectContextValue<unknown>
719
+ }
334
720
  >
335
721
  <SelectStateContext.Provider value={state}>
336
- <div
337
- {...domProps()}
338
- {...cleanHoverProps()}
339
- class={renderProps.class()}
340
- style={renderProps.style()}
341
- data-open={isOpen() || undefined}
342
- data-focused={isFocused() || undefined}
343
- data-focus-visible={isFocusVisible() || undefined}
344
- data-disabled={ariaProps.isDisabled || undefined}
345
- data-required={ariaProps.isRequired || undefined}
346
- data-hovered={isHovered() || undefined}
347
- >
348
- {/* Hidden select for form submission */}
349
- <div {...containerProps}>
350
- <select {...hiddenSelectProps}>
351
- <option />
352
- <For each={stateProps.items}>
353
- {(item) => {
354
- const key = stateProps.getKey?.(item) ?? (item as any).key ?? (item as any).id;
355
- const textValue = stateProps.getTextValue?.(item) ?? (item as any).textValue ?? (item as any).label ?? String(item);
356
- return (
357
- <option value={String(key)} selected={key === state.selectedKey()}>
358
- {textValue}
359
- </option>
360
- );
361
- }}
362
- </For>
363
- </select>
364
- </div>
365
- {props.children}
366
- </div>
722
+ <FieldErrorContext.Provider value={fieldErrorContext}>
723
+ <RootContent />
724
+ </FieldErrorContext.Provider>
367
725
  </SelectStateContext.Provider>
368
726
  </SelectContext.Provider>
369
727
  );
@@ -373,23 +731,31 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
373
731
  * The trigger button for a select.
374
732
  */
375
733
  export function SelectTrigger(props: SelectTriggerProps): JSX.Element {
376
- const [local] = splitProps(props, ['class', 'style', 'slot']);
734
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
377
735
 
378
- // Get context
379
736
  const context = useContext(SelectContext);
380
737
  if (!context) {
381
- throw new Error('SelectTrigger must be used within a Select');
738
+ throw new Error("SelectTrigger must be used within a Select");
382
739
  }
383
- const { triggerProps, isOpen, isFocused, isFocusVisible, state } = context;
740
+ const { isOpen, isFocused, isFocusVisible, state } = context;
741
+ let triggerRef: HTMLButtonElement | undefined;
742
+ const setTriggerRef = (el: HTMLButtonElement) => {
743
+ triggerRef = el;
744
+ context.setTriggerRef(el);
745
+ };
746
+
747
+ createEffect(() => {
748
+ if (context.autoFocus) {
749
+ triggerRef?.focus();
750
+ }
751
+ });
384
752
 
385
- // Create hover
386
753
  const { isHovered, hoverProps } = createHover({
387
754
  get isDisabled() {
388
755
  return state.isDisabled;
389
756
  },
390
757
  });
391
758
 
392
- // Render props values
393
759
  const renderValues = createMemo<SelectTriggerRenderProps>(() => ({
394
760
  isOpen: isOpen(),
395
761
  isFocused: isFocused(),
@@ -398,32 +764,43 @@ export function SelectTrigger(props: SelectTriggerProps): JSX.Element {
398
764
  isDisabled: state.isDisabled,
399
765
  }));
400
766
 
401
- // Resolve render props
402
767
  const renderProps = useRenderProps(
403
768
  {
404
769
  children: props.children,
405
770
  class: local.class,
406
771
  style: local.style,
407
- defaultClassName: 'solidaria-Select-trigger',
772
+ defaultClassName: "solidaria-Select-trigger",
408
773
  },
409
- renderValues
774
+ renderValues,
410
775
  );
411
776
 
412
- // Remove ref from spread props
413
777
  const cleanTriggerProps = () => {
414
- const { ref: _ref1, ...rest } = triggerProps as Record<string, unknown>;
778
+ const { ref: _ref1, ...rest } = context.triggerProps as Record<string, unknown>;
415
779
  return rest;
416
780
  };
417
781
  const cleanHoverProps = () => {
418
782
  const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
419
783
  return rest;
420
784
  };
421
-
785
+ const triggerAriaProps = () => context.triggerProps as Record<string, unknown>;
786
+ const menuAriaProps = () => context.menuProps as Record<string, unknown>;
422
787
  return (
423
788
  <button
789
+ ref={setTriggerRef}
790
+ {...domProps}
424
791
  {...cleanTriggerProps()}
425
792
  {...cleanHoverProps()}
426
793
  type="button"
794
+ id={triggerAriaProps().id as string | undefined}
795
+ tabIndex={state.isDisabled ? undefined : 0}
796
+ aria-label={triggerAriaProps()["aria-label"] as string | undefined}
797
+ aria-labelledby={triggerAriaProps()["aria-labelledby"] as string | undefined}
798
+ aria-haspopup="listbox"
799
+ aria-expanded={isOpen()}
800
+ aria-controls={isOpen() ? (menuAriaProps().id as string | undefined) : undefined}
801
+ aria-disabled={state.isDisabled || undefined}
802
+ aria-required={triggerAriaProps()["aria-required"] as boolean | undefined}
803
+ aria-describedby={triggerAriaProps()["aria-describedby"] as string | undefined}
427
804
  class={renderProps.class()}
428
805
  style={renderProps.style()}
429
806
  data-open={isOpen() || undefined}
@@ -439,56 +816,80 @@ export function SelectTrigger(props: SelectTriggerProps): JSX.Element {
439
816
 
440
817
  // Default children function for SelectValue - defined at module level for SSR stability
441
818
  function defaultSelectValueChildren<T>(values: SelectValueRenderProps<T>) {
442
- return values.selectedText ?? values.placeholder ?? '';
819
+ return values.selectedText ?? values.placeholder ?? "";
443
820
  }
444
821
 
445
822
  /**
446
823
  * Displays the selected value in a select.
447
824
  */
448
825
  export function SelectValue<T>(props: SelectValueProps<T>): JSX.Element {
449
- const [local] = splitProps(props, ['class', 'style', 'slot', 'placeholder']);
826
+ const [local, domProps] = splitProps(props, [
827
+ "class",
828
+ "style",
829
+ "slot",
830
+ "placeholder",
831
+ "children",
832
+ ]);
450
833
 
451
- // Get context
452
834
  const context = useContext(SelectContext);
453
835
  if (!context) {
454
- throw new Error('SelectValue must be used within a Select');
836
+ throw new Error("SelectValue must be used within a Select");
455
837
  }
456
838
  const { valueProps, placeholder: contextPlaceholder } = context;
457
839
  const state = context.state as SelectState<T>;
458
840
 
459
- // Use local placeholder if provided, otherwise fall back to context
460
841
  const placeholder = () => local.placeholder ?? contextPlaceholder;
461
842
 
462
- // Render props values
463
843
  const renderValues = createMemo<SelectValueRenderProps<T>>(() => {
464
- const selectedItem = state.selectedItem();
844
+ const collection = state.collection();
845
+ const selectedItem =
846
+ state.selectedKey() == null ? null : collection.getItem(state.selectedKey() as Key);
847
+ const selectedKeys = state.selectedKeys();
848
+ const selectedItems =
849
+ selectedKeys === "all"
850
+ ? Array.from(collection)
851
+ : Array.from(selectedKeys as Set<Key>)
852
+ .map((key) => collection.getItem(key))
853
+ .filter((item): item is CollectionNode<T> => item != null);
854
+ const selectedText =
855
+ state.selectionMode() === "multiple"
856
+ ? selectedItems.length > 0
857
+ ? new Intl.ListFormat(undefined, { style: "long", type: "conjunction" }).format(
858
+ selectedItems.map((item) => item.textValue),
859
+ )
860
+ : null
861
+ : (selectedItem?.textValue ?? null);
465
862
  return {
466
863
  selectedItem,
467
- selectedText: selectedItem?.textValue ?? null,
468
- isSelected: selectedItem != null,
864
+ selectedItems,
865
+ selectedText,
866
+ isSelected:
867
+ state.selectionMode() === "multiple" ? selectedItems.length > 0 : selectedItem != null,
469
868
  placeholder: placeholder(),
470
869
  };
471
870
  });
472
871
 
473
- // Resolve render props
474
872
  const renderProps = useRenderProps(
475
873
  {
476
874
  children: props.children ?? defaultSelectValueChildren,
477
875
  class: local.class,
478
876
  style: local.style,
479
- defaultClassName: 'solidaria-Select-value',
877
+ defaultClassName: "solidaria-Select-value",
480
878
  },
481
- renderValues
879
+ renderValues,
482
880
  );
483
881
 
484
882
  return (
485
883
  <span
884
+ {...domProps}
486
885
  {...valueProps}
487
886
  class={renderProps.class()}
488
887
  style={renderProps.style()}
489
888
  data-placeholder={!renderValues().isSelected || undefined}
490
889
  >
491
- {renderProps.renderChildren()}
890
+ {props.children == null
891
+ ? (renderValues().selectedText ?? renderValues().placeholder ?? "")
892
+ : renderProps.renderChildren()}
492
893
  </span>
493
894
  );
494
895
  }
@@ -497,75 +898,79 @@ export function SelectValue<T>(props: SelectValueProps<T>): JSX.Element {
497
898
  * The listbox popup for a select.
498
899
  */
499
900
  export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
500
- const [local] = splitProps(props, ['class', 'style', 'slot']);
901
+ const [local, domProps] = splitProps(props, [
902
+ "class",
903
+ "style",
904
+ "slot",
905
+ "children",
906
+ "renderEmptyState",
907
+ "onLoadMore",
908
+ "isLoading",
909
+ "renderLoadMore",
910
+ "loadMoreClass",
911
+ "isInPopover",
912
+ ]);
501
913
 
502
- // Get context
503
914
  const context = useContext(SelectContext);
504
915
  if (!context) {
505
- throw new Error('SelectListBox must be used within a Select');
916
+ throw new Error("SelectListBox must be used within a Select");
506
917
  }
507
- const { menuProps, state: selectState, isOpen } = context;
918
+ const { menuProps, rootRef, state: selectState, isOpen } = context;
508
919
  const state = selectState as SelectState<T>;
509
920
 
510
- // Ref for the listbox element (for click outside detection)
921
+ createEffect(() => {
922
+ if (!isOpen()) {
923
+ return;
924
+ }
925
+ if (state.focusedKey() != null) {
926
+ return;
927
+ }
928
+ const selectedKey = state.selectedKey();
929
+ if (selectedKey != null && !state.collection().getItem(selectedKey)?.isDisabled) {
930
+ state.setFocusedKey(selectedKey);
931
+ }
932
+ });
933
+
511
934
  let listBoxRef: HTMLUListElement | undefined;
512
935
 
513
- // Handle click outside to close select
514
936
  createInteractOutside({
515
- ref: () => listBoxRef ?? null,
937
+ ref: () => rootRef() ?? listBoxRef ?? null,
516
938
  onInteractOutside: () => {
517
939
  if (isOpen()) {
518
940
  state.close();
519
941
  }
520
942
  },
521
943
  get isDisabled() {
522
- return !isOpen();
944
+ return !isOpen() || local.isInPopover === true;
523
945
  },
524
946
  });
525
947
 
526
- // Create listbox aria props - reuse select's internal list state via collection
527
948
  const { listBoxProps } = createListBox(
528
- {},
529
949
  {
530
- collection: state.collection,
531
- focusedKey: state.focusedKey,
532
- setFocusedKey: state.setFocusedKey,
533
- isFocused: state.isFocused,
534
- setFocused: state.setFocused,
535
- selectedKeys: () => {
536
- const key = state.selectedKey();
537
- return key != null ? new Set([key]) : new Set();
950
+ ...(menuProps as unknown as AriaListBoxProps),
951
+ shouldSelectOnPressUp: true,
952
+ shouldFocusOnHover: true,
953
+ shouldSelectOnFocus: local.isInPopover === true ? false : undefined,
954
+ get isDisabled() {
955
+ return state.isDisabled;
538
956
  },
539
- isSelected: (key: Key) => state.selectedKey() === key,
540
- isDisabled: state.isKeyDisabled,
541
- selectionMode: () => 'single' as const,
542
- disallowEmptySelection: () => true,
543
- select: (key: Key) => state.setSelectedKey(key),
544
- toggleSelection: (key: Key) => state.setSelectedKey(key),
545
- replaceSelection: (key: Key) => state.setSelectedKey(key),
546
- extendSelection: () => {},
547
- selectAll: () => {},
548
- clearSelection: () => state.setSelectedKey(null),
549
- childFocusStrategy: () => null,
550
- } as any
957
+ },
958
+ createSelectListStateAdapter(state),
551
959
  );
552
960
 
553
- // Render props values
554
961
  const renderValues = createMemo<SelectListBoxRenderProps>(() => ({
555
962
  isFocused: state.isFocused(),
556
963
  }));
557
964
 
558
- // Resolve render props
559
965
  const renderProps = useRenderProps(
560
966
  {
561
967
  class: local.class,
562
968
  style: local.style,
563
- defaultClassName: 'solidaria-Select-listbox',
969
+ defaultClassName: "solidaria-Select-listbox",
564
970
  },
565
- renderValues
971
+ renderValues,
566
972
  );
567
973
 
568
- // Remove ref from spread props
569
974
  const cleanMenuProps = () => {
570
975
  const { ref: _ref1, ...rest } = menuProps as Record<string, unknown>;
571
976
  return rest;
@@ -576,33 +981,74 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
576
981
  };
577
982
 
578
983
  const items = () => Array.from(state.collection());
984
+ createEffect(() => {
985
+ if (!isOpen()) return;
986
+ const focusedKey = state.focusedKey();
987
+ if (focusedKey == null) return;
579
988
 
580
- return (
581
- <Show when={isOpen()}>
582
- <FocusScope restoreFocus autoFocus>
583
- <ul
584
- ref={(el) => (listBoxRef = el)}
585
- {...cleanMenuProps()}
586
- {...cleanListBoxProps()}
587
- class={renderProps.class()}
588
- style={renderProps.style()}
589
- data-focused={state.isFocused() || undefined}
590
- >
591
- <Show when={props.children} fallback={
592
- <For each={items()}>
593
- {(node) => (
594
- <SelectOption id={node.key}>
595
- {node.textValue}
596
- </SelectOption>
597
- )}
598
- </For>
599
- }>
989
+ queueMicrotask(() => {
990
+ const option = Array.from(
991
+ listBoxRef?.querySelectorAll<HTMLElement>("[role='option']") ?? [],
992
+ ).find((element) => element.id === String(focusedKey));
993
+ if (option && document.activeElement !== option) {
994
+ focusSafely(option);
995
+ }
996
+ });
997
+ });
998
+
999
+ const listBox = () => (
1000
+ <ul
1001
+ ref={(el) => (listBoxRef = el)}
1002
+ {...domProps}
1003
+ {...cleanMenuProps()}
1004
+ {...cleanListBoxProps()}
1005
+ class={renderProps.class()}
1006
+ style={renderProps.style()}
1007
+ data-focused={state.isFocused() || undefined}
1008
+ data-empty={state.collection().size === 0 || undefined}
1009
+ >
1010
+ {state.collection().size === 0 && local.renderEmptyState ? (
1011
+ <li role="option" style={{ display: "contents" }} data-empty-state>
1012
+ {local.renderEmptyState()}
1013
+ </li>
1014
+ ) : (
1015
+ <Show
1016
+ when={local.children}
1017
+ fallback={
600
1018
  <For each={items()}>
601
- {(node) => node.value != null ? props.children!(node.value) : null}
1019
+ {(node) => <SelectOption id={node.key}>{node.textValue}</SelectOption>}
602
1020
  </For>
603
- </Show>
604
- </ul>
605
- </FocusScope>
1021
+ }
1022
+ >
1023
+ <For each={items()}>
1024
+ {(node) => (node.value != null ? local.children!(node.value) : null)}
1025
+ </For>
1026
+ </Show>
1027
+ )}
1028
+ <Show when={local.onLoadMore}>
1029
+ <ListBoxLoadMoreItem
1030
+ onLoadMore={local.onLoadMore!}
1031
+ isLoading={local.isLoading}
1032
+ class={local.loadMoreClass}
1033
+ >
1034
+ {local.renderLoadMore?.()}
1035
+ </ListBoxLoadMoreItem>
1036
+ </Show>
1037
+ </ul>
1038
+ );
1039
+
1040
+ return (
1041
+ <Show when={isOpen()}>
1042
+ <Show
1043
+ when={local.isInPopover}
1044
+ fallback={
1045
+ <FocusScope restoreFocus autoFocus>
1046
+ {listBox()}
1047
+ </FocusScope>
1048
+ }
1049
+ >
1050
+ {listBox()}
1051
+ </Show>
606
1052
  </Show>
607
1053
  );
608
1054
  }
@@ -612,122 +1058,247 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
612
1058
  */
613
1059
  export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
614
1060
  const [local, ariaProps] = splitProps(props, [
615
- 'class',
616
- 'style',
617
- 'slot',
618
- 'id',
619
- 'item',
620
- 'textValue',
1061
+ "class",
1062
+ "style",
1063
+ "slot",
1064
+ "id",
1065
+ "item",
1066
+ "textValue",
621
1067
  ]);
622
1068
 
623
- // Get state from context
624
1069
  const context = useContext(SelectStateContext);
625
1070
  if (!context) {
626
- throw new Error('SelectOption must be used within a Select');
1071
+ throw new Error("SelectOption must be used within a Select");
627
1072
  }
628
1073
  const state = context as SelectState<T>;
1074
+ const selectContext = useContext(SelectContext) as SelectContextValue<T> | null;
629
1075
 
630
- // Create option aria props - adapt select state to list state interface
631
1076
  const optionAria = createOption<T>(
632
1077
  {
633
1078
  key: local.id,
634
1079
  get isDisabled() {
635
- return ariaProps.isDisabled;
1080
+ return Boolean(ariaProps.isDisabled || selectContext?.isDisabled());
1081
+ },
1082
+ get "aria-label"() {
1083
+ return ariaProps["aria-label"] ?? local.textValue;
1084
+ },
1085
+ shouldSelectOnPressUp: true,
1086
+ shouldFocusOnHover: true,
1087
+ get onHoverStart() {
1088
+ return ariaProps.onHoverStart;
636
1089
  },
637
- get 'aria-label'() {
638
- return ariaProps['aria-label'];
1090
+ get onHoverEnd() {
1091
+ return ariaProps.onHoverEnd;
1092
+ },
1093
+ get onHoverChange() {
1094
+ return ariaProps.onHoverChange;
639
1095
  },
640
1096
  },
641
1097
  {
642
- collection: state.collection,
643
- focusedKey: state.focusedKey,
644
- setFocusedKey: state.setFocusedKey,
645
- isFocused: state.isFocused,
646
- setFocused: state.setFocused,
647
- selectedKeys: () => {
648
- const key = state.selectedKey();
649
- return key != null ? new Set([key]) : new Set();
650
- },
651
- isSelected: (key: Key) => state.selectedKey() === key,
652
- isDisabled: state.isKeyDisabled,
653
- selectionMode: () => 'single' as const,
654
- disallowEmptySelection: () => true,
1098
+ ...createSelectListStateAdapter(state),
655
1099
  select: (key: Key) => {
1100
+ if (state.selectionMode() === "multiple") {
1101
+ const keys = state.selectedKeys();
1102
+ if (keys === "all") return;
1103
+ state.setSelectedKeys(new Set([...keys, key]));
1104
+ return;
1105
+ }
656
1106
  state.setSelectedKey(key);
657
1107
  state.close();
658
1108
  },
659
1109
  toggleSelection: (key: Key) => {
1110
+ if (state.selectionMode() === "multiple") {
1111
+ const keys = state.selectedKeys();
1112
+ if (keys === "all") return;
1113
+ const next = new Set(keys);
1114
+ if (next.has(key)) next.delete(key);
1115
+ else next.add(key);
1116
+ state.setSelectedKeys(next);
1117
+ return;
1118
+ }
660
1119
  state.setSelectedKey(key);
661
1120
  state.close();
662
1121
  },
663
1122
  replaceSelection: (key: Key) => {
664
1123
  state.setSelectedKey(key);
665
- state.close();
1124
+ if (state.selectionMode() !== "multiple") {
1125
+ state.close();
1126
+ }
666
1127
  },
667
- extendSelection: () => {},
668
- selectAll: () => {},
669
- clearSelection: () => state.setSelectedKey(null),
670
- childFocusStrategy: () => null,
671
- } as any
672
- );
673
-
674
- // Create hover
675
- const { isHovered, hoverProps } = createHover({
676
- get isDisabled() {
677
- return optionAria.isDisabled();
678
1128
  },
679
- });
1129
+ );
1130
+ const isOptionFocusVisible = () =>
1131
+ optionAria.isFocused() && (selectContext?.isFocusVisible() ?? optionAria.isFocusVisible());
680
1132
 
681
- // Render props values
682
1133
  const renderValues = createMemo<SelectOptionRenderProps>(() => ({
683
1134
  isSelected: optionAria.isSelected(),
684
1135
  isFocused: optionAria.isFocused(),
685
- isFocusVisible: optionAria.isFocusVisible(),
1136
+ isFocusVisible: isOptionFocusVisible(),
686
1137
  isPressed: optionAria.isPressed(),
687
- isHovered: isHovered(),
1138
+ isHovered: optionAria.isHovered(),
688
1139
  isDisabled: optionAria.isDisabled(),
689
1140
  }));
690
1141
 
691
- // Resolve render props
692
1142
  const renderProps = useRenderProps(
693
1143
  {
694
1144
  children: props.children,
695
1145
  class: local.class,
696
1146
  style: local.style,
697
- defaultClassName: 'solidaria-Select-option',
1147
+ defaultClassName: "solidaria-Select-option",
698
1148
  },
699
- renderValues
1149
+ renderValues,
700
1150
  );
1151
+ const hasPrimitiveLabel = () => {
1152
+ return typeof props.children === "string" || typeof props.children === "number";
1153
+ };
1154
+
1155
+ const selectionIndicatorContext = createMemo<SelectionIndicatorContextValue>(() => ({
1156
+ isSelected: optionAria.isSelected,
1157
+ }));
701
1158
 
702
- // Remove ref from spread props
703
1159
  const cleanOptionProps = () => {
704
- const { ref: _ref1, ...rest } = optionAria.optionProps as Record<string, unknown>;
1160
+ const {
1161
+ ref: _ref1,
1162
+ "aria-describedby": _ariaDescribedby,
1163
+ ...rest
1164
+ } = optionAria.optionProps as Record<string, unknown>;
1165
+ if (!hasPrimitiveLabel() && rest["aria-label"] == null) {
1166
+ delete rest["aria-labelledby"];
1167
+ }
1168
+ const onClick = rest.onClick as ((event: MouseEvent) => void) | undefined;
1169
+ rest.onClick = ((event: MouseEvent) => {
1170
+ const wasSelected = optionAria.isSelected();
1171
+ onClick?.(event);
1172
+ if (typeof PointerEvent === "undefined") {
1173
+ return;
1174
+ }
1175
+ queueMicrotask(() => {
1176
+ if (state.selectionMode() !== "multiple" || optionAria.isSelected() === wasSelected) {
1177
+ selectOption();
1178
+ }
1179
+ });
1180
+ }) as JSX.EventHandler<HTMLLIElement, MouseEvent>;
705
1181
  return rest;
706
1182
  };
707
- const cleanHoverProps = () => {
708
- const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
709
- return rest;
1183
+ const selectOption = () => {
1184
+ if (optionAria.isDisabled()) {
1185
+ return;
1186
+ }
1187
+ if (state.selectionMode() === "multiple") {
1188
+ const keys = state.selectedKeys();
1189
+ if (keys === "all") return;
1190
+ const next = new Set(keys);
1191
+ if (next.has(local.id)) next.delete(local.id);
1192
+ else next.add(local.id);
1193
+ state.setSelectedKeys(next);
1194
+ return;
1195
+ }
1196
+ state.setSelectedKey(local.id);
1197
+ state.close();
710
1198
  };
711
1199
 
712
1200
  return (
713
- <li
714
- {...cleanOptionProps()}
715
- {...cleanHoverProps()}
716
- class={renderProps.class()}
717
- style={renderProps.style()}
718
- data-selected={optionAria.isSelected() || undefined}
719
- data-focused={optionAria.isFocused() || undefined}
720
- data-focus-visible={optionAria.isFocusVisible() || undefined}
721
- data-pressed={optionAria.isPressed() || undefined}
722
- data-hovered={isHovered() || undefined}
723
- data-disabled={optionAria.isDisabled() || undefined}
724
- >
725
- {renderProps.renderChildren()}
726
- </li>
1201
+ <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
1202
+ <li
1203
+ {...cleanOptionProps()}
1204
+ class={renderProps.class()}
1205
+ style={renderProps.style()}
1206
+ data-selected={optionAria.isSelected() || undefined}
1207
+ data-focused={optionAria.isFocused() || undefined}
1208
+ data-focus-visible={isOptionFocusVisible() || undefined}
1209
+ data-pressed={optionAria.isPressed() || undefined}
1210
+ data-hovered={optionAria.isHovered() || undefined}
1211
+ data-disabled={optionAria.isDisabled() || undefined}
1212
+ >
1213
+ {hasPrimitiveLabel() ? (
1214
+ <span {...optionAria.labelProps}>{renderProps.renderChildren()}</span>
1215
+ ) : (
1216
+ renderProps.renderChildren()
1217
+ )}
1218
+ </li>
1219
+ </SelectionIndicatorContext.Provider>
727
1220
  );
728
1221
  }
729
1222
 
730
- // Attach sub-components
1223
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
1224
+ return typeof value === "object" && value !== null;
1225
+ }
1226
+
1227
+ function toKey(value: unknown): Key | undefined {
1228
+ if (typeof value === "string" || typeof value === "number") {
1229
+ return value;
1230
+ }
1231
+ return undefined;
1232
+ }
1233
+
1234
+ function toTextValue(value: unknown): string | undefined {
1235
+ if (typeof value === "string" || typeof value === "number") {
1236
+ return String(value);
1237
+ }
1238
+ return undefined;
1239
+ }
1240
+
1241
+ function createSelectListStateAdapter<T>(state: SelectState<T>): ListState<T> {
1242
+ const selectedKeys = createMemo(() => {
1243
+ const keys = state.selectedKeys();
1244
+ return keys === "all" ? new Set(Array.from(state.collection()).map((item) => item.key)) : keys;
1245
+ });
1246
+
1247
+ const disabledKeys = createMemo(() => {
1248
+ const keys = new Set<Key>();
1249
+ for (const node of state.collection()) {
1250
+ if (node.isDisabled) keys.add(node.key);
1251
+ }
1252
+ return keys;
1253
+ });
1254
+
1255
+ return {
1256
+ collection: state.collection,
1257
+ isFocused: state.isFocused,
1258
+ setFocused: state.setFocused,
1259
+ focusedKey: state.focusedKey,
1260
+ setFocusedKey: (key) => state.setFocusedKey(key ?? null),
1261
+ childFocusStrategy: () => null,
1262
+ selectionMode: () => state.selectionMode(),
1263
+ selectionBehavior: () => "replace",
1264
+ disallowEmptySelection: () => true,
1265
+ selectedKeys,
1266
+ disabledKeys,
1267
+ disabledBehavior: () => "all",
1268
+ isEmpty: () => selectedKeys().size === 0,
1269
+ isSelectAll: () => state.selectedKeys() === "all",
1270
+ isSelected: (key) => selectedKeys().has(key),
1271
+ isDisabled: state.isKeyDisabled,
1272
+ setSelectionBehavior: () => {},
1273
+ toggleSelection: (key) => {
1274
+ if (state.selectionMode() !== "multiple") {
1275
+ state.setSelectedKey(key);
1276
+ return;
1277
+ }
1278
+ const keys = state.selectedKeys();
1279
+ if (keys === "all") return;
1280
+ const next = new Set(keys);
1281
+ if (next.has(key)) next.delete(key);
1282
+ else next.add(key);
1283
+ state.setSelectedKeys(next);
1284
+ },
1285
+ replaceSelection: (key) => state.setSelectedKey(key),
1286
+ setSelectedKeys: (keys) => state.setSelectedKeys(keys),
1287
+ selectAll: () => {},
1288
+ clearSelection: () =>
1289
+ state.selectionMode() === "multiple" ? state.setSelectedKeys([]) : state.setSelectedKey(null),
1290
+ toggleSelectAll: () => {},
1291
+ extendSelection: (toKey) => state.setSelectedKey(toKey),
1292
+ select: (key) =>
1293
+ state.selectionMode() === "multiple"
1294
+ ? state.setSelectedKeys([
1295
+ ...(state.selectedKeys() === "all" ? [] : (state.selectedKeys() as Set<Key>)),
1296
+ key,
1297
+ ])
1298
+ : state.setSelectedKey(key),
1299
+ };
1300
+ }
1301
+
731
1302
  Select.Trigger = SelectTrigger;
732
1303
  Select.Value = SelectValue;
733
1304
  Select.ListBox = SelectListBox;