@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/GridList.tsx
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GridList component for solidaria-components
|
|
3
|
+
*
|
|
4
|
+
* A pre-wired headless grid list that combines state + aria hooks.
|
|
5
|
+
* Based on react-aria-components/src/GridList.tsx
|
|
6
|
+
*
|
|
7
|
+
* GridList is similar to ListBox but supports interactive elements within items
|
|
8
|
+
* and uses grid keyboard navigation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type JSX,
|
|
13
|
+
createContext,
|
|
14
|
+
createMemo,
|
|
15
|
+
createSignal,
|
|
16
|
+
splitProps,
|
|
17
|
+
useContext,
|
|
18
|
+
For,
|
|
19
|
+
} from 'solid-js';
|
|
20
|
+
import {
|
|
21
|
+
createGridList,
|
|
22
|
+
createGridListItem,
|
|
23
|
+
createGridListSelectionCheckbox,
|
|
24
|
+
createFocusRing,
|
|
25
|
+
createHover,
|
|
26
|
+
type AriaGridListProps,
|
|
27
|
+
} from '@proyecto-viviana/solidaria';
|
|
28
|
+
import {
|
|
29
|
+
createGridState,
|
|
30
|
+
type GridState,
|
|
31
|
+
type GridCollection,
|
|
32
|
+
type GridNode,
|
|
33
|
+
type Key,
|
|
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 GridListRenderProps {
|
|
49
|
+
/** Whether the grid list has focus. */
|
|
50
|
+
isFocused: boolean;
|
|
51
|
+
/** Whether the grid list has keyboard focus. */
|
|
52
|
+
isFocusVisible: boolean;
|
|
53
|
+
/** Whether the grid list is disabled. */
|
|
54
|
+
isDisabled: boolean;
|
|
55
|
+
/** Whether the grid list is empty. */
|
|
56
|
+
isEmpty: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface GridListProps<T extends object> extends Omit<AriaGridListProps, 'children'>, SlotProps {
|
|
60
|
+
/** The items to render in the grid list. */
|
|
61
|
+
items: T[];
|
|
62
|
+
/** Function to get the key from an item. */
|
|
63
|
+
getKey?: (item: T) => Key;
|
|
64
|
+
/** Function to get the text value from an item. */
|
|
65
|
+
getTextValue?: (item: T) => string;
|
|
66
|
+
/** Function to check if an item is disabled. */
|
|
67
|
+
getDisabled?: (item: T) => boolean;
|
|
68
|
+
/** The selection mode. */
|
|
69
|
+
selectionMode?: 'none' | 'single' | 'multiple';
|
|
70
|
+
/** Keys of disabled items. */
|
|
71
|
+
disabledKeys?: Iterable<Key>;
|
|
72
|
+
/** Currently selected keys (controlled). */
|
|
73
|
+
selectedKeys?: 'all' | Iterable<Key>;
|
|
74
|
+
/** Default selected keys (uncontrolled). */
|
|
75
|
+
defaultSelectedKeys?: 'all' | Iterable<Key>;
|
|
76
|
+
/** Handler called when selection changes. */
|
|
77
|
+
onSelectionChange?: (keys: 'all' | Set<Key>) => void;
|
|
78
|
+
/** The children of the component. A function may be provided to render each item. */
|
|
79
|
+
children: (item: T) => JSX.Element;
|
|
80
|
+
/** The CSS className for the element. */
|
|
81
|
+
class?: ClassNameOrFunction<GridListRenderProps>;
|
|
82
|
+
/** The inline style for the element. */
|
|
83
|
+
style?: StyleOrFunction<GridListRenderProps>;
|
|
84
|
+
/** A function to render when the grid list is empty. */
|
|
85
|
+
renderEmptyState?: () => JSX.Element;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface GridListItemRenderProps {
|
|
89
|
+
/** Whether the item is selected. */
|
|
90
|
+
isSelected: boolean;
|
|
91
|
+
/** Whether the item is focused. */
|
|
92
|
+
isFocused: boolean;
|
|
93
|
+
/** Whether the item has keyboard focus. */
|
|
94
|
+
isFocusVisible: boolean;
|
|
95
|
+
/** Whether the item is pressed. */
|
|
96
|
+
isPressed: boolean;
|
|
97
|
+
/** Whether the item is hovered. */
|
|
98
|
+
isHovered: boolean;
|
|
99
|
+
/** Whether the item is disabled. */
|
|
100
|
+
isDisabled: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface GridListItemProps<T extends object> extends SlotProps {
|
|
104
|
+
/** The unique key for the item. */
|
|
105
|
+
id: Key;
|
|
106
|
+
/** The item value. */
|
|
107
|
+
item?: T;
|
|
108
|
+
/** The children of the item. A function may be provided to receive render props. */
|
|
109
|
+
children?: RenderChildren<GridListItemRenderProps>;
|
|
110
|
+
/** The CSS className for the element. */
|
|
111
|
+
class?: ClassNameOrFunction<GridListItemRenderProps>;
|
|
112
|
+
/** The inline style for the element. */
|
|
113
|
+
style?: StyleOrFunction<GridListItemRenderProps>;
|
|
114
|
+
/** The text value of the item (for accessibility). */
|
|
115
|
+
textValue?: string;
|
|
116
|
+
/** Handler called when the item is activated. */
|
|
117
|
+
onAction?: () => void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================
|
|
121
|
+
// CONTEXT
|
|
122
|
+
// ============================================
|
|
123
|
+
|
|
124
|
+
interface GridListContextValue<T extends object> {
|
|
125
|
+
state: GridState<T, GridCollection<T>>;
|
|
126
|
+
collection: GridCollection<T>;
|
|
127
|
+
isDisabled: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const GridListContext = createContext<GridListContextValue<object> | null>(null);
|
|
131
|
+
export const GridListStateContext = createContext<GridState<object, GridCollection<object>> | null>(null);
|
|
132
|
+
|
|
133
|
+
// ============================================
|
|
134
|
+
// HELPER: Build GridCollection from items
|
|
135
|
+
// ============================================
|
|
136
|
+
|
|
137
|
+
function buildGridCollection<T extends object>(
|
|
138
|
+
items: T[],
|
|
139
|
+
getKey?: (item: T) => Key,
|
|
140
|
+
getTextValue?: (item: T) => string,
|
|
141
|
+
getDisabled?: (item: T) => boolean
|
|
142
|
+
): GridCollection<T> {
|
|
143
|
+
const nodes: GridNode<T>[] = items.map((item, index) => {
|
|
144
|
+
const key = getKey?.(item) ?? index;
|
|
145
|
+
return {
|
|
146
|
+
type: 'item' as const,
|
|
147
|
+
key,
|
|
148
|
+
value: item,
|
|
149
|
+
textValue: getTextValue?.(item) ?? String(key),
|
|
150
|
+
level: 0,
|
|
151
|
+
index,
|
|
152
|
+
hasChildNodes: false,
|
|
153
|
+
childNodes: [],
|
|
154
|
+
isDisabled: getDisabled?.(item),
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const keyMap = new Map<Key, GridNode<T>>();
|
|
159
|
+
nodes.forEach((node) => keyMap.set(node.key, node));
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
rows: nodes,
|
|
163
|
+
columns: [],
|
|
164
|
+
headerRows: [],
|
|
165
|
+
get rowCount() {
|
|
166
|
+
return nodes.length;
|
|
167
|
+
},
|
|
168
|
+
get columnCount() {
|
|
169
|
+
return 1;
|
|
170
|
+
},
|
|
171
|
+
get size() {
|
|
172
|
+
return nodes.length;
|
|
173
|
+
},
|
|
174
|
+
getKeys() {
|
|
175
|
+
return nodes.map((n) => n.key);
|
|
176
|
+
},
|
|
177
|
+
getItem(key: Key) {
|
|
178
|
+
return keyMap.get(key) ?? null;
|
|
179
|
+
},
|
|
180
|
+
at(index: number) {
|
|
181
|
+
return nodes[index] ?? null;
|
|
182
|
+
},
|
|
183
|
+
getKeyBefore(key: Key) {
|
|
184
|
+
const node = keyMap.get(key);
|
|
185
|
+
if (!node) return null;
|
|
186
|
+
return node.index > 0 ? nodes[node.index - 1].key : null;
|
|
187
|
+
},
|
|
188
|
+
getKeyAfter(key: Key) {
|
|
189
|
+
const node = keyMap.get(key);
|
|
190
|
+
if (!node) return null;
|
|
191
|
+
return node.index < nodes.length - 1 ? nodes[node.index + 1].key : null;
|
|
192
|
+
},
|
|
193
|
+
getFirstKey() {
|
|
194
|
+
return nodes[0]?.key ?? null;
|
|
195
|
+
},
|
|
196
|
+
getLastKey() {
|
|
197
|
+
return nodes[nodes.length - 1]?.key ?? null;
|
|
198
|
+
},
|
|
199
|
+
getChildren(_key: Key) {
|
|
200
|
+
return [];
|
|
201
|
+
},
|
|
202
|
+
getTextValue(key: Key) {
|
|
203
|
+
return keyMap.get(key)?.textValue ?? '';
|
|
204
|
+
},
|
|
205
|
+
getCell(_rowKey: Key, _columnKey: Key) {
|
|
206
|
+
return null;
|
|
207
|
+
},
|
|
208
|
+
[Symbol.iterator]() {
|
|
209
|
+
return nodes[Symbol.iterator]();
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================
|
|
215
|
+
// COMPONENTS
|
|
216
|
+
// ============================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A grid list displays a list of interactive items, with support for
|
|
220
|
+
* keyboard navigation, single or multiple selection, and row actions.
|
|
221
|
+
*/
|
|
222
|
+
export function GridList<T extends object>(props: GridListProps<T>): JSX.Element {
|
|
223
|
+
const [local, stateProps, ariaProps] = splitProps(
|
|
224
|
+
props,
|
|
225
|
+
['children', 'class', 'style', 'slot', 'renderEmptyState'],
|
|
226
|
+
[
|
|
227
|
+
'items',
|
|
228
|
+
'getKey',
|
|
229
|
+
'getTextValue',
|
|
230
|
+
'getDisabled',
|
|
231
|
+
'disabledKeys',
|
|
232
|
+
'selectionMode',
|
|
233
|
+
'selectedKeys',
|
|
234
|
+
'defaultSelectedKeys',
|
|
235
|
+
'onSelectionChange',
|
|
236
|
+
]
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Create ref signal
|
|
240
|
+
const [ref, setRef] = createSignal<HTMLUListElement | null>(null);
|
|
241
|
+
|
|
242
|
+
// Build collection
|
|
243
|
+
const collection = createMemo(() =>
|
|
244
|
+
buildGridCollection(
|
|
245
|
+
stateProps.items,
|
|
246
|
+
stateProps.getKey,
|
|
247
|
+
stateProps.getTextValue,
|
|
248
|
+
stateProps.getDisabled
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Get disabled keys from items + explicit disabledKeys
|
|
253
|
+
const allDisabledKeys = createMemo(() => {
|
|
254
|
+
const keys = new Set<Key>();
|
|
255
|
+
|
|
256
|
+
// Add explicitly disabled keys
|
|
257
|
+
if (stateProps.disabledKeys) {
|
|
258
|
+
for (const key of stateProps.disabledKeys) {
|
|
259
|
+
keys.add(key);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Add keys from items marked as disabled
|
|
264
|
+
for (const node of collection().rows) {
|
|
265
|
+
if (node.isDisabled) {
|
|
266
|
+
keys.add(node.key);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return keys;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Create grid state
|
|
274
|
+
const state = createGridState<T, GridCollection<T>>(() => ({
|
|
275
|
+
collection: collection(),
|
|
276
|
+
disabledKeys: allDisabledKeys(),
|
|
277
|
+
selectionMode: stateProps.selectionMode,
|
|
278
|
+
selectedKeys: stateProps.selectedKeys,
|
|
279
|
+
defaultSelectedKeys: stateProps.defaultSelectedKeys,
|
|
280
|
+
onSelectionChange: stateProps.onSelectionChange,
|
|
281
|
+
}));
|
|
282
|
+
|
|
283
|
+
// Create grid list aria props
|
|
284
|
+
const { gridProps } = createGridList<T, GridCollection<T>>(
|
|
285
|
+
() => ({
|
|
286
|
+
id: ariaProps.id,
|
|
287
|
+
'aria-label': ariaProps['aria-label'],
|
|
288
|
+
'aria-labelledby': ariaProps['aria-labelledby'],
|
|
289
|
+
'aria-describedby': ariaProps['aria-describedby'],
|
|
290
|
+
isVirtualized: ariaProps.isVirtualized,
|
|
291
|
+
onAction: ariaProps.onAction,
|
|
292
|
+
isDisabled: ariaProps.isDisabled,
|
|
293
|
+
}),
|
|
294
|
+
() => state,
|
|
295
|
+
ref
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Create focus ring
|
|
299
|
+
const { isFocused, isFocusVisible, focusProps } = createFocusRing();
|
|
300
|
+
|
|
301
|
+
// Render props values
|
|
302
|
+
const renderValues = createMemo<GridListRenderProps>(() => ({
|
|
303
|
+
isFocused: state.isFocused || isFocused(),
|
|
304
|
+
isFocusVisible: isFocusVisible(),
|
|
305
|
+
isDisabled: ariaProps.isDisabled ?? false,
|
|
306
|
+
isEmpty: stateProps.items.length === 0,
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
// Resolve render props
|
|
310
|
+
const renderProps = useRenderProps(
|
|
311
|
+
{
|
|
312
|
+
class: local.class,
|
|
313
|
+
style: local.style,
|
|
314
|
+
defaultClassName: 'solidaria-GridList',
|
|
315
|
+
},
|
|
316
|
+
renderValues
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Filter DOM props
|
|
320
|
+
const domProps = createMemo(() => {
|
|
321
|
+
const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
|
|
322
|
+
return filtered;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Remove ref from spread props
|
|
326
|
+
const cleanGridProps = () => {
|
|
327
|
+
const { ref: _ref1, ...rest } = gridProps as Record<string, unknown>;
|
|
328
|
+
return rest;
|
|
329
|
+
};
|
|
330
|
+
const cleanFocusProps = () => {
|
|
331
|
+
const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
|
|
332
|
+
return rest;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const isEmpty = () => stateProps.items.length === 0;
|
|
336
|
+
|
|
337
|
+
const contextValue = createMemo<GridListContextValue<T>>(() => ({
|
|
338
|
+
state,
|
|
339
|
+
collection: collection(),
|
|
340
|
+
isDisabled: ariaProps.isDisabled ?? false,
|
|
341
|
+
}));
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<GridListContext.Provider value={contextValue() as GridListContextValue<object>}>
|
|
345
|
+
<GridListStateContext.Provider value={state as unknown as GridState<object, GridCollection<object>>}>
|
|
346
|
+
<ul
|
|
347
|
+
ref={setRef}
|
|
348
|
+
{...domProps()}
|
|
349
|
+
{...cleanGridProps()}
|
|
350
|
+
{...cleanFocusProps()}
|
|
351
|
+
class={renderProps.class()}
|
|
352
|
+
style={renderProps.style()}
|
|
353
|
+
data-focused={state.isFocused || undefined}
|
|
354
|
+
data-focus-visible={isFocusVisible() || undefined}
|
|
355
|
+
data-disabled={ariaProps.isDisabled || undefined}
|
|
356
|
+
data-empty={isEmpty() || undefined}
|
|
357
|
+
>
|
|
358
|
+
{isEmpty() && local.renderEmptyState ? (
|
|
359
|
+
local.renderEmptyState()
|
|
360
|
+
) : (
|
|
361
|
+
<For each={stateProps.items}>{(item) => props.children(item)}</For>
|
|
362
|
+
)}
|
|
363
|
+
</ul>
|
|
364
|
+
</GridListStateContext.Provider>
|
|
365
|
+
</GridListContext.Provider>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* An item in a grid list.
|
|
371
|
+
*/
|
|
372
|
+
export function GridListItem<T extends object>(props: GridListItemProps<T>): JSX.Element {
|
|
373
|
+
const [local] = splitProps(props, [
|
|
374
|
+
'class',
|
|
375
|
+
'style',
|
|
376
|
+
'slot',
|
|
377
|
+
'id',
|
|
378
|
+
'item',
|
|
379
|
+
'textValue',
|
|
380
|
+
'onAction',
|
|
381
|
+
]);
|
|
382
|
+
|
|
383
|
+
// Get state from context
|
|
384
|
+
const context = useContext(GridListStateContext);
|
|
385
|
+
if (!context) {
|
|
386
|
+
throw new Error('GridListItem must be used within a GridList');
|
|
387
|
+
}
|
|
388
|
+
const state = context as GridState<T, GridCollection<T>>;
|
|
389
|
+
|
|
390
|
+
// Create ref signal
|
|
391
|
+
const [ref, setRef] = createSignal<HTMLLIElement | null>(null);
|
|
392
|
+
|
|
393
|
+
// Find or create the item node
|
|
394
|
+
const itemNode = createMemo(() => {
|
|
395
|
+
const node = state.collection.getItem(local.id);
|
|
396
|
+
if (!node) {
|
|
397
|
+
// Create a simple node for the item
|
|
398
|
+
return {
|
|
399
|
+
type: 'item' as const,
|
|
400
|
+
key: local.id,
|
|
401
|
+
value: local.item ?? null,
|
|
402
|
+
textValue: local.textValue ?? String(local.id),
|
|
403
|
+
level: 0,
|
|
404
|
+
index: 0,
|
|
405
|
+
hasChildNodes: false,
|
|
406
|
+
childNodes: [],
|
|
407
|
+
} as GridNode<T>;
|
|
408
|
+
}
|
|
409
|
+
return node as GridNode<T>;
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Create item aria props
|
|
413
|
+
const { rowProps, gridCellProps, isSelected, isDisabled, isPressed } = createGridListItem<T, GridCollection<T>>(
|
|
414
|
+
() => ({
|
|
415
|
+
node: itemNode(),
|
|
416
|
+
onAction: local.onAction,
|
|
417
|
+
}),
|
|
418
|
+
() => state,
|
|
419
|
+
ref
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
// Create hover
|
|
423
|
+
const { isHovered, hoverProps } = createHover({
|
|
424
|
+
get isDisabled() {
|
|
425
|
+
return isDisabled;
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Create focus ring
|
|
430
|
+
const { isFocusVisible, focusProps } = createFocusRing();
|
|
431
|
+
|
|
432
|
+
// Check if focused
|
|
433
|
+
const isFocused = createMemo(() => state.focusedKey === local.id);
|
|
434
|
+
|
|
435
|
+
// Render props values
|
|
436
|
+
const renderValues = createMemo<GridListItemRenderProps>(() => ({
|
|
437
|
+
isSelected,
|
|
438
|
+
isFocused: isFocused(),
|
|
439
|
+
isFocusVisible: isFocusVisible() && isFocused(),
|
|
440
|
+
isPressed,
|
|
441
|
+
isHovered: isHovered(),
|
|
442
|
+
isDisabled,
|
|
443
|
+
}));
|
|
444
|
+
|
|
445
|
+
// Resolve render props
|
|
446
|
+
const renderProps = useRenderProps(
|
|
447
|
+
{
|
|
448
|
+
children: props.children,
|
|
449
|
+
class: local.class,
|
|
450
|
+
style: local.style,
|
|
451
|
+
defaultClassName: 'solidaria-GridList-item',
|
|
452
|
+
},
|
|
453
|
+
renderValues
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
// Remove ref from spread props
|
|
457
|
+
const cleanRowProps = () => {
|
|
458
|
+
const { ref: _ref1, ...rest } = rowProps as Record<string, unknown>;
|
|
459
|
+
return rest;
|
|
460
|
+
};
|
|
461
|
+
const cleanHoverProps = () => {
|
|
462
|
+
const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
|
|
463
|
+
return rest;
|
|
464
|
+
};
|
|
465
|
+
const cleanFocusProps = () => {
|
|
466
|
+
const { ref: _ref3, ...rest } = focusProps as Record<string, unknown>;
|
|
467
|
+
return rest;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
return (
|
|
471
|
+
<li
|
|
472
|
+
ref={setRef}
|
|
473
|
+
{...cleanRowProps()}
|
|
474
|
+
{...cleanHoverProps()}
|
|
475
|
+
{...cleanFocusProps()}
|
|
476
|
+
class={renderProps.class()}
|
|
477
|
+
style={renderProps.style()}
|
|
478
|
+
data-selected={isSelected || undefined}
|
|
479
|
+
data-focused={isFocused() || undefined}
|
|
480
|
+
data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
|
|
481
|
+
data-pressed={isPressed || undefined}
|
|
482
|
+
data-hovered={isHovered() || undefined}
|
|
483
|
+
data-disabled={isDisabled || undefined}
|
|
484
|
+
>
|
|
485
|
+
<div {...gridCellProps}>{renderProps.renderChildren()}</div>
|
|
486
|
+
</li>
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* A checkbox for item selection in a grid list.
|
|
492
|
+
*/
|
|
493
|
+
export function GridListSelectionCheckbox(props: { itemKey: Key }): JSX.Element {
|
|
494
|
+
const context = useContext(GridListStateContext);
|
|
495
|
+
if (!context) {
|
|
496
|
+
throw new Error('GridListSelectionCheckbox must be used within a GridList');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const state = context as GridState<object, GridCollection<object>>;
|
|
500
|
+
|
|
501
|
+
const { checkboxProps } = createGridListSelectionCheckbox<object, GridCollection<object>>(
|
|
502
|
+
() => ({ key: props.itemKey }),
|
|
503
|
+
() => state
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
return <input {...checkboxProps} />;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Attach Item and SelectionCheckbox as static properties
|
|
510
|
+
GridList.Item = GridListItem;
|
|
511
|
+
GridList.SelectionCheckbox = GridListSelectionCheckbox;
|
package/src/Landmark.tsx
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Landmark component for solidaria-components
|
|
3
|
+
*
|
|
4
|
+
* Pre-wired headless landmark component that combines aria hooks.
|
|
5
|
+
* Enables F6 keyboard navigation between major page sections.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type JSX,
|
|
10
|
+
createContext,
|
|
11
|
+
createMemo,
|
|
12
|
+
createSignal,
|
|
13
|
+
splitProps,
|
|
14
|
+
} from 'solid-js';
|
|
15
|
+
import { Dynamic } from 'solid-js/web';
|
|
16
|
+
import {
|
|
17
|
+
createLandmark,
|
|
18
|
+
getLandmarkController,
|
|
19
|
+
type AriaLandmarkProps,
|
|
20
|
+
type AriaLandmarkRole,
|
|
21
|
+
type LandmarkController,
|
|
22
|
+
} from '@proyecto-viviana/solidaria';
|
|
23
|
+
import {
|
|
24
|
+
type SlotProps,
|
|
25
|
+
filterDOMProps,
|
|
26
|
+
} from './utils';
|
|
27
|
+
|
|
28
|
+
// ============================================
|
|
29
|
+
// TYPES
|
|
30
|
+
// ============================================
|
|
31
|
+
|
|
32
|
+
export interface LandmarkRenderProps {
|
|
33
|
+
/** The ARIA landmark role. */
|
|
34
|
+
role: AriaLandmarkRole;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LandmarkProps
|
|
38
|
+
extends AriaLandmarkProps,
|
|
39
|
+
SlotProps {
|
|
40
|
+
/**
|
|
41
|
+
* The HTML element type to render.
|
|
42
|
+
* @default 'div' (or semantic element based on role)
|
|
43
|
+
*/
|
|
44
|
+
elementType?: keyof JSX.IntrinsicElements;
|
|
45
|
+
/** The CSS className for the element. A function may be provided to receive render props. */
|
|
46
|
+
class?: string | ((renderProps: LandmarkRenderProps) => string);
|
|
47
|
+
/** The inline style for the element. A function may be provided to receive render props. */
|
|
48
|
+
style?: JSX.CSSProperties | ((renderProps: LandmarkRenderProps) => JSX.CSSProperties);
|
|
49
|
+
/** Children content. */
|
|
50
|
+
children?: JSX.Element;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Re-export types
|
|
54
|
+
export type { AriaLandmarkRole, LandmarkController };
|
|
55
|
+
|
|
56
|
+
// ============================================
|
|
57
|
+
// CONTEXT
|
|
58
|
+
// ============================================
|
|
59
|
+
|
|
60
|
+
export const LandmarkContext = createContext<LandmarkProps | null>(null);
|
|
61
|
+
|
|
62
|
+
// ============================================
|
|
63
|
+
// SEMANTIC ELEMENT MAPPING
|
|
64
|
+
// ============================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Maps ARIA landmark roles to their semantic HTML elements.
|
|
68
|
+
* Using semantic elements is preferred when possible.
|
|
69
|
+
*/
|
|
70
|
+
const roleToSemanticElement: Partial<Record<AriaLandmarkRole, keyof JSX.IntrinsicElements>> = {
|
|
71
|
+
main: 'main',
|
|
72
|
+
navigation: 'nav',
|
|
73
|
+
search: 'search', // HTML5.3 <search> element
|
|
74
|
+
banner: 'header',
|
|
75
|
+
contentinfo: 'footer',
|
|
76
|
+
complementary: 'aside',
|
|
77
|
+
form: 'form',
|
|
78
|
+
region: 'section',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ============================================
|
|
82
|
+
// LANDMARK COMPONENT
|
|
83
|
+
// ============================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A landmark is a region of the page that helps screen reader users navigate.
|
|
87
|
+
* Press F6 to cycle through landmarks, or Shift+F6 to go backwards.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* // Main content area
|
|
92
|
+
* <Landmark role="main" aria-label="Main content">
|
|
93
|
+
* <h1>Welcome</h1>
|
|
94
|
+
* <p>Page content here...</p>
|
|
95
|
+
* </Landmark>
|
|
96
|
+
*
|
|
97
|
+
* // Navigation
|
|
98
|
+
* <Landmark role="navigation" aria-label="Primary navigation">
|
|
99
|
+
* <nav>...</nav>
|
|
100
|
+
* </Landmark>
|
|
101
|
+
*
|
|
102
|
+
* // Search
|
|
103
|
+
* <Landmark role="search" aria-label="Site search">
|
|
104
|
+
* <form>...</form>
|
|
105
|
+
* </Landmark>
|
|
106
|
+
*
|
|
107
|
+
* // Custom element type
|
|
108
|
+
* <Landmark role="region" aria-label="Featured content" elementType="div">
|
|
109
|
+
* ...
|
|
110
|
+
* </Landmark>
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function Landmark(props: LandmarkProps): JSX.Element {
|
|
114
|
+
const [local, ariaProps] = splitProps(props, [
|
|
115
|
+
'class',
|
|
116
|
+
'style',
|
|
117
|
+
'slot',
|
|
118
|
+
'children',
|
|
119
|
+
'elementType',
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// Element ref
|
|
123
|
+
const [ref, setRef] = createSignal<HTMLElement>();
|
|
124
|
+
|
|
125
|
+
// Determine the element type - use semantic element for the role if not specified
|
|
126
|
+
const elementType = createMemo(() => {
|
|
127
|
+
if (local.elementType) {
|
|
128
|
+
return local.elementType;
|
|
129
|
+
}
|
|
130
|
+
return roleToSemanticElement[ariaProps.role] ?? 'div';
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Create landmark aria props
|
|
134
|
+
const landmarkAria = createLandmark(
|
|
135
|
+
{
|
|
136
|
+
get role() { return ariaProps.role; },
|
|
137
|
+
get 'aria-label'() { return ariaProps['aria-label']; },
|
|
138
|
+
get 'aria-labelledby'() { return ariaProps['aria-labelledby']; },
|
|
139
|
+
get id() { return ariaProps.id; },
|
|
140
|
+
get focus() { return ariaProps.focus; },
|
|
141
|
+
},
|
|
142
|
+
ref
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Render props values
|
|
146
|
+
const renderValues = createMemo<LandmarkRenderProps>(() => ({
|
|
147
|
+
role: ariaProps.role,
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
// Resolve class
|
|
151
|
+
const resolvedClass = createMemo(() => {
|
|
152
|
+
const cls = local.class;
|
|
153
|
+
if (typeof cls === 'function') {
|
|
154
|
+
return cls(renderValues());
|
|
155
|
+
}
|
|
156
|
+
return cls ?? `solidaria-Landmark solidaria-Landmark--${ariaProps.role}`;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Resolve style
|
|
160
|
+
const resolvedStyle = createMemo(() => {
|
|
161
|
+
const style = local.style;
|
|
162
|
+
if (typeof style === 'function') {
|
|
163
|
+
return style(renderValues());
|
|
164
|
+
}
|
|
165
|
+
return style;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Filter DOM props
|
|
169
|
+
const domProps = createMemo(() => filterDOMProps(ariaProps, { global: true }));
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Dynamic
|
|
173
|
+
component={elementType()}
|
|
174
|
+
ref={setRef}
|
|
175
|
+
{...domProps()}
|
|
176
|
+
{...landmarkAria.landmarkProps}
|
|
177
|
+
class={resolvedClass()}
|
|
178
|
+
style={resolvedStyle()}
|
|
179
|
+
slot={local.slot}
|
|
180
|
+
>
|
|
181
|
+
{props.children}
|
|
182
|
+
</Dynamic>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================
|
|
187
|
+
// LANDMARK CONTROLLER EXPORT
|
|
188
|
+
// ============================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Returns a controller for programmatic landmark navigation.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```tsx
|
|
195
|
+
* const controller = useLandmarkController();
|
|
196
|
+
*
|
|
197
|
+
* <button onClick={() => controller.focusMain()}>Skip to main content</button>
|
|
198
|
+
* <button onClick={() => controller.focusNext()}>Next landmark</button>
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export function useLandmarkController(): LandmarkController {
|
|
202
|
+
return getLandmarkController();
|
|
203
|
+
}
|