@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.
- package/dist/Color.d.ts +2 -6
- package/dist/Color.d.ts.map +1 -1
- package/dist/ComboBox.d.ts +3 -3
- package/dist/ComboBox.d.ts.map +1 -1
- package/dist/GridList.d.ts +2 -2
- package/dist/GridList.d.ts.map +1 -1
- package/dist/ListBox.d.ts +5 -5
- package/dist/ListBox.d.ts.map +1 -1
- package/dist/Menu.d.ts +3 -3
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Select.d.ts +3 -3
- package/dist/Select.d.ts.map +1 -1
- package/dist/Table.d.ts +2 -2
- package/dist/Table.d.ts.map +1 -1
- package/dist/Tabs.d.ts +1 -1
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/index.js +56 -56
- package/dist/index.js.map +2 -2
- package/dist/index.ssr.js +56 -56
- package/dist/index.ssr.js.map +2 -2
- package/package.json +10 -8
- package/src/Autocomplete.tsx +174 -0
- package/src/Breadcrumbs.tsx +264 -0
- package/src/Button.tsx +238 -0
- package/src/Calendar.tsx +471 -0
- package/src/Checkbox.tsx +387 -0
- package/src/Color.tsx +1370 -0
- package/src/ComboBox.tsx +824 -0
- package/src/DateField.tsx +337 -0
- package/src/DatePicker.tsx +367 -0
- package/src/Dialog.tsx +262 -0
- package/src/Disclosure.tsx +439 -0
- package/src/GridList.tsx +511 -0
- package/src/Landmark.tsx +203 -0
- package/src/Link.tsx +201 -0
- package/src/ListBox.tsx +346 -0
- package/src/Menu.tsx +544 -0
- package/src/Meter.tsx +157 -0
- package/src/Modal.tsx +433 -0
- package/src/NumberField.tsx +542 -0
- package/src/Popover.tsx +540 -0
- package/src/ProgressBar.tsx +162 -0
- package/src/RadioGroup.tsx +356 -0
- package/src/RangeCalendar.tsx +462 -0
- package/src/SearchField.tsx +479 -0
- package/src/Select.tsx +734 -0
- package/src/Separator.tsx +130 -0
- package/src/Slider.tsx +500 -0
- package/src/Switch.tsx +213 -0
- package/src/Table.tsx +857 -0
- package/src/Tabs.tsx +552 -0
- package/src/TagGroup.tsx +421 -0
- package/src/TextField.tsx +271 -0
- package/src/TimeField.tsx +455 -0
- package/src/Toast.tsx +503 -0
- package/src/Toolbar.tsx +160 -0
- package/src/Tooltip.tsx +423 -0
- package/src/Tree.tsx +551 -0
- package/src/VisuallyHidden.tsx +60 -0
- package/src/contexts.ts +74 -0
- package/src/index.ts +620 -0
- package/src/utils.tsx +329 -0
- package/dist/index.jsx +0 -9056
- 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;
|