@proyecto-viviana/solidaria-components 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +39 -272
  2. package/dist/ActionBar.d.ts +21 -13
  3. package/dist/ActionBar.d.ts.map +1 -1
  4. package/dist/ActionGroup.d.ts +8 -8
  5. package/dist/ActionGroup.d.ts.map +1 -1
  6. package/dist/Alert.d.ts +5 -5
  7. package/dist/Alert.d.ts.map +1 -1
  8. package/dist/Autocomplete.d.ts +5 -5
  9. package/dist/Autocomplete.d.ts.map +1 -1
  10. package/dist/Breadcrumbs.d.ts +18 -7
  11. package/dist/Breadcrumbs.d.ts.map +1 -1
  12. package/dist/Button.d.ts +24 -5
  13. package/dist/Button.d.ts.map +1 -1
  14. package/dist/Calendar.d.ts +38 -7
  15. package/dist/Calendar.d.ts.map +1 -1
  16. package/dist/Checkbox.d.ts +32 -7
  17. package/dist/Checkbox.d.ts.map +1 -1
  18. package/dist/Collection.d.ts +19 -14
  19. package/dist/Collection.d.ts.map +1 -1
  20. package/dist/Color.d.ts +103 -14
  21. package/dist/Color.d.ts.map +1 -1
  22. package/dist/ColorEditor.d.ts +6 -6
  23. package/dist/ColorEditor.d.ts.map +1 -1
  24. package/dist/ComboBox.d.ts +85 -19
  25. package/dist/ComboBox.d.ts.map +1 -1
  26. package/dist/ContextualHelpTrigger.d.ts +2 -2
  27. package/dist/ContextualHelpTrigger.d.ts.map +1 -1
  28. package/dist/DateField.d.ts +8 -6
  29. package/dist/DateField.d.ts.map +1 -1
  30. package/dist/DatePicker.d.ts +53 -22
  31. package/dist/DatePicker.d.ts.map +1 -1
  32. package/dist/DateRangePickerContext.d.ts +30 -0
  33. package/dist/DateRangePickerContext.d.ts.map +1 -0
  34. package/dist/Dialog.d.ts +5 -5
  35. package/dist/Dialog.d.ts.map +1 -1
  36. package/dist/Disclosure.d.ts +23 -5
  37. package/dist/Disclosure.d.ts.map +1 -1
  38. package/dist/DragAndDrop.d.ts +6 -6
  39. package/dist/DragAndDrop.d.ts.map +1 -1
  40. package/dist/DragPreview.d.ts +2 -2
  41. package/dist/DragPreview.d.ts.map +1 -1
  42. package/dist/DropZone.d.ts +4 -4
  43. package/dist/DropZone.d.ts.map +1 -1
  44. package/dist/FieldError.d.ts +9 -5
  45. package/dist/FieldError.d.ts.map +1 -1
  46. package/dist/FileTrigger.d.ts +3 -3
  47. package/dist/FileTrigger.d.ts.map +1 -1
  48. package/dist/Focusable.d.ts +2 -2
  49. package/dist/Focusable.d.ts.map +1 -1
  50. package/dist/Form.d.ts +18 -4
  51. package/dist/Form.d.ts.map +1 -1
  52. package/dist/GridList.d.ts +32 -12
  53. package/dist/GridList.d.ts.map +1 -1
  54. package/dist/HiddenDateInput.d.ts +26 -0
  55. package/dist/HiddenDateInput.d.ts.map +1 -0
  56. package/dist/HiddenTimeInput.d.ts +25 -0
  57. package/dist/HiddenTimeInput.d.ts.map +1 -0
  58. package/dist/Icon.d.ts +5 -5
  59. package/dist/Icon.d.ts.map +1 -1
  60. package/dist/Keyboard.d.ts +1 -1
  61. package/dist/Landmark.d.ts +3 -3
  62. package/dist/Landmark.d.ts.map +1 -1
  63. package/dist/Link.d.ts +10 -4
  64. package/dist/Link.d.ts.map +1 -1
  65. package/dist/ListBox.d.ts +32 -12
  66. package/dist/ListBox.d.ts.map +1 -1
  67. package/dist/ListDropTargetDelegate.d.ts +6 -6
  68. package/dist/ListDropTargetDelegate.d.ts.map +1 -1
  69. package/dist/Menu.d.ts +65 -14
  70. package/dist/Menu.d.ts.map +1 -1
  71. package/dist/Meter.d.ts +3 -3
  72. package/dist/Meter.d.ts.map +1 -1
  73. package/dist/Modal.d.ts +5 -5
  74. package/dist/Modal.d.ts.map +1 -1
  75. package/dist/NumberField.d.ts +8 -12
  76. package/dist/NumberField.d.ts.map +1 -1
  77. package/dist/Popover.d.ts +28 -5
  78. package/dist/Popover.d.ts.map +1 -1
  79. package/dist/Pressable.d.ts +2 -2
  80. package/dist/Pressable.d.ts.map +1 -1
  81. package/dist/ProgressBar.d.ts +5 -3
  82. package/dist/ProgressBar.d.ts.map +1 -1
  83. package/dist/RadioGroup.d.ts +43 -9
  84. package/dist/RadioGroup.d.ts.map +1 -1
  85. package/dist/RangeCalendar.d.ts +34 -7
  86. package/dist/RangeCalendar.d.ts.map +1 -1
  87. package/dist/RouterProvider.d.ts +2 -2
  88. package/dist/RouterProvider.d.ts.map +1 -1
  89. package/dist/SearchField.d.ts +23 -20
  90. package/dist/SearchField.d.ts.map +1 -1
  91. package/dist/Select.d.ts +41 -11
  92. package/dist/Select.d.ts.map +1 -1
  93. package/dist/SelectionIndicator.d.ts +3 -3
  94. package/dist/SelectionIndicator.d.ts.map +1 -1
  95. package/dist/Separator.d.ts +9 -3
  96. package/dist/Separator.d.ts.map +1 -1
  97. package/dist/SharedElementTransition.d.ts +6 -4
  98. package/dist/SharedElementTransition.d.ts.map +1 -1
  99. package/dist/Slider.d.ts +12 -8
  100. package/dist/Slider.d.ts.map +1 -1
  101. package/dist/StepList.d.ts +90 -0
  102. package/dist/StepList.d.ts.map +1 -0
  103. package/dist/Switch.d.ts +11 -5
  104. package/dist/Switch.d.ts.map +1 -1
  105. package/dist/Table.d.ts +187 -23
  106. package/dist/Table.d.ts.map +1 -1
  107. package/dist/Tabs.d.ts +45 -9
  108. package/dist/Tabs.d.ts.map +1 -1
  109. package/dist/TagGroup.d.ts +12 -10
  110. package/dist/TagGroup.d.ts.map +1 -1
  111. package/dist/Text.d.ts +2 -2
  112. package/dist/TextField.d.ts +15 -11
  113. package/dist/TextField.d.ts.map +1 -1
  114. package/dist/TimeField.d.ts +6 -6
  115. package/dist/TimeField.d.ts.map +1 -1
  116. package/dist/Toast.d.ts +29 -14
  117. package/dist/Toast.d.ts.map +1 -1
  118. package/dist/ToggleButton.d.ts +11 -5
  119. package/dist/ToggleButton.d.ts.map +1 -1
  120. package/dist/ToggleButtonGroup.d.ts +7 -7
  121. package/dist/ToggleButtonGroup.d.ts.map +1 -1
  122. package/dist/Toolbar.d.ts +7 -3
  123. package/dist/Toolbar.d.ts.map +1 -1
  124. package/dist/Tooltip.d.ts +50 -8
  125. package/dist/Tooltip.d.ts.map +1 -1
  126. package/dist/Tree.d.ts +66 -17
  127. package/dist/Tree.d.ts.map +1 -1
  128. package/dist/Virtualizer.d.ts +12 -12
  129. package/dist/Virtualizer.d.ts.map +1 -1
  130. package/dist/VirtualizerLayouts.d.ts +2 -2
  131. package/dist/VirtualizerLayouts.d.ts.map +1 -1
  132. package/dist/VisuallyHidden.d.ts +1 -1
  133. package/dist/VisuallyHidden.d.ts.map +1 -1
  134. package/dist/contexts.d.ts +5 -1
  135. package/dist/contexts.d.ts.map +1 -1
  136. package/dist/index.d.ts +73 -71
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +23253 -18564
  139. package/dist/index.js.map +1 -1
  140. package/dist/index.jsx +18116 -0
  141. package/dist/index.jsx.map +1 -0
  142. package/dist/useDragAndDrop.d.ts +13 -13
  143. package/dist/useDragAndDrop.d.ts.map +1 -1
  144. package/dist/utils.d.ts +2 -2
  145. package/dist/utils.d.ts.map +1 -1
  146. package/dist/virtualizer/Layout.d.ts +1 -1
  147. package/dist/virtualizer/Layout.d.ts.map +1 -1
  148. package/package.json +31 -32
  149. package/src/ActionBar.tsx +75 -72
  150. package/src/ActionGroup.tsx +53 -61
  151. package/src/Alert.tsx +17 -42
  152. package/src/Autocomplete.tsx +39 -44
  153. package/src/Breadcrumbs.tsx +149 -80
  154. package/src/Button.tsx +267 -70
  155. package/src/Calendar.tsx +218 -138
  156. package/src/Checkbox.tsx +413 -121
  157. package/src/Collection.tsx +67 -58
  158. package/src/Color.tsx +803 -380
  159. package/src/ColorEditor.tsx +131 -149
  160. package/src/ComboBox.tsx +414 -249
  161. package/src/ContextualHelpTrigger.tsx +86 -74
  162. package/src/DateField.tsx +185 -91
  163. package/src/DatePicker.tsx +524 -213
  164. package/src/DateRangePickerContext.tsx +44 -0
  165. package/src/Dialog.tsx +156 -118
  166. package/src/Disclosure.tsx +127 -80
  167. package/src/DragAndDrop.tsx +60 -54
  168. package/src/DragPreview.tsx +13 -11
  169. package/src/DropZone.tsx +42 -22
  170. package/src/FieldError.tsx +45 -23
  171. package/src/FileTrigger.tsx +19 -19
  172. package/src/Focusable.tsx +21 -24
  173. package/src/Form.tsx +71 -16
  174. package/src/GridList.tsx +273 -197
  175. package/src/HiddenDateInput.tsx +153 -0
  176. package/src/HiddenTimeInput.tsx +133 -0
  177. package/src/Icon.tsx +22 -43
  178. package/src/Keyboard.tsx +3 -3
  179. package/src/Landmark.tsx +37 -63
  180. package/src/Link.tsx +125 -75
  181. package/src/ListBox.tsx +332 -233
  182. package/src/ListDropTargetDelegate.ts +81 -80
  183. package/src/Menu.tsx +1023 -274
  184. package/src/Meter.tsx +38 -56
  185. package/src/Modal.tsx +251 -176
  186. package/src/NumberField.tsx +139 -143
  187. package/src/Popover.tsx +396 -234
  188. package/src/Pressable.tsx +21 -21
  189. package/src/ProgressBar.tsx +48 -57
  190. package/src/RadioGroup.tsx +524 -122
  191. package/src/RangeCalendar.tsx +157 -90
  192. package/src/RouterProvider.tsx +30 -47
  193. package/src/SearchField.tsx +362 -143
  194. package/src/Select.tsx +656 -233
  195. package/src/SelectionIndicator.tsx +18 -15
  196. package/src/Separator.tsx +47 -49
  197. package/src/SharedElementTransition.tsx +103 -97
  198. package/src/Slider.tsx +138 -98
  199. package/src/StepList.tsx +272 -0
  200. package/src/Switch.tsx +93 -46
  201. package/src/Table.tsx +1308 -342
  202. package/src/Tabs.tsx +324 -103
  203. package/src/TagGroup.tsx +139 -126
  204. package/src/Text.tsx +3 -3
  205. package/src/TextField.tsx +389 -79
  206. package/src/TimeField.tsx +136 -76
  207. package/src/Toast.tsx +216 -158
  208. package/src/ToggleButton.tsx +47 -37
  209. package/src/ToggleButtonGroup.tsx +39 -34
  210. package/src/Toolbar.tsx +54 -69
  211. package/src/Tooltip.tsx +387 -119
  212. package/src/Tree.tsx +651 -368
  213. package/src/Virtualizer.tsx +208 -180
  214. package/src/VirtualizerLayouts.ts +45 -30
  215. package/src/VisuallyHidden.tsx +19 -19
  216. package/src/contexts.ts +29 -37
  217. package/src/index.ts +110 -195
  218. package/src/useDragAndDrop.ts +87 -71
  219. package/src/utils.tsx +49 -60
  220. package/src/virtualizer/Layout.ts +14 -22
  221. package/dist/index.ssr.js +0 -16996
  222. package/dist/index.ssr.js.map +0 -1
package/src/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,17 +27,22 @@ import {
23
27
  createHover,
24
28
  createInteractOutside,
25
29
  FocusScope,
30
+ focusSafely,
31
+ mergeProps,
26
32
  type AriaSelectProps,
27
33
  type AriaListBoxProps,
28
34
  type AriaOptionProps,
29
- } from '@proyecto-viviana/solidaria';
35
+ } from "@proyecto-viviana/solidaria";
30
36
  import {
31
37
  createSelectState,
32
38
  type ListState,
33
39
  type SelectState,
34
40
  type Key,
35
41
  type CollectionNode,
36
- } 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";
37
46
  import {
38
47
  type RenderChildren,
39
48
  type ClassNameOrFunction,
@@ -41,15 +50,43 @@ import {
41
50
  type SlotProps,
42
51
  useRenderProps,
43
52
  filterDOMProps,
44
- } from './utils';
53
+ } from "./utils";
45
54
  import {
46
55
  SelectionIndicatorContext,
47
56
  type SelectionIndicatorContextValue,
48
- } from './SelectionIndicator';
57
+ } from "./SelectionIndicator";
58
+ import { ListBoxLoadMoreItem } from "./ListBox";
49
59
 
50
- // ============================================
51
- // TYPES
52
- // ============================================
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
+ }
53
90
 
54
91
  export interface SelectRenderProps {
55
92
  /** Whether the select is open. */
@@ -66,9 +103,7 @@ export interface SelectRenderProps {
66
103
  isSelected: boolean;
67
104
  }
68
105
 
69
- export interface SelectProps<T>
70
- extends Omit<AriaSelectProps, 'children'>,
71
- SlotProps {
106
+ export interface SelectProps<T> extends Omit<AriaSelectProps, "children">, SlotProps {
72
107
  /** The items to render in the select. */
73
108
  items: T[];
74
109
  /** Function to get the key from an item. */
@@ -80,19 +115,19 @@ export interface SelectProps<T>
80
115
  /** Keys of disabled items. */
81
116
  disabledKeys?: Iterable<Key>;
82
117
  /** Selection mode. */
83
- selectionMode?: 'single' | 'multiple';
118
+ selectionMode?: "single" | "multiple";
84
119
  /** The currently selected key (controlled). */
85
120
  selectedKey?: Key | null;
86
121
  /** The default selected key (uncontrolled). */
87
122
  defaultSelectedKey?: Key | null;
88
123
  /** Currently selected keys (controlled, for multiple selection). */
89
- selectedKeys?: 'all' | Iterable<Key>;
124
+ selectedKeys?: "all" | Iterable<Key>;
90
125
  /** Default selected keys (uncontrolled, for multiple selection). */
91
- defaultSelectedKeys?: 'all' | Iterable<Key>;
126
+ defaultSelectedKeys?: "all" | Iterable<Key>;
92
127
  /** Handler called when selection changes. */
93
128
  onSelectionChange?: (key: Key | null) => void;
94
129
  /** Handler called when selected keys change. */
95
- onSelectionChangeKeys?: (keys: 'all' | Set<Key>) => void;
130
+ onSelectionChangeKeys?: (keys: "all" | Set<Key>) => void;
96
131
  /** Whether the select is open (controlled). */
97
132
  isOpen?: boolean;
98
133
  /** Whether the select is open by default (uncontrolled). */
@@ -104,16 +139,25 @@ export interface SelectProps<T>
104
139
  /** The name of the select, used when submitting an HTML form. */
105
140
  name?: string;
106
141
  /** The children of the component (compound components: SelectTrigger, SelectListBox). */
107
- children: JSX.Element;
142
+ children: RenderChildren<SelectRenderProps>;
108
143
  /** The CSS className for the element. */
109
144
  class?: ClassNameOrFunction<SelectRenderProps>;
110
145
  /** The inline style for the element. */
111
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>;
112
154
  }
113
155
 
114
156
  export interface SelectValueRenderProps<T> {
115
157
  /** The selected item. */
116
158
  selectedItem: CollectionNode<T> | null;
159
+ /** The selected items. */
160
+ selectedItems: CollectionNode<T>[];
117
161
  /** The text value of the selected item. */
118
162
  selectedText: string | null;
119
163
  /** Whether a value is selected. */
@@ -163,6 +207,18 @@ export interface SelectListBoxRenderProps {
163
207
  export interface SelectListBoxProps<T> extends SlotProps {
164
208
  /** The children of the listbox. A function may be provided to render each item. */
165
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;
166
222
  /** The CSS className for the element. */
167
223
  class?: ClassNameOrFunction<SelectListBoxRenderProps>;
168
224
  /** The inline style for the element. */
@@ -184,9 +240,7 @@ export interface SelectOptionRenderProps {
184
240
  isDisabled: boolean;
185
241
  }
186
242
 
187
- export interface SelectOptionProps<T>
188
- extends Omit<AriaOptionProps, 'children' | 'key'>,
189
- SlotProps {
243
+ export interface SelectOptionProps<T> extends Omit<AriaOptionProps, "children" | "key">, SlotProps {
190
244
  /** The unique key for the option. */
191
245
  id: Key;
192
246
  /** The item value. */
@@ -201,16 +255,17 @@ export interface SelectOptionProps<T>
201
255
  textValue?: string;
202
256
  }
203
257
 
204
- // ============================================
205
- // CONTEXT
206
- // ============================================
207
-
208
258
  interface SelectContextValue<T> {
209
259
  state: SelectState<T>;
260
+ rootRef: Accessor<HTMLElement | null>;
261
+ triggerRef: Accessor<HTMLElement | null>;
262
+ setTriggerRef: (el: HTMLElement | null) => void;
210
263
  triggerProps: JSX.HTMLAttributes<HTMLElement>;
211
264
  valueProps: JSX.HTMLAttributes<HTMLElement>;
212
265
  labelProps: JSX.HTMLAttributes<HTMLElement>;
213
266
  menuProps: JSX.HTMLAttributes<HTMLElement>;
267
+ errorMessageProps?: JSX.HTMLAttributes<HTMLElement>;
268
+ validation?: ValidationResult;
214
269
  isOpen: Accessor<boolean>;
215
270
  isFocused: Accessor<boolean>;
216
271
  isFocusVisible: Accessor<boolean>;
@@ -218,35 +273,67 @@ interface SelectContextValue<T> {
218
273
  placeholder?: string;
219
274
  items: T[];
220
275
  renderItem?: (item: T) => JSX.Element;
276
+ slots?: Record<string, Partial<SelectProps<T>>>;
277
+ autoFocus?: boolean;
221
278
  }
222
279
 
223
280
  export const SelectContext = createContext<SelectContextValue<unknown> | null>(null);
224
281
  export const SelectStateContext = createContext<SelectState<unknown> | null>(null);
225
282
  export const SelectValueContext = SelectContext;
226
283
 
227
- // ============================================
228
- // COMPONENTS
229
- // ============================================
284
+ const selectRootLabelProps = new Set([
285
+ "aria-label",
286
+ "aria-labelledby",
287
+ "aria-describedby",
288
+ "aria-details",
289
+ ]);
230
290
 
231
291
  /**
232
292
  * A select displays a collapsible list of options and allows a user to select one of them.
233
293
  */
234
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>;
235
302
  const [local, stateProps, ariaProps] = splitProps(
236
- props,
237
- ['class', 'style', 'slot'],
238
- ['items', 'getKey', 'getTextValue', 'getDisabled', 'disabledKeys', 'selectionMode', 'selectedKey', 'defaultSelectedKey', 'selectedKeys', 'defaultSelectedKeys', 'onSelectionChange', 'onSelectionChangeKeys', '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
+ ],
239
323
  );
324
+ let rootRef: HTMLDivElement | undefined;
325
+ const [selectValidation, setSelectValidation] =
326
+ createSignal<ValidationResult>(DEFAULT_VALIDATION_RESULT);
327
+ const errorMessageId = createUniqueId();
240
328
 
241
329
  const resolveDisabled = (): boolean => {
242
330
  const disabled = ariaProps.isDisabled;
243
- if (typeof disabled === 'function') {
331
+ if (typeof disabled === "function") {
244
332
  return (disabled as () => boolean)();
245
333
  }
246
334
  return !!disabled;
247
335
  };
248
336
 
249
- // Create select state
250
337
  const state = createSelectState<T>({
251
338
  get items() {
252
339
  return stateProps.items;
@@ -301,48 +388,74 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
301
388
  },
302
389
  });
303
390
 
304
- // Create select aria props
305
- const { labelProps, triggerProps, valueProps, menuProps, isFocused, isFocusVisible, isOpen } = createSelect<T>(
306
- ariaProps,
307
- state
308
- );
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);
309
403
 
310
- // Create hover for wrapper
311
404
  const { isHovered, hoverProps } = createHover({
312
405
  get isDisabled() {
313
406
  return resolveDisabled();
314
407
  },
315
408
  });
316
409
 
317
- // Render props values
318
410
  const renderValues = createMemo<SelectRenderProps>(() => ({
319
411
  isOpen: isOpen(),
320
412
  isFocused: isFocused(),
321
413
  isFocusVisible: isFocusVisible(),
322
414
  isDisabled: resolveDisabled(),
323
415
  isRequired: !!ariaProps.isRequired,
324
- isSelected: state.selectionMode() === 'multiple'
325
- ? state.selectedKeys() === 'all' || (state.selectedKeys() as Set<Key>).size > 0
326
- : state.selectedKey() != null,
416
+ isSelected:
417
+ state.selectionMode() === "multiple"
418
+ ? state.selectedKeys() === "all" || (state.selectedKeys() as Set<Key>).size > 0
419
+ : state.selectedKey() != null,
327
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
+ };
328
441
 
329
- // Resolve render props
330
442
  const renderProps = useRenderProps(
331
443
  {
332
444
  class: local.class,
333
445
  style: local.style,
334
- defaultClassName: 'solidaria-Select',
446
+ defaultClassName: "solidaria-Select",
335
447
  },
336
- renderValues
448
+ renderValues,
337
449
  );
338
450
 
339
- // Filter DOM props
340
451
  const domProps = createMemo(() => {
341
452
  const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
453
+ for (const key of selectRootLabelProps) {
454
+ delete filtered[key];
455
+ }
342
456
  return filtered;
343
457
  });
344
458
 
345
- // Remove ref from hover props
346
459
  const cleanHoverProps = () => {
347
460
  const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
348
461
  return rest;
@@ -351,80 +464,264 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
351
464
  const { ref: _ref, ...rest } = labelProps as Record<string, unknown>;
352
465
  return rest;
353
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
+ };
354
537
 
355
- // Create hidden select for form submission
356
538
  const { containerProps, selectProps: hiddenSelectProps } = createHiddenSelect({
357
539
  state,
358
540
  name: stateProps.name,
541
+ form: ariaProps.form,
542
+ isRequired: ariaProps.isRequired,
543
+ validationBehavior: ariaProps.validationBehavior ?? "native",
359
544
  get isDisabled() {
360
545
  return resolveDisabled();
361
546
  },
362
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
+ };
363
572
 
364
- return (
365
- <SelectContext.Provider
366
- value={{
367
- state,
368
- triggerProps,
369
- valueProps,
370
- labelProps,
371
- menuProps,
372
- isOpen,
373
- isFocused,
374
- isFocusVisible,
375
- isDisabled: resolveDisabled,
376
- placeholder: ariaProps.placeholder,
377
- items: stateProps.items,
378
- }}
379
- >
380
- <SelectStateContext.Provider value={state}>
381
- <div
382
- {...domProps()}
383
- {...cleanHoverProps()}
384
- class={renderProps.class()}
385
- style={renderProps.style()}
386
- data-open={isOpen() || undefined}
387
- data-focused={isFocused() || undefined}
388
- data-focus-visible={isFocusVisible() || undefined}
389
- data-disabled={resolveDisabled() || undefined}
390
- data-required={ariaProps.isRequired || undefined}
391
- data-hovered={isHovered() || undefined}
392
- >
393
- {/* Hidden select for form submission */}
394
- <div {...containerProps}>
395
- <select {...hiddenSelectProps}>
396
- <option />
397
- <For each={stateProps.items}>
398
- {(item) => {
399
- const itemRecord = isObjectRecord(item) ? item : null;
400
- const fallbackKey = itemRecord != null
401
- ? toKey(itemRecord.key) ?? toKey(itemRecord.id)
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))
402
604
  : undefined;
403
- const key = stateProps.getKey?.(item) ?? fallbackKey ?? String(item);
404
- const fallbackTextValue = itemRecord != null
405
- ? toTextValue(itemRecord.textValue) ?? toTextValue(itemRecord.label)
406
- : undefined;
407
- const textValue = stateProps.getTextValue?.(item) ?? fallbackTextValue ?? String(item);
408
- const selectedKeys = state.selectedKeys();
409
- const isSelected = state.selectionMode() === 'multiple'
410
- ? selectedKeys === 'all'
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"
411
611
  ? true
412
612
  : (selectedKeys as Set<Key>).has(key)
413
613
  : key === state.selectedKey();
414
- return (
415
- <option value={String(key)} selected={isSelected}>
416
- {textValue}
417
- </option>
418
- );
419
- }}
420
- </For>
421
- </select>
422
- </div>
423
- <Show when={ariaProps.label}>
424
- <span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
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
+ />
425
653
  </Show>
426
- {props.children}
427
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
+ };
690
+
691
+ return (
692
+ <SelectContext.Provider
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
+ }
720
+ >
721
+ <SelectStateContext.Provider value={state}>
722
+ <FieldErrorContext.Provider value={fieldErrorContext}>
723
+ <RootContent />
724
+ </FieldErrorContext.Provider>
428
725
  </SelectStateContext.Provider>
429
726
  </SelectContext.Provider>
430
727
  );
@@ -434,23 +731,31 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
434
731
  * The trigger button for a select.
435
732
  */
436
733
  export function SelectTrigger(props: SelectTriggerProps): JSX.Element {
437
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
734
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
438
735
 
439
- // Get context
440
736
  const context = useContext(SelectContext);
441
737
  if (!context) {
442
- throw new Error('SelectTrigger must be used within a Select');
738
+ throw new Error("SelectTrigger must be used within a Select");
443
739
  }
444
- 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
+ });
445
752
 
446
- // Create hover
447
753
  const { isHovered, hoverProps } = createHover({
448
754
  get isDisabled() {
449
755
  return state.isDisabled;
450
756
  },
451
757
  });
452
758
 
453
- // Render props values
454
759
  const renderValues = createMemo<SelectTriggerRenderProps>(() => ({
455
760
  isOpen: isOpen(),
456
761
  isFocused: isFocused(),
@@ -459,33 +764,43 @@ export function SelectTrigger(props: SelectTriggerProps): JSX.Element {
459
764
  isDisabled: state.isDisabled,
460
765
  }));
461
766
 
462
- // Resolve render props
463
767
  const renderProps = useRenderProps(
464
768
  {
465
769
  children: props.children,
466
770
  class: local.class,
467
771
  style: local.style,
468
- defaultClassName: 'solidaria-Select-trigger',
772
+ defaultClassName: "solidaria-Select-trigger",
469
773
  },
470
- renderValues
774
+ renderValues,
471
775
  );
472
776
 
473
- // Remove ref from spread props
474
777
  const cleanTriggerProps = () => {
475
- const { ref: _ref1, ...rest } = triggerProps as Record<string, unknown>;
778
+ const { ref: _ref1, ...rest } = context.triggerProps as Record<string, unknown>;
476
779
  return rest;
477
780
  };
478
781
  const cleanHoverProps = () => {
479
782
  const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
480
783
  return rest;
481
784
  };
482
-
785
+ const triggerAriaProps = () => context.triggerProps as Record<string, unknown>;
786
+ const menuAriaProps = () => context.menuProps as Record<string, unknown>;
483
787
  return (
484
788
  <button
789
+ ref={setTriggerRef}
485
790
  {...domProps}
486
791
  {...cleanTriggerProps()}
487
792
  {...cleanHoverProps()}
488
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}
489
804
  class={renderProps.class()}
490
805
  style={renderProps.style()}
491
806
  data-open={isOpen() || undefined}
@@ -501,50 +816,67 @@ export function SelectTrigger(props: SelectTriggerProps): JSX.Element {
501
816
 
502
817
  // Default children function for SelectValue - defined at module level for SSR stability
503
818
  function defaultSelectValueChildren<T>(values: SelectValueRenderProps<T>) {
504
- return values.selectedText ?? values.placeholder ?? '';
819
+ return values.selectedText ?? values.placeholder ?? "";
505
820
  }
506
821
 
507
822
  /**
508
823
  * Displays the selected value in a select.
509
824
  */
510
825
  export function SelectValue<T>(props: SelectValueProps<T>): JSX.Element {
511
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'placeholder', 'children']);
826
+ const [local, domProps] = splitProps(props, [
827
+ "class",
828
+ "style",
829
+ "slot",
830
+ "placeholder",
831
+ "children",
832
+ ]);
512
833
 
513
- // Get context
514
834
  const context = useContext(SelectContext);
515
835
  if (!context) {
516
- throw new Error('SelectValue must be used within a Select');
836
+ throw new Error("SelectValue must be used within a Select");
517
837
  }
518
838
  const { valueProps, placeholder: contextPlaceholder } = context;
519
839
  const state = context.state as SelectState<T>;
520
840
 
521
- // Use local placeholder if provided, otherwise fall back to context
522
841
  const placeholder = () => local.placeholder ?? contextPlaceholder;
523
842
 
524
- // Render props values
525
843
  const renderValues = createMemo<SelectValueRenderProps<T>>(() => {
526
- const selectedItem = state.selectedItem();
527
- const selectedItems = state.selectedItems();
528
- const selectedText = state.selectionMode() === 'multiple'
529
- ? selectedItems.map((item) => item.textValue).join(', ')
530
- : selectedItem?.textValue ?? null;
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);
531
862
  return {
532
863
  selectedItem,
864
+ selectedItems,
533
865
  selectedText,
534
- isSelected: state.selectionMode() === 'multiple' ? selectedItems.length > 0 : selectedItem != null,
866
+ isSelected:
867
+ state.selectionMode() === "multiple" ? selectedItems.length > 0 : selectedItem != null,
535
868
  placeholder: placeholder(),
536
869
  };
537
870
  });
538
871
 
539
- // Resolve render props
540
872
  const renderProps = useRenderProps(
541
873
  {
542
874
  children: props.children ?? defaultSelectValueChildren,
543
875
  class: local.class,
544
876
  style: local.style,
545
- defaultClassName: 'solidaria-Select-value',
877
+ defaultClassName: "solidaria-Select-value",
546
878
  },
547
- renderValues
879
+ renderValues,
548
880
  );
549
881
 
550
882
  return (
@@ -555,7 +887,9 @@ export function SelectValue<T>(props: SelectValueProps<T>): JSX.Element {
555
887
  style={renderProps.style()}
556
888
  data-placeholder={!renderValues().isSelected || undefined}
557
889
  >
558
- {renderProps.renderChildren()}
890
+ {props.children == null
891
+ ? (renderValues().selectedText ?? renderValues().placeholder ?? "")
892
+ : renderProps.renderChildren()}
559
893
  </span>
560
894
  );
561
895
  }
@@ -564,59 +898,79 @@ export function SelectValue<T>(props: SelectValueProps<T>): JSX.Element {
564
898
  * The listbox popup for a select.
565
899
  */
566
900
  export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
567
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
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
+ ]);
568
913
 
569
- // Get context
570
914
  const context = useContext(SelectContext);
571
915
  if (!context) {
572
- throw new Error('SelectListBox must be used within a Select');
916
+ throw new Error("SelectListBox must be used within a Select");
573
917
  }
574
- const { menuProps, state: selectState, isOpen } = context;
918
+ const { menuProps, rootRef, state: selectState, isOpen } = context;
575
919
  const state = selectState as SelectState<T>;
576
920
 
577
- // 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
+
578
934
  let listBoxRef: HTMLUListElement | undefined;
579
935
 
580
- // Handle click outside to close select
581
936
  createInteractOutside({
582
- ref: () => listBoxRef ?? null,
937
+ ref: () => rootRef() ?? listBoxRef ?? null,
583
938
  onInteractOutside: () => {
584
939
  if (isOpen()) {
585
940
  state.close();
586
941
  }
587
942
  },
588
943
  get isDisabled() {
589
- return !isOpen();
944
+ return !isOpen() || local.isInPopover === true;
590
945
  },
591
946
  });
592
947
 
593
- // Create listbox aria props - reuse select's internal list state via collection
594
948
  const { listBoxProps } = createListBox(
595
949
  {
596
950
  ...(menuProps as unknown as AriaListBoxProps),
951
+ shouldSelectOnPressUp: true,
952
+ shouldFocusOnHover: true,
953
+ shouldSelectOnFocus: local.isInPopover === true ? false : undefined,
597
954
  get isDisabled() {
598
955
  return state.isDisabled;
599
956
  },
600
957
  },
601
- createSelectListStateAdapter(state)
958
+ createSelectListStateAdapter(state),
602
959
  );
603
960
 
604
- // Render props values
605
961
  const renderValues = createMemo<SelectListBoxRenderProps>(() => ({
606
962
  isFocused: state.isFocused(),
607
963
  }));
608
964
 
609
- // Resolve render props
610
965
  const renderProps = useRenderProps(
611
966
  {
612
967
  class: local.class,
613
968
  style: local.style,
614
- defaultClassName: 'solidaria-Select-listbox',
969
+ defaultClassName: "solidaria-Select-listbox",
615
970
  },
616
- renderValues
971
+ renderValues,
617
972
  );
618
973
 
619
- // Remove ref from spread props
620
974
  const cleanMenuProps = () => {
621
975
  const { ref: _ref1, ...rest } = menuProps as Record<string, unknown>;
622
976
  return rest;
@@ -627,34 +981,74 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
627
981
  };
628
982
 
629
983
  const items = () => Array.from(state.collection());
984
+ createEffect(() => {
985
+ if (!isOpen()) return;
986
+ const focusedKey = state.focusedKey();
987
+ if (focusedKey == null) return;
988
+
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
+ });
630
998
 
631
- return (
632
- <Show when={isOpen()}>
633
- <FocusScope restoreFocus autoFocus>
634
- <ul
635
- ref={(el) => (listBoxRef = el)}
636
- {...domProps}
637
- {...cleanMenuProps()}
638
- {...cleanListBoxProps()}
639
- class={renderProps.class()}
640
- style={renderProps.style()}
641
- data-focused={state.isFocused() || undefined}
642
- >
643
- <Show when={props.children} fallback={
644
- <For each={items()}>
645
- {(node) => (
646
- <SelectOption id={node.key}>
647
- {node.textValue}
648
- </SelectOption>
649
- )}
650
- </For>
651
- }>
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={
652
1018
  <For each={items()}>
653
- {(node) => node.value != null ? props.children!(node.value) : null}
1019
+ {(node) => <SelectOption id={node.key}>{node.textValue}</SelectOption>}
654
1020
  </For>
655
- </Show>
656
- </ul>
657
- </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>
658
1052
  </Show>
659
1053
  );
660
1054
  }
@@ -664,39 +1058,48 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
664
1058
  */
665
1059
  export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
666
1060
  const [local, ariaProps] = splitProps(props, [
667
- 'class',
668
- 'style',
669
- 'slot',
670
- 'id',
671
- 'item',
672
- 'textValue',
1061
+ "class",
1062
+ "style",
1063
+ "slot",
1064
+ "id",
1065
+ "item",
1066
+ "textValue",
673
1067
  ]);
674
1068
 
675
- // Get state from context
676
1069
  const context = useContext(SelectStateContext);
677
1070
  if (!context) {
678
- throw new Error('SelectOption must be used within a Select');
1071
+ throw new Error("SelectOption must be used within a Select");
679
1072
  }
680
1073
  const state = context as SelectState<T>;
681
1074
  const selectContext = useContext(SelectContext) as SelectContextValue<T> | null;
682
1075
 
683
- // Create option aria props - adapt select state to list state interface
684
1076
  const optionAria = createOption<T>(
685
1077
  {
686
1078
  key: local.id,
687
1079
  get isDisabled() {
688
1080
  return Boolean(ariaProps.isDisabled || selectContext?.isDisabled());
689
1081
  },
690
- get 'aria-label'() {
691
- return ariaProps['aria-label'] ?? local.textValue;
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;
1089
+ },
1090
+ get onHoverEnd() {
1091
+ return ariaProps.onHoverEnd;
1092
+ },
1093
+ get onHoverChange() {
1094
+ return ariaProps.onHoverChange;
692
1095
  },
693
1096
  },
694
1097
  {
695
1098
  ...createSelectListStateAdapter(state),
696
1099
  select: (key: Key) => {
697
- if (state.selectionMode() === 'multiple') {
1100
+ if (state.selectionMode() === "multiple") {
698
1101
  const keys = state.selectedKeys();
699
- if (keys === 'all') return;
1102
+ if (keys === "all") return;
700
1103
  state.setSelectedKeys(new Set([...keys, key]));
701
1104
  return;
702
1105
  }
@@ -704,9 +1107,9 @@ export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
704
1107
  state.close();
705
1108
  },
706
1109
  toggleSelection: (key: Key) => {
707
- if (state.selectionMode() === 'multiple') {
1110
+ if (state.selectionMode() === "multiple") {
708
1111
  const keys = state.selectedKeys();
709
- if (keys === 'all') return;
1112
+ if (keys === "all") return;
710
1113
  const next = new Set(keys);
711
1114
  if (next.has(key)) next.delete(key);
712
1115
  else next.add(key);
@@ -718,100 +1121,118 @@ export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
718
1121
  },
719
1122
  replaceSelection: (key: Key) => {
720
1123
  state.setSelectedKey(key);
721
- if (state.selectionMode() !== 'multiple') {
1124
+ if (state.selectionMode() !== "multiple") {
722
1125
  state.close();
723
1126
  }
724
1127
  },
725
- }
726
- );
727
-
728
- // Create hover
729
- const { isHovered, hoverProps } = createHover({
730
- get isDisabled() {
731
- return optionAria.isDisabled();
732
1128
  },
733
- });
1129
+ );
1130
+ const isOptionFocusVisible = () =>
1131
+ optionAria.isFocused() && (selectContext?.isFocusVisible() ?? optionAria.isFocusVisible());
734
1132
 
735
- // Render props values
736
1133
  const renderValues = createMemo<SelectOptionRenderProps>(() => ({
737
1134
  isSelected: optionAria.isSelected(),
738
1135
  isFocused: optionAria.isFocused(),
739
- isFocusVisible: optionAria.isFocusVisible(),
1136
+ isFocusVisible: isOptionFocusVisible(),
740
1137
  isPressed: optionAria.isPressed(),
741
- isHovered: isHovered(),
1138
+ isHovered: optionAria.isHovered(),
742
1139
  isDisabled: optionAria.isDisabled(),
743
1140
  }));
744
1141
 
745
- // Resolve render props
746
1142
  const renderProps = useRenderProps(
747
1143
  {
748
1144
  children: props.children,
749
1145
  class: local.class,
750
1146
  style: local.style,
751
- defaultClassName: 'solidaria-Select-option',
1147
+ defaultClassName: "solidaria-Select-option",
752
1148
  },
753
- renderValues
1149
+ renderValues,
754
1150
  );
755
1151
  const hasPrimitiveLabel = () => {
756
- return typeof props.children === 'string' || typeof props.children === 'number';
1152
+ return typeof props.children === "string" || typeof props.children === "number";
757
1153
  };
758
1154
 
759
1155
  const selectionIndicatorContext = createMemo<SelectionIndicatorContextValue>(() => ({
760
1156
  isSelected: optionAria.isSelected,
761
1157
  }));
762
1158
 
763
- // Remove ref from spread props
764
1159
  const cleanOptionProps = () => {
765
1160
  const {
766
1161
  ref: _ref1,
767
- 'aria-describedby': _ariaDescribedby,
1162
+ "aria-describedby": _ariaDescribedby,
768
1163
  ...rest
769
1164
  } = optionAria.optionProps as Record<string, unknown>;
770
- if (!hasPrimitiveLabel() && rest['aria-label'] == null) {
771
- delete rest['aria-labelledby'];
1165
+ if (!hasPrimitiveLabel() && rest["aria-label"] == null) {
1166
+ delete rest["aria-labelledby"];
772
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>;
773
1181
  return rest;
774
1182
  };
775
- const cleanHoverProps = () => {
776
- const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
777
- 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();
778
1198
  };
779
1199
 
780
1200
  return (
781
1201
  <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
782
1202
  <li
783
1203
  {...cleanOptionProps()}
784
- {...cleanHoverProps()}
785
1204
  class={renderProps.class()}
786
1205
  style={renderProps.style()}
787
1206
  data-selected={optionAria.isSelected() || undefined}
788
1207
  data-focused={optionAria.isFocused() || undefined}
789
- data-focus-visible={optionAria.isFocusVisible() || undefined}
1208
+ data-focus-visible={isOptionFocusVisible() || undefined}
790
1209
  data-pressed={optionAria.isPressed() || undefined}
791
- data-hovered={isHovered() || undefined}
1210
+ data-hovered={optionAria.isHovered() || undefined}
792
1211
  data-disabled={optionAria.isDisabled() || undefined}
793
1212
  >
794
- {hasPrimitiveLabel()
795
- ? <span {...optionAria.labelProps}>{renderProps.renderChildren()}</span>
796
- : renderProps.renderChildren()}
1213
+ {hasPrimitiveLabel() ? (
1214
+ <span {...optionAria.labelProps}>{renderProps.renderChildren()}</span>
1215
+ ) : (
1216
+ renderProps.renderChildren()
1217
+ )}
797
1218
  </li>
798
1219
  </SelectionIndicatorContext.Provider>
799
1220
  );
800
1221
  }
801
1222
 
802
1223
  function isObjectRecord(value: unknown): value is Record<string, unknown> {
803
- return typeof value === 'object' && value !== null;
1224
+ return typeof value === "object" && value !== null;
804
1225
  }
805
1226
 
806
1227
  function toKey(value: unknown): Key | undefined {
807
- if (typeof value === 'string' || typeof value === 'number') {
1228
+ if (typeof value === "string" || typeof value === "number") {
808
1229
  return value;
809
1230
  }
810
1231
  return undefined;
811
1232
  }
812
1233
 
813
1234
  function toTextValue(value: unknown): string | undefined {
814
- if (typeof value === 'string' || typeof value === 'number') {
1235
+ if (typeof value === "string" || typeof value === "number") {
815
1236
  return String(value);
816
1237
  }
817
1238
  return undefined;
@@ -820,9 +1241,7 @@ function toTextValue(value: unknown): string | undefined {
820
1241
  function createSelectListStateAdapter<T>(state: SelectState<T>): ListState<T> {
821
1242
  const selectedKeys = createMemo(() => {
822
1243
  const keys = state.selectedKeys();
823
- return keys === 'all'
824
- ? new Set(Array.from(state.collection()).map((item) => item.key))
825
- : keys;
1244
+ return keys === "all" ? new Set(Array.from(state.collection()).map((item) => item.key)) : keys;
826
1245
  });
827
1246
 
828
1247
  const disabledKeys = createMemo(() => {
@@ -841,23 +1260,23 @@ function createSelectListStateAdapter<T>(state: SelectState<T>): ListState<T> {
841
1260
  setFocusedKey: (key) => state.setFocusedKey(key ?? null),
842
1261
  childFocusStrategy: () => null,
843
1262
  selectionMode: () => state.selectionMode(),
844
- selectionBehavior: () => 'replace',
1263
+ selectionBehavior: () => "replace",
845
1264
  disallowEmptySelection: () => true,
846
1265
  selectedKeys,
847
1266
  disabledKeys,
848
- disabledBehavior: () => 'all',
1267
+ disabledBehavior: () => "all",
849
1268
  isEmpty: () => selectedKeys().size === 0,
850
- isSelectAll: () => state.selectedKeys() === 'all',
1269
+ isSelectAll: () => state.selectedKeys() === "all",
851
1270
  isSelected: (key) => selectedKeys().has(key),
852
1271
  isDisabled: state.isKeyDisabled,
853
1272
  setSelectionBehavior: () => {},
854
1273
  toggleSelection: (key) => {
855
- if (state.selectionMode() !== 'multiple') {
1274
+ if (state.selectionMode() !== "multiple") {
856
1275
  state.setSelectedKey(key);
857
1276
  return;
858
1277
  }
859
1278
  const keys = state.selectedKeys();
860
- if (keys === 'all') return;
1279
+ if (keys === "all") return;
861
1280
  const next = new Set(keys);
862
1281
  if (next.has(key)) next.delete(key);
863
1282
  else next.add(key);
@@ -866,16 +1285,20 @@ function createSelectListStateAdapter<T>(state: SelectState<T>): ListState<T> {
866
1285
  replaceSelection: (key) => state.setSelectedKey(key),
867
1286
  setSelectedKeys: (keys) => state.setSelectedKeys(keys),
868
1287
  selectAll: () => {},
869
- clearSelection: () => state.selectionMode() === 'multiple' ? state.setSelectedKeys([]) : state.setSelectedKey(null),
1288
+ clearSelection: () =>
1289
+ state.selectionMode() === "multiple" ? state.setSelectedKeys([]) : state.setSelectedKey(null),
870
1290
  toggleSelectAll: () => {},
871
1291
  extendSelection: (toKey) => state.setSelectedKey(toKey),
872
- select: (key) => state.selectionMode() === 'multiple'
873
- ? state.setSelectedKeys([...(state.selectedKeys() === 'all' ? [] : state.selectedKeys() as Set<Key>), key])
874
- : state.setSelectedKey(key),
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),
875
1299
  };
876
1300
  }
877
1301
 
878
- // Attach sub-components
879
1302
  Select.Trigger = SelectTrigger;
880
1303
  Select.Value = SelectValue;
881
1304
  Select.ListBox = SelectListBox;