@proyecto-viviana/solidaria-components 0.2.5 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ActionBar.d.ts +71 -0
  3. package/dist/ActionBar.d.ts.map +1 -0
  4. package/dist/ActionGroup.d.ts +74 -0
  5. package/dist/ActionGroup.d.ts.map +1 -0
  6. package/dist/Alert.d.ts +70 -0
  7. package/dist/Alert.d.ts.map +1 -0
  8. package/dist/Breadcrumbs.d.ts +10 -2
  9. package/dist/Breadcrumbs.d.ts.map +1 -1
  10. package/dist/Button.d.ts +4 -0
  11. package/dist/Button.d.ts.map +1 -1
  12. package/dist/Calendar.d.ts +13 -0
  13. package/dist/Calendar.d.ts.map +1 -1
  14. package/dist/Checkbox.d.ts +2 -2
  15. package/dist/Checkbox.d.ts.map +1 -1
  16. package/dist/Collection.d.ts +125 -0
  17. package/dist/Collection.d.ts.map +1 -0
  18. package/dist/Color.d.ts +114 -2
  19. package/dist/Color.d.ts.map +1 -1
  20. package/dist/ColorEditor.d.ts +42 -0
  21. package/dist/ColorEditor.d.ts.map +1 -0
  22. package/dist/ComboBox.d.ts +64 -0
  23. package/dist/ComboBox.d.ts.map +1 -1
  24. package/dist/ContextualHelpTrigger.d.ts +40 -0
  25. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  26. package/dist/DateField.d.ts +27 -2
  27. package/dist/DateField.d.ts.map +1 -1
  28. package/dist/DatePicker.d.ts +67 -2
  29. package/dist/DatePicker.d.ts.map +1 -1
  30. package/dist/Dialog.d.ts.map +1 -1
  31. package/dist/Disclosure.d.ts +2 -0
  32. package/dist/Disclosure.d.ts.map +1 -1
  33. package/dist/DragAndDrop.d.ts +80 -0
  34. package/dist/DragAndDrop.d.ts.map +1 -0
  35. package/dist/DragPreview.d.ts +14 -0
  36. package/dist/DragPreview.d.ts.map +1 -0
  37. package/dist/DropZone.d.ts +27 -0
  38. package/dist/DropZone.d.ts.map +1 -0
  39. package/dist/FieldError.d.ts +23 -0
  40. package/dist/FieldError.d.ts.map +1 -0
  41. package/dist/FileTrigger.d.ts +26 -0
  42. package/dist/FileTrigger.d.ts.map +1 -0
  43. package/dist/Focusable.d.ts +27 -0
  44. package/dist/Focusable.d.ts.map +1 -0
  45. package/dist/Form.d.ts +27 -0
  46. package/dist/Form.d.ts.map +1 -0
  47. package/dist/GridList.d.ts +40 -1
  48. package/dist/GridList.d.ts.map +1 -1
  49. package/dist/Icon.d.ts +57 -0
  50. package/dist/Icon.d.ts.map +1 -0
  51. package/dist/Keyboard.d.ts +13 -0
  52. package/dist/Keyboard.d.ts.map +1 -0
  53. package/dist/Link.d.ts.map +1 -1
  54. package/dist/ListBox.d.ts +43 -1
  55. package/dist/ListBox.d.ts.map +1 -1
  56. package/dist/ListDropTargetDelegate.d.ts +38 -0
  57. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  58. package/dist/Menu.d.ts +20 -2
  59. package/dist/Menu.d.ts.map +1 -1
  60. package/dist/Meter.d.ts +2 -2
  61. package/dist/Meter.d.ts.map +1 -1
  62. package/dist/Modal.d.ts +2 -0
  63. package/dist/Modal.d.ts.map +1 -1
  64. package/dist/NumberField.d.ts +2 -0
  65. package/dist/NumberField.d.ts.map +1 -1
  66. package/dist/Popover.d.ts +4 -2
  67. package/dist/Popover.d.ts.map +1 -1
  68. package/dist/Pressable.d.ts +27 -0
  69. package/dist/Pressable.d.ts.map +1 -0
  70. package/dist/ProgressBar.d.ts +2 -2
  71. package/dist/ProgressBar.d.ts.map +1 -1
  72. package/dist/RadioGroup.d.ts.map +1 -1
  73. package/dist/RangeCalendar.d.ts +5 -0
  74. package/dist/RangeCalendar.d.ts.map +1 -1
  75. package/dist/RouterProvider.d.ts +75 -0
  76. package/dist/RouterProvider.d.ts.map +1 -0
  77. package/dist/SearchField.d.ts +2 -3
  78. package/dist/SearchField.d.ts.map +1 -1
  79. package/dist/Select.d.ts +11 -0
  80. package/dist/Select.d.ts.map +1 -1
  81. package/dist/SelectionIndicator.d.ts +30 -0
  82. package/dist/SelectionIndicator.d.ts.map +1 -0
  83. package/dist/SharedElementTransition.d.ts +39 -0
  84. package/dist/SharedElementTransition.d.ts.map +1 -0
  85. package/dist/Slider.d.ts +6 -3
  86. package/dist/Slider.d.ts.map +1 -1
  87. package/dist/Table.d.ts +39 -0
  88. package/dist/Table.d.ts.map +1 -1
  89. package/dist/Tabs.d.ts +4 -3
  90. package/dist/Tabs.d.ts.map +1 -1
  91. package/dist/TagGroup.d.ts +12 -2
  92. package/dist/TagGroup.d.ts.map +1 -1
  93. package/dist/Text.d.ts +10 -0
  94. package/dist/Text.d.ts.map +1 -0
  95. package/dist/TextField.d.ts +4 -0
  96. package/dist/TextField.d.ts.map +1 -1
  97. package/dist/TimeField.d.ts +26 -1
  98. package/dist/TimeField.d.ts.map +1 -1
  99. package/dist/Toast.d.ts.map +1 -1
  100. package/dist/ToggleButton.d.ts +30 -0
  101. package/dist/ToggleButton.d.ts.map +1 -0
  102. package/dist/ToggleButtonGroup.d.ts +33 -0
  103. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  104. package/dist/Toolbar.d.ts.map +1 -1
  105. package/dist/Tooltip.d.ts +9 -0
  106. package/dist/Tooltip.d.ts.map +1 -1
  107. package/dist/Tree.d.ts +44 -2
  108. package/dist/Tree.d.ts.map +1 -1
  109. package/dist/Virtualizer.d.ts +61 -0
  110. package/dist/Virtualizer.d.ts.map +1 -0
  111. package/dist/VirtualizerLayouts.d.ts +82 -0
  112. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  113. package/dist/VisuallyHidden.d.ts +3 -1
  114. package/dist/VisuallyHidden.d.ts.map +1 -1
  115. package/dist/contexts.d.ts +1 -0
  116. package/dist/contexts.d.ts.map +1 -1
  117. package/dist/index.d.ts +57 -25
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +13961 -5946
  120. package/dist/index.js.map +1 -7
  121. package/dist/index.ssr.js +9612 -2401
  122. package/dist/index.ssr.js.map +1 -7
  123. package/dist/useDragAndDrop.d.ts +93 -0
  124. package/dist/useDragAndDrop.d.ts.map +1 -0
  125. package/dist/utils.d.ts +7 -1
  126. package/dist/utils.d.ts.map +1 -1
  127. package/dist/virtualizer/Layout.d.ts +79 -0
  128. package/dist/virtualizer/Layout.d.ts.map +1 -0
  129. package/package.json +8 -6
  130. package/src/ActionBar.tsx +248 -0
  131. package/src/ActionGroup.tsx +285 -0
  132. package/src/Alert.tsx +177 -0
  133. package/src/Autocomplete.tsx +1 -1
  134. package/src/Breadcrumbs.tsx +103 -17
  135. package/src/Button.tsx +65 -21
  136. package/src/Calendar.tsx +179 -53
  137. package/src/Checkbox.tsx +1 -2
  138. package/src/Collection.tsx +341 -0
  139. package/src/Color.tsx +652 -34
  140. package/src/ColorEditor.tsx +231 -0
  141. package/src/ComboBox.tsx +315 -81
  142. package/src/ContextualHelpTrigger.tsx +183 -0
  143. package/src/DateField.tsx +93 -19
  144. package/src/DatePicker.tsx +495 -25
  145. package/src/Dialog.tsx +40 -9
  146. package/src/Disclosure.tsx +33 -27
  147. package/src/DragAndDrop.tsx +334 -0
  148. package/src/DragPreview.tsx +45 -0
  149. package/src/DropZone.tsx +213 -0
  150. package/src/FieldError.tsx +67 -0
  151. package/src/FileTrigger.tsx +83 -0
  152. package/src/Focusable.tsx +106 -0
  153. package/src/Form.tsx +85 -0
  154. package/src/GridList.tsx +379 -41
  155. package/src/Icon.tsx +154 -0
  156. package/src/Keyboard.tsx +26 -0
  157. package/src/Link.tsx +14 -1
  158. package/src/ListBox.tsx +484 -33
  159. package/src/ListDropTargetDelegate.ts +282 -0
  160. package/src/Menu.tsx +388 -35
  161. package/src/Meter.tsx +7 -3
  162. package/src/Modal.tsx +32 -4
  163. package/src/NumberField.tsx +163 -43
  164. package/src/Popover.tsx +136 -180
  165. package/src/Pressable.tsx +108 -0
  166. package/src/ProgressBar.tsx +7 -3
  167. package/src/RadioGroup.tsx +35 -25
  168. package/src/RangeCalendar.tsx +100 -68
  169. package/src/RouterProvider.tsx +240 -0
  170. package/src/SearchField.tsx +142 -34
  171. package/src/Select.tsx +221 -73
  172. package/src/SelectionIndicator.tsx +105 -0
  173. package/src/SharedElementTransition.tsx +258 -0
  174. package/src/Slider.tsx +16 -6
  175. package/src/Table.tsx +417 -57
  176. package/src/Tabs.tsx +68 -35
  177. package/src/TagGroup.tsx +121 -36
  178. package/src/Text.tsx +18 -0
  179. package/src/TextField.tsx +25 -8
  180. package/src/TimeField.tsx +101 -151
  181. package/src/Toast.tsx +108 -14
  182. package/src/ToggleButton.tsx +159 -0
  183. package/src/ToggleButtonGroup.tsx +136 -0
  184. package/src/Toolbar.tsx +14 -8
  185. package/src/Tooltip.tsx +108 -19
  186. package/src/Tree.tsx +1143 -87
  187. package/src/Virtualizer.tsx +702 -0
  188. package/src/VirtualizerLayouts.ts +265 -0
  189. package/src/VisuallyHidden.tsx +15 -21
  190. package/src/contexts.ts +1 -0
  191. package/src/index.ts +1057 -620
  192. package/src/useDragAndDrop.ts +351 -0
  193. package/src/utils.tsx +37 -3
  194. package/src/virtualizer/Layout.ts +200 -0
package/src/Select.tsx CHANGED
@@ -24,10 +24,12 @@ import {
24
24
  createInteractOutside,
25
25
  FocusScope,
26
26
  type AriaSelectProps,
27
+ type AriaListBoxProps,
27
28
  type AriaOptionProps,
28
29
  } from '@proyecto-viviana/solidaria';
29
30
  import {
30
31
  createSelectState,
32
+ type ListState,
31
33
  type SelectState,
32
34
  type Key,
33
35
  type CollectionNode,
@@ -40,6 +42,10 @@ import {
40
42
  useRenderProps,
41
43
  filterDOMProps,
42
44
  } from './utils';
45
+ import {
46
+ SelectionIndicatorContext,
47
+ type SelectionIndicatorContextValue,
48
+ } from './SelectionIndicator';
43
49
 
44
50
  // ============================================
45
51
  // TYPES
@@ -73,12 +79,20 @@ export interface SelectProps<T>
73
79
  getDisabled?: (item: T) => boolean;
74
80
  /** Keys of disabled items. */
75
81
  disabledKeys?: Iterable<Key>;
82
+ /** Selection mode. */
83
+ selectionMode?: 'single' | 'multiple';
76
84
  /** The currently selected key (controlled). */
77
85
  selectedKey?: Key | null;
78
86
  /** The default selected key (uncontrolled). */
79
87
  defaultSelectedKey?: Key | null;
88
+ /** Currently selected keys (controlled, for multiple selection). */
89
+ selectedKeys?: 'all' | Iterable<Key>;
90
+ /** Default selected keys (uncontrolled, for multiple selection). */
91
+ defaultSelectedKeys?: 'all' | Iterable<Key>;
80
92
  /** Handler called when selection changes. */
81
93
  onSelectionChange?: (key: Key | null) => void;
94
+ /** Handler called when selected keys change. */
95
+ onSelectionChangeKeys?: (keys: 'all' | Set<Key>) => void;
82
96
  /** Whether the select is open (controlled). */
83
97
  isOpen?: boolean;
84
98
  /** Whether the select is open by default (uncontrolled). */
@@ -195,10 +209,12 @@ interface SelectContextValue<T> {
195
209
  state: SelectState<T>;
196
210
  triggerProps: JSX.HTMLAttributes<HTMLElement>;
197
211
  valueProps: JSX.HTMLAttributes<HTMLElement>;
212
+ labelProps: JSX.HTMLAttributes<HTMLElement>;
198
213
  menuProps: JSX.HTMLAttributes<HTMLElement>;
199
214
  isOpen: Accessor<boolean>;
200
215
  isFocused: Accessor<boolean>;
201
216
  isFocusVisible: Accessor<boolean>;
217
+ isDisabled: Accessor<boolean>;
202
218
  placeholder?: string;
203
219
  items: T[];
204
220
  renderItem?: (item: T) => JSX.Element;
@@ -206,6 +222,7 @@ interface SelectContextValue<T> {
206
222
 
207
223
  export const SelectContext = createContext<SelectContextValue<unknown> | null>(null);
208
224
  export const SelectStateContext = createContext<SelectState<unknown> | null>(null);
225
+ export const SelectValueContext = SelectContext;
209
226
 
210
227
  // ============================================
211
228
  // COMPONENTS
@@ -218,9 +235,17 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
218
235
  const [local, stateProps, ariaProps] = splitProps(
219
236
  props,
220
237
  ['class', 'style', 'slot'],
221
- ['items', 'getKey', 'getTextValue', 'getDisabled', 'disabledKeys', 'selectedKey', 'defaultSelectedKey', 'onSelectionChange', 'isOpen', 'defaultOpen', 'onOpenChange', 'name']
238
+ ['items', 'getKey', 'getTextValue', 'getDisabled', 'disabledKeys', 'selectionMode', 'selectedKey', 'defaultSelectedKey', 'selectedKeys', 'defaultSelectedKeys', 'onSelectionChange', 'onSelectionChangeKeys', 'isOpen', 'defaultOpen', 'onOpenChange', 'name']
222
239
  );
223
240
 
241
+ const resolveDisabled = (): boolean => {
242
+ const disabled = ariaProps.isDisabled;
243
+ if (typeof disabled === 'function') {
244
+ return (disabled as () => boolean)();
245
+ }
246
+ return !!disabled;
247
+ };
248
+
224
249
  // Create select state
225
250
  const state = createSelectState<T>({
226
251
  get items() {
@@ -238,15 +263,27 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
238
263
  get disabledKeys() {
239
264
  return stateProps.disabledKeys;
240
265
  },
266
+ get selectionMode() {
267
+ return stateProps.selectionMode;
268
+ },
241
269
  get selectedKey() {
242
270
  return stateProps.selectedKey;
243
271
  },
244
272
  get defaultSelectedKey() {
245
273
  return stateProps.defaultSelectedKey;
246
274
  },
275
+ get selectedKeys() {
276
+ return stateProps.selectedKeys;
277
+ },
278
+ get defaultSelectedKeys() {
279
+ return stateProps.defaultSelectedKeys;
280
+ },
247
281
  get onSelectionChange() {
248
282
  return stateProps.onSelectionChange;
249
283
  },
284
+ get onSelectionChangeKeys() {
285
+ return stateProps.onSelectionChangeKeys;
286
+ },
250
287
  get isOpen() {
251
288
  return stateProps.isOpen;
252
289
  },
@@ -257,7 +294,7 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
257
294
  return stateProps.onOpenChange;
258
295
  },
259
296
  get isDisabled() {
260
- return ariaProps.isDisabled;
297
+ return resolveDisabled();
261
298
  },
262
299
  get isRequired() {
263
300
  return ariaProps.isRequired;
@@ -265,7 +302,7 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
265
302
  });
266
303
 
267
304
  // Create select aria props
268
- const { triggerProps, valueProps, menuProps, isFocused, isFocusVisible, isOpen } = createSelect<T>(
305
+ const { labelProps, triggerProps, valueProps, menuProps, isFocused, isFocusVisible, isOpen } = createSelect<T>(
269
306
  ariaProps,
270
307
  state
271
308
  );
@@ -273,7 +310,7 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
273
310
  // Create hover for wrapper
274
311
  const { isHovered, hoverProps } = createHover({
275
312
  get isDisabled() {
276
- return ariaProps.isDisabled;
313
+ return resolveDisabled();
277
314
  },
278
315
  });
279
316
 
@@ -282,9 +319,11 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
282
319
  isOpen: isOpen(),
283
320
  isFocused: isFocused(),
284
321
  isFocusVisible: isFocusVisible(),
285
- isDisabled: !!ariaProps.isDisabled,
322
+ isDisabled: resolveDisabled(),
286
323
  isRequired: !!ariaProps.isRequired,
287
- isSelected: state.selectedKey() != null,
324
+ isSelected: state.selectionMode() === 'multiple'
325
+ ? state.selectedKeys() === 'all' || (state.selectedKeys() as Set<Key>).size > 0
326
+ : state.selectedKey() != null,
288
327
  }));
289
328
 
290
329
  // Resolve render props
@@ -308,13 +347,17 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
308
347
  const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
309
348
  return rest;
310
349
  };
350
+ const cleanLabelProps = () => {
351
+ const { ref: _ref, ...rest } = labelProps as Record<string, unknown>;
352
+ return rest;
353
+ };
311
354
 
312
355
  // Create hidden select for form submission
313
356
  const { containerProps, selectProps: hiddenSelectProps } = createHiddenSelect({
314
357
  state,
315
358
  name: stateProps.name,
316
359
  get isDisabled() {
317
- return ariaProps.isDisabled;
360
+ return resolveDisabled();
318
361
  },
319
362
  });
320
363
 
@@ -324,10 +367,12 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
324
367
  state,
325
368
  triggerProps,
326
369
  valueProps,
370
+ labelProps,
327
371
  menuProps,
328
372
  isOpen,
329
373
  isFocused,
330
374
  isFocusVisible,
375
+ isDisabled: resolveDisabled,
331
376
  placeholder: ariaProps.placeholder,
332
377
  items: stateProps.items,
333
378
  }}
@@ -341,7 +386,7 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
341
386
  data-open={isOpen() || undefined}
342
387
  data-focused={isFocused() || undefined}
343
388
  data-focus-visible={isFocusVisible() || undefined}
344
- data-disabled={ariaProps.isDisabled || undefined}
389
+ data-disabled={resolveDisabled() || undefined}
345
390
  data-required={ariaProps.isRequired || undefined}
346
391
  data-hovered={isHovered() || undefined}
347
392
  >
@@ -351,10 +396,23 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
351
396
  <option />
352
397
  <For each={stateProps.items}>
353
398
  {(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);
399
+ const itemRecord = isObjectRecord(item) ? item : null;
400
+ const fallbackKey = itemRecord != null
401
+ ? toKey(itemRecord.key) ?? toKey(itemRecord.id)
402
+ : 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'
411
+ ? true
412
+ : (selectedKeys as Set<Key>).has(key)
413
+ : key === state.selectedKey();
356
414
  return (
357
- <option value={String(key)} selected={key === state.selectedKey()}>
415
+ <option value={String(key)} selected={isSelected}>
358
416
  {textValue}
359
417
  </option>
360
418
  );
@@ -362,6 +420,9 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
362
420
  </For>
363
421
  </select>
364
422
  </div>
423
+ <Show when={ariaProps.label}>
424
+ <span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
425
+ </Show>
365
426
  {props.children}
366
427
  </div>
367
428
  </SelectStateContext.Provider>
@@ -373,7 +434,7 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
373
434
  * The trigger button for a select.
374
435
  */
375
436
  export function SelectTrigger(props: SelectTriggerProps): JSX.Element {
376
- const [local] = splitProps(props, ['class', 'style', 'slot']);
437
+ const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
377
438
 
378
439
  // Get context
379
440
  const context = useContext(SelectContext);
@@ -421,6 +482,7 @@ export function SelectTrigger(props: SelectTriggerProps): JSX.Element {
421
482
 
422
483
  return (
423
484
  <button
485
+ {...domProps}
424
486
  {...cleanTriggerProps()}
425
487
  {...cleanHoverProps()}
426
488
  type="button"
@@ -446,7 +508,7 @@ function defaultSelectValueChildren<T>(values: SelectValueRenderProps<T>) {
446
508
  * Displays the selected value in a select.
447
509
  */
448
510
  export function SelectValue<T>(props: SelectValueProps<T>): JSX.Element {
449
- const [local] = splitProps(props, ['class', 'style', 'slot', 'placeholder']);
511
+ const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'placeholder', 'children']);
450
512
 
451
513
  // Get context
452
514
  const context = useContext(SelectContext);
@@ -462,10 +524,14 @@ export function SelectValue<T>(props: SelectValueProps<T>): JSX.Element {
462
524
  // Render props values
463
525
  const renderValues = createMemo<SelectValueRenderProps<T>>(() => {
464
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;
465
531
  return {
466
532
  selectedItem,
467
- selectedText: selectedItem?.textValue ?? null,
468
- isSelected: selectedItem != null,
533
+ selectedText,
534
+ isSelected: state.selectionMode() === 'multiple' ? selectedItems.length > 0 : selectedItem != null,
469
535
  placeholder: placeholder(),
470
536
  };
471
537
  });
@@ -483,6 +549,7 @@ export function SelectValue<T>(props: SelectValueProps<T>): JSX.Element {
483
549
 
484
550
  return (
485
551
  <span
552
+ {...domProps}
486
553
  {...valueProps}
487
554
  class={renderProps.class()}
488
555
  style={renderProps.style()}
@@ -497,7 +564,7 @@ export function SelectValue<T>(props: SelectValueProps<T>): JSX.Element {
497
564
  * The listbox popup for a select.
498
565
  */
499
566
  export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
500
- const [local] = splitProps(props, ['class', 'style', 'slot']);
567
+ const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
501
568
 
502
569
  // Get context
503
570
  const context = useContext(SelectContext);
@@ -525,29 +592,13 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
525
592
 
526
593
  // Create listbox aria props - reuse select's internal list state via collection
527
594
  const { listBoxProps } = createListBox(
528
- {},
529
595
  {
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();
596
+ ...(menuProps as unknown as AriaListBoxProps),
597
+ get isDisabled() {
598
+ return state.isDisabled;
538
599
  },
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
600
+ },
601
+ createSelectListStateAdapter(state)
551
602
  );
552
603
 
553
604
  // Render props values
@@ -582,6 +633,7 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
582
633
  <FocusScope restoreFocus autoFocus>
583
634
  <ul
584
635
  ref={(el) => (listBoxRef = el)}
636
+ {...domProps}
585
637
  {...cleanMenuProps()}
586
638
  {...cleanListBoxProps()}
587
639
  class={renderProps.class()}
@@ -626,49 +678,51 @@ export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
626
678
  throw new Error('SelectOption must be used within a Select');
627
679
  }
628
680
  const state = context as SelectState<T>;
681
+ const selectContext = useContext(SelectContext) as SelectContextValue<T> | null;
629
682
 
630
683
  // Create option aria props - adapt select state to list state interface
631
684
  const optionAria = createOption<T>(
632
685
  {
633
686
  key: local.id,
634
687
  get isDisabled() {
635
- return ariaProps.isDisabled;
688
+ return Boolean(ariaProps.isDisabled || selectContext?.isDisabled());
636
689
  },
637
690
  get 'aria-label'() {
638
- return ariaProps['aria-label'];
691
+ return ariaProps['aria-label'] ?? local.textValue;
639
692
  },
640
693
  },
641
694
  {
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,
695
+ ...createSelectListStateAdapter(state),
655
696
  select: (key: Key) => {
697
+ if (state.selectionMode() === 'multiple') {
698
+ const keys = state.selectedKeys();
699
+ if (keys === 'all') return;
700
+ state.setSelectedKeys(new Set([...keys, key]));
701
+ return;
702
+ }
656
703
  state.setSelectedKey(key);
657
704
  state.close();
658
705
  },
659
706
  toggleSelection: (key: Key) => {
707
+ if (state.selectionMode() === 'multiple') {
708
+ const keys = state.selectedKeys();
709
+ if (keys === 'all') return;
710
+ const next = new Set(keys);
711
+ if (next.has(key)) next.delete(key);
712
+ else next.add(key);
713
+ state.setSelectedKeys(next);
714
+ return;
715
+ }
660
716
  state.setSelectedKey(key);
661
717
  state.close();
662
718
  },
663
719
  replaceSelection: (key: Key) => {
664
720
  state.setSelectedKey(key);
665
- state.close();
721
+ if (state.selectionMode() !== 'multiple') {
722
+ state.close();
723
+ }
666
724
  },
667
- extendSelection: () => {},
668
- selectAll: () => {},
669
- clearSelection: () => state.setSelectedKey(null),
670
- childFocusStrategy: () => null,
671
- } as any
725
+ }
672
726
  );
673
727
 
674
728
  // Create hover
@@ -698,10 +752,24 @@ export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
698
752
  },
699
753
  renderValues
700
754
  );
755
+ const hasPrimitiveLabel = () => {
756
+ return typeof props.children === 'string' || typeof props.children === 'number';
757
+ };
758
+
759
+ const selectionIndicatorContext = createMemo<SelectionIndicatorContextValue>(() => ({
760
+ isSelected: optionAria.isSelected,
761
+ }));
701
762
 
702
763
  // Remove ref from spread props
703
764
  const cleanOptionProps = () => {
704
- const { ref: _ref1, ...rest } = optionAria.optionProps as Record<string, unknown>;
765
+ const {
766
+ ref: _ref1,
767
+ 'aria-describedby': _ariaDescribedby,
768
+ ...rest
769
+ } = optionAria.optionProps as Record<string, unknown>;
770
+ if (!hasPrimitiveLabel() && rest['aria-label'] == null) {
771
+ delete rest['aria-labelledby'];
772
+ }
705
773
  return rest;
706
774
  };
707
775
  const cleanHoverProps = () => {
@@ -710,23 +778,103 @@ export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
710
778
  };
711
779
 
712
780
  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>
781
+ <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
782
+ <li
783
+ {...cleanOptionProps()}
784
+ {...cleanHoverProps()}
785
+ class={renderProps.class()}
786
+ style={renderProps.style()}
787
+ data-selected={optionAria.isSelected() || undefined}
788
+ data-focused={optionAria.isFocused() || undefined}
789
+ data-focus-visible={optionAria.isFocusVisible() || undefined}
790
+ data-pressed={optionAria.isPressed() || undefined}
791
+ data-hovered={isHovered() || undefined}
792
+ data-disabled={optionAria.isDisabled() || undefined}
793
+ >
794
+ {hasPrimitiveLabel()
795
+ ? <span {...optionAria.labelProps}>{renderProps.renderChildren()}</span>
796
+ : renderProps.renderChildren()}
797
+ </li>
798
+ </SelectionIndicatorContext.Provider>
727
799
  );
728
800
  }
729
801
 
802
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
803
+ return typeof value === 'object' && value !== null;
804
+ }
805
+
806
+ function toKey(value: unknown): Key | undefined {
807
+ if (typeof value === 'string' || typeof value === 'number') {
808
+ return value;
809
+ }
810
+ return undefined;
811
+ }
812
+
813
+ function toTextValue(value: unknown): string | undefined {
814
+ if (typeof value === 'string' || typeof value === 'number') {
815
+ return String(value);
816
+ }
817
+ return undefined;
818
+ }
819
+
820
+ function createSelectListStateAdapter<T>(state: SelectState<T>): ListState<T> {
821
+ const selectedKeys = createMemo(() => {
822
+ const keys = state.selectedKeys();
823
+ return keys === 'all'
824
+ ? new Set(Array.from(state.collection()).map((item) => item.key))
825
+ : keys;
826
+ });
827
+
828
+ const disabledKeys = createMemo(() => {
829
+ const keys = new Set<Key>();
830
+ for (const node of state.collection()) {
831
+ if (node.isDisabled) keys.add(node.key);
832
+ }
833
+ return keys;
834
+ });
835
+
836
+ return {
837
+ collection: state.collection,
838
+ isFocused: state.isFocused,
839
+ setFocused: state.setFocused,
840
+ focusedKey: state.focusedKey,
841
+ setFocusedKey: (key) => state.setFocusedKey(key ?? null),
842
+ childFocusStrategy: () => null,
843
+ selectionMode: () => state.selectionMode(),
844
+ selectionBehavior: () => 'replace',
845
+ disallowEmptySelection: () => true,
846
+ selectedKeys,
847
+ disabledKeys,
848
+ disabledBehavior: () => 'all',
849
+ isEmpty: () => selectedKeys().size === 0,
850
+ isSelectAll: () => state.selectedKeys() === 'all',
851
+ isSelected: (key) => selectedKeys().has(key),
852
+ isDisabled: state.isKeyDisabled,
853
+ setSelectionBehavior: () => {},
854
+ toggleSelection: (key) => {
855
+ if (state.selectionMode() !== 'multiple') {
856
+ state.setSelectedKey(key);
857
+ return;
858
+ }
859
+ const keys = state.selectedKeys();
860
+ if (keys === 'all') return;
861
+ const next = new Set(keys);
862
+ if (next.has(key)) next.delete(key);
863
+ else next.add(key);
864
+ state.setSelectedKeys(next);
865
+ },
866
+ replaceSelection: (key) => state.setSelectedKey(key),
867
+ setSelectedKeys: (keys) => state.setSelectedKeys(keys),
868
+ selectAll: () => {},
869
+ clearSelection: () => state.selectionMode() === 'multiple' ? state.setSelectedKeys([]) : state.setSelectedKey(null),
870
+ toggleSelectAll: () => {},
871
+ 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),
875
+ };
876
+ }
877
+
730
878
  // Attach sub-components
731
879
  Select.Trigger = SelectTrigger;
732
880
  Select.Value = SelectValue;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared SelectionIndicator primitive for selected collection items.
3
+ */
4
+
5
+ import {
6
+ type JSX,
7
+ createContext,
8
+ createMemo,
9
+ splitProps,
10
+ useContext,
11
+ Show,
12
+ } from 'solid-js';
13
+ import {
14
+ type RenderChildren,
15
+ type ClassNameOrFunction,
16
+ type StyleOrFunction,
17
+ type SlotProps,
18
+ useRenderProps,
19
+ } from './utils';
20
+ import { SharedElement, useHasSharedElementTransitionScope } from './SharedElementTransition';
21
+
22
+ export interface SelectionIndicatorContextValue {
23
+ isSelected: () => boolean;
24
+ }
25
+
26
+ export const SelectionIndicatorContext = createContext<SelectionIndicatorContextValue | null>(null);
27
+
28
+ export interface SelectionIndicatorRenderProps {
29
+ /** Whether the parent item is selected. */
30
+ isSelected: boolean;
31
+ }
32
+
33
+ export interface SelectionIndicatorProps extends SlotProps, Omit<JSX.HTMLAttributes<HTMLSpanElement>, 'class' | 'style' | 'children'> {
34
+ /** Optional controlled selected state override. */
35
+ isSelected?: boolean;
36
+ /** Whether to keep mounted when not selected. */
37
+ shouldForceMount?: boolean;
38
+ /** The children content. */
39
+ children?: RenderChildren<SelectionIndicatorRenderProps>;
40
+ /** The CSS className for the element. */
41
+ class?: ClassNameOrFunction<SelectionIndicatorRenderProps>;
42
+ /** The inline style for the element. */
43
+ style?: StyleOrFunction<SelectionIndicatorRenderProps>;
44
+ }
45
+
46
+ /**
47
+ * SelectionIndicator renders when its parent item is selected.
48
+ */
49
+ export function SelectionIndicator(props: SelectionIndicatorProps): JSX.Element {
50
+ const [local, domProps] = splitProps(props, ['isSelected', 'shouldForceMount', 'children', 'class', 'style', 'slot']);
51
+
52
+ const context = useContext(SelectionIndicatorContext);
53
+ const hasSharedElementScope = useHasSharedElementTransitionScope();
54
+ const isSelected = () => local.isSelected ?? context?.isSelected() ?? false;
55
+ const isVisible = () => local.shouldForceMount || isSelected();
56
+
57
+ const renderValues = createMemo<SelectionIndicatorRenderProps>(() => ({
58
+ isSelected: isSelected(),
59
+ }));
60
+
61
+ const renderProps = useRenderProps(
62
+ {
63
+ children: local.children,
64
+ class: local.class,
65
+ style: local.style,
66
+ defaultClassName: 'solidaria-SelectionIndicator',
67
+ },
68
+ renderValues
69
+ );
70
+
71
+ const sharedElementProps = createMemo(() => {
72
+ const { ref: _ref, ...rest } = domProps as JSX.HTMLAttributes<HTMLSpanElement> & { ref?: unknown };
73
+ return rest;
74
+ });
75
+
76
+ if (hasSharedElementScope) {
77
+ return (
78
+ <SharedElement
79
+ {...sharedElementProps()}
80
+ name="SelectionIndicator"
81
+ isVisible={isVisible()}
82
+ aria-hidden="true"
83
+ class={renderProps.class()}
84
+ style={renderProps.style()}
85
+ data-selected={isSelected() || undefined}
86
+ >
87
+ {renderProps.renderChildren()}
88
+ </SharedElement>
89
+ );
90
+ }
91
+
92
+ return (
93
+ <Show when={isVisible()}>
94
+ <span
95
+ {...domProps}
96
+ aria-hidden="true"
97
+ class={renderProps.class()}
98
+ style={renderProps.style()}
99
+ data-selected={isSelected() || undefined}
100
+ >
101
+ {renderProps.renderChildren()}
102
+ </span>
103
+ </Show>
104
+ );
105
+ }