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