@proyecto-viviana/solidaria-components 0.2.2 → 0.2.3
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/Table.tsx
ADDED
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table component for solidaria-components
|
|
3
|
+
*
|
|
4
|
+
* A pre-wired headless table that combines state + aria hooks.
|
|
5
|
+
* Based on react-aria-components/src/Table.tsx
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type JSX,
|
|
10
|
+
createContext,
|
|
11
|
+
createMemo,
|
|
12
|
+
createSignal,
|
|
13
|
+
splitProps,
|
|
14
|
+
useContext,
|
|
15
|
+
For,
|
|
16
|
+
Show,
|
|
17
|
+
} from 'solid-js';
|
|
18
|
+
import {
|
|
19
|
+
createTable,
|
|
20
|
+
createTableColumnHeader,
|
|
21
|
+
createTableRow,
|
|
22
|
+
createTableCell,
|
|
23
|
+
createTableRowGroup,
|
|
24
|
+
createTableSelectionCheckbox,
|
|
25
|
+
createTableSelectAllCheckbox,
|
|
26
|
+
createFocusRing,
|
|
27
|
+
createHover,
|
|
28
|
+
type AriaTableProps,
|
|
29
|
+
} from '@proyecto-viviana/solidaria';
|
|
30
|
+
import {
|
|
31
|
+
createTableState,
|
|
32
|
+
createTableCollection,
|
|
33
|
+
type TableState,
|
|
34
|
+
type TableCollection,
|
|
35
|
+
type Key,
|
|
36
|
+
type SortDescriptor,
|
|
37
|
+
type ColumnDefinition,
|
|
38
|
+
type GridNode,
|
|
39
|
+
} from '@proyecto-viviana/solid-stately';
|
|
40
|
+
import {
|
|
41
|
+
type RenderChildren,
|
|
42
|
+
type ClassNameOrFunction,
|
|
43
|
+
type StyleOrFunction,
|
|
44
|
+
type SlotProps,
|
|
45
|
+
useRenderProps,
|
|
46
|
+
filterDOMProps,
|
|
47
|
+
} from './utils';
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// TYPES
|
|
51
|
+
// ============================================
|
|
52
|
+
|
|
53
|
+
export interface TableRenderProps {
|
|
54
|
+
/** Whether the table has focus. */
|
|
55
|
+
isFocused: boolean;
|
|
56
|
+
/** Whether the table has keyboard focus. */
|
|
57
|
+
isFocusVisible: boolean;
|
|
58
|
+
/** Whether the table is disabled. */
|
|
59
|
+
isDisabled: boolean;
|
|
60
|
+
/** Whether the table is empty. */
|
|
61
|
+
isEmpty: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TableProps<T extends object> extends Omit<AriaTableProps, 'children'>, SlotProps {
|
|
65
|
+
/** The data items to render in the table. */
|
|
66
|
+
items: T[];
|
|
67
|
+
/** The column definitions. */
|
|
68
|
+
columns: ColumnDefinition<T>[];
|
|
69
|
+
/** Function to get the key from an item. */
|
|
70
|
+
getKey?: (item: T) => Key;
|
|
71
|
+
/** Function to get the text value from an item for a column. */
|
|
72
|
+
getTextValue?: (item: T, column: ColumnDefinition<T>) => string;
|
|
73
|
+
/** The selection mode. */
|
|
74
|
+
selectionMode?: 'none' | 'single' | 'multiple';
|
|
75
|
+
/** Keys of disabled items. */
|
|
76
|
+
disabledKeys?: Iterable<Key>;
|
|
77
|
+
/** Currently selected keys (controlled). */
|
|
78
|
+
selectedKeys?: 'all' | Iterable<Key>;
|
|
79
|
+
/** Default selected keys (uncontrolled). */
|
|
80
|
+
defaultSelectedKeys?: 'all' | Iterable<Key>;
|
|
81
|
+
/** Handler called when selection changes. */
|
|
82
|
+
onSelectionChange?: (keys: 'all' | Set<Key>) => void;
|
|
83
|
+
/** The current sort descriptor. */
|
|
84
|
+
sortDescriptor?: SortDescriptor;
|
|
85
|
+
/** Handler called when sort changes. */
|
|
86
|
+
onSortChange?: (descriptor: SortDescriptor) => void;
|
|
87
|
+
/** Whether to show selection checkboxes. */
|
|
88
|
+
showSelectionCheckboxes?: boolean;
|
|
89
|
+
/** The children of the component. */
|
|
90
|
+
children?: JSX.Element | RenderChildren<TableRenderProps>;
|
|
91
|
+
/** The CSS className for the element. */
|
|
92
|
+
class?: ClassNameOrFunction<TableRenderProps>;
|
|
93
|
+
/** The inline style for the element. */
|
|
94
|
+
style?: StyleOrFunction<TableRenderProps>;
|
|
95
|
+
/** A function to render when the table is empty. */
|
|
96
|
+
renderEmptyState?: () => JSX.Element;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface TableHeaderRenderProps {
|
|
100
|
+
/** Whether the header has focus. */
|
|
101
|
+
isFocused: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TableHeaderProps extends SlotProps {
|
|
105
|
+
/** The children (usually TableColumn components). */
|
|
106
|
+
children?: JSX.Element;
|
|
107
|
+
/** The CSS className for the element. */
|
|
108
|
+
class?: ClassNameOrFunction<TableHeaderRenderProps>;
|
|
109
|
+
/** The inline style for the element. */
|
|
110
|
+
style?: StyleOrFunction<TableHeaderRenderProps>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface TableColumnRenderProps {
|
|
114
|
+
/** Whether the column is focused. */
|
|
115
|
+
isFocused: boolean;
|
|
116
|
+
/** Whether the column has keyboard focus. */
|
|
117
|
+
isFocusVisible: boolean;
|
|
118
|
+
/** Whether the column is sortable. */
|
|
119
|
+
isSortable: boolean;
|
|
120
|
+
/** The current sort direction ('ascending', 'descending', or undefined). */
|
|
121
|
+
sortDirection: 'ascending' | 'descending' | undefined;
|
|
122
|
+
/** Whether the column is being hovered. */
|
|
123
|
+
isHovered: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface TableColumnProps extends SlotProps {
|
|
127
|
+
/** The unique key for the column. */
|
|
128
|
+
id: Key;
|
|
129
|
+
/** Whether the column allows sorting. */
|
|
130
|
+
allowsSorting?: boolean;
|
|
131
|
+
/** The children of the column. */
|
|
132
|
+
children?: RenderChildren<TableColumnRenderProps>;
|
|
133
|
+
/** The CSS className for the element. */
|
|
134
|
+
class?: ClassNameOrFunction<TableColumnRenderProps>;
|
|
135
|
+
/** The inline style for the element. */
|
|
136
|
+
style?: StyleOrFunction<TableColumnRenderProps>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface TableBodyRenderProps {
|
|
140
|
+
/** Whether the body is empty. */
|
|
141
|
+
isEmpty: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface TableBodyProps<T> extends SlotProps {
|
|
145
|
+
/** The items to render. If not provided, uses items from Table. */
|
|
146
|
+
items?: T[];
|
|
147
|
+
/** The children (usually a render function for TableRow). */
|
|
148
|
+
children?: (item: T) => JSX.Element;
|
|
149
|
+
/** The CSS className for the element. */
|
|
150
|
+
class?: ClassNameOrFunction<TableBodyRenderProps>;
|
|
151
|
+
/** The inline style for the element. */
|
|
152
|
+
style?: StyleOrFunction<TableBodyRenderProps>;
|
|
153
|
+
/** A function to render when the body is empty. */
|
|
154
|
+
renderEmptyState?: () => JSX.Element;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface TableRowRenderProps {
|
|
158
|
+
/** Whether the row is selected. */
|
|
159
|
+
isSelected: boolean;
|
|
160
|
+
/** Whether the row is focused. */
|
|
161
|
+
isFocused: boolean;
|
|
162
|
+
/** Whether the row has keyboard focus. */
|
|
163
|
+
isFocusVisible: boolean;
|
|
164
|
+
/** Whether the row is pressed. */
|
|
165
|
+
isPressed: boolean;
|
|
166
|
+
/** Whether the row is hovered. */
|
|
167
|
+
isHovered: boolean;
|
|
168
|
+
/** Whether the row is disabled. */
|
|
169
|
+
isDisabled: boolean;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface TableRowProps<T> extends SlotProps {
|
|
173
|
+
/** The unique key for the row. */
|
|
174
|
+
id: Key;
|
|
175
|
+
/** The item value. */
|
|
176
|
+
item?: T;
|
|
177
|
+
/** The children of the row (usually TableCell components). */
|
|
178
|
+
children?: JSX.Element | RenderChildren<TableRowRenderProps>;
|
|
179
|
+
/** The CSS className for the element. */
|
|
180
|
+
class?: ClassNameOrFunction<TableRowRenderProps>;
|
|
181
|
+
/** The inline style for the element. */
|
|
182
|
+
style?: StyleOrFunction<TableRowRenderProps>;
|
|
183
|
+
/** Handler called when the row is activated (double-click or Enter). */
|
|
184
|
+
onAction?: () => void;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface TableCellRenderProps {
|
|
188
|
+
/** Whether the cell is focused. */
|
|
189
|
+
isFocused: boolean;
|
|
190
|
+
/** Whether the cell has keyboard focus. */
|
|
191
|
+
isFocusVisible: boolean;
|
|
192
|
+
/** Whether the cell is pressed. */
|
|
193
|
+
isPressed: boolean;
|
|
194
|
+
/** Whether the cell is hovered. */
|
|
195
|
+
isHovered: boolean;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface TableCellProps extends SlotProps {
|
|
199
|
+
/** The unique key for the cell. */
|
|
200
|
+
id?: Key;
|
|
201
|
+
/** The children of the cell. */
|
|
202
|
+
children?: RenderChildren<TableCellRenderProps>;
|
|
203
|
+
/** The CSS className for the element. */
|
|
204
|
+
class?: ClassNameOrFunction<TableCellRenderProps>;
|
|
205
|
+
/** The inline style for the element. */
|
|
206
|
+
style?: StyleOrFunction<TableCellRenderProps>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============================================
|
|
210
|
+
// CONTEXT
|
|
211
|
+
// ============================================
|
|
212
|
+
|
|
213
|
+
interface TableContextValue<T extends object> {
|
|
214
|
+
state: TableState<T, TableCollection<T>>;
|
|
215
|
+
collection: TableCollection<T>;
|
|
216
|
+
items: T[];
|
|
217
|
+
columns: ColumnDefinition<T>[];
|
|
218
|
+
isDisabled: boolean;
|
|
219
|
+
showSelectionCheckboxes: boolean;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const TableContext = createContext<TableContextValue<object> | null>(null);
|
|
223
|
+
export const TableStateContext = createContext<TableState<object, TableCollection<object>> | null>(null);
|
|
224
|
+
|
|
225
|
+
// Row-level context for cells
|
|
226
|
+
interface TableRowContextValue {
|
|
227
|
+
rowKey: Key;
|
|
228
|
+
rowNode: GridNode<unknown>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export const TableRowContext = createContext<TableRowContextValue | null>(null);
|
|
232
|
+
|
|
233
|
+
// ============================================
|
|
234
|
+
// COMPONENTS
|
|
235
|
+
// ============================================
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* A table displays data in rows and columns and enables a user to navigate its contents via directional navigation keys,
|
|
239
|
+
* and optionally supports row selection and sorting.
|
|
240
|
+
*/
|
|
241
|
+
export function Table<T extends object>(props: TableProps<T>): JSX.Element {
|
|
242
|
+
const [local, stateProps, ariaProps] = splitProps(
|
|
243
|
+
props,
|
|
244
|
+
['class', 'style', 'slot', 'renderEmptyState'],
|
|
245
|
+
[
|
|
246
|
+
'items',
|
|
247
|
+
'columns',
|
|
248
|
+
'getKey',
|
|
249
|
+
'getTextValue',
|
|
250
|
+
'disabledKeys',
|
|
251
|
+
'selectionMode',
|
|
252
|
+
'selectedKeys',
|
|
253
|
+
'defaultSelectedKeys',
|
|
254
|
+
'onSelectionChange',
|
|
255
|
+
'sortDescriptor',
|
|
256
|
+
'onSortChange',
|
|
257
|
+
'showSelectionCheckboxes',
|
|
258
|
+
]
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Create ref signal
|
|
262
|
+
const [ref, setRef] = createSignal<HTMLTableElement | null>(null);
|
|
263
|
+
|
|
264
|
+
// Create collection
|
|
265
|
+
const collection = createMemo(() =>
|
|
266
|
+
createTableCollection<T>({
|
|
267
|
+
columns: stateProps.columns,
|
|
268
|
+
rows: stateProps.items,
|
|
269
|
+
getKey: stateProps.getKey,
|
|
270
|
+
getTextValue: stateProps.getTextValue,
|
|
271
|
+
showSelectionCheckboxes: stateProps.showSelectionCheckboxes ?? false,
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// Create table state
|
|
276
|
+
const state = createTableState<T, TableCollection<T>>(() => ({
|
|
277
|
+
collection: collection(),
|
|
278
|
+
disabledKeys: stateProps.disabledKeys,
|
|
279
|
+
selectionMode: stateProps.selectionMode,
|
|
280
|
+
selectedKeys: stateProps.selectedKeys,
|
|
281
|
+
defaultSelectedKeys: stateProps.defaultSelectedKeys,
|
|
282
|
+
onSelectionChange: stateProps.onSelectionChange,
|
|
283
|
+
sortDescriptor: stateProps.sortDescriptor,
|
|
284
|
+
onSortChange: stateProps.onSortChange,
|
|
285
|
+
showSelectionCheckboxes: stateProps.showSelectionCheckboxes,
|
|
286
|
+
}));
|
|
287
|
+
|
|
288
|
+
// Create table aria props
|
|
289
|
+
const { gridProps } = createTable<T>(
|
|
290
|
+
() => ({
|
|
291
|
+
id: ariaProps.id,
|
|
292
|
+
'aria-label': ariaProps['aria-label'],
|
|
293
|
+
'aria-labelledby': ariaProps['aria-labelledby'],
|
|
294
|
+
'aria-describedby': ariaProps['aria-describedby'],
|
|
295
|
+
isVirtualized: ariaProps.isVirtualized,
|
|
296
|
+
onRowAction: ariaProps.onRowAction,
|
|
297
|
+
onCellAction: ariaProps.onCellAction,
|
|
298
|
+
focusMode: ariaProps.focusMode,
|
|
299
|
+
}),
|
|
300
|
+
() => state,
|
|
301
|
+
ref
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Create focus ring
|
|
305
|
+
const { isFocused, isFocusVisible, focusProps } = createFocusRing();
|
|
306
|
+
|
|
307
|
+
// Render props values
|
|
308
|
+
const renderValues = createMemo<TableRenderProps>(() => ({
|
|
309
|
+
isFocused: state.isFocused || isFocused(),
|
|
310
|
+
isFocusVisible: isFocusVisible(),
|
|
311
|
+
isDisabled: false, // Tables don't have a global disabled state
|
|
312
|
+
isEmpty: stateProps.items.length === 0,
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
// Resolve render props
|
|
316
|
+
const renderProps = useRenderProps(
|
|
317
|
+
{
|
|
318
|
+
children: props.children,
|
|
319
|
+
class: local.class,
|
|
320
|
+
style: local.style,
|
|
321
|
+
defaultClassName: 'solidaria-Table',
|
|
322
|
+
},
|
|
323
|
+
renderValues
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Filter DOM props
|
|
327
|
+
const domProps = createMemo(() => {
|
|
328
|
+
const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
|
|
329
|
+
return filtered;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Remove ref from spread props
|
|
333
|
+
const cleanGridProps = () => {
|
|
334
|
+
const { ref: _ref1, ...rest } = gridProps as Record<string, unknown>;
|
|
335
|
+
return rest;
|
|
336
|
+
};
|
|
337
|
+
const cleanFocusProps = () => {
|
|
338
|
+
const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
|
|
339
|
+
return rest;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const contextValue = createMemo<TableContextValue<T>>(() => ({
|
|
343
|
+
state,
|
|
344
|
+
collection: collection(),
|
|
345
|
+
items: stateProps.items,
|
|
346
|
+
columns: stateProps.columns,
|
|
347
|
+
isDisabled: false,
|
|
348
|
+
showSelectionCheckboxes: stateProps.showSelectionCheckboxes ?? false,
|
|
349
|
+
}));
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<TableContext.Provider value={contextValue() as TableContextValue<object>}>
|
|
353
|
+
<TableStateContext.Provider value={state as unknown as TableState<object, TableCollection<object>>}>
|
|
354
|
+
<table
|
|
355
|
+
ref={setRef}
|
|
356
|
+
{...domProps()}
|
|
357
|
+
{...cleanGridProps()}
|
|
358
|
+
{...cleanFocusProps()}
|
|
359
|
+
class={renderProps.class()}
|
|
360
|
+
style={renderProps.style()}
|
|
361
|
+
data-focused={state.isFocused || undefined}
|
|
362
|
+
data-focus-visible={isFocusVisible() || undefined}
|
|
363
|
+
data-empty={stateProps.items.length === 0 || undefined}
|
|
364
|
+
>
|
|
365
|
+
{renderProps.renderChildren()}
|
|
366
|
+
</table>
|
|
367
|
+
</TableStateContext.Provider>
|
|
368
|
+
</TableContext.Provider>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* A header row in a table containing column headers.
|
|
374
|
+
*/
|
|
375
|
+
export function TableHeader(props: TableHeaderProps): JSX.Element {
|
|
376
|
+
const [local] = splitProps(props, ['class', 'style', 'slot']);
|
|
377
|
+
|
|
378
|
+
// Get context
|
|
379
|
+
const context = useContext(TableContext);
|
|
380
|
+
if (!context) {
|
|
381
|
+
throw new Error('TableHeader must be used within a Table');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const { rowGroupProps } = createTableRowGroup(() => ({ type: 'thead' }));
|
|
385
|
+
|
|
386
|
+
// Render props values
|
|
387
|
+
const renderValues = createMemo<TableHeaderRenderProps>(() => ({
|
|
388
|
+
isFocused: false,
|
|
389
|
+
}));
|
|
390
|
+
|
|
391
|
+
// Resolve render props
|
|
392
|
+
const renderProps = useRenderProps(
|
|
393
|
+
{
|
|
394
|
+
class: local.class,
|
|
395
|
+
style: local.style,
|
|
396
|
+
defaultClassName: 'solidaria-Table-header',
|
|
397
|
+
},
|
|
398
|
+
renderValues
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const cleanRowGroupProps = () => {
|
|
402
|
+
const { ref: _ref, ...rest } = rowGroupProps as Record<string, unknown>;
|
|
403
|
+
return rest;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<thead {...cleanRowGroupProps()} class={renderProps.class()} style={renderProps.style()}>
|
|
408
|
+
<tr role="row">{props.children}</tr>
|
|
409
|
+
</thead>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* A column header in a table.
|
|
415
|
+
*/
|
|
416
|
+
export function TableColumn(props: TableColumnProps): JSX.Element {
|
|
417
|
+
const [local] = splitProps(props, ['class', 'style', 'slot', 'id', 'allowsSorting']);
|
|
418
|
+
|
|
419
|
+
// Get context
|
|
420
|
+
const context = useContext(TableContext);
|
|
421
|
+
if (!context) {
|
|
422
|
+
throw new Error('TableColumn must be used within a Table');
|
|
423
|
+
}
|
|
424
|
+
const { state, collection } = context;
|
|
425
|
+
|
|
426
|
+
// Create ref signal
|
|
427
|
+
const [ref, setRef] = createSignal<HTMLTableCellElement | null>(null);
|
|
428
|
+
|
|
429
|
+
// Find the column node
|
|
430
|
+
const columnNode = createMemo(() => {
|
|
431
|
+
const node = collection.getItem(local.id);
|
|
432
|
+
if (!node) {
|
|
433
|
+
// Create a simple node for the column
|
|
434
|
+
return {
|
|
435
|
+
type: 'column' as const,
|
|
436
|
+
key: local.id,
|
|
437
|
+
value: null,
|
|
438
|
+
textValue: String(local.id),
|
|
439
|
+
level: 0,
|
|
440
|
+
index: 0,
|
|
441
|
+
hasChildNodes: false,
|
|
442
|
+
childNodes: [],
|
|
443
|
+
} as GridNode<unknown>;
|
|
444
|
+
}
|
|
445
|
+
return node;
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Create column header aria props
|
|
449
|
+
const { columnHeaderProps } = createTableColumnHeader<object>(
|
|
450
|
+
() => ({
|
|
451
|
+
node: columnNode(),
|
|
452
|
+
allowsSorting: local.allowsSorting,
|
|
453
|
+
}),
|
|
454
|
+
() => state as TableState<object, TableCollection<object>>,
|
|
455
|
+
ref
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// Create hover
|
|
459
|
+
const { isHovered, hoverProps } = createHover({
|
|
460
|
+
isDisabled: false,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Create focus ring
|
|
464
|
+
const { isFocusVisible, focusProps } = createFocusRing();
|
|
465
|
+
|
|
466
|
+
// Get sort direction
|
|
467
|
+
const sortDirection = createMemo(() => {
|
|
468
|
+
const sortDescriptor = state.sortDescriptor;
|
|
469
|
+
if (sortDescriptor?.column === local.id) {
|
|
470
|
+
return sortDescriptor.direction;
|
|
471
|
+
}
|
|
472
|
+
return undefined;
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Render props values
|
|
476
|
+
const renderValues = createMemo<TableColumnRenderProps>(() => ({
|
|
477
|
+
isFocused: state.focusedKey === local.id,
|
|
478
|
+
isFocusVisible: isFocusVisible() && state.focusedKey === local.id,
|
|
479
|
+
isSortable: local.allowsSorting ?? false,
|
|
480
|
+
sortDirection: sortDirection(),
|
|
481
|
+
isHovered: isHovered(),
|
|
482
|
+
}));
|
|
483
|
+
|
|
484
|
+
// Resolve render props
|
|
485
|
+
const renderProps = useRenderProps(
|
|
486
|
+
{
|
|
487
|
+
children: props.children,
|
|
488
|
+
class: local.class,
|
|
489
|
+
style: local.style,
|
|
490
|
+
defaultClassName: 'solidaria-Table-column',
|
|
491
|
+
},
|
|
492
|
+
renderValues
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// Remove ref from spread props
|
|
496
|
+
const cleanColumnHeaderProps = () => {
|
|
497
|
+
const { ref: _ref1, ...rest } = columnHeaderProps as Record<string, unknown>;
|
|
498
|
+
return rest;
|
|
499
|
+
};
|
|
500
|
+
const cleanHoverProps = () => {
|
|
501
|
+
const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
|
|
502
|
+
return rest;
|
|
503
|
+
};
|
|
504
|
+
const cleanFocusProps = () => {
|
|
505
|
+
const { ref: _ref3, ...rest } = focusProps as Record<string, unknown>;
|
|
506
|
+
return rest;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
return (
|
|
510
|
+
<th
|
|
511
|
+
ref={setRef}
|
|
512
|
+
{...cleanColumnHeaderProps()}
|
|
513
|
+
{...cleanHoverProps()}
|
|
514
|
+
{...cleanFocusProps()}
|
|
515
|
+
class={renderProps.class()}
|
|
516
|
+
style={renderProps.style()}
|
|
517
|
+
data-sortable={local.allowsSorting || undefined}
|
|
518
|
+
data-sort-direction={sortDirection() || undefined}
|
|
519
|
+
data-hovered={isHovered() || undefined}
|
|
520
|
+
data-focused={state.focusedKey === local.id || undefined}
|
|
521
|
+
data-focus-visible={(isFocusVisible() && state.focusedKey === local.id) || undefined}
|
|
522
|
+
>
|
|
523
|
+
{renderProps.renderChildren()}
|
|
524
|
+
</th>
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* The body of a table containing data rows.
|
|
530
|
+
*/
|
|
531
|
+
export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Element {
|
|
532
|
+
const [local] = splitProps(props, ['items', 'class', 'style', 'slot', 'renderEmptyState']);
|
|
533
|
+
|
|
534
|
+
// Get context
|
|
535
|
+
const context = useContext(TableContext);
|
|
536
|
+
if (!context) {
|
|
537
|
+
throw new Error('TableBody must be used within a Table');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const { rowGroupProps } = createTableRowGroup(() => ({ type: 'tbody' }));
|
|
541
|
+
|
|
542
|
+
// Use provided items or context items
|
|
543
|
+
const items = createMemo(() => (local.items ?? context.items) as T[]);
|
|
544
|
+
|
|
545
|
+
// Render props values
|
|
546
|
+
const renderValues = createMemo<TableBodyRenderProps>(() => ({
|
|
547
|
+
isEmpty: items().length === 0,
|
|
548
|
+
}));
|
|
549
|
+
|
|
550
|
+
// Resolve render props
|
|
551
|
+
const renderProps = useRenderProps(
|
|
552
|
+
{
|
|
553
|
+
class: local.class,
|
|
554
|
+
style: local.style,
|
|
555
|
+
defaultClassName: 'solidaria-Table-body',
|
|
556
|
+
},
|
|
557
|
+
renderValues
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
const cleanRowGroupProps = () => {
|
|
561
|
+
const { ref: _ref, ...rest } = rowGroupProps as Record<string, unknown>;
|
|
562
|
+
return rest;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const isEmpty = () => items().length === 0;
|
|
566
|
+
|
|
567
|
+
return (
|
|
568
|
+
<tbody {...cleanRowGroupProps()} class={renderProps.class()} style={renderProps.style()}>
|
|
569
|
+
<Show when={isEmpty() && local.renderEmptyState} fallback={<For each={items()}>{(item) => props.children?.(item)}</For>}>
|
|
570
|
+
{local.renderEmptyState?.()}
|
|
571
|
+
</Show>
|
|
572
|
+
</tbody>
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* A row in a table.
|
|
578
|
+
*/
|
|
579
|
+
export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element {
|
|
580
|
+
const [local] = splitProps(props, ['class', 'style', 'slot', 'id', 'item', 'onAction']);
|
|
581
|
+
|
|
582
|
+
// Get context
|
|
583
|
+
const context = useContext(TableContext);
|
|
584
|
+
if (!context) {
|
|
585
|
+
throw new Error('TableRow must be used within a Table');
|
|
586
|
+
}
|
|
587
|
+
const { state, collection } = context;
|
|
588
|
+
|
|
589
|
+
// Create ref signal
|
|
590
|
+
const [ref, setRef] = createSignal<HTMLTableRowElement | null>(null);
|
|
591
|
+
|
|
592
|
+
// Find the row node
|
|
593
|
+
const rowNode = createMemo(() => {
|
|
594
|
+
const node = collection.getItem(local.id);
|
|
595
|
+
if (!node) {
|
|
596
|
+
// Create a simple node for the row
|
|
597
|
+
return {
|
|
598
|
+
type: 'item' as const,
|
|
599
|
+
key: local.id,
|
|
600
|
+
value: local.item ?? null,
|
|
601
|
+
textValue: String(local.id),
|
|
602
|
+
level: 0,
|
|
603
|
+
index: 0,
|
|
604
|
+
hasChildNodes: true,
|
|
605
|
+
childNodes: [],
|
|
606
|
+
} as GridNode<unknown>;
|
|
607
|
+
}
|
|
608
|
+
return node;
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Create row aria props
|
|
612
|
+
const { rowProps, isSelected, isDisabled, isPressed } = createTableRow<object>(
|
|
613
|
+
() => ({
|
|
614
|
+
node: rowNode(),
|
|
615
|
+
onAction: local.onAction,
|
|
616
|
+
}),
|
|
617
|
+
() => state as TableState<object, TableCollection<object>>,
|
|
618
|
+
ref
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
// Create hover
|
|
622
|
+
const { isHovered, hoverProps } = createHover({
|
|
623
|
+
get isDisabled() {
|
|
624
|
+
return isDisabled;
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Create focus ring
|
|
629
|
+
const { isFocusVisible, focusProps } = createFocusRing();
|
|
630
|
+
|
|
631
|
+
// Check if focused
|
|
632
|
+
const isFocused = createMemo(() => state.focusedKey === local.id);
|
|
633
|
+
|
|
634
|
+
// Render props values
|
|
635
|
+
const renderValues = createMemo<TableRowRenderProps>(() => ({
|
|
636
|
+
isSelected,
|
|
637
|
+
isFocused: isFocused(),
|
|
638
|
+
isFocusVisible: isFocusVisible() && isFocused(),
|
|
639
|
+
isPressed,
|
|
640
|
+
isHovered: isHovered(),
|
|
641
|
+
isDisabled,
|
|
642
|
+
}));
|
|
643
|
+
|
|
644
|
+
// Resolve render props
|
|
645
|
+
const renderProps = useRenderProps(
|
|
646
|
+
{
|
|
647
|
+
children: props.children,
|
|
648
|
+
class: local.class,
|
|
649
|
+
style: local.style,
|
|
650
|
+
defaultClassName: 'solidaria-Table-row',
|
|
651
|
+
},
|
|
652
|
+
renderValues
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
// Remove ref from spread props
|
|
656
|
+
const cleanRowProps = () => {
|
|
657
|
+
const { ref: _ref1, ...rest } = rowProps as Record<string, unknown>;
|
|
658
|
+
return rest;
|
|
659
|
+
};
|
|
660
|
+
const cleanHoverProps = () => {
|
|
661
|
+
const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
|
|
662
|
+
return rest;
|
|
663
|
+
};
|
|
664
|
+
const cleanFocusProps = () => {
|
|
665
|
+
const { ref: _ref3, ...rest } = focusProps as Record<string, unknown>;
|
|
666
|
+
return rest;
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const rowContextValue: TableRowContextValue = {
|
|
670
|
+
rowKey: local.id,
|
|
671
|
+
rowNode: rowNode(),
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
return (
|
|
675
|
+
<TableRowContext.Provider value={rowContextValue}>
|
|
676
|
+
<tr
|
|
677
|
+
ref={setRef}
|
|
678
|
+
{...cleanRowProps()}
|
|
679
|
+
{...cleanHoverProps()}
|
|
680
|
+
{...cleanFocusProps()}
|
|
681
|
+
class={renderProps.class()}
|
|
682
|
+
style={renderProps.style()}
|
|
683
|
+
data-selected={isSelected || undefined}
|
|
684
|
+
data-focused={isFocused() || undefined}
|
|
685
|
+
data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
|
|
686
|
+
data-pressed={isPressed || undefined}
|
|
687
|
+
data-hovered={isHovered() || undefined}
|
|
688
|
+
data-disabled={isDisabled || undefined}
|
|
689
|
+
>
|
|
690
|
+
{renderProps.renderChildren()}
|
|
691
|
+
</tr>
|
|
692
|
+
</TableRowContext.Provider>
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* A cell in a table row.
|
|
698
|
+
*/
|
|
699
|
+
export function TableCell(props: TableCellProps): JSX.Element {
|
|
700
|
+
const [local] = splitProps(props, ['class', 'style', 'slot', 'id']);
|
|
701
|
+
|
|
702
|
+
// Get context
|
|
703
|
+
const tableContext = useContext(TableContext);
|
|
704
|
+
const rowContext = useContext(TableRowContext);
|
|
705
|
+
|
|
706
|
+
if (!tableContext) {
|
|
707
|
+
throw new Error('TableCell must be used within a Table');
|
|
708
|
+
}
|
|
709
|
+
if (!rowContext) {
|
|
710
|
+
throw new Error('TableCell must be used within a Table');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const { state, collection } = tableContext;
|
|
714
|
+
const { rowKey, rowNode } = rowContext;
|
|
715
|
+
|
|
716
|
+
// Create ref signal
|
|
717
|
+
const [ref, setRef] = createSignal<HTMLTableCellElement | null>(null);
|
|
718
|
+
|
|
719
|
+
// Find the cell node
|
|
720
|
+
const cellNode = createMemo(() => {
|
|
721
|
+
// If id is provided, look for that specific cell
|
|
722
|
+
if (local.id != null) {
|
|
723
|
+
const cellKey = `${rowKey}-${local.id}`;
|
|
724
|
+
const node = collection.getItem(cellKey);
|
|
725
|
+
if (node) return node;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Otherwise create a simple node
|
|
729
|
+
return {
|
|
730
|
+
type: 'cell' as const,
|
|
731
|
+
key: local.id ?? `${rowKey}-cell`,
|
|
732
|
+
value: rowNode.value,
|
|
733
|
+
textValue: '',
|
|
734
|
+
level: 1,
|
|
735
|
+
index: 0,
|
|
736
|
+
parentKey: rowKey,
|
|
737
|
+
hasChildNodes: false,
|
|
738
|
+
childNodes: [],
|
|
739
|
+
} as GridNode<unknown>;
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// Create cell aria props
|
|
743
|
+
const { gridCellProps, isPressed } = createTableCell<object>(
|
|
744
|
+
() => ({
|
|
745
|
+
node: cellNode(),
|
|
746
|
+
}),
|
|
747
|
+
() => state as TableState<object, TableCollection<object>>,
|
|
748
|
+
ref
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
// Create hover
|
|
752
|
+
const { isHovered, hoverProps } = createHover({
|
|
753
|
+
isDisabled: false,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// Create focus ring
|
|
757
|
+
const { isFocusVisible, focusProps } = createFocusRing();
|
|
758
|
+
|
|
759
|
+
// Check if focused
|
|
760
|
+
const isFocused = createMemo(() => state.focusedKey === cellNode().key);
|
|
761
|
+
|
|
762
|
+
// Render props values
|
|
763
|
+
const renderValues = createMemo<TableCellRenderProps>(() => ({
|
|
764
|
+
isFocused: isFocused(),
|
|
765
|
+
isFocusVisible: isFocusVisible() && isFocused(),
|
|
766
|
+
isPressed,
|
|
767
|
+
isHovered: isHovered(),
|
|
768
|
+
}));
|
|
769
|
+
|
|
770
|
+
// Resolve render props
|
|
771
|
+
const renderProps = useRenderProps(
|
|
772
|
+
{
|
|
773
|
+
children: props.children,
|
|
774
|
+
class: local.class,
|
|
775
|
+
style: local.style,
|
|
776
|
+
defaultClassName: 'solidaria-Table-cell',
|
|
777
|
+
},
|
|
778
|
+
renderValues
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
// Remove ref from spread props
|
|
782
|
+
const cleanCellProps = () => {
|
|
783
|
+
const { ref: _ref1, ...rest } = gridCellProps as Record<string, unknown>;
|
|
784
|
+
return rest;
|
|
785
|
+
};
|
|
786
|
+
const cleanHoverProps = () => {
|
|
787
|
+
const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
|
|
788
|
+
return rest;
|
|
789
|
+
};
|
|
790
|
+
const cleanFocusProps = () => {
|
|
791
|
+
const { ref: _ref3, ...rest } = focusProps as Record<string, unknown>;
|
|
792
|
+
return rest;
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
return (
|
|
796
|
+
<td
|
|
797
|
+
ref={setRef}
|
|
798
|
+
{...cleanCellProps()}
|
|
799
|
+
{...cleanHoverProps()}
|
|
800
|
+
{...cleanFocusProps()}
|
|
801
|
+
class={renderProps.class()}
|
|
802
|
+
style={renderProps.style()}
|
|
803
|
+
data-focused={isFocused() || undefined}
|
|
804
|
+
data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
|
|
805
|
+
data-pressed={isPressed || undefined}
|
|
806
|
+
data-hovered={isHovered() || undefined}
|
|
807
|
+
>
|
|
808
|
+
{renderProps.renderChildren()}
|
|
809
|
+
</td>
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* A checkbox cell for row selection.
|
|
815
|
+
*/
|
|
816
|
+
export function TableSelectionCheckbox(props: { rowKey: Key }): JSX.Element {
|
|
817
|
+
const context = useContext(TableContext);
|
|
818
|
+
if (!context) {
|
|
819
|
+
throw new Error('TableSelectionCheckbox must be used within a Table');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const { state } = context;
|
|
823
|
+
|
|
824
|
+
const { checkboxProps } = createTableSelectionCheckbox<object>(
|
|
825
|
+
() => ({ key: props.rowKey }),
|
|
826
|
+
() => state as TableState<object, TableCollection<object>>
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
return <input {...checkboxProps} />;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* A checkbox for select-all functionality.
|
|
834
|
+
*/
|
|
835
|
+
export function TableSelectAllCheckbox(): JSX.Element {
|
|
836
|
+
const context = useContext(TableContext);
|
|
837
|
+
if (!context) {
|
|
838
|
+
throw new Error('TableSelectAllCheckbox must be used within a Table');
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const { state } = context;
|
|
842
|
+
|
|
843
|
+
const { checkboxProps } = createTableSelectAllCheckbox<object>(
|
|
844
|
+
() => state as TableState<object, TableCollection<object>>
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
return <input {...checkboxProps} />;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Attach components as static properties
|
|
851
|
+
Table.Header = TableHeader;
|
|
852
|
+
Table.Column = TableColumn;
|
|
853
|
+
Table.Body = TableBody;
|
|
854
|
+
Table.Row = TableRow;
|
|
855
|
+
Table.Cell = TableCell;
|
|
856
|
+
Table.SelectionCheckbox = TableSelectionCheckbox;
|
|
857
|
+
Table.SelectAllCheckbox = TableSelectAllCheckbox;
|