@proyecto-viviana/solidaria-components 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +39 -272
- package/dist/ActionBar.d.ts +79 -0
- package/dist/ActionBar.d.ts.map +1 -0
- package/dist/ActionGroup.d.ts +74 -0
- package/dist/ActionGroup.d.ts.map +1 -0
- package/dist/Alert.d.ts +70 -0
- package/dist/Alert.d.ts.map +1 -0
- package/dist/Autocomplete.d.ts +5 -5
- package/dist/Autocomplete.d.ts.map +1 -1
- package/dist/Breadcrumbs.d.ts +27 -8
- package/dist/Breadcrumbs.d.ts.map +1 -1
- package/dist/Button.d.ts +28 -5
- package/dist/Button.d.ts.map +1 -1
- package/dist/Calendar.d.ts +51 -7
- package/dist/Calendar.d.ts.map +1 -1
- package/dist/Checkbox.d.ts +33 -8
- package/dist/Checkbox.d.ts.map +1 -1
- package/dist/Collection.d.ts +130 -0
- package/dist/Collection.d.ts.map +1 -0
- package/dist/Color.d.ts +210 -9
- package/dist/Color.d.ts.map +1 -1
- package/dist/ColorEditor.d.ts +42 -0
- package/dist/ColorEditor.d.ts.map +1 -0
- package/dist/ComboBox.d.ts +146 -16
- package/dist/ComboBox.d.ts.map +1 -1
- package/dist/ContextualHelpTrigger.d.ts +40 -0
- package/dist/ContextualHelpTrigger.d.ts.map +1 -0
- package/dist/DateField.d.ts +35 -8
- package/dist/DateField.d.ts.map +1 -1
- package/dist/DatePicker.d.ts +101 -5
- package/dist/DatePicker.d.ts.map +1 -1
- package/dist/DateRangePickerContext.d.ts +30 -0
- package/dist/DateRangePickerContext.d.ts.map +1 -0
- package/dist/Dialog.d.ts +5 -5
- package/dist/Dialog.d.ts.map +1 -1
- package/dist/Disclosure.d.ts +25 -5
- package/dist/Disclosure.d.ts.map +1 -1
- package/dist/DragAndDrop.d.ts +80 -0
- package/dist/DragAndDrop.d.ts.map +1 -0
- package/dist/DragPreview.d.ts +14 -0
- package/dist/DragPreview.d.ts.map +1 -0
- package/dist/DropZone.d.ts +27 -0
- package/dist/DropZone.d.ts.map +1 -0
- package/dist/FieldError.d.ts +27 -0
- package/dist/FieldError.d.ts.map +1 -0
- package/dist/FileTrigger.d.ts +26 -0
- package/dist/FileTrigger.d.ts.map +1 -0
- package/dist/Focusable.d.ts +27 -0
- package/dist/Focusable.d.ts.map +1 -0
- package/dist/Form.d.ts +41 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/GridList.d.ts +69 -10
- package/dist/GridList.d.ts.map +1 -1
- package/dist/HiddenDateInput.d.ts +26 -0
- package/dist/HiddenDateInput.d.ts.map +1 -0
- package/dist/HiddenTimeInput.d.ts +25 -0
- package/dist/HiddenTimeInput.d.ts.map +1 -0
- package/dist/Icon.d.ts +57 -0
- package/dist/Icon.d.ts.map +1 -0
- package/dist/Keyboard.d.ts +13 -0
- package/dist/Keyboard.d.ts.map +1 -0
- package/dist/Landmark.d.ts +3 -3
- package/dist/Landmark.d.ts.map +1 -1
- package/dist/Link.d.ts +10 -4
- package/dist/Link.d.ts.map +1 -1
- package/dist/ListBox.d.ts +73 -11
- package/dist/ListBox.d.ts.map +1 -1
- package/dist/ListDropTargetDelegate.d.ts +38 -0
- package/dist/ListDropTargetDelegate.d.ts.map +1 -0
- package/dist/Menu.d.ts +79 -10
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Meter.d.ts +4 -4
- package/dist/Meter.d.ts.map +1 -1
- package/dist/Modal.d.ts +6 -4
- package/dist/Modal.d.ts.map +1 -1
- package/dist/NumberField.d.ts +10 -12
- package/dist/NumberField.d.ts.map +1 -1
- package/dist/Popover.d.ts +32 -7
- package/dist/Popover.d.ts.map +1 -1
- package/dist/Pressable.d.ts +27 -0
- package/dist/Pressable.d.ts.map +1 -0
- package/dist/ProgressBar.d.ts +6 -4
- package/dist/ProgressBar.d.ts.map +1 -1
- package/dist/RadioGroup.d.ts +43 -9
- package/dist/RadioGroup.d.ts.map +1 -1
- package/dist/RangeCalendar.d.ts +39 -7
- package/dist/RangeCalendar.d.ts.map +1 -1
- package/dist/RouterProvider.d.ts +75 -0
- package/dist/RouterProvider.d.ts.map +1 -0
- package/dist/SearchField.d.ts +23 -21
- package/dist/SearchField.d.ts.map +1 -1
- package/dist/Select.d.ts +48 -7
- package/dist/Select.d.ts.map +1 -1
- package/dist/SelectionIndicator.d.ts +30 -0
- package/dist/SelectionIndicator.d.ts.map +1 -0
- package/dist/Separator.d.ts +9 -3
- package/dist/Separator.d.ts.map +1 -1
- package/dist/SharedElementTransition.d.ts +41 -0
- package/dist/SharedElementTransition.d.ts.map +1 -0
- package/dist/Slider.d.ts +15 -8
- package/dist/Slider.d.ts.map +1 -1
- package/dist/StepList.d.ts +90 -0
- package/dist/StepList.d.ts.map +1 -0
- package/dist/Switch.d.ts +11 -5
- package/dist/Switch.d.ts.map +1 -1
- package/dist/Table.d.ts +222 -19
- package/dist/Table.d.ts.map +1 -1
- package/dist/Tabs.d.ts +47 -10
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/TagGroup.d.ts +22 -10
- package/dist/TagGroup.d.ts.map +1 -1
- package/dist/Text.d.ts +10 -0
- package/dist/Text.d.ts.map +1 -0
- package/dist/TextField.d.ts +19 -11
- package/dist/TextField.d.ts.map +1 -1
- package/dist/TimeField.d.ts +32 -7
- package/dist/TimeField.d.ts.map +1 -1
- package/dist/Toast.d.ts +29 -14
- package/dist/Toast.d.ts.map +1 -1
- package/dist/ToggleButton.d.ts +36 -0
- package/dist/ToggleButton.d.ts.map +1 -0
- package/dist/ToggleButtonGroup.d.ts +33 -0
- package/dist/ToggleButtonGroup.d.ts.map +1 -0
- package/dist/Toolbar.d.ts +7 -3
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Tooltip.d.ts +58 -7
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/Tree.d.ts +102 -11
- package/dist/Tree.d.ts.map +1 -1
- package/dist/Virtualizer.d.ts +61 -0
- package/dist/Virtualizer.d.ts.map +1 -0
- package/dist/VirtualizerLayouts.d.ts +82 -0
- package/dist/VirtualizerLayouts.d.ts.map +1 -0
- package/dist/VisuallyHidden.d.ts +4 -2
- package/dist/VisuallyHidden.d.ts.map +1 -1
- package/dist/contexts.d.ts +6 -1
- package/dist/contexts.d.ts.map +1 -1
- package/dist/index.d.ts +73 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23342 -10644
- package/dist/index.js.map +1 -7
- package/dist/index.jsx +18110 -0
- package/dist/index.jsx.map +1 -0
- package/dist/useDragAndDrop.d.ts +93 -0
- package/dist/useDragAndDrop.d.ts.map +1 -0
- package/dist/utils.d.ts +8 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/virtualizer/Layout.d.ts +79 -0
- package/dist/virtualizer/Layout.d.ts.map +1 -0
- package/package.json +33 -32
- package/src/ActionBar.tsx +251 -0
- package/src/ActionGroup.tsx +277 -0
- package/src/Alert.tsx +152 -0
- package/src/Autocomplete.tsx +39 -44
- package/src/Breadcrumbs.tsx +227 -72
- package/src/Button.tsx +315 -74
- package/src/Calendar.tsx +347 -141
- package/src/Checkbox.tsx +414 -123
- package/src/Collection.tsx +350 -0
- package/src/Color.tsx +1325 -284
- package/src/ColorEditor.tsx +213 -0
- package/src/ComboBox.tsx +644 -245
- package/src/ContextualHelpTrigger.tsx +195 -0
- package/src/DateField.tsx +274 -106
- package/src/DatePicker.tsx +892 -111
- package/src/DateRangePickerContext.tsx +44 -0
- package/src/Dialog.tsx +173 -104
- package/src/Disclosure.tsx +158 -105
- package/src/DragAndDrop.tsx +340 -0
- package/src/DragPreview.tsx +47 -0
- package/src/DropZone.tsx +233 -0
- package/src/FieldError.tsx +89 -0
- package/src/FileTrigger.tsx +83 -0
- package/src/Focusable.tsx +103 -0
- package/src/Form.tsx +140 -0
- package/src/GridList.tsx +542 -128
- package/src/HiddenDateInput.tsx +153 -0
- package/src/HiddenTimeInput.tsx +133 -0
- package/src/Icon.tsx +133 -0
- package/src/Keyboard.tsx +26 -0
- package/src/Landmark.tsx +37 -63
- package/src/Link.tsx +132 -69
- package/src/ListBox.tsx +656 -106
- package/src/ListDropTargetDelegate.ts +283 -0
- package/src/Menu.tsx +1234 -132
- package/src/Meter.tsx +44 -58
- package/src/Modal.tsx +262 -166
- package/src/NumberField.tsx +267 -151
- package/src/Popover.tsx +452 -343
- package/src/Pressable.tsx +108 -0
- package/src/ProgressBar.tsx +54 -59
- package/src/RadioGroup.tsx +533 -121
- package/src/RangeCalendar.tsx +249 -150
- package/src/RouterProvider.tsx +223 -0
- package/src/SearchField.tsx +460 -133
- package/src/Select.tsx +804 -233
- package/src/SelectionIndicator.tsx +108 -0
- package/src/Separator.tsx +47 -49
- package/src/SharedElementTransition.tsx +264 -0
- package/src/Slider.tsx +148 -98
- package/src/StepList.tsx +272 -0
- package/src/Switch.tsx +93 -46
- package/src/Table.tsx +1551 -225
- package/src/Tabs.tsx +377 -123
- package/src/TagGroup.tsx +233 -135
- package/src/Text.tsx +18 -0
- package/src/TextField.tsx +413 -86
- package/src/TimeField.tsx +232 -222
- package/src/Toast.tsx +306 -160
- package/src/ToggleButton.tsx +169 -0
- package/src/ToggleButtonGroup.tsx +141 -0
- package/src/Toolbar.tsx +61 -70
- package/src/Tooltip.tsx +473 -116
- package/src/Tree.tsx +1514 -175
- package/src/Virtualizer.tsx +730 -0
- package/src/VirtualizerLayouts.ts +280 -0
- package/src/VisuallyHidden.tsx +32 -38
- package/src/contexts.ts +29 -36
- package/src/index.ts +972 -620
- package/src/useDragAndDrop.ts +367 -0
- package/src/utils.tsx +69 -50
- package/src/virtualizer/Layout.ts +192 -0
- package/dist/index.ssr.js +0 -9785
- package/dist/index.ssr.js.map +0 -7
package/src/Table.tsx
CHANGED
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
import {
|
|
9
9
|
type JSX,
|
|
10
10
|
createContext,
|
|
11
|
+
createEffect,
|
|
11
12
|
createMemo,
|
|
13
|
+
createUniqueId,
|
|
12
14
|
createSignal,
|
|
15
|
+
onCleanup,
|
|
13
16
|
splitProps,
|
|
14
17
|
useContext,
|
|
15
18
|
For,
|
|
16
19
|
Show,
|
|
17
|
-
} from
|
|
20
|
+
} from "solid-js";
|
|
18
21
|
import {
|
|
19
22
|
createTable,
|
|
20
23
|
createTableColumnHeader,
|
|
@@ -24,31 +27,58 @@ import {
|
|
|
24
27
|
createTableSelectionCheckbox,
|
|
25
28
|
createTableSelectAllCheckbox,
|
|
26
29
|
createFocusRing,
|
|
30
|
+
isFocusVisible as isGlobalFocusVisible,
|
|
31
|
+
getTableData,
|
|
27
32
|
createHover,
|
|
33
|
+
mergeProps,
|
|
28
34
|
type AriaTableProps,
|
|
29
|
-
|
|
35
|
+
createTableColumnResize,
|
|
36
|
+
} from "@proyecto-viviana/solidaria";
|
|
30
37
|
import {
|
|
31
38
|
createTableState,
|
|
32
39
|
createTableCollection,
|
|
40
|
+
createTableColumnResizeState,
|
|
33
41
|
type TableState,
|
|
34
42
|
type TableCollection,
|
|
43
|
+
type TableColumnResizeState,
|
|
35
44
|
type Key,
|
|
36
45
|
type SortDescriptor,
|
|
37
46
|
type ColumnDefinition,
|
|
47
|
+
type ColumnSize,
|
|
38
48
|
type GridNode,
|
|
39
|
-
|
|
49
|
+
type DropTarget,
|
|
50
|
+
} from "@proyecto-viviana/solid-stately";
|
|
40
51
|
import {
|
|
41
52
|
type RenderChildren,
|
|
42
53
|
type ClassNameOrFunction,
|
|
43
54
|
type StyleOrFunction,
|
|
44
55
|
type SlotProps,
|
|
56
|
+
dataAttr,
|
|
45
57
|
useRenderProps,
|
|
46
58
|
filterDOMProps,
|
|
47
|
-
} from
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
} from "./utils";
|
|
60
|
+
import { SharedElementTransition } from "./SharedElementTransition";
|
|
61
|
+
import { type DragAndDropHooks } from "./useDragAndDrop";
|
|
62
|
+
import { ButtonContext, type ButtonProps } from "./Button";
|
|
63
|
+
import {
|
|
64
|
+
CollectionRendererContext,
|
|
65
|
+
type CollectionRendererContextValue,
|
|
66
|
+
useCollectionRenderer,
|
|
67
|
+
} from "./Collection";
|
|
68
|
+
import { useVirtualizerContext } from "./Virtualizer";
|
|
69
|
+
import {
|
|
70
|
+
type LinkDOMProps,
|
|
71
|
+
type RouterOptions,
|
|
72
|
+
useRouter,
|
|
73
|
+
useLinkProps,
|
|
74
|
+
type RouterClickModifiers,
|
|
75
|
+
} from "./RouterProvider";
|
|
76
|
+
import {
|
|
77
|
+
getNormalizedDropTargetKey,
|
|
78
|
+
mergePersistedKeysIntoVirtualRange,
|
|
79
|
+
useDndPersistedKeys,
|
|
80
|
+
useRenderDropIndicator,
|
|
81
|
+
} from "./DragAndDrop";
|
|
52
82
|
|
|
53
83
|
export interface TableRenderProps {
|
|
54
84
|
/** Whether the table has focus. */
|
|
@@ -61,25 +91,84 @@ export interface TableRenderProps {
|
|
|
61
91
|
isEmpty: boolean;
|
|
62
92
|
}
|
|
63
93
|
|
|
64
|
-
|
|
94
|
+
type RefLike<T> = ((el: T) => void) | { current?: T | null } | undefined;
|
|
95
|
+
export type TableColumnDefinition<T = unknown> = Omit<ColumnDefinition<T>, "key" | "children"> & {
|
|
96
|
+
/** React Spectrum-style alias for the column key. */
|
|
97
|
+
id?: Key;
|
|
98
|
+
key?: Key;
|
|
99
|
+
children?: TableColumnDefinition<T>[];
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
function assignRef<T>(ref: RefLike<T>, el: T): void {
|
|
103
|
+
if (!ref) return;
|
|
104
|
+
if (typeof ref === "function") ref(el);
|
|
105
|
+
else ref.current = el;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeColumnDefinitions<T>(
|
|
109
|
+
columns: TableColumnDefinition<T>[],
|
|
110
|
+
parentKey?: Key,
|
|
111
|
+
): (ColumnDefinition<T> & { id?: Key })[] {
|
|
112
|
+
return columns.map((column, index) => {
|
|
113
|
+
const key = column.key ?? column.id ?? (parentKey == null ? index : `${parentKey}-${index}`);
|
|
114
|
+
const id = column.id ?? column.key;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...column,
|
|
118
|
+
key,
|
|
119
|
+
id,
|
|
120
|
+
children: column.children ? normalizeColumnDefinitions(column.children, key) : undefined,
|
|
121
|
+
} as ColumnDefinition<T> & { id?: Key };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getRowHeaderColumnKeys<T>(columns: (ColumnDefinition<T> & { id?: Key })[]): Set<Key> {
|
|
126
|
+
const keys = new Set<Key>();
|
|
127
|
+
|
|
128
|
+
for (const column of columns) {
|
|
129
|
+
if (column.isRowHeader) {
|
|
130
|
+
keys.add(column.key);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (column.children) {
|
|
134
|
+
for (const key of getRowHeaderColumnKeys(
|
|
135
|
+
column.children as (ColumnDefinition<T> & {
|
|
136
|
+
id?: Key;
|
|
137
|
+
})[],
|
|
138
|
+
)) {
|
|
139
|
+
keys.add(key);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return keys;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface TableProps<T extends object> extends Omit<AriaTableProps, "children">, SlotProps {
|
|
65
148
|
/** The data items to render in the table. */
|
|
66
149
|
items: T[];
|
|
67
150
|
/** The column definitions. */
|
|
68
|
-
columns:
|
|
151
|
+
columns: TableColumnDefinition<T>[];
|
|
69
152
|
/** Function to get the key from an item. */
|
|
70
153
|
getKey?: (item: T) => Key;
|
|
71
154
|
/** Function to get the text value from an item for a column. */
|
|
72
|
-
getTextValue?: (item: T, column:
|
|
155
|
+
getTextValue?: (item: T, column: TableColumnDefinition<T>) => string;
|
|
73
156
|
/** The selection mode. */
|
|
74
|
-
selectionMode?:
|
|
157
|
+
selectionMode?: "none" | "single" | "multiple";
|
|
158
|
+
/** The selection behavior (toggle vs replace). */
|
|
159
|
+
selectionBehavior?: "toggle" | "replace";
|
|
160
|
+
/** Whether disabled rows remain focusable. */
|
|
161
|
+
disabledBehavior?: "selection" | "all";
|
|
162
|
+
/** Whether Escape clears selection. */
|
|
163
|
+
escapeKeyBehavior?: "clearSelection" | "none";
|
|
75
164
|
/** Keys of disabled items. */
|
|
76
165
|
disabledKeys?: Iterable<Key>;
|
|
77
166
|
/** Currently selected keys (controlled). */
|
|
78
|
-
selectedKeys?:
|
|
167
|
+
selectedKeys?: "all" | Iterable<Key>;
|
|
79
168
|
/** Default selected keys (uncontrolled). */
|
|
80
|
-
defaultSelectedKeys?:
|
|
169
|
+
defaultSelectedKeys?: "all" | Iterable<Key>;
|
|
81
170
|
/** Handler called when selection changes. */
|
|
82
|
-
onSelectionChange?: (keys:
|
|
171
|
+
onSelectionChange?: (keys: "all" | Set<Key>) => void;
|
|
83
172
|
/** The current sort descriptor. */
|
|
84
173
|
sortDescriptor?: SortDescriptor;
|
|
85
174
|
/** Handler called when sort changes. */
|
|
@@ -94,11 +183,22 @@ export interface TableProps<T extends object> extends Omit<AriaTableProps, 'chil
|
|
|
94
183
|
style?: StyleOrFunction<TableRenderProps>;
|
|
95
184
|
/** A function to render when the table is empty. */
|
|
96
185
|
renderEmptyState?: () => JSX.Element;
|
|
186
|
+
/** Ref for the table element. */
|
|
187
|
+
ref?: RefLike<HTMLTableElement>;
|
|
188
|
+
/** Custom renderer for the table element. */
|
|
189
|
+
render?: (
|
|
190
|
+
props: JSX.HTMLAttributes<HTMLTableElement>,
|
|
191
|
+
renderProps: TableRenderProps,
|
|
192
|
+
) => JSX.Element;
|
|
193
|
+
/** Drag and drop hooks from `useDragAndDrop`. */
|
|
194
|
+
dragAndDropHooks?: DragAndDropHooks<T>;
|
|
97
195
|
}
|
|
98
196
|
|
|
99
197
|
export interface TableHeaderRenderProps {
|
|
100
198
|
/** Whether the header has focus. */
|
|
101
199
|
isFocused: boolean;
|
|
200
|
+
/** Whether the header is being hovered. */
|
|
201
|
+
isHovered: boolean;
|
|
102
202
|
}
|
|
103
203
|
|
|
104
204
|
export interface TableHeaderProps extends SlotProps {
|
|
@@ -108,6 +208,13 @@ export interface TableHeaderProps extends SlotProps {
|
|
|
108
208
|
class?: ClassNameOrFunction<TableHeaderRenderProps>;
|
|
109
209
|
/** The inline style for the element. */
|
|
110
210
|
style?: StyleOrFunction<TableHeaderRenderProps>;
|
|
211
|
+
/** Ref for the table header element. */
|
|
212
|
+
ref?: RefLike<HTMLTableSectionElement>;
|
|
213
|
+
/** Custom renderer for the table header element. */
|
|
214
|
+
render?: (
|
|
215
|
+
props: JSX.HTMLAttributes<HTMLTableSectionElement>,
|
|
216
|
+
renderProps: TableHeaderRenderProps,
|
|
217
|
+
) => JSX.Element;
|
|
111
218
|
}
|
|
112
219
|
|
|
113
220
|
export interface TableColumnRenderProps {
|
|
@@ -118,9 +225,13 @@ export interface TableColumnRenderProps {
|
|
|
118
225
|
/** Whether the column is sortable. */
|
|
119
226
|
isSortable: boolean;
|
|
120
227
|
/** The current sort direction ('ascending', 'descending', or undefined). */
|
|
121
|
-
sortDirection:
|
|
228
|
+
sortDirection: "ascending" | "descending" | undefined;
|
|
122
229
|
/** Whether the column is being hovered. */
|
|
123
230
|
isHovered: boolean;
|
|
231
|
+
/** Whether the column allows resizing. */
|
|
232
|
+
allowsResizing: boolean;
|
|
233
|
+
/** Whether the column is currently being resized. */
|
|
234
|
+
isResizing: boolean;
|
|
124
235
|
}
|
|
125
236
|
|
|
126
237
|
export interface TableColumnProps extends SlotProps {
|
|
@@ -128,12 +239,29 @@ export interface TableColumnProps extends SlotProps {
|
|
|
128
239
|
id: Key;
|
|
129
240
|
/** Whether the column allows sorting. */
|
|
130
241
|
allowsSorting?: boolean;
|
|
242
|
+
/** Whether the column allows resizing. */
|
|
243
|
+
allowsResizing?: boolean;
|
|
244
|
+
/** Column width (number for px, string for 'Xfr', 'X%', 'Xpx'). */
|
|
245
|
+
width?: ColumnSize;
|
|
246
|
+
/** Default width for uncontrolled mode. */
|
|
247
|
+
defaultWidth?: ColumnSize;
|
|
248
|
+
/** Minimum column width in px. */
|
|
249
|
+
minWidth?: number;
|
|
250
|
+
/** Maximum column width in px. */
|
|
251
|
+
maxWidth?: number;
|
|
131
252
|
/** The children of the column. */
|
|
132
253
|
children?: RenderChildren<TableColumnRenderProps>;
|
|
133
254
|
/** The CSS className for the element. */
|
|
134
255
|
class?: ClassNameOrFunction<TableColumnRenderProps>;
|
|
135
256
|
/** The inline style for the element. */
|
|
136
257
|
style?: StyleOrFunction<TableColumnRenderProps>;
|
|
258
|
+
/** Ref for the column header element. */
|
|
259
|
+
ref?: RefLike<HTMLTableCellElement>;
|
|
260
|
+
/** Custom renderer for the column header element. */
|
|
261
|
+
render?: (
|
|
262
|
+
props: JSX.ThHTMLAttributes<HTMLTableCellElement>,
|
|
263
|
+
renderProps: TableColumnRenderProps,
|
|
264
|
+
) => JSX.Element;
|
|
137
265
|
}
|
|
138
266
|
|
|
139
267
|
export interface TableBodyRenderProps {
|
|
@@ -152,6 +280,35 @@ export interface TableBodyProps<T> extends SlotProps {
|
|
|
152
280
|
style?: StyleOrFunction<TableBodyRenderProps>;
|
|
153
281
|
/** A function to render when the body is empty. */
|
|
154
282
|
renderEmptyState?: () => JSX.Element;
|
|
283
|
+
/** Whether there are more rows to load. */
|
|
284
|
+
hasMore?: boolean;
|
|
285
|
+
/** Whether additional rows are currently loading. */
|
|
286
|
+
isLoading?: boolean;
|
|
287
|
+
/** Called when the load more sentinel becomes visible. */
|
|
288
|
+
onLoadMore?: () => void | Promise<void>;
|
|
289
|
+
/** Ref for the table body element. */
|
|
290
|
+
ref?: RefLike<HTMLTableSectionElement>;
|
|
291
|
+
/** Custom renderer for the table body element. */
|
|
292
|
+
render?: (
|
|
293
|
+
props: JSX.HTMLAttributes<HTMLTableSectionElement>,
|
|
294
|
+
renderProps: TableBodyRenderProps,
|
|
295
|
+
) => JSX.Element;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface TableFooterRenderProps {
|
|
299
|
+
/** Whether the footer has no items. */
|
|
300
|
+
isEmpty: boolean;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface TableFooterProps<T> extends SlotProps {
|
|
304
|
+
/** The footer items to render. */
|
|
305
|
+
items?: T[];
|
|
306
|
+
/** The children, or a render function when `items` is provided. */
|
|
307
|
+
children?: JSX.Element | ((item: T) => JSX.Element);
|
|
308
|
+
/** The CSS className for the element. */
|
|
309
|
+
class?: ClassNameOrFunction<TableFooterRenderProps>;
|
|
310
|
+
/** The inline style for the element. */
|
|
311
|
+
style?: StyleOrFunction<TableFooterRenderProps>;
|
|
155
312
|
}
|
|
156
313
|
|
|
157
314
|
export interface TableRowRenderProps {
|
|
@@ -171,17 +328,45 @@ export interface TableRowRenderProps {
|
|
|
171
328
|
|
|
172
329
|
export interface TableRowProps<T> extends SlotProps {
|
|
173
330
|
/** The unique key for the row. */
|
|
174
|
-
id
|
|
331
|
+
id?: Key;
|
|
175
332
|
/** The item value. */
|
|
176
333
|
item?: T;
|
|
334
|
+
/** Columns to render when children is a column render function. */
|
|
335
|
+
columns?: TableColumnDefinition<T>[];
|
|
336
|
+
/** Whether the row is disabled. */
|
|
337
|
+
isDisabled?: boolean;
|
|
177
338
|
/** The children of the row (usually TableCell components). */
|
|
178
|
-
children?:
|
|
339
|
+
children?:
|
|
340
|
+
| JSX.Element
|
|
341
|
+
| RenderChildren<TableRowRenderProps>
|
|
342
|
+
| ((column: TableColumnDefinition<T>) => JSX.Element);
|
|
179
343
|
/** The CSS className for the element. */
|
|
180
344
|
class?: ClassNameOrFunction<TableRowRenderProps>;
|
|
181
345
|
/** The inline style for the element. */
|
|
182
346
|
style?: StyleOrFunction<TableRowRenderProps>;
|
|
183
347
|
/** Handler called when the row is activated (double-click or Enter). */
|
|
184
348
|
onAction?: () => void;
|
|
349
|
+
/** The URL this row links to. */
|
|
350
|
+
href?: string;
|
|
351
|
+
/** Link target for linked rows. */
|
|
352
|
+
target?: LinkDOMProps["target"];
|
|
353
|
+
/** Link relationship for linked rows. */
|
|
354
|
+
rel?: LinkDOMProps["rel"];
|
|
355
|
+
/** Download attribute for linked rows. */
|
|
356
|
+
download?: LinkDOMProps["download"];
|
|
357
|
+
/** Ping attribute for linked rows. */
|
|
358
|
+
ping?: LinkDOMProps["ping"];
|
|
359
|
+
/** Referrer policy for linked rows. */
|
|
360
|
+
referrerPolicy?: LinkDOMProps["referrerPolicy"];
|
|
361
|
+
/** Router options for linked rows. */
|
|
362
|
+
routerOptions?: RouterOptions;
|
|
363
|
+
/** Ref for the table row element. */
|
|
364
|
+
ref?: RefLike<HTMLTableRowElement>;
|
|
365
|
+
/** Custom renderer for the table row element. */
|
|
366
|
+
render?: (
|
|
367
|
+
props: JSX.HTMLAttributes<HTMLTableRowElement>,
|
|
368
|
+
renderProps: TableRowRenderProps,
|
|
369
|
+
) => JSX.Element;
|
|
185
370
|
}
|
|
186
371
|
|
|
187
372
|
export interface TableCellRenderProps {
|
|
@@ -189,6 +374,8 @@ export interface TableCellRenderProps {
|
|
|
189
374
|
isFocused: boolean;
|
|
190
375
|
/** Whether the cell has keyboard focus. */
|
|
191
376
|
isFocusVisible: boolean;
|
|
377
|
+
/** The zero-based column index for the cell. */
|
|
378
|
+
columnIndex: number;
|
|
192
379
|
/** Whether the cell is pressed. */
|
|
193
380
|
isPressed: boolean;
|
|
194
381
|
/** Whether the cell is hovered. */
|
|
@@ -198,42 +385,69 @@ export interface TableCellRenderProps {
|
|
|
198
385
|
export interface TableCellProps extends SlotProps {
|
|
199
386
|
/** The unique key for the cell. */
|
|
200
387
|
id?: Key;
|
|
388
|
+
/** Number of columns spanned by the cell. */
|
|
389
|
+
colSpan?: number;
|
|
201
390
|
/** The children of the cell. */
|
|
202
391
|
children?: RenderChildren<TableCellRenderProps>;
|
|
203
392
|
/** The CSS className for the element. */
|
|
204
393
|
class?: ClassNameOrFunction<TableCellRenderProps>;
|
|
205
394
|
/** The inline style for the element. */
|
|
206
395
|
style?: StyleOrFunction<TableCellRenderProps>;
|
|
396
|
+
/** Ref for the table cell element. */
|
|
397
|
+
ref?: RefLike<HTMLTableCellElement>;
|
|
398
|
+
/** Custom renderer for the table cell element. */
|
|
399
|
+
render?: (
|
|
400
|
+
props: JSX.TdHTMLAttributes<HTMLTableCellElement>,
|
|
401
|
+
renderProps: TableCellRenderProps,
|
|
402
|
+
) => JSX.Element;
|
|
207
403
|
}
|
|
208
404
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
405
|
+
export interface TableLoadMoreItemProps extends SlotProps {
|
|
406
|
+
onLoadMore: () => void | Promise<void>;
|
|
407
|
+
isLoading?: boolean;
|
|
408
|
+
/** Scroll offset multiplier for early loading trigger (default: 1 = 100% of viewport height). */
|
|
409
|
+
scrollOffset?: number;
|
|
410
|
+
colSpan?: number;
|
|
411
|
+
children?: JSX.Element;
|
|
412
|
+
class?: ClassNameOrFunction<{ isLoading: boolean }>;
|
|
413
|
+
style?: StyleOrFunction<{ isLoading: boolean }>;
|
|
414
|
+
}
|
|
212
415
|
|
|
213
416
|
interface TableContextValue<T extends object> {
|
|
214
417
|
state: TableState<T, TableCollection<T>>;
|
|
215
418
|
collection: TableCollection<T>;
|
|
216
419
|
items: T[];
|
|
217
|
-
columns: ColumnDefinition<T>[];
|
|
420
|
+
columns: (ColumnDefinition<T> & { id?: Key })[];
|
|
218
421
|
isDisabled: boolean;
|
|
219
422
|
showSelectionCheckboxes: boolean;
|
|
423
|
+
dragAndDropHooks?: DragAndDropHooks<T>;
|
|
424
|
+
dragState?: unknown;
|
|
425
|
+
dropState?: unknown;
|
|
426
|
+
isVirtualized: boolean;
|
|
220
427
|
}
|
|
221
428
|
|
|
222
429
|
export const TableContext = createContext<TableContextValue<object> | null>(null);
|
|
223
|
-
export const TableStateContext = createContext<TableState<object, TableCollection<object>> | null>(
|
|
430
|
+
export const TableStateContext = createContext<TableState<object, TableCollection<object>> | null>(
|
|
431
|
+
null,
|
|
432
|
+
);
|
|
433
|
+
/** The resize context carries a getter for the resize state. The getter may return null before columns register. */
|
|
434
|
+
export const TableColumnResizeStateContext = createContext<{
|
|
435
|
+
getState: () => TableColumnResizeState | null;
|
|
436
|
+
getCallbacks?: () => {
|
|
437
|
+
onResizeStart?: (widths: Map<Key, number>) => void;
|
|
438
|
+
onResize?: (widths: Map<Key, number>) => void;
|
|
439
|
+
onResizeEnd?: (widths: Map<Key, number>) => void;
|
|
440
|
+
};
|
|
441
|
+
} | null>(null);
|
|
224
442
|
|
|
225
|
-
// Row-level context for cells
|
|
226
443
|
interface TableRowContextValue {
|
|
227
444
|
rowKey: Key;
|
|
228
445
|
rowNode: GridNode<unknown>;
|
|
446
|
+
getCellColumnKey(cellId: string, explicitId?: Key): Key | undefined;
|
|
229
447
|
}
|
|
230
448
|
|
|
231
449
|
export const TableRowContext = createContext<TableRowContextValue | null>(null);
|
|
232
450
|
|
|
233
|
-
// ============================================
|
|
234
|
-
// COMPONENTS
|
|
235
|
-
// ============================================
|
|
236
|
-
|
|
237
451
|
/**
|
|
238
452
|
* A table displays data in rows and columns and enables a user to navigate its contents via directional navigation keys,
|
|
239
453
|
* and optionally supports row selection and sorting.
|
|
@@ -241,42 +455,48 @@ export const TableRowContext = createContext<TableRowContextValue | null>(null);
|
|
|
241
455
|
export function Table<T extends object>(props: TableProps<T>): JSX.Element {
|
|
242
456
|
const [local, stateProps, ariaProps] = splitProps(
|
|
243
457
|
props,
|
|
244
|
-
[
|
|
458
|
+
["class", "style", "render", "slot", "renderEmptyState", "dragAndDropHooks", "ref"],
|
|
245
459
|
[
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
460
|
+
"items",
|
|
461
|
+
"columns",
|
|
462
|
+
"getKey",
|
|
463
|
+
"getTextValue",
|
|
464
|
+
"disabledKeys",
|
|
465
|
+
"disabledBehavior",
|
|
466
|
+
"escapeKeyBehavior",
|
|
467
|
+
"selectionMode",
|
|
468
|
+
"selectionBehavior",
|
|
469
|
+
"selectedKeys",
|
|
470
|
+
"defaultSelectedKeys",
|
|
471
|
+
"onSelectionChange",
|
|
472
|
+
"sortDescriptor",
|
|
473
|
+
"onSortChange",
|
|
474
|
+
"showSelectionCheckboxes",
|
|
475
|
+
],
|
|
476
|
+
);
|
|
477
|
+
|
|
262
478
|
const [ref, setRef] = createSignal<HTMLTableElement | null>(null);
|
|
479
|
+
const normalizedColumns = createMemo(() => normalizeColumnDefinitions(stateProps.columns));
|
|
480
|
+
const rowHeaderColumnKeys = createMemo(() => getRowHeaderColumnKeys(normalizedColumns()));
|
|
263
481
|
|
|
264
|
-
// Create collection
|
|
265
482
|
const collection = createMemo(() =>
|
|
266
483
|
createTableCollection<T>({
|
|
267
|
-
columns:
|
|
484
|
+
columns: normalizedColumns(),
|
|
268
485
|
rows: stateProps.items,
|
|
269
486
|
getKey: stateProps.getKey,
|
|
270
|
-
getTextValue: stateProps.getTextValue
|
|
487
|
+
getTextValue: stateProps.getTextValue as
|
|
488
|
+
| ((item: T, column: ColumnDefinition<T>) => string)
|
|
489
|
+
| undefined,
|
|
271
490
|
showSelectionCheckboxes: stateProps.showSelectionCheckboxes ?? false,
|
|
272
|
-
|
|
491
|
+
rowHeaderColumnKeys: rowHeaderColumnKeys().size > 0 ? rowHeaderColumnKeys() : undefined,
|
|
492
|
+
}),
|
|
273
493
|
);
|
|
274
494
|
|
|
275
|
-
// Create table state
|
|
276
495
|
const state = createTableState<T, TableCollection<T>>(() => ({
|
|
277
496
|
collection: collection(),
|
|
278
497
|
disabledKeys: stateProps.disabledKeys,
|
|
279
498
|
selectionMode: stateProps.selectionMode,
|
|
499
|
+
selectionBehavior: stateProps.selectionBehavior,
|
|
280
500
|
selectedKeys: stateProps.selectedKeys,
|
|
281
501
|
defaultSelectedKeys: stateProps.defaultSelectedKeys,
|
|
282
502
|
onSelectionChange: stateProps.onSelectionChange,
|
|
@@ -284,27 +504,27 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
|
|
|
284
504
|
onSortChange: stateProps.onSortChange,
|
|
285
505
|
showSelectionCheckboxes: stateProps.showSelectionCheckboxes,
|
|
286
506
|
}));
|
|
507
|
+
const parentCollectionRenderer = useCollectionRenderer<T>();
|
|
287
508
|
|
|
288
|
-
// Create table aria props
|
|
289
509
|
const { gridProps } = createTable<T>(
|
|
290
510
|
() => ({
|
|
291
511
|
id: ariaProps.id,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
isVirtualized: ariaProps.isVirtualized,
|
|
512
|
+
"aria-label": ariaProps["aria-label"],
|
|
513
|
+
"aria-labelledby": ariaProps["aria-labelledby"],
|
|
514
|
+
"aria-describedby": ariaProps["aria-describedby"],
|
|
515
|
+
isVirtualized: ariaProps.isVirtualized ?? parentCollectionRenderer?.isVirtualized,
|
|
296
516
|
onRowAction: ariaProps.onRowAction,
|
|
297
517
|
onCellAction: ariaProps.onCellAction,
|
|
518
|
+
shouldSelectOnPressUp: ariaProps.shouldSelectOnPressUp,
|
|
298
519
|
focusMode: ariaProps.focusMode,
|
|
520
|
+
escapeKeyBehavior: stateProps.escapeKeyBehavior,
|
|
299
521
|
}),
|
|
300
522
|
() => state,
|
|
301
|
-
ref
|
|
523
|
+
ref,
|
|
302
524
|
);
|
|
303
525
|
|
|
304
|
-
// Create focus ring
|
|
305
526
|
const { isFocused, isFocusVisible, focusProps } = createFocusRing();
|
|
306
527
|
|
|
307
|
-
// Render props values
|
|
308
528
|
const renderValues = createMemo<TableRenderProps>(() => ({
|
|
309
529
|
isFocused: state.isFocused || isFocused(),
|
|
310
530
|
isFocusVisible: isFocusVisible(),
|
|
@@ -312,24 +532,22 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
|
|
|
312
532
|
isEmpty: stateProps.items.length === 0,
|
|
313
533
|
}));
|
|
314
534
|
|
|
315
|
-
// Resolve render props
|
|
535
|
+
// Resolve render props (class and style only — children rendered directly in JSX
|
|
536
|
+
// to avoid eager evaluation before context providers mount)
|
|
316
537
|
const renderProps = useRenderProps(
|
|
317
538
|
{
|
|
318
|
-
children: props.children,
|
|
319
539
|
class: local.class,
|
|
320
540
|
style: local.style,
|
|
321
|
-
defaultClassName:
|
|
541
|
+
defaultClassName: "solidaria-Table",
|
|
322
542
|
},
|
|
323
|
-
renderValues
|
|
543
|
+
renderValues,
|
|
324
544
|
);
|
|
325
545
|
|
|
326
|
-
// Filter DOM props
|
|
327
546
|
const domProps = createMemo(() => {
|
|
328
547
|
const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
|
|
329
548
|
return filtered;
|
|
330
549
|
});
|
|
331
550
|
|
|
332
|
-
// Remove ref from spread props
|
|
333
551
|
const cleanGridProps = () => {
|
|
334
552
|
const { ref: _ref1, ...rest } = gridProps as Record<string, unknown>;
|
|
335
553
|
return rest;
|
|
@@ -338,32 +556,189 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
|
|
|
338
556
|
const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
|
|
339
557
|
return rest;
|
|
340
558
|
};
|
|
559
|
+
const getItemNodes = createMemo(() =>
|
|
560
|
+
Array.from(state.collection).filter((node) => node.type === "item"),
|
|
561
|
+
);
|
|
562
|
+
const getDropTargetByIndex = (
|
|
563
|
+
index: number,
|
|
564
|
+
position: "before" | "after" | "on",
|
|
565
|
+
): DropTarget | null => {
|
|
566
|
+
const node = getItemNodes()[index];
|
|
567
|
+
if (!node) return null;
|
|
568
|
+
return { type: "item", key: node.key, dropPosition: position };
|
|
569
|
+
};
|
|
570
|
+
const hasDroppableDnd = createMemo(() => {
|
|
571
|
+
const hooks = local.dragAndDropHooks;
|
|
572
|
+
return Boolean(
|
|
573
|
+
hooks?.useDroppableCollectionState &&
|
|
574
|
+
hooks.useDroppableCollection &&
|
|
575
|
+
(hooks.dropTargetDelegate ||
|
|
576
|
+
parentCollectionRenderer?.dropTargetDelegate ||
|
|
577
|
+
hooks.ListDropTargetDelegate),
|
|
578
|
+
);
|
|
579
|
+
});
|
|
580
|
+
const hasDraggableDnd = createMemo(() => {
|
|
581
|
+
const hooks = local.dragAndDropHooks;
|
|
582
|
+
return Boolean(hooks?.useDraggableCollectionState && hooks.useDraggableCollection);
|
|
583
|
+
});
|
|
584
|
+
const dragState = createMemo(() => {
|
|
585
|
+
if (!hasDraggableDnd()) return undefined;
|
|
586
|
+
return local.dragAndDropHooks?.useDraggableCollectionState?.({
|
|
587
|
+
items: stateProps.items,
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
const dropState = createMemo(() => {
|
|
591
|
+
if (!hasDroppableDnd()) return undefined;
|
|
592
|
+
return local.dragAndDropHooks?.useDroppableCollectionState?.({});
|
|
593
|
+
});
|
|
594
|
+
createEffect(() => {
|
|
595
|
+
if (!hasDraggableDnd()) return;
|
|
596
|
+
const hooks = local.dragAndDropHooks;
|
|
597
|
+
const activeDragState = dragState();
|
|
598
|
+
if (!hooks?.useDraggableCollection || !activeDragState) return;
|
|
599
|
+
hooks.useDraggableCollection({}, activeDragState, () => ref());
|
|
600
|
+
});
|
|
601
|
+
const droppableCollection = createMemo(() => {
|
|
602
|
+
if (!hasDroppableDnd()) return undefined;
|
|
603
|
+
const hooks = local.dragAndDropHooks;
|
|
604
|
+
const activeDropState = dropState();
|
|
605
|
+
if (!hooks?.useDroppableCollection || !activeDropState) return undefined;
|
|
606
|
+
const resolveDirection = (): "ltr" | "rtl" => {
|
|
607
|
+
const el = ref();
|
|
608
|
+
if (el && typeof window !== "undefined" && typeof window.getComputedStyle === "function") {
|
|
609
|
+
const dir = window.getComputedStyle(el).direction;
|
|
610
|
+
if (dir === "rtl") return "rtl";
|
|
611
|
+
}
|
|
612
|
+
return typeof document !== "undefined" && document.dir === "rtl" ? "rtl" : "ltr";
|
|
613
|
+
};
|
|
614
|
+
const dropTargetDelegate =
|
|
615
|
+
hooks.dropTargetDelegate ??
|
|
616
|
+
parentCollectionRenderer?.dropTargetDelegate ??
|
|
617
|
+
(hooks.ListDropTargetDelegate
|
|
618
|
+
? new hooks.ListDropTargetDelegate(
|
|
619
|
+
() => state.collection,
|
|
620
|
+
() => ref(),
|
|
621
|
+
{ layout: "grid", orientation: "vertical", direction: resolveDirection() },
|
|
622
|
+
)
|
|
623
|
+
: undefined);
|
|
624
|
+
if (!dropTargetDelegate) return undefined;
|
|
625
|
+
return hooks.useDroppableCollection(
|
|
626
|
+
{
|
|
627
|
+
dropTargetDelegate,
|
|
628
|
+
keyboardDelegate: {
|
|
629
|
+
getFirstKey: () => state.collection.getFirstKey?.() ?? null,
|
|
630
|
+
getLastKey: () => state.collection.getLastKey?.() ?? null,
|
|
631
|
+
getKeyBelow: (key) => state.collection.getKeyAfter?.(key) ?? null,
|
|
632
|
+
getKeyAbove: (key) => state.collection.getKeyBefore?.(key) ?? null,
|
|
633
|
+
getKeyLeftOf: (key) =>
|
|
634
|
+
resolveDirection() === "rtl"
|
|
635
|
+
? (state.collection.getKeyAfter?.(key) ?? null)
|
|
636
|
+
: (state.collection.getKeyBefore?.(key) ?? null),
|
|
637
|
+
getKeyRightOf: (key) =>
|
|
638
|
+
resolveDirection() === "rtl"
|
|
639
|
+
? (state.collection.getKeyBefore?.(key) ?? null)
|
|
640
|
+
: (state.collection.getKeyAfter?.(key) ?? null),
|
|
641
|
+
getKeyPageBelow: (key) => state.collection.getKeyAfter?.(key) ?? null,
|
|
642
|
+
getKeyPageAbove: (key) => state.collection.getKeyBefore?.(key) ?? null,
|
|
643
|
+
},
|
|
644
|
+
get collection() {
|
|
645
|
+
return state.collection;
|
|
646
|
+
},
|
|
647
|
+
get selectedKeys() {
|
|
648
|
+
return state.selectedKeys;
|
|
649
|
+
},
|
|
650
|
+
setSelectedKeys: (keys: Set<Key>) => {
|
|
651
|
+
if (state.selectionMode === "none") return;
|
|
652
|
+
state.clearSelection();
|
|
653
|
+
for (const key of keys) {
|
|
654
|
+
state.toggleSelection(key);
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
setFocusedKey: (key) => state.setFocusedKey(key),
|
|
658
|
+
},
|
|
659
|
+
activeDropState,
|
|
660
|
+
() => ref(),
|
|
661
|
+
);
|
|
662
|
+
});
|
|
663
|
+
const isRootDropTarget = createMemo(() => {
|
|
664
|
+
return Boolean(dropState()?.target?.type === "root");
|
|
665
|
+
});
|
|
666
|
+
const dndRenderDropIndicator = createMemo(() =>
|
|
667
|
+
useRenderDropIndicator(local.dragAndDropHooks, dropState()),
|
|
668
|
+
);
|
|
669
|
+
const dndDropIndicator = (index: number, position: "before" | "after" | "on") => {
|
|
670
|
+
const target = getDropTargetByIndex(index, position);
|
|
671
|
+
if (!target || target.type !== "item") return undefined;
|
|
672
|
+
return dndRenderDropIndicator()?.(target);
|
|
673
|
+
};
|
|
341
674
|
|
|
342
|
-
const contextValue
|
|
675
|
+
const contextValue: TableContextValue<T> = {
|
|
343
676
|
state,
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
677
|
+
get collection() {
|
|
678
|
+
return collection();
|
|
679
|
+
},
|
|
680
|
+
get items() {
|
|
681
|
+
return stateProps.items;
|
|
682
|
+
},
|
|
683
|
+
get columns() {
|
|
684
|
+
return normalizedColumns();
|
|
685
|
+
},
|
|
347
686
|
isDisabled: false,
|
|
348
|
-
|
|
687
|
+
get showSelectionCheckboxes() {
|
|
688
|
+
return stateProps.showSelectionCheckboxes ?? false;
|
|
689
|
+
},
|
|
690
|
+
get dragAndDropHooks() {
|
|
691
|
+
return local.dragAndDropHooks;
|
|
692
|
+
},
|
|
693
|
+
get dragState() {
|
|
694
|
+
return dragState();
|
|
695
|
+
},
|
|
696
|
+
get dropState() {
|
|
697
|
+
return dropState();
|
|
698
|
+
},
|
|
699
|
+
get isVirtualized() {
|
|
700
|
+
return ariaProps.isVirtualized ?? parentCollectionRenderer?.isVirtualized ?? false;
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
const collectionRenderer = createMemo<CollectionRendererContextValue<unknown>>(() => ({
|
|
704
|
+
...parentCollectionRenderer,
|
|
705
|
+
renderItem: (item) => item as JSX.Element,
|
|
706
|
+
renderDropIndicator: (index: number, position: "before" | "after" | "on") =>
|
|
707
|
+
dndDropIndicator(index, position) ??
|
|
708
|
+
parentCollectionRenderer?.renderDropIndicator?.(index, position),
|
|
349
709
|
}));
|
|
710
|
+
const tableChildren = () =>
|
|
711
|
+
typeof props.children === "function" ? props.children(renderValues()) : props.children;
|
|
712
|
+
const tableProps = () =>
|
|
713
|
+
({
|
|
714
|
+
ref: (el: HTMLTableElement) => {
|
|
715
|
+
setRef(el);
|
|
716
|
+
assignRef(local.ref, el);
|
|
717
|
+
},
|
|
718
|
+
...mergeProps(
|
|
719
|
+
domProps(),
|
|
720
|
+
cleanGridProps(),
|
|
721
|
+
cleanFocusProps(),
|
|
722
|
+
(droppableCollection()?.collectionProps as Record<string, unknown> | undefined) ?? {},
|
|
723
|
+
),
|
|
724
|
+
class: renderProps.class(),
|
|
725
|
+
style: renderProps.style(),
|
|
726
|
+
"data-focused": state.isFocused || undefined,
|
|
727
|
+
"data-focus-visible": isFocusVisible() || undefined,
|
|
728
|
+
"data-empty": stateProps.items.length === 0 || undefined,
|
|
729
|
+
"data-drop-target": isRootDropTarget() || undefined,
|
|
730
|
+
slot: local.slot,
|
|
731
|
+
children: tableChildren(),
|
|
732
|
+
}) as JSX.HTMLAttributes<HTMLTableElement>;
|
|
350
733
|
|
|
351
734
|
return (
|
|
352
|
-
<TableContext.Provider value={contextValue
|
|
353
|
-
<TableStateContext.Provider
|
|
354
|
-
<
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
{...
|
|
358
|
-
|
|
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>
|
|
735
|
+
<TableContext.Provider value={contextValue as unknown as TableContextValue<object>}>
|
|
736
|
+
<TableStateContext.Provider
|
|
737
|
+
value={state as unknown as TableState<object, TableCollection<object>>}
|
|
738
|
+
>
|
|
739
|
+
<CollectionRendererContext.Provider value={collectionRenderer()}>
|
|
740
|
+
{local.render ? local.render(tableProps(), renderValues()) : <table {...tableProps()} />}
|
|
741
|
+
</CollectionRendererContext.Provider>
|
|
367
742
|
</TableStateContext.Provider>
|
|
368
743
|
</TableContext.Provider>
|
|
369
744
|
);
|
|
@@ -373,39 +748,83 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
|
|
|
373
748
|
* A header row in a table containing column headers.
|
|
374
749
|
*/
|
|
375
750
|
export function TableHeader(props: TableHeaderProps): JSX.Element {
|
|
376
|
-
const [local] = splitProps(props, [
|
|
751
|
+
const [local, domProps] = splitProps(props, [
|
|
752
|
+
"class",
|
|
753
|
+
"style",
|
|
754
|
+
"render",
|
|
755
|
+
"slot",
|
|
756
|
+
"children",
|
|
757
|
+
"ref",
|
|
758
|
+
]);
|
|
377
759
|
|
|
378
|
-
// Get context
|
|
379
760
|
const context = useContext(TableContext);
|
|
380
761
|
if (!context) {
|
|
381
|
-
throw new Error(
|
|
762
|
+
throw new Error("TableHeader must be used within a Table");
|
|
382
763
|
}
|
|
383
764
|
|
|
384
|
-
const { rowGroupProps } = createTableRowGroup(() => ({ type:
|
|
765
|
+
const { rowGroupProps } = createTableRowGroup(() => ({ type: "thead" }));
|
|
766
|
+
|
|
767
|
+
const { isHovered, hoverProps } = createHover({
|
|
768
|
+
isDisabled: false,
|
|
769
|
+
onHoverStart(e) {
|
|
770
|
+
(domProps as Record<string, (e: unknown) => void>).onHoverStart?.(e);
|
|
771
|
+
},
|
|
772
|
+
onHoverEnd(e) {
|
|
773
|
+
(domProps as Record<string, (e: unknown) => void>).onHoverEnd?.(e);
|
|
774
|
+
},
|
|
775
|
+
onHoverChange(isHovering) {
|
|
776
|
+
(domProps as Record<string, (isHovering: boolean) => void>).onHoverChange?.(isHovering);
|
|
777
|
+
},
|
|
778
|
+
});
|
|
385
779
|
|
|
386
|
-
// Render props values
|
|
387
780
|
const renderValues = createMemo<TableHeaderRenderProps>(() => ({
|
|
388
781
|
isFocused: false,
|
|
782
|
+
isHovered: isHovered(),
|
|
389
783
|
}));
|
|
390
784
|
|
|
391
|
-
// Resolve render props
|
|
392
785
|
const renderProps = useRenderProps(
|
|
393
786
|
{
|
|
394
787
|
class: local.class,
|
|
395
788
|
style: local.style,
|
|
396
|
-
defaultClassName:
|
|
789
|
+
defaultClassName: "solidaria-Table-header",
|
|
397
790
|
},
|
|
398
|
-
renderValues
|
|
791
|
+
renderValues,
|
|
399
792
|
);
|
|
400
793
|
|
|
401
794
|
const cleanRowGroupProps = () => {
|
|
402
795
|
const { ref: _ref, ...rest } = rowGroupProps as Record<string, unknown>;
|
|
403
796
|
return rest;
|
|
404
797
|
};
|
|
798
|
+
const cleanHoverProps = () => {
|
|
799
|
+
const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
|
|
800
|
+
return rest;
|
|
801
|
+
};
|
|
405
802
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
803
|
+
const headerProps = () =>
|
|
804
|
+
({
|
|
805
|
+
ref: (el: HTMLTableSectionElement) => assignRef(local.ref, el),
|
|
806
|
+
...domProps,
|
|
807
|
+
...cleanRowGroupProps(),
|
|
808
|
+
...cleanHoverProps(),
|
|
809
|
+
class: renderProps.class(),
|
|
810
|
+
style: renderProps.style(),
|
|
811
|
+
"data-hovered": isHovered() || undefined,
|
|
812
|
+
children: <tr role="row">{local.children}</tr>,
|
|
813
|
+
}) as JSX.HTMLAttributes<HTMLTableSectionElement>;
|
|
814
|
+
|
|
815
|
+
return local.render ? (
|
|
816
|
+
local.render(headerProps(), renderValues())
|
|
817
|
+
) : (
|
|
818
|
+
<thead
|
|
819
|
+
ref={(el) => assignRef(local.ref, el)}
|
|
820
|
+
{...domProps}
|
|
821
|
+
{...cleanRowGroupProps()}
|
|
822
|
+
{...cleanHoverProps()}
|
|
823
|
+
class={renderProps.class()}
|
|
824
|
+
style={renderProps.style()}
|
|
825
|
+
data-hovered={isHovered() || undefined}
|
|
826
|
+
>
|
|
827
|
+
<tr role="row">{local.children}</tr>
|
|
409
828
|
</thead>
|
|
410
829
|
);
|
|
411
830
|
}
|
|
@@ -414,25 +833,35 @@ export function TableHeader(props: TableHeaderProps): JSX.Element {
|
|
|
414
833
|
* A column header in a table.
|
|
415
834
|
*/
|
|
416
835
|
export function TableColumn(props: TableColumnProps): JSX.Element {
|
|
417
|
-
const [local] = splitProps(props, [
|
|
836
|
+
const [local, domProps] = splitProps(props, [
|
|
837
|
+
"class",
|
|
838
|
+
"style",
|
|
839
|
+
"render",
|
|
840
|
+
"slot",
|
|
841
|
+
"id",
|
|
842
|
+
"allowsSorting",
|
|
843
|
+
"allowsResizing",
|
|
844
|
+
"width",
|
|
845
|
+
"defaultWidth",
|
|
846
|
+
"minWidth",
|
|
847
|
+
"maxWidth",
|
|
848
|
+
"children",
|
|
849
|
+
"ref",
|
|
850
|
+
]);
|
|
418
851
|
|
|
419
|
-
// Get context
|
|
420
852
|
const context = useContext(TableContext);
|
|
421
853
|
if (!context) {
|
|
422
|
-
throw new Error(
|
|
854
|
+
throw new Error("TableColumn must be used within a Table");
|
|
423
855
|
}
|
|
424
856
|
const { state, collection } = context;
|
|
425
857
|
|
|
426
|
-
// Create ref signal
|
|
427
858
|
const [ref, setRef] = createSignal<HTMLTableCellElement | null>(null);
|
|
428
859
|
|
|
429
|
-
// Find the column node
|
|
430
860
|
const columnNode = createMemo(() => {
|
|
431
861
|
const node = collection.getItem(local.id);
|
|
432
862
|
if (!node) {
|
|
433
|
-
// Create a simple node for the column
|
|
434
863
|
return {
|
|
435
|
-
type:
|
|
864
|
+
type: "column" as const,
|
|
436
865
|
key: local.id,
|
|
437
866
|
value: null,
|
|
438
867
|
textValue: String(local.id),
|
|
@@ -445,25 +874,32 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
|
|
|
445
874
|
return node;
|
|
446
875
|
});
|
|
447
876
|
|
|
448
|
-
|
|
449
|
-
const { columnHeaderProps } = createTableColumnHeader<object>(
|
|
877
|
+
const columnHeaderAria = createTableColumnHeader<object>(
|
|
450
878
|
() => ({
|
|
451
879
|
node: columnNode(),
|
|
452
880
|
allowsSorting: local.allowsSorting,
|
|
453
881
|
}),
|
|
454
882
|
() => state as TableState<object, TableCollection<object>>,
|
|
455
|
-
ref
|
|
883
|
+
ref,
|
|
456
884
|
);
|
|
457
885
|
|
|
458
|
-
// Create hover
|
|
459
886
|
const { isHovered, hoverProps } = createHover({
|
|
460
|
-
isDisabled
|
|
887
|
+
get isDisabled() {
|
|
888
|
+
return !local.allowsSorting;
|
|
889
|
+
},
|
|
890
|
+
onHoverStart(e) {
|
|
891
|
+
(domProps as Record<string, (e: unknown) => void>).onHoverStart?.(e);
|
|
892
|
+
},
|
|
893
|
+
onHoverEnd(e) {
|
|
894
|
+
(domProps as Record<string, (e: unknown) => void>).onHoverEnd?.(e);
|
|
895
|
+
},
|
|
896
|
+
onHoverChange(isHovering) {
|
|
897
|
+
(domProps as Record<string, (isHovering: boolean) => void>).onHoverChange?.(isHovering);
|
|
898
|
+
},
|
|
461
899
|
});
|
|
462
900
|
|
|
463
|
-
// Create focus ring
|
|
464
901
|
const { isFocusVisible, focusProps } = createFocusRing();
|
|
465
902
|
|
|
466
|
-
// Get sort direction
|
|
467
903
|
const sortDirection = createMemo(() => {
|
|
468
904
|
const sortDescriptor = state.sortDescriptor;
|
|
469
905
|
if (sortDescriptor?.column === local.id) {
|
|
@@ -472,29 +908,43 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
|
|
|
472
908
|
return undefined;
|
|
473
909
|
});
|
|
474
910
|
|
|
475
|
-
|
|
911
|
+
const resizeCtx = useContext(TableColumnResizeStateContext);
|
|
912
|
+
|
|
913
|
+
const isResizing = createMemo(() => {
|
|
914
|
+
const rs = resizeCtx?.getState();
|
|
915
|
+
if (!rs) return false;
|
|
916
|
+
return rs.resizingColumn() === local.id;
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const resizeWidth = createMemo(() => {
|
|
920
|
+
const rs = resizeCtx?.getState();
|
|
921
|
+
if (!rs) return undefined;
|
|
922
|
+
const w = rs.getColumnWidth(local.id);
|
|
923
|
+
return w > 0 ? w : undefined;
|
|
924
|
+
});
|
|
925
|
+
|
|
476
926
|
const renderValues = createMemo<TableColumnRenderProps>(() => ({
|
|
477
927
|
isFocused: state.focusedKey === local.id,
|
|
478
928
|
isFocusVisible: isFocusVisible() && state.focusedKey === local.id,
|
|
479
929
|
isSortable: local.allowsSorting ?? false,
|
|
480
930
|
sortDirection: sortDirection(),
|
|
481
931
|
isHovered: isHovered(),
|
|
932
|
+
allowsResizing: local.allowsResizing ?? false,
|
|
933
|
+
isResizing: isResizing(),
|
|
482
934
|
}));
|
|
483
935
|
|
|
484
|
-
// Resolve render props
|
|
936
|
+
// Resolve render props (children rendered directly in JSX to avoid eager evaluation)
|
|
485
937
|
const renderProps = useRenderProps(
|
|
486
938
|
{
|
|
487
|
-
children: props.children,
|
|
488
939
|
class: local.class,
|
|
489
940
|
style: local.style,
|
|
490
|
-
defaultClassName:
|
|
941
|
+
defaultClassName: "solidaria-Table-column",
|
|
491
942
|
},
|
|
492
|
-
renderValues
|
|
943
|
+
renderValues,
|
|
493
944
|
);
|
|
494
945
|
|
|
495
|
-
// Remove ref from spread props
|
|
496
946
|
const cleanColumnHeaderProps = () => {
|
|
497
|
-
const { ref: _ref1, ...rest } = columnHeaderProps as Record<string, unknown>;
|
|
947
|
+
const { ref: _ref1, ...rest } = columnHeaderAria.columnHeaderProps as Record<string, unknown>;
|
|
498
948
|
return rest;
|
|
499
949
|
};
|
|
500
950
|
const cleanHoverProps = () => {
|
|
@@ -506,21 +956,59 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
|
|
|
506
956
|
return rest;
|
|
507
957
|
};
|
|
508
958
|
|
|
509
|
-
|
|
959
|
+
const columnStyle = createMemo(() => {
|
|
960
|
+
const base = renderProps.style();
|
|
961
|
+
const rw = resizeWidth();
|
|
962
|
+
if (rw == null) return base;
|
|
963
|
+
const widthStyle = { width: `${rw}px`, "min-width": `${rw}px`, "max-width": `${rw}px` };
|
|
964
|
+
if (!base) return widthStyle;
|
|
965
|
+
if (typeof base === "string") return widthStyle; // fallback
|
|
966
|
+
return { ...base, ...widthStyle };
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
const columnChildren = () =>
|
|
970
|
+
typeof local.children === "function" ? local.children(renderValues()) : local.children;
|
|
971
|
+
const columnProps = () =>
|
|
972
|
+
({
|
|
973
|
+
ref: (el: HTMLTableCellElement) => {
|
|
974
|
+
setRef(el);
|
|
975
|
+
assignRef(local.ref, el);
|
|
976
|
+
},
|
|
977
|
+
...domProps,
|
|
978
|
+
...mergeProps(cleanColumnHeaderProps(), cleanHoverProps(), cleanFocusProps()),
|
|
979
|
+
class: renderProps.class(),
|
|
980
|
+
style: columnStyle(),
|
|
981
|
+
"data-sortable": local.allowsSorting || undefined,
|
|
982
|
+
"data-sort-direction": sortDirection() || undefined,
|
|
983
|
+
"data-resizable": local.allowsResizing || undefined,
|
|
984
|
+
"data-resizing": isResizing() || undefined,
|
|
985
|
+
"data-hovered": isHovered() || undefined,
|
|
986
|
+
"data-focused": state.focusedKey === local.id || undefined,
|
|
987
|
+
"data-focus-visible": (isFocusVisible() && state.focusedKey === local.id) || undefined,
|
|
988
|
+
children: columnChildren(),
|
|
989
|
+
}) as JSX.ThHTMLAttributes<HTMLTableCellElement>;
|
|
990
|
+
|
|
991
|
+
return local.render ? (
|
|
992
|
+
local.render(columnProps(), renderValues())
|
|
993
|
+
) : (
|
|
510
994
|
<th
|
|
511
|
-
ref={
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
995
|
+
ref={(el) => {
|
|
996
|
+
setRef(el);
|
|
997
|
+
assignRef(local.ref, el);
|
|
998
|
+
}}
|
|
999
|
+
{...domProps}
|
|
1000
|
+
{...mergeProps(cleanColumnHeaderProps(), cleanHoverProps(), cleanFocusProps())}
|
|
515
1001
|
class={renderProps.class()}
|
|
516
|
-
style={
|
|
1002
|
+
style={columnStyle()}
|
|
517
1003
|
data-sortable={local.allowsSorting || undefined}
|
|
518
1004
|
data-sort-direction={sortDirection() || undefined}
|
|
1005
|
+
data-resizable={local.allowsResizing || undefined}
|
|
1006
|
+
data-resizing={isResizing() || undefined}
|
|
519
1007
|
data-hovered={isHovered() || undefined}
|
|
520
1008
|
data-focused={state.focusedKey === local.id || undefined}
|
|
521
1009
|
data-focus-visible={(isFocusVisible() && state.focusedKey === local.id) || undefined}
|
|
522
1010
|
>
|
|
523
|
-
{
|
|
1011
|
+
{columnChildren()}
|
|
524
1012
|
</th>
|
|
525
1013
|
);
|
|
526
1014
|
}
|
|
@@ -529,32 +1017,40 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
|
|
|
529
1017
|
* The body of a table containing data rows.
|
|
530
1018
|
*/
|
|
531
1019
|
export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Element {
|
|
532
|
-
const [local] = splitProps(props, [
|
|
1020
|
+
const [local, domProps] = splitProps(props, [
|
|
1021
|
+
"items",
|
|
1022
|
+
"class",
|
|
1023
|
+
"style",
|
|
1024
|
+
"render",
|
|
1025
|
+
"slot",
|
|
1026
|
+
"renderEmptyState",
|
|
1027
|
+
"hasMore",
|
|
1028
|
+
"isLoading",
|
|
1029
|
+
"onLoadMore",
|
|
1030
|
+
"children",
|
|
1031
|
+
"ref",
|
|
1032
|
+
]);
|
|
533
1033
|
|
|
534
|
-
// Get context
|
|
535
1034
|
const context = useContext(TableContext);
|
|
536
1035
|
if (!context) {
|
|
537
|
-
throw new Error(
|
|
1036
|
+
throw new Error("TableBody must be used within a Table");
|
|
538
1037
|
}
|
|
539
1038
|
|
|
540
|
-
const { rowGroupProps } = createTableRowGroup(() => ({ type:
|
|
1039
|
+
const { rowGroupProps } = createTableRowGroup(() => ({ type: "tbody" }));
|
|
541
1040
|
|
|
542
|
-
// Use provided items or context items
|
|
543
1041
|
const items = createMemo(() => (local.items ?? context.items) as T[]);
|
|
544
1042
|
|
|
545
|
-
// Render props values
|
|
546
1043
|
const renderValues = createMemo<TableBodyRenderProps>(() => ({
|
|
547
1044
|
isEmpty: items().length === 0,
|
|
548
1045
|
}));
|
|
549
1046
|
|
|
550
|
-
// Resolve render props
|
|
551
1047
|
const renderProps = useRenderProps(
|
|
552
1048
|
{
|
|
553
1049
|
class: local.class,
|
|
554
1050
|
style: local.style,
|
|
555
|
-
defaultClassName:
|
|
1051
|
+
defaultClassName: "solidaria-Table-body",
|
|
556
1052
|
},
|
|
557
|
-
renderValues
|
|
1053
|
+
renderValues,
|
|
558
1054
|
);
|
|
559
1055
|
|
|
560
1056
|
const cleanRowGroupProps = () => {
|
|
@@ -563,13 +1059,350 @@ export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Eleme
|
|
|
563
1059
|
};
|
|
564
1060
|
|
|
565
1061
|
const isEmpty = () => items().length === 0;
|
|
1062
|
+
const virtualizer = useVirtualizerContext();
|
|
1063
|
+
const parentCollectionRenderer = useCollectionRenderer<T>();
|
|
1064
|
+
const rowNodes = createMemo(() =>
|
|
1065
|
+
Array.from(context.collection).filter((node) => node.type === "item"),
|
|
1066
|
+
);
|
|
1067
|
+
const persistedKeys = useDndPersistedKeys(
|
|
1068
|
+
{ focusedKey: () => context.state.focusedKey },
|
|
1069
|
+
context.dragAndDropHooks,
|
|
1070
|
+
context.dropState as { target?: DropTarget | null } | undefined,
|
|
1071
|
+
context.collection,
|
|
1072
|
+
);
|
|
1073
|
+
const virtualRange = createMemo(() => {
|
|
1074
|
+
if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return null;
|
|
1075
|
+
const rowCount = items().length;
|
|
1076
|
+
const baseRange = virtualizer.getVisibleRange(rowCount);
|
|
1077
|
+
const persistedIndexes = Array.from(persistedKeys())
|
|
1078
|
+
.map((key) => rowNodes().findIndex((node) => node.key === key))
|
|
1079
|
+
.filter((index) => index >= 0);
|
|
1080
|
+
const dropTarget = (context.dropState as { target?: DropTarget | null } | undefined)?.target;
|
|
1081
|
+
const normalizedDropKey = getNormalizedDropTargetKey(dropTarget, context.collection);
|
|
1082
|
+
const focusedKey = context.state.focusedKey;
|
|
1083
|
+
const focusedIndex =
|
|
1084
|
+
focusedKey != null ? rowNodes().findIndex((node) => node.key === focusedKey) : -1;
|
|
1085
|
+
const forceIncludeIndexes = [
|
|
1086
|
+
dropTarget?.type === "item"
|
|
1087
|
+
? rowNodes().findIndex((node) => node.key === dropTarget.key)
|
|
1088
|
+
: -1,
|
|
1089
|
+
normalizedDropKey != null
|
|
1090
|
+
? rowNodes().findIndex((node) => node.key === normalizedDropKey)
|
|
1091
|
+
: -1,
|
|
1092
|
+
dropTarget?.type === "item" ? -1 : focusedIndex,
|
|
1093
|
+
].filter((index) => index >= 0);
|
|
1094
|
+
return mergePersistedKeysIntoVirtualRange(
|
|
1095
|
+
baseRange,
|
|
1096
|
+
persistedIndexes,
|
|
1097
|
+
rowCount,
|
|
1098
|
+
virtualizer,
|
|
1099
|
+
80,
|
|
1100
|
+
{
|
|
1101
|
+
forceIncludeIndexes,
|
|
1102
|
+
forceIncludeMaxSpan: 320,
|
|
1103
|
+
},
|
|
1104
|
+
);
|
|
1105
|
+
});
|
|
1106
|
+
createEffect(() => {
|
|
1107
|
+
if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return;
|
|
1108
|
+
virtualizer.setDropTargetItemCountResolver(() => items().length);
|
|
1109
|
+
virtualizer.setDropTargetIndexResolver((key) => {
|
|
1110
|
+
const index = rowNodes().findIndex((node) => node.key === key);
|
|
1111
|
+
return index >= 0 ? index : null;
|
|
1112
|
+
});
|
|
1113
|
+
virtualizer.setDropTargetResolver((target) => {
|
|
1114
|
+
const node = rowNodes()[target.index];
|
|
1115
|
+
if (!node) return target;
|
|
1116
|
+
return {
|
|
1117
|
+
...target,
|
|
1118
|
+
key: typeof node.key === "string" || typeof node.key === "number" ? node.key : undefined,
|
|
1119
|
+
};
|
|
1120
|
+
});
|
|
1121
|
+
onCleanup(() => {
|
|
1122
|
+
virtualizer.setDropTargetIndexResolver(undefined);
|
|
1123
|
+
virtualizer.setDropTargetItemCountResolver(undefined);
|
|
1124
|
+
virtualizer.setDropTargetResolver(undefined);
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
const visibleItems = createMemo(() => {
|
|
1128
|
+
const range = virtualRange();
|
|
1129
|
+
if (!range) return items();
|
|
1130
|
+
return items().slice(range.start, range.end);
|
|
1131
|
+
});
|
|
1132
|
+
const spacerColSpan = () => context.columns.length + (context.showSelectionCheckboxes ? 1 : 0);
|
|
1133
|
+
|
|
1134
|
+
const bodyChildren = () => (
|
|
1135
|
+
<>
|
|
1136
|
+
<SharedElementTransition>
|
|
1137
|
+
<Show
|
|
1138
|
+
when={isEmpty() && local.renderEmptyState && !local.isLoading}
|
|
1139
|
+
fallback={
|
|
1140
|
+
<>
|
|
1141
|
+
{virtualRange()?.offsetTop ? (
|
|
1142
|
+
<tr role="presentation" aria-hidden="true" data-virtualizer-spacer="top">
|
|
1143
|
+
<td
|
|
1144
|
+
colSpan={spacerColSpan()}
|
|
1145
|
+
style={{ height: `${virtualRange()!.offsetTop}px`, padding: "0", border: "0" }}
|
|
1146
|
+
/>
|
|
1147
|
+
</tr>
|
|
1148
|
+
) : null}
|
|
1149
|
+
<For each={visibleItems()}>
|
|
1150
|
+
{(item, index) => {
|
|
1151
|
+
const itemIndex = () => (virtualRange()?.start ?? 0) + index();
|
|
1152
|
+
const beforeIndicator = () =>
|
|
1153
|
+
parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "before");
|
|
1154
|
+
const onIndicator = () =>
|
|
1155
|
+
parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "on");
|
|
1156
|
+
const afterIndicator = () =>
|
|
1157
|
+
parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "after");
|
|
1158
|
+
return (
|
|
1159
|
+
<>
|
|
1160
|
+
{beforeIndicator()}
|
|
1161
|
+
{onIndicator()}
|
|
1162
|
+
{local.children?.(item)}
|
|
1163
|
+
{afterIndicator()}
|
|
1164
|
+
</>
|
|
1165
|
+
);
|
|
1166
|
+
}}
|
|
1167
|
+
</For>
|
|
1168
|
+
{virtualRange()?.offsetBottom ? (
|
|
1169
|
+
<tr role="presentation" aria-hidden="true" data-virtualizer-spacer="bottom">
|
|
1170
|
+
<td
|
|
1171
|
+
colSpan={spacerColSpan()}
|
|
1172
|
+
style={{
|
|
1173
|
+
height: `${virtualRange()!.offsetBottom}px`,
|
|
1174
|
+
padding: "0",
|
|
1175
|
+
border: "0",
|
|
1176
|
+
}}
|
|
1177
|
+
/>
|
|
1178
|
+
</tr>
|
|
1179
|
+
) : null}
|
|
1180
|
+
</>
|
|
1181
|
+
}
|
|
1182
|
+
>
|
|
1183
|
+
<tr role="row" data-empty-state>
|
|
1184
|
+
<th role="rowheader" colSpan={spacerColSpan()}>
|
|
1185
|
+
{local.renderEmptyState?.()}
|
|
1186
|
+
</th>
|
|
1187
|
+
</tr>
|
|
1188
|
+
</Show>
|
|
1189
|
+
</SharedElementTransition>
|
|
1190
|
+
<Show when={local.hasMore && local.onLoadMore}>
|
|
1191
|
+
<TableLoadMoreItem
|
|
1192
|
+
onLoadMore={local.onLoadMore!}
|
|
1193
|
+
isLoading={local.isLoading}
|
|
1194
|
+
colSpan={spacerColSpan()}
|
|
1195
|
+
/>
|
|
1196
|
+
</Show>
|
|
1197
|
+
</>
|
|
1198
|
+
);
|
|
1199
|
+
const bodyProps = () =>
|
|
1200
|
+
({
|
|
1201
|
+
ref: (el: HTMLTableSectionElement) => assignRef(local.ref, el),
|
|
1202
|
+
...domProps,
|
|
1203
|
+
...cleanRowGroupProps(),
|
|
1204
|
+
class: renderProps.class(),
|
|
1205
|
+
style: renderProps.style(),
|
|
1206
|
+
"data-empty": isEmpty() || undefined,
|
|
1207
|
+
children: bodyChildren(),
|
|
1208
|
+
}) as JSX.HTMLAttributes<HTMLTableSectionElement>;
|
|
1209
|
+
|
|
1210
|
+
return local.render ? (
|
|
1211
|
+
local.render(bodyProps(), renderValues())
|
|
1212
|
+
) : (
|
|
1213
|
+
<tbody
|
|
1214
|
+
ref={(el) => assignRef(local.ref, el)}
|
|
1215
|
+
{...domProps}
|
|
1216
|
+
{...cleanRowGroupProps()}
|
|
1217
|
+
class={renderProps.class()}
|
|
1218
|
+
style={renderProps.style()}
|
|
1219
|
+
data-empty={isEmpty() || undefined}
|
|
1220
|
+
>
|
|
1221
|
+
<SharedElementTransition>
|
|
1222
|
+
<Show
|
|
1223
|
+
when={isEmpty() && local.renderEmptyState && !local.isLoading}
|
|
1224
|
+
fallback={
|
|
1225
|
+
<>
|
|
1226
|
+
{virtualRange()?.offsetTop ? (
|
|
1227
|
+
<tr role="presentation" aria-hidden="true" data-virtualizer-spacer="top">
|
|
1228
|
+
<td
|
|
1229
|
+
colSpan={spacerColSpan()}
|
|
1230
|
+
style={{ height: `${virtualRange()!.offsetTop}px`, padding: "0", border: "0" }}
|
|
1231
|
+
/>
|
|
1232
|
+
</tr>
|
|
1233
|
+
) : null}
|
|
1234
|
+
<For each={visibleItems()}>
|
|
1235
|
+
{(item, index) => {
|
|
1236
|
+
const itemIndex = () => (virtualRange()?.start ?? 0) + index();
|
|
1237
|
+
const beforeIndicator = () =>
|
|
1238
|
+
parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "before");
|
|
1239
|
+
const onIndicator = () =>
|
|
1240
|
+
parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "on");
|
|
1241
|
+
const afterIndicator = () =>
|
|
1242
|
+
parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "after");
|
|
1243
|
+
return (
|
|
1244
|
+
<>
|
|
1245
|
+
{beforeIndicator()}
|
|
1246
|
+
{onIndicator()}
|
|
1247
|
+
{local.children?.(item)}
|
|
1248
|
+
{afterIndicator()}
|
|
1249
|
+
</>
|
|
1250
|
+
);
|
|
1251
|
+
}}
|
|
1252
|
+
</For>
|
|
1253
|
+
{virtualRange()?.offsetBottom ? (
|
|
1254
|
+
<tr role="presentation" aria-hidden="true" data-virtualizer-spacer="bottom">
|
|
1255
|
+
<td
|
|
1256
|
+
colSpan={spacerColSpan()}
|
|
1257
|
+
style={{
|
|
1258
|
+
height: `${virtualRange()!.offsetBottom}px`,
|
|
1259
|
+
padding: "0",
|
|
1260
|
+
border: "0",
|
|
1261
|
+
}}
|
|
1262
|
+
/>
|
|
1263
|
+
</tr>
|
|
1264
|
+
) : null}
|
|
1265
|
+
</>
|
|
1266
|
+
}
|
|
1267
|
+
>
|
|
1268
|
+
<tr role="row" data-empty-state>
|
|
1269
|
+
<th role="rowheader" colSpan={spacerColSpan()}>
|
|
1270
|
+
{local.renderEmptyState?.()}
|
|
1271
|
+
</th>
|
|
1272
|
+
</tr>
|
|
1273
|
+
</Show>
|
|
1274
|
+
</SharedElementTransition>
|
|
1275
|
+
<Show when={local.hasMore && local.onLoadMore}>
|
|
1276
|
+
<TableLoadMoreItem
|
|
1277
|
+
onLoadMore={local.onLoadMore!}
|
|
1278
|
+
isLoading={local.isLoading}
|
|
1279
|
+
colSpan={spacerColSpan()}
|
|
1280
|
+
/>
|
|
1281
|
+
</Show>
|
|
1282
|
+
</tbody>
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* The footer of a table containing summary rows.
|
|
1288
|
+
*/
|
|
1289
|
+
export function TableFooter<T extends object>(props: TableFooterProps<T>): JSX.Element {
|
|
1290
|
+
const [local, domProps] = splitProps(props, ["items", "class", "style", "slot", "children"]);
|
|
1291
|
+
|
|
1292
|
+
const context = useContext(TableContext);
|
|
1293
|
+
if (!context) {
|
|
1294
|
+
throw new Error("TableFooter must be used within a Table");
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const { rowGroupProps } = createTableRowGroup(() => ({ type: "tfoot" }));
|
|
1298
|
+
const items = createMemo(() => local.items ?? []);
|
|
1299
|
+
|
|
1300
|
+
const renderValues = createMemo<TableFooterRenderProps>(() => ({
|
|
1301
|
+
isEmpty: items().length === 0,
|
|
1302
|
+
}));
|
|
1303
|
+
|
|
1304
|
+
const renderProps = useRenderProps(
|
|
1305
|
+
{
|
|
1306
|
+
class: local.class,
|
|
1307
|
+
style: local.style,
|
|
1308
|
+
defaultClassName: "solidaria-Table-footer",
|
|
1309
|
+
},
|
|
1310
|
+
renderValues,
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
const cleanRowGroupProps = () => {
|
|
1314
|
+
const { ref: _ref, ...rest } = rowGroupProps as Record<string, unknown>;
|
|
1315
|
+
return rest;
|
|
1316
|
+
};
|
|
566
1317
|
|
|
567
1318
|
return (
|
|
568
|
-
<
|
|
569
|
-
|
|
570
|
-
|
|
1319
|
+
<tfoot
|
|
1320
|
+
{...domProps}
|
|
1321
|
+
{...cleanRowGroupProps()}
|
|
1322
|
+
class={renderProps.class()}
|
|
1323
|
+
style={renderProps.style()}
|
|
1324
|
+
>
|
|
1325
|
+
<Show
|
|
1326
|
+
when={local.items && typeof local.children === "function"}
|
|
1327
|
+
fallback={local.children as JSX.Element}
|
|
1328
|
+
>
|
|
1329
|
+
<For each={items()}>{(item) => (local.children as (item: T) => JSX.Element)(item)}</For>
|
|
571
1330
|
</Show>
|
|
572
|
-
</
|
|
1331
|
+
</tfoot>
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
export function TableLoadMoreItem(props: TableLoadMoreItemProps): JSX.Element {
|
|
1336
|
+
let sentinelRef: HTMLDivElement | undefined;
|
|
1337
|
+
const [isPending, setIsPending] = createSignal(false);
|
|
1338
|
+
const isLoading = () => !!props.isLoading || isPending();
|
|
1339
|
+
|
|
1340
|
+
const triggerLoadMore = async () => {
|
|
1341
|
+
if (isPending()) return;
|
|
1342
|
+
setIsPending(true);
|
|
1343
|
+
try {
|
|
1344
|
+
await props.onLoadMore();
|
|
1345
|
+
} finally {
|
|
1346
|
+
setIsPending(false);
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
createEffect(() => {
|
|
1351
|
+
if (!sentinelRef || typeof IntersectionObserver !== "function") return;
|
|
1352
|
+
const offset = props.scrollOffset ?? 1;
|
|
1353
|
+
const margin = `0px 0px ${100 * offset}% 0px`;
|
|
1354
|
+
const observer = new IntersectionObserver(
|
|
1355
|
+
(entries) => {
|
|
1356
|
+
if (entries[0]?.isIntersecting) {
|
|
1357
|
+
void triggerLoadMore();
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
{ rootMargin: margin },
|
|
1361
|
+
);
|
|
1362
|
+
observer.observe(sentinelRef);
|
|
1363
|
+
return () => observer.disconnect();
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
const renderProps = useRenderProps(
|
|
1367
|
+
{
|
|
1368
|
+
children:
|
|
1369
|
+
props.children ??
|
|
1370
|
+
(() => (isLoading() ? <div role="progressbar" aria-label="loading" /> : null)),
|
|
1371
|
+
class: props.class,
|
|
1372
|
+
style: props.style,
|
|
1373
|
+
defaultClassName: "solidaria-Table-loadMore",
|
|
1374
|
+
},
|
|
1375
|
+
() => ({ isLoading: isLoading() }),
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
return (
|
|
1379
|
+
<>
|
|
1380
|
+
<tr style={{ position: "relative", width: 0, height: 0, overflow: "hidden" }} inert>
|
|
1381
|
+
<td>
|
|
1382
|
+
<div
|
|
1383
|
+
ref={sentinelRef}
|
|
1384
|
+
data-testid="loadMoreSentinel"
|
|
1385
|
+
style={{ position: "absolute", height: "1px", width: "1px" }}
|
|
1386
|
+
/>
|
|
1387
|
+
</td>
|
|
1388
|
+
</tr>
|
|
1389
|
+
<Show when={isLoading()}>
|
|
1390
|
+
<tr
|
|
1391
|
+
role="row"
|
|
1392
|
+
tabIndex={0}
|
|
1393
|
+
onFocus={() => {
|
|
1394
|
+
void triggerLoadMore();
|
|
1395
|
+
}}
|
|
1396
|
+
class={renderProps.class()}
|
|
1397
|
+
style={renderProps.style()}
|
|
1398
|
+
data-loading
|
|
1399
|
+
>
|
|
1400
|
+
<td role="rowheader" colSpan={props.colSpan ?? 1}>
|
|
1401
|
+
{renderProps.renderChildren()}
|
|
1402
|
+
</td>
|
|
1403
|
+
</tr>
|
|
1404
|
+
</Show>
|
|
1405
|
+
</>
|
|
573
1406
|
);
|
|
574
1407
|
}
|
|
575
1408
|
|
|
@@ -577,26 +1410,56 @@ export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Eleme
|
|
|
577
1410
|
* A row in a table.
|
|
578
1411
|
*/
|
|
579
1412
|
export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element {
|
|
580
|
-
const [local] = splitProps(props, [
|
|
1413
|
+
const [local, domProps] = splitProps(props, [
|
|
1414
|
+
"class",
|
|
1415
|
+
"style",
|
|
1416
|
+
"render",
|
|
1417
|
+
"slot",
|
|
1418
|
+
"id",
|
|
1419
|
+
"item",
|
|
1420
|
+
"columns",
|
|
1421
|
+
"isDisabled",
|
|
1422
|
+
"onAction",
|
|
1423
|
+
"children",
|
|
1424
|
+
"ref",
|
|
1425
|
+
"href",
|
|
1426
|
+
"target",
|
|
1427
|
+
"rel",
|
|
1428
|
+
"download",
|
|
1429
|
+
"ping",
|
|
1430
|
+
"referrerPolicy",
|
|
1431
|
+
"routerOptions",
|
|
1432
|
+
]);
|
|
581
1433
|
|
|
582
|
-
// Get context
|
|
583
1434
|
const context = useContext(TableContext);
|
|
584
1435
|
if (!context) {
|
|
585
|
-
throw new Error(
|
|
1436
|
+
throw new Error("TableRow must be used within a Table");
|
|
586
1437
|
}
|
|
587
1438
|
const { state, collection } = context;
|
|
1439
|
+
const tableContext = context as unknown as TableContextValue<T>;
|
|
1440
|
+
const registeredCellIds: string[] = [];
|
|
1441
|
+
const generatedId = createUniqueId();
|
|
1442
|
+
const rowKey = () => local.id ?? generatedId;
|
|
1443
|
+
const router = useRouter();
|
|
1444
|
+
const linkProps = createMemo(() =>
|
|
1445
|
+
useLinkProps({
|
|
1446
|
+
href: local.href,
|
|
1447
|
+
target: local.target,
|
|
1448
|
+
rel: local.rel,
|
|
1449
|
+
download: local.download,
|
|
1450
|
+
ping: local.ping,
|
|
1451
|
+
referrerPolicy: local.referrerPolicy,
|
|
1452
|
+
}),
|
|
1453
|
+
);
|
|
588
1454
|
|
|
589
|
-
// Create ref signal
|
|
590
1455
|
const [ref, setRef] = createSignal<HTMLTableRowElement | null>(null);
|
|
591
1456
|
|
|
592
|
-
// Find the row node
|
|
593
1457
|
const rowNode = createMemo(() => {
|
|
594
|
-
const node = collection.getItem(
|
|
1458
|
+
const node = collection.getItem(rowKey());
|
|
595
1459
|
if (!node) {
|
|
596
|
-
// Create a simple node for the row
|
|
597
1460
|
return {
|
|
598
|
-
type:
|
|
599
|
-
key:
|
|
1461
|
+
type: "item" as const,
|
|
1462
|
+
key: rowKey(),
|
|
600
1463
|
value: local.item ?? null,
|
|
601
1464
|
textValue: String(local.id),
|
|
602
1465
|
level: 0,
|
|
@@ -608,53 +1471,105 @@ export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element
|
|
|
608
1471
|
return node;
|
|
609
1472
|
});
|
|
610
1473
|
|
|
611
|
-
|
|
612
|
-
const { rowProps, isSelected, isDisabled, isPressed } = createTableRow<object>(
|
|
1474
|
+
const rowAria = createTableRow<object>(
|
|
613
1475
|
() => ({
|
|
614
1476
|
node: rowNode(),
|
|
1477
|
+
isVirtualized: tableContext.isVirtualized,
|
|
1478
|
+
isDisabled: local.isDisabled,
|
|
615
1479
|
onAction: local.onAction,
|
|
1480
|
+
href: linkProps().href,
|
|
1481
|
+
onLinkAction: (event) => {
|
|
1482
|
+
const target = ref();
|
|
1483
|
+
if (!target || !local.href) return;
|
|
1484
|
+
router.open(target, event as RouterClickModifiers, local.href, local.routerOptions);
|
|
1485
|
+
},
|
|
616
1486
|
}),
|
|
617
1487
|
() => state as TableState<object, TableCollection<object>>,
|
|
618
|
-
ref
|
|
1488
|
+
ref,
|
|
619
1489
|
);
|
|
1490
|
+
const isSelected = () => rowAria.isSelected;
|
|
1491
|
+
const isDisabled = () => rowAria.isDisabled;
|
|
1492
|
+
const isPressed = () => rowAria.isPressed;
|
|
1493
|
+
const isInteractive = () => {
|
|
1494
|
+
const tableData = getTableData(state as TableState<object, TableCollection<object>>);
|
|
1495
|
+
return state.selectionMode !== "none" || !!tableData?.actions.onRowAction || !!local.onAction;
|
|
1496
|
+
};
|
|
620
1497
|
|
|
621
|
-
// Create hover
|
|
622
1498
|
const { isHovered, hoverProps } = createHover({
|
|
623
1499
|
get isDisabled() {
|
|
624
|
-
return isDisabled;
|
|
1500
|
+
return isDisabled() || !isInteractive();
|
|
1501
|
+
},
|
|
1502
|
+
onHoverStart(e) {
|
|
1503
|
+
(domProps as Record<string, (e: unknown) => void>).onHoverStart?.(e);
|
|
1504
|
+
},
|
|
1505
|
+
onHoverEnd(e) {
|
|
1506
|
+
(domProps as Record<string, (e: unknown) => void>).onHoverEnd?.(e);
|
|
1507
|
+
},
|
|
1508
|
+
onHoverChange(isHovering) {
|
|
1509
|
+
(domProps as Record<string, (isHovering: boolean) => void>).onHoverChange?.(isHovering);
|
|
625
1510
|
},
|
|
626
1511
|
});
|
|
627
1512
|
|
|
628
|
-
// Create focus ring
|
|
629
1513
|
const { isFocusVisible, focusProps } = createFocusRing();
|
|
1514
|
+
const [isFocusWithin, setIsFocusWithin] = createSignal(false);
|
|
1515
|
+
const focusWithinProps = {
|
|
1516
|
+
onFocusIn() {
|
|
1517
|
+
setIsFocusWithin(true);
|
|
1518
|
+
},
|
|
1519
|
+
onFocusOut(e: FocusEvent) {
|
|
1520
|
+
const currentTarget = e.currentTarget as HTMLElement;
|
|
1521
|
+
const nextTarget = e.relatedTarget as Node | null;
|
|
1522
|
+
if (!nextTarget || !currentTarget.contains(nextTarget)) {
|
|
1523
|
+
setIsFocusWithin(false);
|
|
1524
|
+
}
|
|
1525
|
+
},
|
|
1526
|
+
};
|
|
630
1527
|
|
|
631
|
-
|
|
632
|
-
const
|
|
1528
|
+
const isFocused = createMemo(() => state.focusedKey === rowKey());
|
|
1529
|
+
const draggableItem = createMemo(() => {
|
|
1530
|
+
if (!tableContext.dragAndDropHooks?.useDraggableItem || !tableContext.dragState)
|
|
1531
|
+
return undefined;
|
|
1532
|
+
return tableContext.dragAndDropHooks.useDraggableItem(
|
|
1533
|
+
{
|
|
1534
|
+
key: rowKey() as string | number,
|
|
1535
|
+
hasDragButton: true,
|
|
1536
|
+
},
|
|
1537
|
+
tableContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>["useDraggableItem"]>>[1],
|
|
1538
|
+
);
|
|
1539
|
+
});
|
|
1540
|
+
const droppableItem = createMemo(() => {
|
|
1541
|
+
if (!tableContext.dragAndDropHooks?.useDroppableItem || !tableContext.dropState)
|
|
1542
|
+
return undefined;
|
|
1543
|
+
return tableContext.dragAndDropHooks.useDroppableItem(
|
|
1544
|
+
{
|
|
1545
|
+
key: rowKey() as string | number,
|
|
1546
|
+
},
|
|
1547
|
+
tableContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>["useDroppableItem"]>>[1],
|
|
1548
|
+
() => ref(),
|
|
1549
|
+
);
|
|
1550
|
+
});
|
|
633
1551
|
|
|
634
|
-
// Render props values
|
|
635
1552
|
const renderValues = createMemo<TableRowRenderProps>(() => ({
|
|
636
|
-
isSelected,
|
|
1553
|
+
isSelected: isSelected(),
|
|
637
1554
|
isFocused: isFocused(),
|
|
638
1555
|
isFocusVisible: isFocusVisible() && isFocused(),
|
|
639
|
-
isPressed,
|
|
1556
|
+
isPressed: isPressed(),
|
|
640
1557
|
isHovered: isHovered(),
|
|
641
|
-
isDisabled,
|
|
1558
|
+
isDisabled: isDisabled(),
|
|
642
1559
|
}));
|
|
643
1560
|
|
|
644
|
-
// Resolve render props
|
|
1561
|
+
// Resolve render props (children rendered directly in JSX to avoid eager evaluation)
|
|
645
1562
|
const renderProps = useRenderProps(
|
|
646
1563
|
{
|
|
647
|
-
children: props.children,
|
|
648
1564
|
class: local.class,
|
|
649
1565
|
style: local.style,
|
|
650
|
-
defaultClassName:
|
|
1566
|
+
defaultClassName: "solidaria-Table-row",
|
|
651
1567
|
},
|
|
652
|
-
renderValues
|
|
1568
|
+
renderValues,
|
|
653
1569
|
);
|
|
654
1570
|
|
|
655
|
-
// Remove ref from spread props
|
|
656
1571
|
const cleanRowProps = () => {
|
|
657
|
-
const { ref: _ref1, ...rest } = rowProps as Record<string, unknown>;
|
|
1572
|
+
const { ref: _ref1, ...rest } = rowAria.rowProps as Record<string, unknown>;
|
|
658
1573
|
return rest;
|
|
659
1574
|
};
|
|
660
1575
|
const cleanHoverProps = () => {
|
|
@@ -667,28 +1582,101 @@ export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element
|
|
|
667
1582
|
};
|
|
668
1583
|
|
|
669
1584
|
const rowContextValue: TableRowContextValue = {
|
|
670
|
-
rowKey:
|
|
1585
|
+
rowKey: rowKey(),
|
|
671
1586
|
rowNode: rowNode(),
|
|
1587
|
+
getCellColumnKey(cellId, explicitId) {
|
|
1588
|
+
if (explicitId === "__selection__") {
|
|
1589
|
+
return explicitId;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
let index = registeredCellIds.indexOf(cellId);
|
|
1593
|
+
if (index < 0) {
|
|
1594
|
+
index = registeredCellIds.length;
|
|
1595
|
+
registeredCellIds.push(cellId);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
return explicitId ?? tableContext.columns[index]?.key;
|
|
1599
|
+
},
|
|
672
1600
|
};
|
|
1601
|
+
const dragButtonProps = createMemo<ButtonProps>(() => {
|
|
1602
|
+
const props = (draggableItem()?.dragButtonProps as ButtonProps | undefined) ?? {};
|
|
1603
|
+
const textValue = (rowNode().textValue || String(rowKey())).trim();
|
|
1604
|
+
return {
|
|
1605
|
+
...props,
|
|
1606
|
+
"aria-label": `Drag ${textValue}`,
|
|
1607
|
+
style: {
|
|
1608
|
+
...(typeof props.style === "object" ? props.style : {}),
|
|
1609
|
+
"pointer-events": "none",
|
|
1610
|
+
},
|
|
1611
|
+
};
|
|
1612
|
+
});
|
|
1613
|
+
const buttonContextValue = createMemo(() => ({
|
|
1614
|
+
slots: {
|
|
1615
|
+
default: {},
|
|
1616
|
+
drag: dragButtonProps(),
|
|
1617
|
+
},
|
|
1618
|
+
}));
|
|
1619
|
+
|
|
1620
|
+
const rowChildren = () => (
|
|
1621
|
+
<ButtonContext.Provider value={buttonContextValue()}>
|
|
1622
|
+
{typeof local.children === "function" ? (
|
|
1623
|
+
local.columns ? (
|
|
1624
|
+
<For each={local.columns}>
|
|
1625
|
+
{(column) =>
|
|
1626
|
+
(local.children as (column: TableColumnDefinition<T>) => JSX.Element)(column)
|
|
1627
|
+
}
|
|
1628
|
+
</For>
|
|
1629
|
+
) : (
|
|
1630
|
+
(local.children as (renderProps: TableRowRenderProps) => JSX.Element)(renderValues())
|
|
1631
|
+
)
|
|
1632
|
+
) : (
|
|
1633
|
+
local.children
|
|
1634
|
+
)}
|
|
1635
|
+
</ButtonContext.Provider>
|
|
1636
|
+
);
|
|
1637
|
+
const tableRowProps = () =>
|
|
1638
|
+
({
|
|
1639
|
+
ref: (el: HTMLTableRowElement) => {
|
|
1640
|
+
setRef(el);
|
|
1641
|
+
assignRef(local.ref, el);
|
|
1642
|
+
},
|
|
1643
|
+
...domProps,
|
|
1644
|
+
...mergeProps(
|
|
1645
|
+
cleanRowProps(),
|
|
1646
|
+
cleanHoverProps(),
|
|
1647
|
+
cleanFocusProps(),
|
|
1648
|
+
focusWithinProps as Record<string, unknown>,
|
|
1649
|
+
(draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
|
|
1650
|
+
(droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {},
|
|
1651
|
+
),
|
|
1652
|
+
class: renderProps.class(),
|
|
1653
|
+
style: renderProps.style(),
|
|
1654
|
+
"data-selected": isSelected() || undefined,
|
|
1655
|
+
"data-focused": isFocused() || undefined,
|
|
1656
|
+
"data-focus-visible": (isFocusVisible() && isFocused()) || undefined,
|
|
1657
|
+
"data-focus-visible-within": dataAttr(isFocusWithin() && isGlobalFocusVisible()),
|
|
1658
|
+
"data-pressed": isPressed() || undefined,
|
|
1659
|
+
"data-hovered": isHovered() || undefined,
|
|
1660
|
+
"data-disabled": isDisabled() || undefined,
|
|
1661
|
+
"data-href": linkProps().href,
|
|
1662
|
+
"data-target": linkProps().target,
|
|
1663
|
+
"data-rel": linkProps().rel,
|
|
1664
|
+
"data-download":
|
|
1665
|
+
typeof linkProps().download === "string"
|
|
1666
|
+
? linkProps().download
|
|
1667
|
+
: linkProps().download
|
|
1668
|
+
? ""
|
|
1669
|
+
: undefined,
|
|
1670
|
+
"data-ping": linkProps().ping,
|
|
1671
|
+
"data-referrer-policy": linkProps().referrerPolicy,
|
|
1672
|
+
"data-dragging": draggableItem()?.isDragging || undefined,
|
|
1673
|
+
"data-drop-target": droppableItem()?.isDropTarget || undefined,
|
|
1674
|
+
children: rowChildren(),
|
|
1675
|
+
}) as JSX.HTMLAttributes<HTMLTableRowElement>;
|
|
673
1676
|
|
|
674
1677
|
return (
|
|
675
1678
|
<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>
|
|
1679
|
+
{local.render ? local.render(tableRowProps(), renderValues()) : <tr {...tableRowProps()} />}
|
|
692
1680
|
</TableRowContext.Provider>
|
|
693
1681
|
);
|
|
694
1682
|
}
|
|
@@ -697,40 +1685,47 @@ export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element
|
|
|
697
1685
|
* A cell in a table row.
|
|
698
1686
|
*/
|
|
699
1687
|
export function TableCell(props: TableCellProps): JSX.Element {
|
|
700
|
-
const [local] = splitProps(props, [
|
|
1688
|
+
const [local, domProps] = splitProps(props, [
|
|
1689
|
+
"class",
|
|
1690
|
+
"style",
|
|
1691
|
+
"render",
|
|
1692
|
+
"slot",
|
|
1693
|
+
"id",
|
|
1694
|
+
"colSpan",
|
|
1695
|
+
"children",
|
|
1696
|
+
"ref",
|
|
1697
|
+
]);
|
|
701
1698
|
|
|
702
|
-
// Get context
|
|
703
1699
|
const tableContext = useContext(TableContext);
|
|
704
1700
|
const rowContext = useContext(TableRowContext);
|
|
705
1701
|
|
|
706
1702
|
if (!tableContext) {
|
|
707
|
-
throw new Error(
|
|
1703
|
+
throw new Error("TableCell must be used within a Table");
|
|
708
1704
|
}
|
|
709
1705
|
if (!rowContext) {
|
|
710
|
-
throw new Error(
|
|
1706
|
+
throw new Error("TableCell must be used within a Table");
|
|
711
1707
|
}
|
|
712
1708
|
|
|
713
1709
|
const { state, collection } = tableContext;
|
|
714
1710
|
const { rowKey, rowNode } = rowContext;
|
|
715
1711
|
|
|
716
|
-
// Create ref signal
|
|
717
1712
|
const [ref, setRef] = createSignal<HTMLTableCellElement | null>(null);
|
|
1713
|
+
const cellId = createUniqueId();
|
|
1714
|
+
const columnKey = createMemo(() => rowContext.getCellColumnKey(cellId, local.id));
|
|
718
1715
|
|
|
719
|
-
// Find the cell node
|
|
720
1716
|
const cellNode = createMemo(() => {
|
|
721
|
-
|
|
722
|
-
if (
|
|
723
|
-
const cellKey = `${rowKey}-${
|
|
1717
|
+
const key = columnKey();
|
|
1718
|
+
if (key != null) {
|
|
1719
|
+
const cellKey = `${rowKey}-${key}`;
|
|
724
1720
|
const node = collection.getItem(cellKey);
|
|
725
1721
|
if (node) return node;
|
|
726
1722
|
}
|
|
727
1723
|
|
|
728
|
-
// Otherwise create a simple node
|
|
729
1724
|
return {
|
|
730
|
-
type:
|
|
731
|
-
key:
|
|
1725
|
+
type: "cell" as const,
|
|
1726
|
+
key: key ?? `${rowKey}-cell`,
|
|
732
1727
|
value: rowNode.value,
|
|
733
|
-
textValue:
|
|
1728
|
+
textValue: "",
|
|
734
1729
|
level: 1,
|
|
735
1730
|
index: 0,
|
|
736
1731
|
parentKey: rowKey,
|
|
@@ -738,49 +1733,50 @@ export function TableCell(props: TableCellProps): JSX.Element {
|
|
|
738
1733
|
childNodes: [],
|
|
739
1734
|
} as GridNode<unknown>;
|
|
740
1735
|
});
|
|
1736
|
+
const cellColumnIndex = createMemo(() => {
|
|
1737
|
+
const key = columnKey();
|
|
1738
|
+
if (key == null) return undefined;
|
|
1739
|
+
const cellKey = `${rowKey}-${key}`;
|
|
1740
|
+
return collection.getItem(cellKey) ? (cellNode().index ?? 0) : undefined;
|
|
1741
|
+
});
|
|
741
1742
|
|
|
742
|
-
|
|
743
|
-
const { gridCellProps, isPressed } = createTableCell<object>(
|
|
1743
|
+
const cellAria = createTableCell<object>(
|
|
744
1744
|
() => ({
|
|
745
1745
|
node: cellNode(),
|
|
746
1746
|
}),
|
|
747
1747
|
() => state as TableState<object, TableCollection<object>>,
|
|
748
|
-
ref
|
|
1748
|
+
ref,
|
|
749
1749
|
);
|
|
1750
|
+
const isPressed = () => cellAria.isPressed;
|
|
750
1751
|
|
|
751
|
-
// Create hover
|
|
752
1752
|
const { isHovered, hoverProps } = createHover({
|
|
753
1753
|
isDisabled: false,
|
|
754
1754
|
});
|
|
755
1755
|
|
|
756
|
-
// Create focus ring
|
|
757
1756
|
const { isFocusVisible, focusProps } = createFocusRing();
|
|
758
1757
|
|
|
759
|
-
// Check if focused
|
|
760
1758
|
const isFocused = createMemo(() => state.focusedKey === cellNode().key);
|
|
761
1759
|
|
|
762
|
-
// Render props values
|
|
763
1760
|
const renderValues = createMemo<TableCellRenderProps>(() => ({
|
|
764
1761
|
isFocused: isFocused(),
|
|
765
1762
|
isFocusVisible: isFocusVisible() && isFocused(),
|
|
766
|
-
|
|
1763
|
+
columnIndex: cellColumnIndex() ?? 0,
|
|
1764
|
+
isPressed: isPressed(),
|
|
767
1765
|
isHovered: isHovered(),
|
|
768
1766
|
}));
|
|
769
1767
|
|
|
770
|
-
// Resolve render props
|
|
1768
|
+
// Resolve render props (children rendered directly in JSX to avoid eager evaluation)
|
|
771
1769
|
const renderProps = useRenderProps(
|
|
772
1770
|
{
|
|
773
|
-
children: props.children,
|
|
774
1771
|
class: local.class,
|
|
775
1772
|
style: local.style,
|
|
776
|
-
defaultClassName:
|
|
1773
|
+
defaultClassName: "solidaria-Table-cell",
|
|
777
1774
|
},
|
|
778
|
-
renderValues
|
|
1775
|
+
renderValues,
|
|
779
1776
|
);
|
|
780
1777
|
|
|
781
|
-
// Remove ref from spread props
|
|
782
1778
|
const cleanCellProps = () => {
|
|
783
|
-
const { ref: _ref1, ...rest } = gridCellProps as Record<string, unknown>;
|
|
1779
|
+
const { ref: _ref1, ...rest } = cellAria.gridCellProps as Record<string, unknown>;
|
|
784
1780
|
return rest;
|
|
785
1781
|
};
|
|
786
1782
|
const cleanHoverProps = () => {
|
|
@@ -792,66 +1788,396 @@ export function TableCell(props: TableCellProps): JSX.Element {
|
|
|
792
1788
|
return rest;
|
|
793
1789
|
};
|
|
794
1790
|
|
|
795
|
-
|
|
1791
|
+
const cellChildren = () =>
|
|
1792
|
+
typeof local.children === "function" ? local.children(renderValues()) : local.children;
|
|
1793
|
+
const tableCellProps = () =>
|
|
1794
|
+
({
|
|
1795
|
+
ref: (el: HTMLTableCellElement) => {
|
|
1796
|
+
setRef(el);
|
|
1797
|
+
assignRef(local.ref, el);
|
|
1798
|
+
},
|
|
1799
|
+
...domProps,
|
|
1800
|
+
...mergeProps(cleanCellProps(), cleanHoverProps(), cleanFocusProps()),
|
|
1801
|
+
colSpan: local.colSpan,
|
|
1802
|
+
class: renderProps.class(),
|
|
1803
|
+
style: renderProps.style(),
|
|
1804
|
+
"data-focused": isFocused() || undefined,
|
|
1805
|
+
"data-focus-visible": (isFocusVisible() && isFocused()) || undefined,
|
|
1806
|
+
"data-column-index": cellColumnIndex(),
|
|
1807
|
+
"data-pressed": isPressed() || undefined,
|
|
1808
|
+
"data-hovered": isHovered() || undefined,
|
|
1809
|
+
children: cellChildren(),
|
|
1810
|
+
}) as JSX.TdHTMLAttributes<HTMLTableCellElement>;
|
|
1811
|
+
|
|
1812
|
+
return local.render ? (
|
|
1813
|
+
local.render(tableCellProps(), renderValues())
|
|
1814
|
+
) : (
|
|
796
1815
|
<td
|
|
797
|
-
ref={
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1816
|
+
ref={(el) => {
|
|
1817
|
+
setRef(el);
|
|
1818
|
+
assignRef(local.ref, el);
|
|
1819
|
+
}}
|
|
1820
|
+
{...domProps}
|
|
1821
|
+
{...mergeProps(cleanCellProps(), cleanHoverProps(), cleanFocusProps())}
|
|
1822
|
+
colSpan={local.colSpan}
|
|
801
1823
|
class={renderProps.class()}
|
|
802
1824
|
style={renderProps.style()}
|
|
803
1825
|
data-focused={isFocused() || undefined}
|
|
804
1826
|
data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
|
|
805
|
-
data-
|
|
1827
|
+
data-column-index={cellColumnIndex()}
|
|
1828
|
+
data-pressed={isPressed() || undefined}
|
|
806
1829
|
data-hovered={isHovered() || undefined}
|
|
807
1830
|
>
|
|
808
|
-
{
|
|
1831
|
+
{cellChildren()}
|
|
809
1832
|
</td>
|
|
810
1833
|
);
|
|
811
1834
|
}
|
|
812
1835
|
|
|
1836
|
+
export interface TableSelectionCheckboxProps {
|
|
1837
|
+
rowKey: Key;
|
|
1838
|
+
class?: string;
|
|
1839
|
+
style?: JSX.CSSProperties;
|
|
1840
|
+
excludeFromTabOrder?: boolean;
|
|
1841
|
+
"aria-label"?: string;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
813
1844
|
/**
|
|
814
1845
|
* A checkbox cell for row selection.
|
|
815
1846
|
*/
|
|
816
|
-
export function TableSelectionCheckbox(props:
|
|
1847
|
+
export function TableSelectionCheckbox(props: TableSelectionCheckboxProps): JSX.Element {
|
|
817
1848
|
const context = useContext(TableContext);
|
|
818
1849
|
if (!context) {
|
|
819
|
-
throw new Error(
|
|
1850
|
+
throw new Error("TableSelectionCheckbox must be used within a Table");
|
|
820
1851
|
}
|
|
821
1852
|
|
|
822
1853
|
const { state } = context;
|
|
823
1854
|
|
|
824
|
-
const
|
|
1855
|
+
const selectionCheckboxAria = createTableSelectionCheckbox<object>(
|
|
825
1856
|
() => ({ key: props.rowKey }),
|
|
826
|
-
() => state as TableState<object, TableCollection<object
|
|
1857
|
+
() => state as TableState<object, TableCollection<object>>,
|
|
1858
|
+
);
|
|
1859
|
+
|
|
1860
|
+
return (
|
|
1861
|
+
<input
|
|
1862
|
+
{...selectionCheckboxAria.checkboxProps}
|
|
1863
|
+
class={props.class}
|
|
1864
|
+
style={props.style}
|
|
1865
|
+
tabIndex={props.excludeFromTabOrder ? -1 : selectionCheckboxAria.checkboxProps.tabIndex}
|
|
1866
|
+
aria-label={props["aria-label"] ?? selectionCheckboxAria.checkboxProps["aria-label"]}
|
|
1867
|
+
/>
|
|
827
1868
|
);
|
|
1869
|
+
}
|
|
828
1870
|
|
|
829
|
-
|
|
1871
|
+
export interface TableSelectAllCheckboxProps {
|
|
1872
|
+
class?: string;
|
|
1873
|
+
style?: JSX.CSSProperties;
|
|
1874
|
+
"aria-label"?: string;
|
|
830
1875
|
}
|
|
831
1876
|
|
|
832
1877
|
/**
|
|
833
1878
|
* A checkbox for select-all functionality.
|
|
834
1879
|
*/
|
|
835
|
-
export function TableSelectAllCheckbox(): JSX.Element {
|
|
1880
|
+
export function TableSelectAllCheckbox(props: TableSelectAllCheckboxProps = {}): JSX.Element {
|
|
836
1881
|
const context = useContext(TableContext);
|
|
837
1882
|
if (!context) {
|
|
838
|
-
throw new Error(
|
|
1883
|
+
throw new Error("TableSelectAllCheckbox must be used within a Table");
|
|
839
1884
|
}
|
|
840
1885
|
|
|
841
1886
|
const { state } = context;
|
|
842
1887
|
|
|
843
|
-
const
|
|
844
|
-
() => state as TableState<object, TableCollection<object
|
|
1888
|
+
const selectAllCheckboxAria = createTableSelectAllCheckbox<object>(
|
|
1889
|
+
() => state as TableState<object, TableCollection<object>>,
|
|
845
1890
|
);
|
|
846
1891
|
|
|
847
|
-
return
|
|
1892
|
+
return (
|
|
1893
|
+
<input
|
|
1894
|
+
{...selectAllCheckboxAria.checkboxProps}
|
|
1895
|
+
class={props.class}
|
|
1896
|
+
style={props.style}
|
|
1897
|
+
aria-label={props["aria-label"] ?? selectAllCheckboxAria.checkboxProps["aria-label"]}
|
|
1898
|
+
/>
|
|
1899
|
+
);
|
|
848
1900
|
}
|
|
849
1901
|
|
|
850
|
-
// Attach components as static properties
|
|
851
1902
|
Table.Header = TableHeader;
|
|
852
1903
|
Table.Column = TableColumn;
|
|
853
1904
|
Table.Body = TableBody;
|
|
1905
|
+
Table.LoadMoreItem = TableLoadMoreItem;
|
|
854
1906
|
Table.Row = TableRow;
|
|
855
1907
|
Table.Cell = TableCell;
|
|
856
1908
|
Table.SelectionCheckbox = TableSelectionCheckbox;
|
|
857
1909
|
Table.SelectAllCheckbox = TableSelectAllCheckbox;
|
|
1910
|
+
|
|
1911
|
+
export interface ColumnResizerRenderProps {
|
|
1912
|
+
/** Whether the resizer handle is hovered. */
|
|
1913
|
+
isHovered: boolean;
|
|
1914
|
+
/** Whether the resizer's hidden input is focused. */
|
|
1915
|
+
isFocused: boolean;
|
|
1916
|
+
/** Whether the column is currently being resized. */
|
|
1917
|
+
isResizing: boolean;
|
|
1918
|
+
/** The direction(s) the column can be resized: 'both', 'left', 'right'. */
|
|
1919
|
+
resizableDirection: "both" | "left" | "right";
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
export interface ColumnResizerProps extends SlotProps {
|
|
1923
|
+
/** The column key this resizer belongs to. */
|
|
1924
|
+
column: { key: Key };
|
|
1925
|
+
/** Accessible label for the resizer. */
|
|
1926
|
+
"aria-label"?: string;
|
|
1927
|
+
/** Whether resizing is disabled. */
|
|
1928
|
+
isDisabled?: boolean;
|
|
1929
|
+
/** Called when resize starts. */
|
|
1930
|
+
onResizeStart?: (widths: Map<Key, number>) => void;
|
|
1931
|
+
/** Called during resize. */
|
|
1932
|
+
onResize?: (widths: Map<Key, number>) => void;
|
|
1933
|
+
/** Called when resize ends. */
|
|
1934
|
+
onResizeEnd?: (widths: Map<Key, number>) => void;
|
|
1935
|
+
/** CSS class — can be a string or function of render props. */
|
|
1936
|
+
class?: ClassNameOrFunction<ColumnResizerRenderProps>;
|
|
1937
|
+
/** Inline style — can be object or function of render props. */
|
|
1938
|
+
style?: StyleOrFunction<ColumnResizerRenderProps>;
|
|
1939
|
+
/** Children — can be JSX or render function. */
|
|
1940
|
+
children?: JSX.Element | RenderChildren<ColumnResizerRenderProps>;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
export function ColumnResizer(props: ColumnResizerProps): JSX.Element {
|
|
1944
|
+
const [local, domProps] = splitProps(props, [
|
|
1945
|
+
"column",
|
|
1946
|
+
"aria-label",
|
|
1947
|
+
"isDisabled",
|
|
1948
|
+
"onResizeStart",
|
|
1949
|
+
"onResize",
|
|
1950
|
+
"onResizeEnd",
|
|
1951
|
+
"class",
|
|
1952
|
+
"style",
|
|
1953
|
+
"slot",
|
|
1954
|
+
"children",
|
|
1955
|
+
]);
|
|
1956
|
+
|
|
1957
|
+
// Register this column with the ResizableTableContainer (auto-collect columns)
|
|
1958
|
+
const registerColumn = useContext(ResizableTableRegisterContext);
|
|
1959
|
+
if (registerColumn) {
|
|
1960
|
+
registerColumn(local.column.key, { key: local.column.key });
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
const resizeCtx = useContext(TableColumnResizeStateContext);
|
|
1964
|
+
const hasResizeContext = !!resizeCtx;
|
|
1965
|
+
|
|
1966
|
+
// Create a fallback "no-op" resize state for when there's no ResizableTableContainer
|
|
1967
|
+
const noopResizeState: TableColumnResizeState = {
|
|
1968
|
+
resizingColumn: () => null,
|
|
1969
|
+
columnWidths: () => new Map(),
|
|
1970
|
+
startResize() {},
|
|
1971
|
+
endResize() {},
|
|
1972
|
+
updateResizedColumns(_key: Key, _width: number) {
|
|
1973
|
+
return new Map();
|
|
1974
|
+
},
|
|
1975
|
+
getColumnWidth() {
|
|
1976
|
+
return 0;
|
|
1977
|
+
},
|
|
1978
|
+
getColumnMinWidth() {
|
|
1979
|
+
return 75;
|
|
1980
|
+
},
|
|
1981
|
+
getColumnMaxWidth() {
|
|
1982
|
+
return Infinity;
|
|
1983
|
+
},
|
|
1984
|
+
};
|
|
1985
|
+
|
|
1986
|
+
const { isHovered, hoverProps } = createHover({ isDisabled: local.isDisabled ?? false });
|
|
1987
|
+
const [isFocused, setIsFocused] = createSignal(false);
|
|
1988
|
+
|
|
1989
|
+
// Create the ARIA resize hook — always create it but use reactive state getter
|
|
1990
|
+
const columnResize = createTableColumnResize(
|
|
1991
|
+
() => ({
|
|
1992
|
+
column: local.column,
|
|
1993
|
+
"aria-label": local["aria-label"] ?? "Resizer",
|
|
1994
|
+
isDisabled: local.isDisabled,
|
|
1995
|
+
onResizeStart: (widths) => {
|
|
1996
|
+
resizeCtx?.getCallbacks?.().onResizeStart?.(widths);
|
|
1997
|
+
local.onResizeStart?.(widths);
|
|
1998
|
+
},
|
|
1999
|
+
onResize: (widths) => {
|
|
2000
|
+
resizeCtx?.getCallbacks?.().onResize?.(widths);
|
|
2001
|
+
local.onResize?.(widths);
|
|
2002
|
+
},
|
|
2003
|
+
onResizeEnd: (widths) => {
|
|
2004
|
+
resizeCtx?.getCallbacks?.().onResizeEnd?.(widths);
|
|
2005
|
+
local.onResizeEnd?.(widths);
|
|
2006
|
+
},
|
|
2007
|
+
}),
|
|
2008
|
+
() => resizeCtx?.getState() ?? noopResizeState,
|
|
2009
|
+
);
|
|
2010
|
+
|
|
2011
|
+
const renderValues = createMemo<ColumnResizerRenderProps>(() => ({
|
|
2012
|
+
isHovered: isHovered(),
|
|
2013
|
+
isFocused: isFocused(),
|
|
2014
|
+
isResizing: columnResize.isResizing(),
|
|
2015
|
+
resizableDirection: "both",
|
|
2016
|
+
}));
|
|
2017
|
+
|
|
2018
|
+
const renderProps = useRenderProps(
|
|
2019
|
+
{
|
|
2020
|
+
class: local.class,
|
|
2021
|
+
style: local.style,
|
|
2022
|
+
defaultClassName: "solidaria-Table-columnResizer",
|
|
2023
|
+
},
|
|
2024
|
+
renderValues,
|
|
2025
|
+
);
|
|
2026
|
+
|
|
2027
|
+
const cleanHoverProps = () => {
|
|
2028
|
+
const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
|
|
2029
|
+
return rest;
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
return (
|
|
2033
|
+
<div
|
|
2034
|
+
{...domProps}
|
|
2035
|
+
{...columnResize.resizerProps}
|
|
2036
|
+
{...cleanHoverProps()}
|
|
2037
|
+
class={renderProps.class()}
|
|
2038
|
+
style={renderProps.style()}
|
|
2039
|
+
data-hovered={isHovered() || undefined}
|
|
2040
|
+
data-resizing={columnResize.isResizing() || undefined}
|
|
2041
|
+
>
|
|
2042
|
+
<Show when={hasResizeContext}>
|
|
2043
|
+
<input
|
|
2044
|
+
{...columnResize.inputProps}
|
|
2045
|
+
onFocus={() => setIsFocused(true)}
|
|
2046
|
+
onBlur={() => setIsFocused(false)}
|
|
2047
|
+
/>
|
|
2048
|
+
</Show>
|
|
2049
|
+
{typeof local.children === "function"
|
|
2050
|
+
? (local.children as (props: ColumnResizerRenderProps) => JSX.Element)(renderValues())
|
|
2051
|
+
: local.children}
|
|
2052
|
+
</div>
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
export interface ResizableTableContainerProps extends SlotProps {
|
|
2057
|
+
/** Children (should contain a Table). */
|
|
2058
|
+
children?: JSX.Element;
|
|
2059
|
+
/** Column resize definitions. If not provided, columns from child ColumnResizers are auto-detected. */
|
|
2060
|
+
columns?: Array<{ key: Key; width?: ColumnSize; minWidth?: number; maxWidth?: number }>;
|
|
2061
|
+
/** CSS class name. */
|
|
2062
|
+
class?: string;
|
|
2063
|
+
/** Inline style. */
|
|
2064
|
+
style?: JSX.CSSProperties;
|
|
2065
|
+
/** Called when column resize starts. */
|
|
2066
|
+
onResizeStart?: (widths: Map<Key, number>) => void;
|
|
2067
|
+
/** Called during column resize. */
|
|
2068
|
+
onResize?: (widths: Map<Key, number>) => void;
|
|
2069
|
+
/** Called when column resize ends. */
|
|
2070
|
+
onResizeEnd?: (widths: Map<Key, number>) => void;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
export function ResizableTableContainer(props: ResizableTableContainerProps): JSX.Element {
|
|
2074
|
+
const [local, domProps] = splitProps(props, [
|
|
2075
|
+
"class",
|
|
2076
|
+
"style",
|
|
2077
|
+
"slot",
|
|
2078
|
+
"children",
|
|
2079
|
+
"columns",
|
|
2080
|
+
"onResizeStart",
|
|
2081
|
+
"onResize",
|
|
2082
|
+
"onResizeEnd",
|
|
2083
|
+
]);
|
|
2084
|
+
|
|
2085
|
+
const [containerRef, setContainerRef] = createSignal<HTMLDivElement | null>(null);
|
|
2086
|
+
const [tableWidth, setTableWidth] = createSignal(0);
|
|
2087
|
+
|
|
2088
|
+
// Track container width via ResizeObserver
|
|
2089
|
+
createEffect(() => {
|
|
2090
|
+
const el = containerRef();
|
|
2091
|
+
if (!el) return;
|
|
2092
|
+
|
|
2093
|
+
// Initial measurement
|
|
2094
|
+
setTableWidth(el.clientWidth);
|
|
2095
|
+
|
|
2096
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
2097
|
+
const observer = new ResizeObserver((entries) => {
|
|
2098
|
+
for (const entry of entries) {
|
|
2099
|
+
setTableWidth(entry.contentRect.width);
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
observer.observe(el);
|
|
2103
|
+
onCleanup(() => observer.disconnect());
|
|
2104
|
+
}
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
// Auto-collected columns from ColumnResizer children
|
|
2108
|
+
const [autoColumns, setAutoColumns] = createSignal<
|
|
2109
|
+
Map<Key, { key: Key; width?: ColumnSize; minWidth?: number; maxWidth?: number }>
|
|
2110
|
+
>(new Map());
|
|
2111
|
+
|
|
2112
|
+
const registerColumn = (
|
|
2113
|
+
key: Key,
|
|
2114
|
+
def: { key: Key; width?: ColumnSize; minWidth?: number; maxWidth?: number },
|
|
2115
|
+
) => {
|
|
2116
|
+
setAutoColumns((prev) => {
|
|
2117
|
+
const next = new Map(prev);
|
|
2118
|
+
next.set(key, def);
|
|
2119
|
+
return next;
|
|
2120
|
+
});
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
// Columns: prefer explicit prop, fall back to auto-collected
|
|
2124
|
+
const effectiveColumns = createMemo(() => {
|
|
2125
|
+
if (local.columns && local.columns.length > 0) return local.columns;
|
|
2126
|
+
return Array.from(autoColumns().values());
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
// Use measured width, with a reasonable fallback for environments without layout (e.g. jsdom)
|
|
2130
|
+
const effectiveWidth = createMemo(() => {
|
|
2131
|
+
const w = tableWidth();
|
|
2132
|
+
return w > 0 ? w : 800; // fallback to 800px
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2135
|
+
// Create resize state
|
|
2136
|
+
const resizeState = createMemo(() => {
|
|
2137
|
+
const cols = effectiveColumns();
|
|
2138
|
+
if (cols.length === 0) return null;
|
|
2139
|
+
|
|
2140
|
+
return createTableColumnResizeState(() => ({
|
|
2141
|
+
tableWidth: effectiveWidth(),
|
|
2142
|
+
columns: cols,
|
|
2143
|
+
}));
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
// Provide a stable context object with a reactive getter
|
|
2147
|
+
const contextValue = {
|
|
2148
|
+
getState: () => resizeState(),
|
|
2149
|
+
getCallbacks: () => ({
|
|
2150
|
+
onResizeStart: local.onResizeStart,
|
|
2151
|
+
onResize: local.onResize,
|
|
2152
|
+
onResizeEnd: local.onResizeEnd,
|
|
2153
|
+
}),
|
|
2154
|
+
};
|
|
2155
|
+
|
|
2156
|
+
return (
|
|
2157
|
+
<ResizableTableRegisterContext.Provider value={registerColumn}>
|
|
2158
|
+
<TableColumnResizeStateContext.Provider value={contextValue}>
|
|
2159
|
+
<div
|
|
2160
|
+
ref={setContainerRef}
|
|
2161
|
+
{...domProps}
|
|
2162
|
+
class={local.class ?? "solidaria-ResizableTableContainer"}
|
|
2163
|
+
style={{ position: "relative", overflow: "auto", ...local.style }}
|
|
2164
|
+
>
|
|
2165
|
+
{local.children}
|
|
2166
|
+
</div>
|
|
2167
|
+
</TableColumnResizeStateContext.Provider>
|
|
2168
|
+
</ResizableTableRegisterContext.Provider>
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/** Internal context for ColumnResizer to register itself with ResizableTableContainer */
|
|
2173
|
+
const ResizableTableRegisterContext = createContext<
|
|
2174
|
+
| ((
|
|
2175
|
+
key: Key,
|
|
2176
|
+
def: { key: Key; width?: ColumnSize; minWidth?: number; maxWidth?: number },
|
|
2177
|
+
) => void)
|
|
2178
|
+
| null
|
|
2179
|
+
>(null);
|
|
2180
|
+
|
|
2181
|
+
export function useTableOptions() {
|
|
2182
|
+
return useContext(TableContext);
|
|
2183
|
+
}
|