@proyecto-viviana/solidaria-components 0.2.2 → 0.2.4

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 (64) hide show
  1. package/dist/Color.d.ts +2 -6
  2. package/dist/Color.d.ts.map +1 -1
  3. package/dist/ComboBox.d.ts +3 -3
  4. package/dist/ComboBox.d.ts.map +1 -1
  5. package/dist/GridList.d.ts +2 -2
  6. package/dist/GridList.d.ts.map +1 -1
  7. package/dist/ListBox.d.ts +5 -5
  8. package/dist/ListBox.d.ts.map +1 -1
  9. package/dist/Menu.d.ts +3 -3
  10. package/dist/Menu.d.ts.map +1 -1
  11. package/dist/Select.d.ts +3 -3
  12. package/dist/Select.d.ts.map +1 -1
  13. package/dist/Table.d.ts +2 -2
  14. package/dist/Table.d.ts.map +1 -1
  15. package/dist/Tabs.d.ts +1 -1
  16. package/dist/Tabs.d.ts.map +1 -1
  17. package/dist/index.js +56 -56
  18. package/dist/index.js.map +2 -2
  19. package/dist/index.ssr.js +56 -56
  20. package/dist/index.ssr.js.map +2 -2
  21. package/package.json +10 -8
  22. package/src/Autocomplete.tsx +174 -0
  23. package/src/Breadcrumbs.tsx +264 -0
  24. package/src/Button.tsx +238 -0
  25. package/src/Calendar.tsx +471 -0
  26. package/src/Checkbox.tsx +387 -0
  27. package/src/Color.tsx +1370 -0
  28. package/src/ComboBox.tsx +824 -0
  29. package/src/DateField.tsx +337 -0
  30. package/src/DatePicker.tsx +367 -0
  31. package/src/Dialog.tsx +262 -0
  32. package/src/Disclosure.tsx +439 -0
  33. package/src/GridList.tsx +511 -0
  34. package/src/Landmark.tsx +203 -0
  35. package/src/Link.tsx +201 -0
  36. package/src/ListBox.tsx +346 -0
  37. package/src/Menu.tsx +544 -0
  38. package/src/Meter.tsx +157 -0
  39. package/src/Modal.tsx +433 -0
  40. package/src/NumberField.tsx +542 -0
  41. package/src/Popover.tsx +540 -0
  42. package/src/ProgressBar.tsx +162 -0
  43. package/src/RadioGroup.tsx +356 -0
  44. package/src/RangeCalendar.tsx +462 -0
  45. package/src/SearchField.tsx +479 -0
  46. package/src/Select.tsx +734 -0
  47. package/src/Separator.tsx +130 -0
  48. package/src/Slider.tsx +500 -0
  49. package/src/Switch.tsx +213 -0
  50. package/src/Table.tsx +857 -0
  51. package/src/Tabs.tsx +552 -0
  52. package/src/TagGroup.tsx +421 -0
  53. package/src/TextField.tsx +271 -0
  54. package/src/TimeField.tsx +455 -0
  55. package/src/Toast.tsx +503 -0
  56. package/src/Toolbar.tsx +160 -0
  57. package/src/Tooltip.tsx +423 -0
  58. package/src/Tree.tsx +551 -0
  59. package/src/VisuallyHidden.tsx +60 -0
  60. package/src/contexts.ts +74 -0
  61. package/src/index.ts +620 -0
  62. package/src/utils.tsx +329 -0
  63. package/dist/index.jsx +0 -9056
  64. package/dist/index.jsx.map +0 -7
package/src/Select.tsx ADDED
@@ -0,0 +1,734 @@
1
+ /**
2
+ * Select component for solidaria-components
3
+ *
4
+ * A pre-wired headless select that combines state + aria hooks.
5
+ * Port of react-aria-components/src/Select.tsx
6
+ */
7
+
8
+ import {
9
+ type JSX,
10
+ type Accessor,
11
+ createContext,
12
+ createMemo,
13
+ splitProps,
14
+ useContext,
15
+ For,
16
+ Show,
17
+ } from 'solid-js';
18
+ import {
19
+ createSelect,
20
+ createHiddenSelect,
21
+ createListBox,
22
+ createOption,
23
+ createHover,
24
+ createInteractOutside,
25
+ FocusScope,
26
+ type AriaSelectProps,
27
+ type AriaOptionProps,
28
+ } from '@proyecto-viviana/solidaria';
29
+ import {
30
+ createSelectState,
31
+ type SelectState,
32
+ type Key,
33
+ type CollectionNode,
34
+ } from '@proyecto-viviana/solid-stately';
35
+ import {
36
+ type RenderChildren,
37
+ type ClassNameOrFunction,
38
+ type StyleOrFunction,
39
+ type SlotProps,
40
+ useRenderProps,
41
+ filterDOMProps,
42
+ } from './utils';
43
+
44
+ // ============================================
45
+ // TYPES
46
+ // ============================================
47
+
48
+ export interface SelectRenderProps {
49
+ /** Whether the select is open. */
50
+ isOpen: boolean;
51
+ /** Whether the select is focused. */
52
+ isFocused: boolean;
53
+ /** Whether the select has keyboard focus. */
54
+ isFocusVisible: boolean;
55
+ /** Whether the select is disabled. */
56
+ isDisabled: boolean;
57
+ /** Whether the select is required. */
58
+ isRequired: boolean;
59
+ /** Whether a value is selected. */
60
+ isSelected: boolean;
61
+ }
62
+
63
+ export interface SelectProps<T>
64
+ extends Omit<AriaSelectProps, 'children'>,
65
+ SlotProps {
66
+ /** The items to render in the select. */
67
+ items: T[];
68
+ /** Function to get the key from an item. */
69
+ getKey?: (item: T) => Key;
70
+ /** Function to get the text value from an item. */
71
+ getTextValue?: (item: T) => string;
72
+ /** Function to check if an item is disabled. */
73
+ getDisabled?: (item: T) => boolean;
74
+ /** Keys of disabled items. */
75
+ disabledKeys?: Iterable<Key>;
76
+ /** The currently selected key (controlled). */
77
+ selectedKey?: Key | null;
78
+ /** The default selected key (uncontrolled). */
79
+ defaultSelectedKey?: Key | null;
80
+ /** Handler called when selection changes. */
81
+ onSelectionChange?: (key: Key | null) => void;
82
+ /** Whether the select is open (controlled). */
83
+ isOpen?: boolean;
84
+ /** Whether the select is open by default (uncontrolled). */
85
+ defaultOpen?: boolean;
86
+ /** Handler called when the open state changes. */
87
+ onOpenChange?: (isOpen: boolean) => void;
88
+ /** Placeholder text when no option is selected. */
89
+ placeholder?: string;
90
+ /** The name of the select, used when submitting an HTML form. */
91
+ name?: string;
92
+ /** The children of the component (compound components: SelectTrigger, SelectListBox). */
93
+ children: JSX.Element;
94
+ /** The CSS className for the element. */
95
+ class?: ClassNameOrFunction<SelectRenderProps>;
96
+ /** The inline style for the element. */
97
+ style?: StyleOrFunction<SelectRenderProps>;
98
+ }
99
+
100
+ export interface SelectValueRenderProps<T> {
101
+ /** The selected item. */
102
+ selectedItem: CollectionNode<T> | null;
103
+ /** The text value of the selected item. */
104
+ selectedText: string | null;
105
+ /** Whether a value is selected. */
106
+ isSelected: boolean;
107
+ /** The placeholder text. */
108
+ placeholder: string | undefined;
109
+ }
110
+
111
+ export interface SelectValueProps<T> extends SlotProps {
112
+ /** The children of the value. A function may be provided to receive render props. */
113
+ children?: RenderChildren<SelectValueRenderProps<T>>;
114
+ /** The CSS className for the element. */
115
+ class?: ClassNameOrFunction<SelectValueRenderProps<T>>;
116
+ /** The inline style for the element. */
117
+ style?: StyleOrFunction<SelectValueRenderProps<T>>;
118
+ /** Placeholder text when no option is selected. Overrides the placeholder from Select. */
119
+ placeholder?: string;
120
+ }
121
+
122
+ export interface SelectTriggerRenderProps {
123
+ /** Whether the select is open. */
124
+ isOpen: boolean;
125
+ /** Whether the trigger is focused. */
126
+ isFocused: boolean;
127
+ /** Whether the trigger has keyboard focus. */
128
+ isFocusVisible: boolean;
129
+ /** Whether the trigger is hovered. */
130
+ isHovered: boolean;
131
+ /** Whether the trigger is disabled. */
132
+ isDisabled: boolean;
133
+ }
134
+
135
+ export interface SelectTriggerProps extends SlotProps {
136
+ /** The children of the trigger. A function may be provided to receive render props. */
137
+ children?: RenderChildren<SelectTriggerRenderProps>;
138
+ /** The CSS className for the element. */
139
+ class?: ClassNameOrFunction<SelectTriggerRenderProps>;
140
+ /** The inline style for the element. */
141
+ style?: StyleOrFunction<SelectTriggerRenderProps>;
142
+ }
143
+
144
+ export interface SelectListBoxRenderProps {
145
+ /** Whether the listbox is focused. */
146
+ isFocused: boolean;
147
+ }
148
+
149
+ export interface SelectListBoxProps<T> extends SlotProps {
150
+ /** The children of the listbox. A function may be provided to render each item. */
151
+ children?: (item: T) => JSX.Element;
152
+ /** The CSS className for the element. */
153
+ class?: ClassNameOrFunction<SelectListBoxRenderProps>;
154
+ /** The inline style for the element. */
155
+ style?: StyleOrFunction<SelectListBoxRenderProps>;
156
+ }
157
+
158
+ export interface SelectOptionRenderProps {
159
+ /** Whether the option is selected. */
160
+ isSelected: boolean;
161
+ /** Whether the option is focused. */
162
+ isFocused: boolean;
163
+ /** Whether the option has keyboard focus. */
164
+ isFocusVisible: boolean;
165
+ /** Whether the option is pressed. */
166
+ isPressed: boolean;
167
+ /** Whether the option is hovered. */
168
+ isHovered: boolean;
169
+ /** Whether the option is disabled. */
170
+ isDisabled: boolean;
171
+ }
172
+
173
+ export interface SelectOptionProps<T>
174
+ extends Omit<AriaOptionProps, 'children' | 'key'>,
175
+ SlotProps {
176
+ /** The unique key for the option. */
177
+ id: Key;
178
+ /** The item value. */
179
+ item?: T;
180
+ /** The children of the option. A function may be provided to receive render props. */
181
+ children?: RenderChildren<SelectOptionRenderProps>;
182
+ /** The CSS className for the element. */
183
+ class?: ClassNameOrFunction<SelectOptionRenderProps>;
184
+ /** The inline style for the element. */
185
+ style?: StyleOrFunction<SelectOptionRenderProps>;
186
+ /** The text value of the option (for typeahead). */
187
+ textValue?: string;
188
+ }
189
+
190
+ // ============================================
191
+ // CONTEXT
192
+ // ============================================
193
+
194
+ interface SelectContextValue<T> {
195
+ state: SelectState<T>;
196
+ triggerProps: JSX.HTMLAttributes<HTMLElement>;
197
+ valueProps: JSX.HTMLAttributes<HTMLElement>;
198
+ menuProps: JSX.HTMLAttributes<HTMLElement>;
199
+ isOpen: Accessor<boolean>;
200
+ isFocused: Accessor<boolean>;
201
+ isFocusVisible: Accessor<boolean>;
202
+ placeholder?: string;
203
+ items: T[];
204
+ renderItem?: (item: T) => JSX.Element;
205
+ }
206
+
207
+ export const SelectContext = createContext<SelectContextValue<unknown> | null>(null);
208
+ export const SelectStateContext = createContext<SelectState<unknown> | null>(null);
209
+
210
+ // ============================================
211
+ // COMPONENTS
212
+ // ============================================
213
+
214
+ /**
215
+ * A select displays a collapsible list of options and allows a user to select one of them.
216
+ */
217
+ export function Select<T>(props: SelectProps<T>): JSX.Element {
218
+ const [local, stateProps, ariaProps] = splitProps(
219
+ props,
220
+ ['class', 'style', 'slot'],
221
+ ['items', 'getKey', 'getTextValue', 'getDisabled', 'disabledKeys', 'selectedKey', 'defaultSelectedKey', 'onSelectionChange', 'isOpen', 'defaultOpen', 'onOpenChange', 'name']
222
+ );
223
+
224
+ // Create select state
225
+ const state = createSelectState<T>({
226
+ get items() {
227
+ return stateProps.items;
228
+ },
229
+ get getKey() {
230
+ return stateProps.getKey;
231
+ },
232
+ get getTextValue() {
233
+ return stateProps.getTextValue;
234
+ },
235
+ get getDisabled() {
236
+ return stateProps.getDisabled;
237
+ },
238
+ get disabledKeys() {
239
+ return stateProps.disabledKeys;
240
+ },
241
+ get selectedKey() {
242
+ return stateProps.selectedKey;
243
+ },
244
+ get defaultSelectedKey() {
245
+ return stateProps.defaultSelectedKey;
246
+ },
247
+ get onSelectionChange() {
248
+ return stateProps.onSelectionChange;
249
+ },
250
+ get isOpen() {
251
+ return stateProps.isOpen;
252
+ },
253
+ get defaultOpen() {
254
+ return stateProps.defaultOpen;
255
+ },
256
+ get onOpenChange() {
257
+ return stateProps.onOpenChange;
258
+ },
259
+ get isDisabled() {
260
+ return ariaProps.isDisabled;
261
+ },
262
+ get isRequired() {
263
+ return ariaProps.isRequired;
264
+ },
265
+ });
266
+
267
+ // Create select aria props
268
+ const { triggerProps, valueProps, menuProps, isFocused, isFocusVisible, isOpen } = createSelect<T>(
269
+ ariaProps,
270
+ state
271
+ );
272
+
273
+ // Create hover for wrapper
274
+ const { isHovered, hoverProps } = createHover({
275
+ get isDisabled() {
276
+ return ariaProps.isDisabled;
277
+ },
278
+ });
279
+
280
+ // Render props values
281
+ const renderValues = createMemo<SelectRenderProps>(() => ({
282
+ isOpen: isOpen(),
283
+ isFocused: isFocused(),
284
+ isFocusVisible: isFocusVisible(),
285
+ isDisabled: !!ariaProps.isDisabled,
286
+ isRequired: !!ariaProps.isRequired,
287
+ isSelected: state.selectedKey() != null,
288
+ }));
289
+
290
+ // Resolve render props
291
+ const renderProps = useRenderProps(
292
+ {
293
+ class: local.class,
294
+ style: local.style,
295
+ defaultClassName: 'solidaria-Select',
296
+ },
297
+ renderValues
298
+ );
299
+
300
+ // Filter DOM props
301
+ const domProps = createMemo(() => {
302
+ const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
303
+ return filtered;
304
+ });
305
+
306
+ // Remove ref from hover props
307
+ const cleanHoverProps = () => {
308
+ const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
309
+ return rest;
310
+ };
311
+
312
+ // Create hidden select for form submission
313
+ const { containerProps, selectProps: hiddenSelectProps } = createHiddenSelect({
314
+ state,
315
+ name: stateProps.name,
316
+ get isDisabled() {
317
+ return ariaProps.isDisabled;
318
+ },
319
+ });
320
+
321
+ return (
322
+ <SelectContext.Provider
323
+ value={{
324
+ state,
325
+ triggerProps,
326
+ valueProps,
327
+ menuProps,
328
+ isOpen,
329
+ isFocused,
330
+ isFocusVisible,
331
+ placeholder: ariaProps.placeholder,
332
+ items: stateProps.items,
333
+ }}
334
+ >
335
+ <SelectStateContext.Provider value={state}>
336
+ <div
337
+ {...domProps()}
338
+ {...cleanHoverProps()}
339
+ class={renderProps.class()}
340
+ style={renderProps.style()}
341
+ data-open={isOpen() || undefined}
342
+ data-focused={isFocused() || undefined}
343
+ data-focus-visible={isFocusVisible() || undefined}
344
+ data-disabled={ariaProps.isDisabled || undefined}
345
+ data-required={ariaProps.isRequired || undefined}
346
+ data-hovered={isHovered() || undefined}
347
+ >
348
+ {/* Hidden select for form submission */}
349
+ <div {...containerProps}>
350
+ <select {...hiddenSelectProps}>
351
+ <option />
352
+ <For each={stateProps.items}>
353
+ {(item) => {
354
+ const key = stateProps.getKey?.(item) ?? (item as any).key ?? (item as any).id;
355
+ const textValue = stateProps.getTextValue?.(item) ?? (item as any).textValue ?? (item as any).label ?? String(item);
356
+ return (
357
+ <option value={String(key)} selected={key === state.selectedKey()}>
358
+ {textValue}
359
+ </option>
360
+ );
361
+ }}
362
+ </For>
363
+ </select>
364
+ </div>
365
+ {props.children}
366
+ </div>
367
+ </SelectStateContext.Provider>
368
+ </SelectContext.Provider>
369
+ );
370
+ }
371
+
372
+ /**
373
+ * The trigger button for a select.
374
+ */
375
+ export function SelectTrigger(props: SelectTriggerProps): JSX.Element {
376
+ const [local] = splitProps(props, ['class', 'style', 'slot']);
377
+
378
+ // Get context
379
+ const context = useContext(SelectContext);
380
+ if (!context) {
381
+ throw new Error('SelectTrigger must be used within a Select');
382
+ }
383
+ const { triggerProps, isOpen, isFocused, isFocusVisible, state } = context;
384
+
385
+ // Create hover
386
+ const { isHovered, hoverProps } = createHover({
387
+ get isDisabled() {
388
+ return state.isDisabled;
389
+ },
390
+ });
391
+
392
+ // Render props values
393
+ const renderValues = createMemo<SelectTriggerRenderProps>(() => ({
394
+ isOpen: isOpen(),
395
+ isFocused: isFocused(),
396
+ isFocusVisible: isFocusVisible(),
397
+ isHovered: isHovered(),
398
+ isDisabled: state.isDisabled,
399
+ }));
400
+
401
+ // Resolve render props
402
+ const renderProps = useRenderProps(
403
+ {
404
+ children: props.children,
405
+ class: local.class,
406
+ style: local.style,
407
+ defaultClassName: 'solidaria-Select-trigger',
408
+ },
409
+ renderValues
410
+ );
411
+
412
+ // Remove ref from spread props
413
+ const cleanTriggerProps = () => {
414
+ const { ref: _ref1, ...rest } = triggerProps as Record<string, unknown>;
415
+ return rest;
416
+ };
417
+ const cleanHoverProps = () => {
418
+ const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
419
+ return rest;
420
+ };
421
+
422
+ return (
423
+ <button
424
+ {...cleanTriggerProps()}
425
+ {...cleanHoverProps()}
426
+ type="button"
427
+ class={renderProps.class()}
428
+ style={renderProps.style()}
429
+ data-open={isOpen() || undefined}
430
+ data-focused={isFocused() || undefined}
431
+ data-focus-visible={isFocusVisible() || undefined}
432
+ data-hovered={isHovered() || undefined}
433
+ data-disabled={state.isDisabled || undefined}
434
+ >
435
+ {renderProps.renderChildren()}
436
+ </button>
437
+ );
438
+ }
439
+
440
+ // Default children function for SelectValue - defined at module level for SSR stability
441
+ function defaultSelectValueChildren<T>(values: SelectValueRenderProps<T>) {
442
+ return values.selectedText ?? values.placeholder ?? '';
443
+ }
444
+
445
+ /**
446
+ * Displays the selected value in a select.
447
+ */
448
+ export function SelectValue<T>(props: SelectValueProps<T>): JSX.Element {
449
+ const [local] = splitProps(props, ['class', 'style', 'slot', 'placeholder']);
450
+
451
+ // Get context
452
+ const context = useContext(SelectContext);
453
+ if (!context) {
454
+ throw new Error('SelectValue must be used within a Select');
455
+ }
456
+ const { valueProps, placeholder: contextPlaceholder } = context;
457
+ const state = context.state as SelectState<T>;
458
+
459
+ // Use local placeholder if provided, otherwise fall back to context
460
+ const placeholder = () => local.placeholder ?? contextPlaceholder;
461
+
462
+ // Render props values
463
+ const renderValues = createMemo<SelectValueRenderProps<T>>(() => {
464
+ const selectedItem = state.selectedItem();
465
+ return {
466
+ selectedItem,
467
+ selectedText: selectedItem?.textValue ?? null,
468
+ isSelected: selectedItem != null,
469
+ placeholder: placeholder(),
470
+ };
471
+ });
472
+
473
+ // Resolve render props
474
+ const renderProps = useRenderProps(
475
+ {
476
+ children: props.children ?? defaultSelectValueChildren,
477
+ class: local.class,
478
+ style: local.style,
479
+ defaultClassName: 'solidaria-Select-value',
480
+ },
481
+ renderValues
482
+ );
483
+
484
+ return (
485
+ <span
486
+ {...valueProps}
487
+ class={renderProps.class()}
488
+ style={renderProps.style()}
489
+ data-placeholder={!renderValues().isSelected || undefined}
490
+ >
491
+ {renderProps.renderChildren()}
492
+ </span>
493
+ );
494
+ }
495
+
496
+ /**
497
+ * The listbox popup for a select.
498
+ */
499
+ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
500
+ const [local] = splitProps(props, ['class', 'style', 'slot']);
501
+
502
+ // Get context
503
+ const context = useContext(SelectContext);
504
+ if (!context) {
505
+ throw new Error('SelectListBox must be used within a Select');
506
+ }
507
+ const { menuProps, state: selectState, isOpen } = context;
508
+ const state = selectState as SelectState<T>;
509
+
510
+ // Ref for the listbox element (for click outside detection)
511
+ let listBoxRef: HTMLUListElement | undefined;
512
+
513
+ // Handle click outside to close select
514
+ createInteractOutside({
515
+ ref: () => listBoxRef ?? null,
516
+ onInteractOutside: () => {
517
+ if (isOpen()) {
518
+ state.close();
519
+ }
520
+ },
521
+ get isDisabled() {
522
+ return !isOpen();
523
+ },
524
+ });
525
+
526
+ // Create listbox aria props - reuse select's internal list state via collection
527
+ const { listBoxProps } = createListBox(
528
+ {},
529
+ {
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();
538
+ },
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
551
+ );
552
+
553
+ // Render props values
554
+ const renderValues = createMemo<SelectListBoxRenderProps>(() => ({
555
+ isFocused: state.isFocused(),
556
+ }));
557
+
558
+ // Resolve render props
559
+ const renderProps = useRenderProps(
560
+ {
561
+ class: local.class,
562
+ style: local.style,
563
+ defaultClassName: 'solidaria-Select-listbox',
564
+ },
565
+ renderValues
566
+ );
567
+
568
+ // Remove ref from spread props
569
+ const cleanMenuProps = () => {
570
+ const { ref: _ref1, ...rest } = menuProps as Record<string, unknown>;
571
+ return rest;
572
+ };
573
+ const cleanListBoxProps = () => {
574
+ const { ref: _ref2, ...rest } = listBoxProps as Record<string, unknown>;
575
+ return rest;
576
+ };
577
+
578
+ const items = () => Array.from(state.collection());
579
+
580
+ return (
581
+ <Show when={isOpen()}>
582
+ <FocusScope restoreFocus autoFocus>
583
+ <ul
584
+ ref={(el) => (listBoxRef = el)}
585
+ {...cleanMenuProps()}
586
+ {...cleanListBoxProps()}
587
+ class={renderProps.class()}
588
+ style={renderProps.style()}
589
+ data-focused={state.isFocused() || undefined}
590
+ >
591
+ <Show when={props.children} fallback={
592
+ <For each={items()}>
593
+ {(node) => (
594
+ <SelectOption id={node.key}>
595
+ {node.textValue}
596
+ </SelectOption>
597
+ )}
598
+ </For>
599
+ }>
600
+ <For each={items()}>
601
+ {(node) => node.value != null ? props.children!(node.value) : null}
602
+ </For>
603
+ </Show>
604
+ </ul>
605
+ </FocusScope>
606
+ </Show>
607
+ );
608
+ }
609
+
610
+ /**
611
+ * An option in a select listbox.
612
+ */
613
+ export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
614
+ const [local, ariaProps] = splitProps(props, [
615
+ 'class',
616
+ 'style',
617
+ 'slot',
618
+ 'id',
619
+ 'item',
620
+ 'textValue',
621
+ ]);
622
+
623
+ // Get state from context
624
+ const context = useContext(SelectStateContext);
625
+ if (!context) {
626
+ throw new Error('SelectOption must be used within a Select');
627
+ }
628
+ const state = context as SelectState<T>;
629
+
630
+ // Create option aria props - adapt select state to list state interface
631
+ const optionAria = createOption<T>(
632
+ {
633
+ key: local.id,
634
+ get isDisabled() {
635
+ return ariaProps.isDisabled;
636
+ },
637
+ get 'aria-label'() {
638
+ return ariaProps['aria-label'];
639
+ },
640
+ },
641
+ {
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,
655
+ select: (key: Key) => {
656
+ state.setSelectedKey(key);
657
+ state.close();
658
+ },
659
+ toggleSelection: (key: Key) => {
660
+ state.setSelectedKey(key);
661
+ state.close();
662
+ },
663
+ replaceSelection: (key: Key) => {
664
+ state.setSelectedKey(key);
665
+ state.close();
666
+ },
667
+ extendSelection: () => {},
668
+ selectAll: () => {},
669
+ clearSelection: () => state.setSelectedKey(null),
670
+ childFocusStrategy: () => null,
671
+ } as any
672
+ );
673
+
674
+ // Create hover
675
+ const { isHovered, hoverProps } = createHover({
676
+ get isDisabled() {
677
+ return optionAria.isDisabled();
678
+ },
679
+ });
680
+
681
+ // Render props values
682
+ const renderValues = createMemo<SelectOptionRenderProps>(() => ({
683
+ isSelected: optionAria.isSelected(),
684
+ isFocused: optionAria.isFocused(),
685
+ isFocusVisible: optionAria.isFocusVisible(),
686
+ isPressed: optionAria.isPressed(),
687
+ isHovered: isHovered(),
688
+ isDisabled: optionAria.isDisabled(),
689
+ }));
690
+
691
+ // Resolve render props
692
+ const renderProps = useRenderProps(
693
+ {
694
+ children: props.children,
695
+ class: local.class,
696
+ style: local.style,
697
+ defaultClassName: 'solidaria-Select-option',
698
+ },
699
+ renderValues
700
+ );
701
+
702
+ // Remove ref from spread props
703
+ const cleanOptionProps = () => {
704
+ const { ref: _ref1, ...rest } = optionAria.optionProps as Record<string, unknown>;
705
+ return rest;
706
+ };
707
+ const cleanHoverProps = () => {
708
+ const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
709
+ return rest;
710
+ };
711
+
712
+ 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>
727
+ );
728
+ }
729
+
730
+ // Attach sub-components
731
+ Select.Trigger = SelectTrigger;
732
+ Select.Value = SelectValue;
733
+ Select.ListBox = SelectListBox;
734
+ Select.Option = SelectOption;