@proyecto-viviana/solidaria-components 0.2.9 → 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.
Files changed (222) hide show
  1. package/README.md +39 -272
  2. package/dist/ActionBar.d.ts +21 -13
  3. package/dist/ActionBar.d.ts.map +1 -1
  4. package/dist/ActionGroup.d.ts +8 -8
  5. package/dist/ActionGroup.d.ts.map +1 -1
  6. package/dist/Alert.d.ts +5 -5
  7. package/dist/Alert.d.ts.map +1 -1
  8. package/dist/Autocomplete.d.ts +5 -5
  9. package/dist/Autocomplete.d.ts.map +1 -1
  10. package/dist/Breadcrumbs.d.ts +18 -7
  11. package/dist/Breadcrumbs.d.ts.map +1 -1
  12. package/dist/Button.d.ts +24 -5
  13. package/dist/Button.d.ts.map +1 -1
  14. package/dist/Calendar.d.ts +38 -7
  15. package/dist/Calendar.d.ts.map +1 -1
  16. package/dist/Checkbox.d.ts +32 -7
  17. package/dist/Checkbox.d.ts.map +1 -1
  18. package/dist/Collection.d.ts +19 -14
  19. package/dist/Collection.d.ts.map +1 -1
  20. package/dist/Color.d.ts +103 -14
  21. package/dist/Color.d.ts.map +1 -1
  22. package/dist/ColorEditor.d.ts +6 -6
  23. package/dist/ColorEditor.d.ts.map +1 -1
  24. package/dist/ComboBox.d.ts +85 -19
  25. package/dist/ComboBox.d.ts.map +1 -1
  26. package/dist/ContextualHelpTrigger.d.ts +2 -2
  27. package/dist/ContextualHelpTrigger.d.ts.map +1 -1
  28. package/dist/DateField.d.ts +8 -6
  29. package/dist/DateField.d.ts.map +1 -1
  30. package/dist/DatePicker.d.ts +53 -22
  31. package/dist/DatePicker.d.ts.map +1 -1
  32. package/dist/DateRangePickerContext.d.ts +30 -0
  33. package/dist/DateRangePickerContext.d.ts.map +1 -0
  34. package/dist/Dialog.d.ts +5 -5
  35. package/dist/Dialog.d.ts.map +1 -1
  36. package/dist/Disclosure.d.ts +23 -5
  37. package/dist/Disclosure.d.ts.map +1 -1
  38. package/dist/DragAndDrop.d.ts +6 -6
  39. package/dist/DragAndDrop.d.ts.map +1 -1
  40. package/dist/DragPreview.d.ts +2 -2
  41. package/dist/DragPreview.d.ts.map +1 -1
  42. package/dist/DropZone.d.ts +4 -4
  43. package/dist/DropZone.d.ts.map +1 -1
  44. package/dist/FieldError.d.ts +9 -5
  45. package/dist/FieldError.d.ts.map +1 -1
  46. package/dist/FileTrigger.d.ts +3 -3
  47. package/dist/FileTrigger.d.ts.map +1 -1
  48. package/dist/Focusable.d.ts +2 -2
  49. package/dist/Focusable.d.ts.map +1 -1
  50. package/dist/Form.d.ts +18 -4
  51. package/dist/Form.d.ts.map +1 -1
  52. package/dist/GridList.d.ts +32 -12
  53. package/dist/GridList.d.ts.map +1 -1
  54. package/dist/HiddenDateInput.d.ts +26 -0
  55. package/dist/HiddenDateInput.d.ts.map +1 -0
  56. package/dist/HiddenTimeInput.d.ts +25 -0
  57. package/dist/HiddenTimeInput.d.ts.map +1 -0
  58. package/dist/Icon.d.ts +5 -5
  59. package/dist/Icon.d.ts.map +1 -1
  60. package/dist/Keyboard.d.ts +1 -1
  61. package/dist/Landmark.d.ts +3 -3
  62. package/dist/Landmark.d.ts.map +1 -1
  63. package/dist/Link.d.ts +10 -4
  64. package/dist/Link.d.ts.map +1 -1
  65. package/dist/ListBox.d.ts +32 -12
  66. package/dist/ListBox.d.ts.map +1 -1
  67. package/dist/ListDropTargetDelegate.d.ts +6 -6
  68. package/dist/ListDropTargetDelegate.d.ts.map +1 -1
  69. package/dist/Menu.d.ts +65 -14
  70. package/dist/Menu.d.ts.map +1 -1
  71. package/dist/Meter.d.ts +3 -3
  72. package/dist/Meter.d.ts.map +1 -1
  73. package/dist/Modal.d.ts +5 -5
  74. package/dist/Modal.d.ts.map +1 -1
  75. package/dist/NumberField.d.ts +8 -12
  76. package/dist/NumberField.d.ts.map +1 -1
  77. package/dist/Popover.d.ts +28 -5
  78. package/dist/Popover.d.ts.map +1 -1
  79. package/dist/Pressable.d.ts +2 -2
  80. package/dist/Pressable.d.ts.map +1 -1
  81. package/dist/ProgressBar.d.ts +5 -3
  82. package/dist/ProgressBar.d.ts.map +1 -1
  83. package/dist/RadioGroup.d.ts +43 -9
  84. package/dist/RadioGroup.d.ts.map +1 -1
  85. package/dist/RangeCalendar.d.ts +34 -7
  86. package/dist/RangeCalendar.d.ts.map +1 -1
  87. package/dist/RouterProvider.d.ts +2 -2
  88. package/dist/RouterProvider.d.ts.map +1 -1
  89. package/dist/SearchField.d.ts +23 -20
  90. package/dist/SearchField.d.ts.map +1 -1
  91. package/dist/Select.d.ts +41 -11
  92. package/dist/Select.d.ts.map +1 -1
  93. package/dist/SelectionIndicator.d.ts +3 -3
  94. package/dist/SelectionIndicator.d.ts.map +1 -1
  95. package/dist/Separator.d.ts +9 -3
  96. package/dist/Separator.d.ts.map +1 -1
  97. package/dist/SharedElementTransition.d.ts +6 -4
  98. package/dist/SharedElementTransition.d.ts.map +1 -1
  99. package/dist/Slider.d.ts +12 -8
  100. package/dist/Slider.d.ts.map +1 -1
  101. package/dist/StepList.d.ts +90 -0
  102. package/dist/StepList.d.ts.map +1 -0
  103. package/dist/Switch.d.ts +11 -5
  104. package/dist/Switch.d.ts.map +1 -1
  105. package/dist/Table.d.ts +187 -23
  106. package/dist/Table.d.ts.map +1 -1
  107. package/dist/Tabs.d.ts +45 -9
  108. package/dist/Tabs.d.ts.map +1 -1
  109. package/dist/TagGroup.d.ts +12 -10
  110. package/dist/TagGroup.d.ts.map +1 -1
  111. package/dist/Text.d.ts +2 -2
  112. package/dist/TextField.d.ts +15 -11
  113. package/dist/TextField.d.ts.map +1 -1
  114. package/dist/TimeField.d.ts +6 -6
  115. package/dist/TimeField.d.ts.map +1 -1
  116. package/dist/Toast.d.ts +29 -14
  117. package/dist/Toast.d.ts.map +1 -1
  118. package/dist/ToggleButton.d.ts +11 -5
  119. package/dist/ToggleButton.d.ts.map +1 -1
  120. package/dist/ToggleButtonGroup.d.ts +7 -7
  121. package/dist/ToggleButtonGroup.d.ts.map +1 -1
  122. package/dist/Toolbar.d.ts +7 -3
  123. package/dist/Toolbar.d.ts.map +1 -1
  124. package/dist/Tooltip.d.ts +50 -8
  125. package/dist/Tooltip.d.ts.map +1 -1
  126. package/dist/Tree.d.ts +66 -17
  127. package/dist/Tree.d.ts.map +1 -1
  128. package/dist/Virtualizer.d.ts +12 -12
  129. package/dist/Virtualizer.d.ts.map +1 -1
  130. package/dist/VirtualizerLayouts.d.ts +2 -2
  131. package/dist/VirtualizerLayouts.d.ts.map +1 -1
  132. package/dist/VisuallyHidden.d.ts +1 -1
  133. package/dist/VisuallyHidden.d.ts.map +1 -1
  134. package/dist/contexts.d.ts +5 -1
  135. package/dist/contexts.d.ts.map +1 -1
  136. package/dist/index.d.ts +73 -71
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +23247 -18564
  139. package/dist/index.js.map +1 -1
  140. package/dist/index.jsx +18110 -0
  141. package/dist/index.jsx.map +1 -0
  142. package/dist/useDragAndDrop.d.ts +13 -13
  143. package/dist/useDragAndDrop.d.ts.map +1 -1
  144. package/dist/utils.d.ts +2 -2
  145. package/dist/utils.d.ts.map +1 -1
  146. package/dist/virtualizer/Layout.d.ts +1 -1
  147. package/dist/virtualizer/Layout.d.ts.map +1 -1
  148. package/package.json +31 -32
  149. package/src/ActionBar.tsx +75 -72
  150. package/src/ActionGroup.tsx +53 -61
  151. package/src/Alert.tsx +17 -42
  152. package/src/Autocomplete.tsx +39 -44
  153. package/src/Breadcrumbs.tsx +149 -80
  154. package/src/Button.tsx +267 -70
  155. package/src/Calendar.tsx +218 -138
  156. package/src/Checkbox.tsx +413 -121
  157. package/src/Collection.tsx +67 -58
  158. package/src/Color.tsx +803 -380
  159. package/src/ColorEditor.tsx +131 -149
  160. package/src/ComboBox.tsx +414 -249
  161. package/src/ContextualHelpTrigger.tsx +86 -74
  162. package/src/DateField.tsx +185 -91
  163. package/src/DatePicker.tsx +524 -213
  164. package/src/DateRangePickerContext.tsx +44 -0
  165. package/src/Dialog.tsx +156 -118
  166. package/src/Disclosure.tsx +127 -80
  167. package/src/DragAndDrop.tsx +60 -54
  168. package/src/DragPreview.tsx +13 -11
  169. package/src/DropZone.tsx +42 -22
  170. package/src/FieldError.tsx +45 -23
  171. package/src/FileTrigger.tsx +19 -19
  172. package/src/Focusable.tsx +21 -24
  173. package/src/Form.tsx +71 -16
  174. package/src/GridList.tsx +273 -197
  175. package/src/HiddenDateInput.tsx +153 -0
  176. package/src/HiddenTimeInput.tsx +133 -0
  177. package/src/Icon.tsx +22 -43
  178. package/src/Keyboard.tsx +3 -3
  179. package/src/Landmark.tsx +37 -63
  180. package/src/Link.tsx +125 -75
  181. package/src/ListBox.tsx +332 -233
  182. package/src/ListDropTargetDelegate.ts +81 -80
  183. package/src/Menu.tsx +1023 -274
  184. package/src/Meter.tsx +38 -56
  185. package/src/Modal.tsx +243 -175
  186. package/src/NumberField.tsx +139 -143
  187. package/src/Popover.tsx +386 -233
  188. package/src/Pressable.tsx +21 -21
  189. package/src/ProgressBar.tsx +48 -57
  190. package/src/RadioGroup.tsx +524 -122
  191. package/src/RangeCalendar.tsx +157 -90
  192. package/src/RouterProvider.tsx +30 -47
  193. package/src/SearchField.tsx +362 -143
  194. package/src/Select.tsx +656 -233
  195. package/src/SelectionIndicator.tsx +18 -15
  196. package/src/Separator.tsx +47 -49
  197. package/src/SharedElementTransition.tsx +103 -97
  198. package/src/Slider.tsx +138 -98
  199. package/src/StepList.tsx +272 -0
  200. package/src/Switch.tsx +93 -46
  201. package/src/Table.tsx +1308 -342
  202. package/src/Tabs.tsx +324 -103
  203. package/src/TagGroup.tsx +139 -126
  204. package/src/Text.tsx +3 -3
  205. package/src/TextField.tsx +389 -79
  206. package/src/TimeField.tsx +136 -76
  207. package/src/Toast.tsx +209 -157
  208. package/src/ToggleButton.tsx +47 -37
  209. package/src/ToggleButtonGroup.tsx +39 -34
  210. package/src/Toolbar.tsx +54 -69
  211. package/src/Tooltip.tsx +387 -119
  212. package/src/Tree.tsx +651 -368
  213. package/src/Virtualizer.tsx +208 -180
  214. package/src/VirtualizerLayouts.ts +45 -30
  215. package/src/VisuallyHidden.tsx +19 -19
  216. package/src/contexts.ts +29 -37
  217. package/src/index.ts +110 -195
  218. package/src/useDragAndDrop.ts +87 -71
  219. package/src/utils.tsx +40 -55
  220. package/src/virtualizer/Layout.ts +14 -22
  221. package/dist/index.ssr.js +0 -16996
  222. package/dist/index.ssr.js.map +0 -1
package/src/Table.tsx CHANGED
@@ -10,13 +10,14 @@ import {
10
10
  createContext,
11
11
  createEffect,
12
12
  createMemo,
13
+ createUniqueId,
13
14
  createSignal,
14
15
  onCleanup,
15
16
  splitProps,
16
17
  useContext,
17
18
  For,
18
19
  Show,
19
- } from 'solid-js';
20
+ } from "solid-js";
20
21
  import {
21
22
  createTable,
22
23
  createTableColumnHeader,
@@ -26,43 +27,58 @@ import {
26
27
  createTableSelectionCheckbox,
27
28
  createTableSelectAllCheckbox,
28
29
  createFocusRing,
30
+ isFocusVisible as isGlobalFocusVisible,
31
+ getTableData,
29
32
  createHover,
30
33
  mergeProps,
31
34
  type AriaTableProps,
32
- } from '@proyecto-viviana/solidaria';
35
+ createTableColumnResize,
36
+ } from "@proyecto-viviana/solidaria";
33
37
  import {
34
38
  createTableState,
35
39
  createTableCollection,
40
+ createTableColumnResizeState,
36
41
  type TableState,
37
42
  type TableCollection,
43
+ type TableColumnResizeState,
38
44
  type Key,
39
45
  type SortDescriptor,
40
46
  type ColumnDefinition,
47
+ type ColumnSize,
41
48
  type GridNode,
42
49
  type DropTarget,
43
- } from '@proyecto-viviana/solid-stately';
50
+ } from "@proyecto-viviana/solid-stately";
44
51
  import {
45
52
  type RenderChildren,
46
53
  type ClassNameOrFunction,
47
54
  type StyleOrFunction,
48
55
  type SlotProps,
56
+ dataAttr,
49
57
  useRenderProps,
50
58
  filterDOMProps,
51
- } from './utils';
52
- import { SharedElementTransition } from './SharedElementTransition';
53
- import { type DragAndDropHooks } from './useDragAndDrop';
54
- import { CollectionRendererContext, type CollectionRendererContextValue, useCollectionRenderer } from './Collection';
55
- import { useVirtualizerContext } from './Virtualizer';
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";
56
76
  import {
57
77
  getNormalizedDropTargetKey,
58
78
  mergePersistedKeysIntoVirtualRange,
59
79
  useDndPersistedKeys,
60
80
  useRenderDropIndicator,
61
- } from './DragAndDrop';
62
-
63
- // ============================================
64
- // TYPES
65
- // ============================================
81
+ } from "./DragAndDrop";
66
82
 
67
83
  export interface TableRenderProps {
68
84
  /** Whether the table has focus. */
@@ -75,25 +91,84 @@ export interface TableRenderProps {
75
91
  isEmpty: boolean;
76
92
  }
77
93
 
78
- export interface TableProps<T extends object> extends Omit<AriaTableProps, 'children'>, SlotProps {
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 {
79
148
  /** The data items to render in the table. */
80
149
  items: T[];
81
150
  /** The column definitions. */
82
- columns: ColumnDefinition<T>[];
151
+ columns: TableColumnDefinition<T>[];
83
152
  /** Function to get the key from an item. */
84
153
  getKey?: (item: T) => Key;
85
154
  /** Function to get the text value from an item for a column. */
86
- getTextValue?: (item: T, column: ColumnDefinition<T>) => string;
155
+ getTextValue?: (item: T, column: TableColumnDefinition<T>) => string;
87
156
  /** The selection mode. */
88
- selectionMode?: 'none' | 'single' | 'multiple';
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";
89
164
  /** Keys of disabled items. */
90
165
  disabledKeys?: Iterable<Key>;
91
166
  /** Currently selected keys (controlled). */
92
- selectedKeys?: 'all' | Iterable<Key>;
167
+ selectedKeys?: "all" | Iterable<Key>;
93
168
  /** Default selected keys (uncontrolled). */
94
- defaultSelectedKeys?: 'all' | Iterable<Key>;
169
+ defaultSelectedKeys?: "all" | Iterable<Key>;
95
170
  /** Handler called when selection changes. */
96
- onSelectionChange?: (keys: 'all' | Set<Key>) => void;
171
+ onSelectionChange?: (keys: "all" | Set<Key>) => void;
97
172
  /** The current sort descriptor. */
98
173
  sortDescriptor?: SortDescriptor;
99
174
  /** Handler called when sort changes. */
@@ -108,6 +183,13 @@ export interface TableProps<T extends object> extends Omit<AriaTableProps, 'chil
108
183
  style?: StyleOrFunction<TableRenderProps>;
109
184
  /** A function to render when the table is empty. */
110
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;
111
193
  /** Drag and drop hooks from `useDragAndDrop`. */
112
194
  dragAndDropHooks?: DragAndDropHooks<T>;
113
195
  }
@@ -115,6 +197,8 @@ export interface TableProps<T extends object> extends Omit<AriaTableProps, 'chil
115
197
  export interface TableHeaderRenderProps {
116
198
  /** Whether the header has focus. */
117
199
  isFocused: boolean;
200
+ /** Whether the header is being hovered. */
201
+ isHovered: boolean;
118
202
  }
119
203
 
120
204
  export interface TableHeaderProps extends SlotProps {
@@ -124,6 +208,13 @@ export interface TableHeaderProps extends SlotProps {
124
208
  class?: ClassNameOrFunction<TableHeaderRenderProps>;
125
209
  /** The inline style for the element. */
126
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;
127
218
  }
128
219
 
129
220
  export interface TableColumnRenderProps {
@@ -134,9 +225,13 @@ export interface TableColumnRenderProps {
134
225
  /** Whether the column is sortable. */
135
226
  isSortable: boolean;
136
227
  /** The current sort direction ('ascending', 'descending', or undefined). */
137
- sortDirection: 'ascending' | 'descending' | undefined;
228
+ sortDirection: "ascending" | "descending" | undefined;
138
229
  /** Whether the column is being hovered. */
139
230
  isHovered: boolean;
231
+ /** Whether the column allows resizing. */
232
+ allowsResizing: boolean;
233
+ /** Whether the column is currently being resized. */
234
+ isResizing: boolean;
140
235
  }
141
236
 
142
237
  export interface TableColumnProps extends SlotProps {
@@ -144,12 +239,29 @@ export interface TableColumnProps extends SlotProps {
144
239
  id: Key;
145
240
  /** Whether the column allows sorting. */
146
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;
147
252
  /** The children of the column. */
148
253
  children?: RenderChildren<TableColumnRenderProps>;
149
254
  /** The CSS className for the element. */
150
255
  class?: ClassNameOrFunction<TableColumnRenderProps>;
151
256
  /** The inline style for the element. */
152
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;
153
265
  }
154
266
 
155
267
  export interface TableBodyRenderProps {
@@ -174,6 +286,29 @@ export interface TableBodyProps<T> extends SlotProps {
174
286
  isLoading?: boolean;
175
287
  /** Called when the load more sentinel becomes visible. */
176
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>;
177
312
  }
178
313
 
179
314
  export interface TableRowRenderProps {
@@ -193,17 +328,45 @@ export interface TableRowRenderProps {
193
328
 
194
329
  export interface TableRowProps<T> extends SlotProps {
195
330
  /** The unique key for the row. */
196
- id: Key;
331
+ id?: Key;
197
332
  /** The item value. */
198
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;
199
338
  /** The children of the row (usually TableCell components). */
200
- children?: JSX.Element | RenderChildren<TableRowRenderProps>;
339
+ children?:
340
+ | JSX.Element
341
+ | RenderChildren<TableRowRenderProps>
342
+ | ((column: TableColumnDefinition<T>) => JSX.Element);
201
343
  /** The CSS className for the element. */
202
344
  class?: ClassNameOrFunction<TableRowRenderProps>;
203
345
  /** The inline style for the element. */
204
346
  style?: StyleOrFunction<TableRowRenderProps>;
205
347
  /** Handler called when the row is activated (double-click or Enter). */
206
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;
207
370
  }
208
371
 
209
372
  export interface TableCellRenderProps {
@@ -211,6 +374,8 @@ export interface TableCellRenderProps {
211
374
  isFocused: boolean;
212
375
  /** Whether the cell has keyboard focus. */
213
376
  isFocusVisible: boolean;
377
+ /** The zero-based column index for the cell. */
378
+ columnIndex: number;
214
379
  /** Whether the cell is pressed. */
215
380
  isPressed: boolean;
216
381
  /** Whether the cell is hovered. */
@@ -220,55 +385,69 @@ export interface TableCellRenderProps {
220
385
  export interface TableCellProps extends SlotProps {
221
386
  /** The unique key for the cell. */
222
387
  id?: Key;
388
+ /** Number of columns spanned by the cell. */
389
+ colSpan?: number;
223
390
  /** The children of the cell. */
224
391
  children?: RenderChildren<TableCellRenderProps>;
225
392
  /** The CSS className for the element. */
226
393
  class?: ClassNameOrFunction<TableCellRenderProps>;
227
394
  /** The inline style for the element. */
228
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;
229
403
  }
230
404
 
231
405
  export interface TableLoadMoreItemProps extends SlotProps {
232
406
  onLoadMore: () => void | Promise<void>;
233
407
  isLoading?: boolean;
408
+ /** Scroll offset multiplier for early loading trigger (default: 1 = 100% of viewport height). */
409
+ scrollOffset?: number;
234
410
  colSpan?: number;
235
411
  children?: JSX.Element;
236
412
  class?: ClassNameOrFunction<{ isLoading: boolean }>;
237
413
  style?: StyleOrFunction<{ isLoading: boolean }>;
238
414
  }
239
415
 
240
- // ============================================
241
- // CONTEXT
242
- // ============================================
243
-
244
416
  interface TableContextValue<T extends object> {
245
417
  state: TableState<T, TableCollection<T>>;
246
418
  collection: TableCollection<T>;
247
419
  items: T[];
248
- columns: ColumnDefinition<T>[];
420
+ columns: (ColumnDefinition<T> & { id?: Key })[];
249
421
  isDisabled: boolean;
250
422
  showSelectionCheckboxes: boolean;
251
423
  dragAndDropHooks?: DragAndDropHooks<T>;
252
424
  dragState?: unknown;
253
425
  dropState?: unknown;
426
+ isVirtualized: boolean;
254
427
  }
255
428
 
256
429
  export const TableContext = createContext<TableContextValue<object> | null>(null);
257
- export const TableStateContext = createContext<TableState<object, TableCollection<object>> | null>(null);
258
- export const TableColumnResizeStateContext = createContext<null>(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);
259
442
 
260
- // Row-level context for cells
261
443
  interface TableRowContextValue {
262
444
  rowKey: Key;
263
445
  rowNode: GridNode<unknown>;
446
+ getCellColumnKey(cellId: string, explicitId?: Key): Key | undefined;
264
447
  }
265
448
 
266
449
  export const TableRowContext = createContext<TableRowContextValue | null>(null);
267
450
 
268
- // ============================================
269
- // COMPONENTS
270
- // ============================================
271
-
272
451
  /**
273
452
  * A table displays data in rows and columns and enables a user to navigate its contents via directional navigation keys,
274
453
  * and optionally supports row selection and sorting.
@@ -276,42 +455,48 @@ export const TableRowContext = createContext<TableRowContextValue | null>(null);
276
455
  export function Table<T extends object>(props: TableProps<T>): JSX.Element {
277
456
  const [local, stateProps, ariaProps] = splitProps(
278
457
  props,
279
- ['class', 'style', 'slot', 'renderEmptyState', 'dragAndDropHooks'],
458
+ ["class", "style", "render", "slot", "renderEmptyState", "dragAndDropHooks", "ref"],
280
459
  [
281
- 'items',
282
- 'columns',
283
- 'getKey',
284
- 'getTextValue',
285
- 'disabledKeys',
286
- 'selectionMode',
287
- 'selectedKeys',
288
- 'defaultSelectedKeys',
289
- 'onSelectionChange',
290
- 'sortDescriptor',
291
- 'onSortChange',
292
- 'showSelectionCheckboxes',
293
- ]
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
+ ],
294
476
  );
295
477
 
296
- // Create ref signal
297
478
  const [ref, setRef] = createSignal<HTMLTableElement | null>(null);
479
+ const normalizedColumns = createMemo(() => normalizeColumnDefinitions(stateProps.columns));
480
+ const rowHeaderColumnKeys = createMemo(() => getRowHeaderColumnKeys(normalizedColumns()));
298
481
 
299
- // Create collection
300
482
  const collection = createMemo(() =>
301
483
  createTableCollection<T>({
302
- columns: stateProps.columns,
484
+ columns: normalizedColumns(),
303
485
  rows: stateProps.items,
304
486
  getKey: stateProps.getKey,
305
- getTextValue: stateProps.getTextValue,
487
+ getTextValue: stateProps.getTextValue as
488
+ | ((item: T, column: ColumnDefinition<T>) => string)
489
+ | undefined,
306
490
  showSelectionCheckboxes: stateProps.showSelectionCheckboxes ?? false,
307
- })
491
+ rowHeaderColumnKeys: rowHeaderColumnKeys().size > 0 ? rowHeaderColumnKeys() : undefined,
492
+ }),
308
493
  );
309
494
 
310
- // Create table state
311
495
  const state = createTableState<T, TableCollection<T>>(() => ({
312
496
  collection: collection(),
313
497
  disabledKeys: stateProps.disabledKeys,
314
498
  selectionMode: stateProps.selectionMode,
499
+ selectionBehavior: stateProps.selectionBehavior,
315
500
  selectedKeys: stateProps.selectedKeys,
316
501
  defaultSelectedKeys: stateProps.defaultSelectedKeys,
317
502
  onSelectionChange: stateProps.onSelectionChange,
@@ -319,27 +504,27 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
319
504
  onSortChange: stateProps.onSortChange,
320
505
  showSelectionCheckboxes: stateProps.showSelectionCheckboxes,
321
506
  }));
507
+ const parentCollectionRenderer = useCollectionRenderer<T>();
322
508
 
323
- // Create table aria props
324
509
  const { gridProps } = createTable<T>(
325
510
  () => ({
326
511
  id: ariaProps.id,
327
- 'aria-label': ariaProps['aria-label'],
328
- 'aria-labelledby': ariaProps['aria-labelledby'],
329
- 'aria-describedby': ariaProps['aria-describedby'],
330
- 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,
331
516
  onRowAction: ariaProps.onRowAction,
332
517
  onCellAction: ariaProps.onCellAction,
518
+ shouldSelectOnPressUp: ariaProps.shouldSelectOnPressUp,
333
519
  focusMode: ariaProps.focusMode,
520
+ escapeKeyBehavior: stateProps.escapeKeyBehavior,
334
521
  }),
335
522
  () => state,
336
- ref
523
+ ref,
337
524
  );
338
525
 
339
- // Create focus ring
340
526
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
341
527
 
342
- // Render props values
343
528
  const renderValues = createMemo<TableRenderProps>(() => ({
344
529
  isFocused: state.isFocused || isFocused(),
345
530
  isFocusVisible: isFocusVisible(),
@@ -353,18 +538,16 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
353
538
  {
354
539
  class: local.class,
355
540
  style: local.style,
356
- defaultClassName: 'solidaria-Table',
541
+ defaultClassName: "solidaria-Table",
357
542
  },
358
- renderValues
543
+ renderValues,
359
544
  );
360
545
 
361
- // Filter DOM props
362
546
  const domProps = createMemo(() => {
363
547
  const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
364
548
  return filtered;
365
549
  });
366
550
 
367
- // Remove ref from spread props
368
551
  const cleanGridProps = () => {
369
552
  const { ref: _ref1, ...rest } = gridProps as Record<string, unknown>;
370
553
  return rest;
@@ -373,19 +556,25 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
373
556
  const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
374
557
  return rest;
375
558
  };
376
- const parentCollectionRenderer = useCollectionRenderer<T>();
377
- const getItemNodes = createMemo(() => Array.from(state.collection).filter((node) => node.type === 'item'));
378
- const getDropTargetByIndex = (index: number, position: 'before' | 'after' | 'on'): DropTarget | null => {
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 => {
379
566
  const node = getItemNodes()[index];
380
567
  if (!node) return null;
381
- return { type: 'item', key: node.key, dropPosition: position };
568
+ return { type: "item", key: node.key, dropPosition: position };
382
569
  };
383
570
  const hasDroppableDnd = createMemo(() => {
384
571
  const hooks = local.dragAndDropHooks;
385
572
  return Boolean(
386
573
  hooks?.useDroppableCollectionState &&
387
574
  hooks.useDroppableCollection &&
388
- (hooks.dropTargetDelegate || parentCollectionRenderer?.dropTargetDelegate || hooks.ListDropTargetDelegate)
575
+ (hooks.dropTargetDelegate ||
576
+ parentCollectionRenderer?.dropTargetDelegate ||
577
+ hooks.ListDropTargetDelegate),
389
578
  );
390
579
  });
391
580
  const hasDraggableDnd = createMemo(() => {
@@ -414,22 +603,23 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
414
603
  const hooks = local.dragAndDropHooks;
415
604
  const activeDropState = dropState();
416
605
  if (!hooks?.useDroppableCollection || !activeDropState) return undefined;
417
- const resolveDirection = (): 'ltr' | 'rtl' => {
606
+ const resolveDirection = (): "ltr" | "rtl" => {
418
607
  const el = ref();
419
- if (el && typeof window !== 'undefined' && typeof window.getComputedStyle === 'function') {
608
+ if (el && typeof window !== "undefined" && typeof window.getComputedStyle === "function") {
420
609
  const dir = window.getComputedStyle(el).direction;
421
- if (dir === 'rtl') return 'rtl';
610
+ if (dir === "rtl") return "rtl";
422
611
  }
423
- return typeof document !== 'undefined' && document.dir === 'rtl' ? 'rtl' : 'ltr';
612
+ return typeof document !== "undefined" && document.dir === "rtl" ? "rtl" : "ltr";
424
613
  };
425
- const dropTargetDelegate = hooks.dropTargetDelegate
426
- ?? parentCollectionRenderer?.dropTargetDelegate
427
- ?? (hooks.ListDropTargetDelegate
614
+ const dropTargetDelegate =
615
+ hooks.dropTargetDelegate ??
616
+ parentCollectionRenderer?.dropTargetDelegate ??
617
+ (hooks.ListDropTargetDelegate
428
618
  ? new hooks.ListDropTargetDelegate(
429
- () => state.collection,
430
- () => ref(),
431
- { layout: 'grid', orientation: 'vertical', direction: resolveDirection() }
432
- )
619
+ () => state.collection,
620
+ () => ref(),
621
+ { layout: "grid", orientation: "vertical", direction: resolveDirection() },
622
+ )
433
623
  : undefined);
434
624
  if (!dropTargetDelegate) return undefined;
435
625
  return hooks.useDroppableCollection(
@@ -441,72 +631,113 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
441
631
  getKeyBelow: (key) => state.collection.getKeyAfter?.(key) ?? null,
442
632
  getKeyAbove: (key) => state.collection.getKeyBefore?.(key) ?? null,
443
633
  getKeyLeftOf: (key) =>
444
- resolveDirection() === 'rtl'
445
- ? state.collection.getKeyAfter?.(key) ?? null
446
- : state.collection.getKeyBefore?.(key) ?? null,
634
+ resolveDirection() === "rtl"
635
+ ? (state.collection.getKeyAfter?.(key) ?? null)
636
+ : (state.collection.getKeyBefore?.(key) ?? null),
447
637
  getKeyRightOf: (key) =>
448
- resolveDirection() === 'rtl'
449
- ? state.collection.getKeyBefore?.(key) ?? null
450
- : state.collection.getKeyAfter?.(key) ?? null,
638
+ resolveDirection() === "rtl"
639
+ ? (state.collection.getKeyBefore?.(key) ?? null)
640
+ : (state.collection.getKeyAfter?.(key) ?? null),
451
641
  getKeyPageBelow: (key) => state.collection.getKeyAfter?.(key) ?? null,
452
642
  getKeyPageAbove: (key) => state.collection.getKeyBefore?.(key) ?? null,
453
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),
454
658
  },
455
659
  activeDropState,
456
- () => ref()
660
+ () => ref(),
457
661
  );
458
662
  });
459
663
  const isRootDropTarget = createMemo(() => {
460
- return Boolean(dropState()?.target?.type === 'root');
664
+ return Boolean(dropState()?.target?.type === "root");
461
665
  });
462
- const dndRenderDropIndicator = createMemo(() => useRenderDropIndicator(local.dragAndDropHooks, dropState()));
463
- const dndDropIndicator = (index: number, position: 'before' | 'after' | 'on') => {
666
+ const dndRenderDropIndicator = createMemo(() =>
667
+ useRenderDropIndicator(local.dragAndDropHooks, dropState()),
668
+ );
669
+ const dndDropIndicator = (index: number, position: "before" | "after" | "on") => {
464
670
  const target = getDropTargetByIndex(index, position);
465
- if (!target || target.type !== 'item') return undefined;
671
+ if (!target || target.type !== "item") return undefined;
466
672
  return dndRenderDropIndicator()?.(target);
467
673
  };
468
674
 
469
- const contextValue = createMemo<TableContextValue<T>>(() => ({
675
+ const contextValue: TableContextValue<T> = {
470
676
  state,
471
- collection: collection(),
472
- items: stateProps.items,
473
- columns: stateProps.columns,
677
+ get collection() {
678
+ return collection();
679
+ },
680
+ get items() {
681
+ return stateProps.items;
682
+ },
683
+ get columns() {
684
+ return normalizedColumns();
685
+ },
474
686
  isDisabled: false,
475
- showSelectionCheckboxes: stateProps.showSelectionCheckboxes ?? false,
476
- dragAndDropHooks: local.dragAndDropHooks,
477
- dragState: dragState(),
478
- dropState: dropState(),
479
- }));
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
+ };
480
703
  const collectionRenderer = createMemo<CollectionRendererContextValue<unknown>>(() => ({
481
704
  ...parentCollectionRenderer,
482
705
  renderItem: (item) => item as JSX.Element,
483
- renderDropIndicator: (index: number, position: 'before' | 'after' | 'on') =>
484
- dndDropIndicator(index, position) ?? parentCollectionRenderer?.renderDropIndicator?.(index, position),
706
+ renderDropIndicator: (index: number, position: "before" | "after" | "on") =>
707
+ dndDropIndicator(index, position) ??
708
+ parentCollectionRenderer?.renderDropIndicator?.(index, position),
485
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>;
486
733
 
487
734
  return (
488
- <TableContext.Provider value={contextValue() as unknown as TableContextValue<object>}>
489
- <TableStateContext.Provider value={state as unknown as TableState<object, TableCollection<object>>}>
735
+ <TableContext.Provider value={contextValue as unknown as TableContextValue<object>}>
736
+ <TableStateContext.Provider
737
+ value={state as unknown as TableState<object, TableCollection<object>>}
738
+ >
490
739
  <CollectionRendererContext.Provider value={collectionRenderer()}>
491
- <table
492
- ref={setRef}
493
- {...mergeProps(
494
- domProps(),
495
- cleanGridProps(),
496
- cleanFocusProps(),
497
- (droppableCollection()?.collectionProps as Record<string, unknown> | undefined) ?? {}
498
- )}
499
- class={renderProps.class()}
500
- style={renderProps.style()}
501
- data-focused={state.isFocused || undefined}
502
- data-focus-visible={isFocusVisible() || undefined}
503
- data-empty={stateProps.items.length === 0 || undefined}
504
- data-drop-target={isRootDropTarget() || undefined}
505
- >
506
- {typeof props.children === 'function'
507
- ? props.children(renderValues())
508
- : props.children}
509
- </table>
740
+ {local.render ? local.render(tableProps(), renderValues()) : <table {...tableProps()} />}
510
741
  </CollectionRendererContext.Provider>
511
742
  </TableStateContext.Provider>
512
743
  </TableContext.Provider>
@@ -517,38 +748,82 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
517
748
  * A header row in a table containing column headers.
518
749
  */
519
750
  export function TableHeader(props: TableHeaderProps): JSX.Element {
520
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'children']);
751
+ const [local, domProps] = splitProps(props, [
752
+ "class",
753
+ "style",
754
+ "render",
755
+ "slot",
756
+ "children",
757
+ "ref",
758
+ ]);
521
759
 
522
- // Get context
523
760
  const context = useContext(TableContext);
524
761
  if (!context) {
525
- throw new Error('TableHeader must be used within a Table');
762
+ throw new Error("TableHeader must be used within a Table");
526
763
  }
527
764
 
528
- const { rowGroupProps } = createTableRowGroup(() => ({ type: 'thead' }));
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
+ });
529
779
 
530
- // Render props values
531
780
  const renderValues = createMemo<TableHeaderRenderProps>(() => ({
532
781
  isFocused: false,
782
+ isHovered: isHovered(),
533
783
  }));
534
784
 
535
- // Resolve render props
536
785
  const renderProps = useRenderProps(
537
786
  {
538
787
  class: local.class,
539
788
  style: local.style,
540
- defaultClassName: 'solidaria-Table-header',
789
+ defaultClassName: "solidaria-Table-header",
541
790
  },
542
- renderValues
791
+ renderValues,
543
792
  );
544
793
 
545
794
  const cleanRowGroupProps = () => {
546
795
  const { ref: _ref, ...rest } = rowGroupProps as Record<string, unknown>;
547
796
  return rest;
548
797
  };
798
+ const cleanHoverProps = () => {
799
+ const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
800
+ return rest;
801
+ };
549
802
 
550
- return (
551
- <thead {...domProps} {...cleanRowGroupProps()} class={renderProps.class()} style={renderProps.style()}>
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
+ >
552
827
  <tr role="row">{local.children}</tr>
553
828
  </thead>
554
829
  );
@@ -558,25 +833,35 @@ export function TableHeader(props: TableHeaderProps): JSX.Element {
558
833
  * A column header in a table.
559
834
  */
560
835
  export function TableColumn(props: TableColumnProps): JSX.Element {
561
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'id', 'allowsSorting', 'children']);
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
+ ]);
562
851
 
563
- // Get context
564
852
  const context = useContext(TableContext);
565
853
  if (!context) {
566
- throw new Error('TableColumn must be used within a Table');
854
+ throw new Error("TableColumn must be used within a Table");
567
855
  }
568
856
  const { state, collection } = context;
569
857
 
570
- // Create ref signal
571
858
  const [ref, setRef] = createSignal<HTMLTableCellElement | null>(null);
572
859
 
573
- // Find the column node
574
860
  const columnNode = createMemo(() => {
575
861
  const node = collection.getItem(local.id);
576
862
  if (!node) {
577
- // Create a simple node for the column
578
863
  return {
579
- type: 'column' as const,
864
+ type: "column" as const,
580
865
  key: local.id,
581
866
  value: null,
582
867
  textValue: String(local.id),
@@ -589,25 +874,32 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
589
874
  return node;
590
875
  });
591
876
 
592
- // Create column header aria props
593
877
  const columnHeaderAria = createTableColumnHeader<object>(
594
878
  () => ({
595
879
  node: columnNode(),
596
880
  allowsSorting: local.allowsSorting,
597
881
  }),
598
882
  () => state as TableState<object, TableCollection<object>>,
599
- ref
883
+ ref,
600
884
  );
601
885
 
602
- // Create hover
603
886
  const { isHovered, hoverProps } = createHover({
604
- isDisabled: false,
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
+ },
605
899
  });
606
900
 
607
- // Create focus ring
608
901
  const { isFocusVisible, focusProps } = createFocusRing();
609
902
 
610
- // Get sort direction
611
903
  const sortDirection = createMemo(() => {
612
904
  const sortDescriptor = state.sortDescriptor;
613
905
  if (sortDescriptor?.column === local.id) {
@@ -616,13 +908,29 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
616
908
  return undefined;
617
909
  });
618
910
 
619
- // Render props values
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
+
620
926
  const renderValues = createMemo<TableColumnRenderProps>(() => ({
621
927
  isFocused: state.focusedKey === local.id,
622
928
  isFocusVisible: isFocusVisible() && state.focusedKey === local.id,
623
929
  isSortable: local.allowsSorting ?? false,
624
930
  sortDirection: sortDirection(),
625
931
  isHovered: isHovered(),
932
+ allowsResizing: local.allowsResizing ?? false,
933
+ isResizing: isResizing(),
626
934
  }));
627
935
 
628
936
  // Resolve render props (children rendered directly in JSX to avoid eager evaluation)
@@ -630,12 +938,11 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
630
938
  {
631
939
  class: local.class,
632
940
  style: local.style,
633
- defaultClassName: 'solidaria-Table-column',
941
+ defaultClassName: "solidaria-Table-column",
634
942
  },
635
- renderValues
943
+ renderValues,
636
944
  );
637
945
 
638
- // Remove ref from spread props
639
946
  const cleanColumnHeaderProps = () => {
640
947
  const { ref: _ref1, ...rest } = columnHeaderAria.columnHeaderProps as Record<string, unknown>;
641
948
  return rest;
@@ -649,24 +956,59 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
649
956
  return rest;
650
957
  };
651
958
 
652
- return (
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
+ ) : (
653
994
  <th
654
- ref={setRef}
995
+ ref={(el) => {
996
+ setRef(el);
997
+ assignRef(local.ref, el);
998
+ }}
655
999
  {...domProps}
656
- {...cleanColumnHeaderProps()}
657
- {...cleanHoverProps()}
658
- {...cleanFocusProps()}
1000
+ {...mergeProps(cleanColumnHeaderProps(), cleanHoverProps(), cleanFocusProps())}
659
1001
  class={renderProps.class()}
660
- style={renderProps.style()}
1002
+ style={columnStyle()}
661
1003
  data-sortable={local.allowsSorting || undefined}
662
1004
  data-sort-direction={sortDirection() || undefined}
1005
+ data-resizable={local.allowsResizing || undefined}
1006
+ data-resizing={isResizing() || undefined}
663
1007
  data-hovered={isHovered() || undefined}
664
1008
  data-focused={state.focusedKey === local.id || undefined}
665
1009
  data-focus-visible={(isFocusVisible() && state.focusedKey === local.id) || undefined}
666
1010
  >
667
- {typeof local.children === 'function'
668
- ? local.children(renderValues())
669
- : local.children}
1011
+ {columnChildren()}
670
1012
  </th>
671
1013
  );
672
1014
  }
@@ -675,32 +1017,40 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
675
1017
  * The body of a table containing data rows.
676
1018
  */
677
1019
  export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Element {
678
- const [local, domProps] = splitProps(props, ['items', 'class', 'style', 'slot', 'renderEmptyState', 'hasMore', 'isLoading', 'onLoadMore', 'children']);
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
+ ]);
679
1033
 
680
- // Get context
681
1034
  const context = useContext(TableContext);
682
1035
  if (!context) {
683
- throw new Error('TableBody must be used within a Table');
1036
+ throw new Error("TableBody must be used within a Table");
684
1037
  }
685
1038
 
686
- const { rowGroupProps } = createTableRowGroup(() => ({ type: 'tbody' }));
1039
+ const { rowGroupProps } = createTableRowGroup(() => ({ type: "tbody" }));
687
1040
 
688
- // Use provided items or context items
689
1041
  const items = createMemo(() => (local.items ?? context.items) as T[]);
690
1042
 
691
- // Render props values
692
1043
  const renderValues = createMemo<TableBodyRenderProps>(() => ({
693
1044
  isEmpty: items().length === 0,
694
1045
  }));
695
1046
 
696
- // Resolve render props
697
1047
  const renderProps = useRenderProps(
698
1048
  {
699
1049
  class: local.class,
700
1050
  style: local.style,
701
- defaultClassName: 'solidaria-Table-body',
1051
+ defaultClassName: "solidaria-Table-body",
702
1052
  },
703
- renderValues
1053
+ renderValues,
704
1054
  );
705
1055
 
706
1056
  const cleanRowGroupProps = () => {
@@ -711,12 +1061,14 @@ export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Eleme
711
1061
  const isEmpty = () => items().length === 0;
712
1062
  const virtualizer = useVirtualizerContext();
713
1063
  const parentCollectionRenderer = useCollectionRenderer<T>();
714
- const rowNodes = createMemo(() => Array.from(context.collection).filter((node) => node.type === 'item'));
1064
+ const rowNodes = createMemo(() =>
1065
+ Array.from(context.collection).filter((node) => node.type === "item"),
1066
+ );
715
1067
  const persistedKeys = useDndPersistedKeys(
716
1068
  { focusedKey: () => context.state.focusedKey },
717
1069
  context.dragAndDropHooks,
718
1070
  context.dropState as { target?: DropTarget | null } | undefined,
719
- context.collection
1071
+ context.collection,
720
1072
  );
721
1073
  const virtualRange = createMemo(() => {
722
1074
  if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return null;
@@ -728,16 +1080,28 @@ export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Eleme
728
1080
  const dropTarget = (context.dropState as { target?: DropTarget | null } | undefined)?.target;
729
1081
  const normalizedDropKey = getNormalizedDropTargetKey(dropTarget, context.collection);
730
1082
  const focusedKey = context.state.focusedKey;
731
- const focusedIndex = focusedKey != null ? rowNodes().findIndex((node) => node.key === focusedKey) : -1;
1083
+ const focusedIndex =
1084
+ focusedKey != null ? rowNodes().findIndex((node) => node.key === focusedKey) : -1;
732
1085
  const forceIncludeIndexes = [
733
- dropTarget?.type === 'item' ? rowNodes().findIndex((node) => node.key === dropTarget.key) : -1,
734
- normalizedDropKey != null ? rowNodes().findIndex((node) => node.key === normalizedDropKey) : -1,
735
- dropTarget?.type === 'item' ? -1 : focusedIndex,
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,
736
1093
  ].filter((index) => index >= 0);
737
- return mergePersistedKeysIntoVirtualRange(baseRange, persistedIndexes, rowCount, virtualizer, 80, {
738
- forceIncludeIndexes,
739
- forceIncludeMaxSpan: 320,
740
- });
1094
+ return mergePersistedKeysIntoVirtualRange(
1095
+ baseRange,
1096
+ persistedIndexes,
1097
+ rowCount,
1098
+ virtualizer,
1099
+ 80,
1100
+ {
1101
+ forceIncludeIndexes,
1102
+ forceIncludeMaxSpan: 320,
1103
+ },
1104
+ );
741
1105
  });
742
1106
  createEffect(() => {
743
1107
  if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return;
@@ -751,7 +1115,7 @@ export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Eleme
751
1115
  if (!node) return target;
752
1116
  return {
753
1117
  ...target,
754
- key: typeof node.key === 'string' || typeof node.key === 'number' ? node.key : undefined,
1118
+ key: typeof node.key === "string" || typeof node.key === "number" ? node.key : undefined,
755
1119
  };
756
1120
  });
757
1121
  onCleanup(() => {
@@ -767,45 +1131,146 @@ export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Eleme
767
1131
  });
768
1132
  const spacerColSpan = () => context.columns.length + (context.showSelectionCheckboxes ? 1 : 0);
769
1133
 
770
- return (
771
- <tbody {...domProps} {...cleanRowGroupProps()} class={renderProps.class()} style={renderProps.style()}>
1134
+ const bodyChildren = () => (
1135
+ <>
772
1136
  <SharedElementTransition>
773
- <Show when={isEmpty() && local.renderEmptyState} fallback={
774
- <>
775
- {virtualRange()?.offsetTop
776
- ? (
777
- <tr role="presentation" aria-hidden="true" data-virtualizer-spacer="top">
778
- <td colSpan={spacerColSpan()} style={{ height: `${virtualRange()!.offsetTop}px`, padding: '0', border: '0' }} />
779
- </tr>
780
- )
781
- : null}
782
- <For each={visibleItems()}>
783
- {(item, index) => {
784
- const itemIndex = () => (virtualRange()?.start ?? 0) + index();
785
- const beforeIndicator = () => parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), 'before');
786
- const onIndicator = () => parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), 'on');
787
- const afterIndicator = () => parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), 'after');
788
- return (
789
- <>
790
- {beforeIndicator()}
791
- {onIndicator()}
792
- {local.children?.(item)}
793
- {afterIndicator()}
794
- </>
795
- );
796
- }}
797
- </For>
798
- {virtualRange()?.offsetBottom
799
- ? (
800
- <tr role="presentation" aria-hidden="true" data-virtualizer-spacer="bottom">
801
- <td colSpan={spacerColSpan()} style={{ height: `${virtualRange()!.offsetBottom}px`, padding: '0', border: '0' }} />
802
- </tr>
803
- )
804
- : null}
805
- </>
806
- }>
807
- {local.renderEmptyState?.()}
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
+ />
808
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>
809
1274
  </SharedElementTransition>
810
1275
  <Show when={local.hasMore && local.onLoadMore}>
811
1276
  <TableLoadMoreItem
@@ -818,13 +1283,62 @@ export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Eleme
818
1283
  );
819
1284
  }
820
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
+ };
1317
+
1318
+ return (
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>
1330
+ </Show>
1331
+ </tfoot>
1332
+ );
1333
+ }
1334
+
821
1335
  export function TableLoadMoreItem(props: TableLoadMoreItemProps): JSX.Element {
822
- let ref: HTMLTableRowElement | undefined;
1336
+ let sentinelRef: HTMLDivElement | undefined;
823
1337
  const [isPending, setIsPending] = createSignal(false);
824
1338
  const isLoading = () => !!props.isLoading || isPending();
825
1339
 
826
1340
  const triggerLoadMore = async () => {
827
- if (isLoading()) return;
1341
+ if (isPending()) return;
828
1342
  setIsPending(true);
829
1343
  try {
830
1344
  await props.onLoadMore();
@@ -834,40 +1348,61 @@ export function TableLoadMoreItem(props: TableLoadMoreItemProps): JSX.Element {
834
1348
  };
835
1349
 
836
1350
  createEffect(() => {
837
- if (!ref || typeof IntersectionObserver !== 'function') return;
838
- const observer = new IntersectionObserver((entries) => {
839
- if (entries[0]?.isIntersecting) {
840
- void triggerLoadMore();
841
- }
842
- });
843
- observer.observe(ref);
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);
844
1363
  return () => observer.disconnect();
845
1364
  });
846
1365
 
847
1366
  const renderProps = useRenderProps(
848
1367
  {
849
- children: props.children ?? (() => (isLoading() ? 'Loading more...' : 'Load more')),
1368
+ children:
1369
+ props.children ??
1370
+ (() => (isLoading() ? <div role="progressbar" aria-label="loading" /> : null)),
850
1371
  class: props.class,
851
1372
  style: props.style,
852
- defaultClassName: 'solidaria-Table-loadMore',
1373
+ defaultClassName: "solidaria-Table-loadMore",
853
1374
  },
854
- () => ({ isLoading: isLoading() })
1375
+ () => ({ isLoading: isLoading() }),
855
1376
  );
856
1377
 
857
1378
  return (
858
- <tr
859
- ref={ref}
860
- role="row"
861
- tabIndex={0}
862
- onFocus={() => {
863
- void triggerLoadMore();
864
- }}
865
- class={renderProps.class()}
866
- style={renderProps.style()}
867
- data-loading={isLoading() || undefined}
868
- >
869
- <td colSpan={props.colSpan ?? 1}>{renderProps.renderChildren()}</td>
870
- </tr>
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
+ </>
871
1406
  );
872
1407
  }
873
1408
 
@@ -875,27 +1410,56 @@ export function TableLoadMoreItem(props: TableLoadMoreItemProps): JSX.Element {
875
1410
  * A row in a table.
876
1411
  */
877
1412
  export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element {
878
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'id', 'item', 'onAction', 'children']);
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
+ ]);
879
1433
 
880
- // Get context
881
1434
  const context = useContext(TableContext);
882
1435
  if (!context) {
883
- throw new Error('TableRow must be used within a Table');
1436
+ throw new Error("TableRow must be used within a Table");
884
1437
  }
885
1438
  const { state, collection } = context;
886
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
+ );
887
1454
 
888
- // Create ref signal
889
1455
  const [ref, setRef] = createSignal<HTMLTableRowElement | null>(null);
890
1456
 
891
- // Find the row node
892
1457
  const rowNode = createMemo(() => {
893
- const node = collection.getItem(local.id);
1458
+ const node = collection.getItem(rowKey());
894
1459
  if (!node) {
895
- // Create a simple node for the row
896
1460
  return {
897
- type: 'item' as const,
898
- key: local.id,
1461
+ type: "item" as const,
1462
+ key: rowKey(),
899
1463
  value: local.item ?? null,
900
1464
  textValue: String(local.id),
901
1465
  level: 0,
@@ -907,52 +1471,84 @@ export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element
907
1471
  return node;
908
1472
  });
909
1473
 
910
- // Create row aria props
911
1474
  const rowAria = createTableRow<object>(
912
1475
  () => ({
913
1476
  node: rowNode(),
1477
+ isVirtualized: tableContext.isVirtualized,
1478
+ isDisabled: local.isDisabled,
914
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
+ },
915
1486
  }),
916
1487
  () => state as TableState<object, TableCollection<object>>,
917
- ref
1488
+ ref,
918
1489
  );
919
1490
  const isSelected = () => rowAria.isSelected;
920
1491
  const isDisabled = () => rowAria.isDisabled;
921
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
+ };
922
1497
 
923
- // Create hover
924
1498
  const { isHovered, hoverProps } = createHover({
925
1499
  get isDisabled() {
926
- 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);
927
1510
  },
928
1511
  });
929
1512
 
930
- // Create focus ring
931
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
+ };
932
1527
 
933
- // Check if focused
934
- const isFocused = createMemo(() => state.focusedKey === local.id);
1528
+ const isFocused = createMemo(() => state.focusedKey === rowKey());
935
1529
  const draggableItem = createMemo(() => {
936
- if (!tableContext.dragAndDropHooks?.useDraggableItem || !tableContext.dragState) return undefined;
1530
+ if (!tableContext.dragAndDropHooks?.useDraggableItem || !tableContext.dragState)
1531
+ return undefined;
937
1532
  return tableContext.dragAndDropHooks.useDraggableItem(
938
1533
  {
939
- key: local.id as string | number,
1534
+ key: rowKey() as string | number,
1535
+ hasDragButton: true,
940
1536
  },
941
- tableContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>['useDraggableItem']>>[1]
1537
+ tableContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>["useDraggableItem"]>>[1],
942
1538
  );
943
1539
  });
944
1540
  const droppableItem = createMemo(() => {
945
- if (!tableContext.dragAndDropHooks?.useDroppableItem || !tableContext.dropState) return undefined;
1541
+ if (!tableContext.dragAndDropHooks?.useDroppableItem || !tableContext.dropState)
1542
+ return undefined;
946
1543
  return tableContext.dragAndDropHooks.useDroppableItem(
947
1544
  {
948
- key: local.id as string | number,
1545
+ key: rowKey() as string | number,
949
1546
  },
950
- tableContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>['useDroppableItem']>>[1],
951
- () => ref()
1547
+ tableContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>["useDroppableItem"]>>[1],
1548
+ () => ref(),
952
1549
  );
953
1550
  });
954
1551
 
955
- // Render props values
956
1552
  const renderValues = createMemo<TableRowRenderProps>(() => ({
957
1553
  isSelected: isSelected(),
958
1554
  isFocused: isFocused(),
@@ -967,12 +1563,11 @@ export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element
967
1563
  {
968
1564
  class: local.class,
969
1565
  style: local.style,
970
- defaultClassName: 'solidaria-Table-row',
1566
+ defaultClassName: "solidaria-Table-row",
971
1567
  },
972
- renderValues
1568
+ renderValues,
973
1569
  );
974
1570
 
975
- // Remove ref from spread props
976
1571
  const cleanRowProps = () => {
977
1572
  const { ref: _ref1, ...rest } = rowAria.rowProps as Record<string, unknown>;
978
1573
  return rest;
@@ -987,37 +1582,101 @@ export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element
987
1582
  };
988
1583
 
989
1584
  const rowContextValue: TableRowContextValue = {
990
- rowKey: local.id,
1585
+ rowKey: rowKey(),
991
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
+ },
992
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>;
993
1676
 
994
1677
  return (
995
1678
  <TableRowContext.Provider value={rowContextValue}>
996
- <tr
997
- ref={setRef}
998
- {...domProps}
999
- {...mergeProps(
1000
- cleanRowProps(),
1001
- cleanHoverProps(),
1002
- cleanFocusProps(),
1003
- (draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
1004
- (droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {}
1005
- )}
1006
- class={renderProps.class()}
1007
- style={renderProps.style()}
1008
- data-selected={isSelected() || undefined}
1009
- data-focused={isFocused() || undefined}
1010
- data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
1011
- data-pressed={isPressed() || undefined}
1012
- data-hovered={isHovered() || undefined}
1013
- data-disabled={isDisabled() || undefined}
1014
- data-dragging={draggableItem()?.isDragging || undefined}
1015
- data-drop-target={droppableItem()?.isDropTarget || undefined}
1016
- >
1017
- {typeof local.children === 'function'
1018
- ? local.children(renderValues())
1019
- : local.children}
1020
- </tr>
1679
+ {local.render ? local.render(tableRowProps(), renderValues()) : <tr {...tableRowProps()} />}
1021
1680
  </TableRowContext.Provider>
1022
1681
  );
1023
1682
  }
@@ -1026,40 +1685,47 @@ export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element
1026
1685
  * A cell in a table row.
1027
1686
  */
1028
1687
  export function TableCell(props: TableCellProps): JSX.Element {
1029
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'id', 'children']);
1688
+ const [local, domProps] = splitProps(props, [
1689
+ "class",
1690
+ "style",
1691
+ "render",
1692
+ "slot",
1693
+ "id",
1694
+ "colSpan",
1695
+ "children",
1696
+ "ref",
1697
+ ]);
1030
1698
 
1031
- // Get context
1032
1699
  const tableContext = useContext(TableContext);
1033
1700
  const rowContext = useContext(TableRowContext);
1034
1701
 
1035
1702
  if (!tableContext) {
1036
- throw new Error('TableCell must be used within a Table');
1703
+ throw new Error("TableCell must be used within a Table");
1037
1704
  }
1038
1705
  if (!rowContext) {
1039
- throw new Error('TableCell must be used within a Table');
1706
+ throw new Error("TableCell must be used within a Table");
1040
1707
  }
1041
1708
 
1042
1709
  const { state, collection } = tableContext;
1043
1710
  const { rowKey, rowNode } = rowContext;
1044
1711
 
1045
- // Create ref signal
1046
1712
  const [ref, setRef] = createSignal<HTMLTableCellElement | null>(null);
1713
+ const cellId = createUniqueId();
1714
+ const columnKey = createMemo(() => rowContext.getCellColumnKey(cellId, local.id));
1047
1715
 
1048
- // Find the cell node
1049
1716
  const cellNode = createMemo(() => {
1050
- // If id is provided, look for that specific cell
1051
- if (local.id != null) {
1052
- const cellKey = `${rowKey}-${local.id}`;
1717
+ const key = columnKey();
1718
+ if (key != null) {
1719
+ const cellKey = `${rowKey}-${key}`;
1053
1720
  const node = collection.getItem(cellKey);
1054
1721
  if (node) return node;
1055
1722
  }
1056
1723
 
1057
- // Otherwise create a simple node
1058
1724
  return {
1059
- type: 'cell' as const,
1060
- key: local.id ?? `${rowKey}-cell`,
1725
+ type: "cell" as const,
1726
+ key: key ?? `${rowKey}-cell`,
1061
1727
  value: rowNode.value,
1062
- textValue: '',
1728
+ textValue: "",
1063
1729
  level: 1,
1064
1730
  index: 0,
1065
1731
  parentKey: rowKey,
@@ -1067,32 +1733,34 @@ export function TableCell(props: TableCellProps): JSX.Element {
1067
1733
  childNodes: [],
1068
1734
  } as GridNode<unknown>;
1069
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
+ });
1070
1742
 
1071
- // Create cell aria props
1072
1743
  const cellAria = createTableCell<object>(
1073
1744
  () => ({
1074
1745
  node: cellNode(),
1075
1746
  }),
1076
1747
  () => state as TableState<object, TableCollection<object>>,
1077
- ref
1748
+ ref,
1078
1749
  );
1079
1750
  const isPressed = () => cellAria.isPressed;
1080
1751
 
1081
- // Create hover
1082
1752
  const { isHovered, hoverProps } = createHover({
1083
1753
  isDisabled: false,
1084
1754
  });
1085
1755
 
1086
- // Create focus ring
1087
1756
  const { isFocusVisible, focusProps } = createFocusRing();
1088
1757
 
1089
- // Check if focused
1090
1758
  const isFocused = createMemo(() => state.focusedKey === cellNode().key);
1091
1759
 
1092
- // Render props values
1093
1760
  const renderValues = createMemo<TableCellRenderProps>(() => ({
1094
1761
  isFocused: isFocused(),
1095
1762
  isFocusVisible: isFocusVisible() && isFocused(),
1763
+ columnIndex: cellColumnIndex() ?? 0,
1096
1764
  isPressed: isPressed(),
1097
1765
  isHovered: isHovered(),
1098
1766
  }));
@@ -1102,12 +1770,11 @@ export function TableCell(props: TableCellProps): JSX.Element {
1102
1770
  {
1103
1771
  class: local.class,
1104
1772
  style: local.style,
1105
- defaultClassName: 'solidaria-Table-cell',
1773
+ defaultClassName: "solidaria-Table-cell",
1106
1774
  },
1107
- renderValues
1775
+ renderValues,
1108
1776
  );
1109
1777
 
1110
- // Remove ref from spread props
1111
1778
  const cleanCellProps = () => {
1112
1779
  const { ref: _ref1, ...rest } = cellAria.gridCellProps as Record<string, unknown>;
1113
1780
  return rest;
@@ -1121,65 +1788,117 @@ export function TableCell(props: TableCellProps): JSX.Element {
1121
1788
  return rest;
1122
1789
  };
1123
1790
 
1124
- return (
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
+ ) : (
1125
1815
  <td
1126
- ref={setRef}
1816
+ ref={(el) => {
1817
+ setRef(el);
1818
+ assignRef(local.ref, el);
1819
+ }}
1127
1820
  {...domProps}
1128
- {...cleanCellProps()}
1129
- {...cleanHoverProps()}
1130
- {...cleanFocusProps()}
1821
+ {...mergeProps(cleanCellProps(), cleanHoverProps(), cleanFocusProps())}
1822
+ colSpan={local.colSpan}
1131
1823
  class={renderProps.class()}
1132
1824
  style={renderProps.style()}
1133
1825
  data-focused={isFocused() || undefined}
1134
1826
  data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
1827
+ data-column-index={cellColumnIndex()}
1135
1828
  data-pressed={isPressed() || undefined}
1136
1829
  data-hovered={isHovered() || undefined}
1137
1830
  >
1138
- {typeof local.children === 'function'
1139
- ? local.children(renderValues())
1140
- : local.children}
1831
+ {cellChildren()}
1141
1832
  </td>
1142
1833
  );
1143
1834
  }
1144
1835
 
1836
+ export interface TableSelectionCheckboxProps {
1837
+ rowKey: Key;
1838
+ class?: string;
1839
+ style?: JSX.CSSProperties;
1840
+ excludeFromTabOrder?: boolean;
1841
+ "aria-label"?: string;
1842
+ }
1843
+
1145
1844
  /**
1146
1845
  * A checkbox cell for row selection.
1147
1846
  */
1148
- export function TableSelectionCheckbox(props: { rowKey: Key }): JSX.Element {
1847
+ export function TableSelectionCheckbox(props: TableSelectionCheckboxProps): JSX.Element {
1149
1848
  const context = useContext(TableContext);
1150
1849
  if (!context) {
1151
- throw new Error('TableSelectionCheckbox must be used within a Table');
1850
+ throw new Error("TableSelectionCheckbox must be used within a Table");
1152
1851
  }
1153
1852
 
1154
1853
  const { state } = context;
1155
1854
 
1156
1855
  const selectionCheckboxAria = createTableSelectionCheckbox<object>(
1157
1856
  () => ({ key: props.rowKey }),
1158
- () => state as TableState<object, TableCollection<object>>
1857
+ () => state as TableState<object, TableCollection<object>>,
1159
1858
  );
1160
1859
 
1161
- return <input {...selectionCheckboxAria.checkboxProps} />;
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
+ />
1868
+ );
1869
+ }
1870
+
1871
+ export interface TableSelectAllCheckboxProps {
1872
+ class?: string;
1873
+ style?: JSX.CSSProperties;
1874
+ "aria-label"?: string;
1162
1875
  }
1163
1876
 
1164
1877
  /**
1165
1878
  * A checkbox for select-all functionality.
1166
1879
  */
1167
- export function TableSelectAllCheckbox(): JSX.Element {
1880
+ export function TableSelectAllCheckbox(props: TableSelectAllCheckboxProps = {}): JSX.Element {
1168
1881
  const context = useContext(TableContext);
1169
1882
  if (!context) {
1170
- throw new Error('TableSelectAllCheckbox must be used within a Table');
1883
+ throw new Error("TableSelectAllCheckbox must be used within a Table");
1171
1884
  }
1172
1885
 
1173
1886
  const { state } = context;
1174
1887
 
1175
1888
  const selectAllCheckboxAria = createTableSelectAllCheckbox<object>(
1176
- () => state as TableState<object, TableCollection<object>>
1889
+ () => state as TableState<object, TableCollection<object>>,
1177
1890
  );
1178
1891
 
1179
- return <input {...selectAllCheckboxAria.checkboxProps} />;
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
+ );
1180
1900
  }
1181
1901
 
1182
- // Attach components as static properties
1183
1902
  Table.Header = TableHeader;
1184
1903
  Table.Column = TableColumn;
1185
1904
  Table.Body = TableBody;
@@ -1189,29 +1908,276 @@ Table.Cell = TableCell;
1189
1908
  Table.SelectionCheckbox = TableSelectionCheckbox;
1190
1909
  Table.SelectAllCheckbox = TableSelectAllCheckbox;
1191
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
+
1192
1922
  export interface ColumnResizerProps extends SlotProps {
1193
- class?: string;
1194
- style?: JSX.CSSProperties;
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>;
1195
1941
  }
1196
1942
 
1197
1943
  export function ColumnResizer(props: ColumnResizerProps): JSX.Element {
1198
- return <div role="separator" class={props.class ?? 'solidaria-Table-columnResizer'} style={props.style} />;
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
+ );
1199
2054
  }
1200
2055
 
1201
2056
  export interface ResizableTableContainerProps extends SlotProps {
2057
+ /** Children (should contain a Table). */
1202
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. */
1203
2062
  class?: string;
2063
+ /** Inline style. */
1204
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;
1205
2071
  }
1206
2072
 
1207
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
+
1208
2156
  return (
1209
- <div class={props.class ?? 'solidaria-ResizableTableContainer'} style={props.style}>
1210
- {props.children}
1211
- </div>
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>
1212
2169
  );
1213
2170
  }
1214
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
+
1215
2181
  export function useTableOptions() {
1216
2182
  return useContext(TableContext);
1217
2183
  }