@human-kit/svelte-components 1.0.0-alpha.4 → 1.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/checkbox/README.md +53 -0
- package/dist/checkbox/TODO.md +16 -0
- package/dist/checkbox/index.d.ts +6 -0
- package/dist/checkbox/index.js +6 -0
- package/dist/checkbox/index.parts.d.ts +2 -0
- package/dist/checkbox/index.parts.js +2 -0
- package/dist/checkbox/indicator/README.md +23 -0
- package/dist/checkbox/indicator/checkbox-indicator.svelte +43 -0
- package/dist/checkbox/indicator/checkbox-indicator.svelte.d.ts +10 -0
- package/dist/checkbox/root/README.md +47 -0
- package/dist/checkbox/root/checkbox-label-test.svelte +10 -0
- package/dist/checkbox/root/checkbox-label-test.svelte.d.ts +18 -0
- package/dist/checkbox/root/checkbox-root.svelte +361 -0
- package/dist/checkbox/root/checkbox-root.svelte.d.ts +23 -0
- package/dist/checkbox/root/checkbox-test.svelte +59 -0
- package/dist/checkbox/root/checkbox-test.svelte.d.ts +18 -0
- package/dist/checkbox/root/context.d.ts +21 -0
- package/dist/checkbox/root/context.js +15 -0
- package/dist/combobox/list/combobox-listbox.svelte.d.ts +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/table/IMPLEMENTATION_NOTES.md +8 -0
- package/dist/table/PLAN-HIDDEN-COLUMNS.md +152 -0
- package/dist/table/PLAN.md +924 -0
- package/dist/table/README.md +116 -0
- package/dist/table/SELECTION_CHECKBOX_PLAN.md +234 -0
- package/dist/table/TODO.md +100 -0
- package/dist/table/body/README.md +24 -0
- package/dist/table/body/table-body.svelte +25 -0
- package/dist/table/body/table-body.svelte.d.ts +9 -0
- package/dist/table/cell/README.md +25 -0
- package/dist/table/cell/table-cell.svelte +247 -0
- package/dist/table/cell/table-cell.svelte.d.ts +9 -0
- package/dist/table/checkbox/README.md +38 -0
- package/dist/table/checkbox/table-checkbox-test.svelte +121 -0
- package/dist/table/checkbox/table-checkbox-test.svelte.d.ts +16 -0
- package/dist/table/checkbox/table-checkbox.svelte +274 -0
- package/dist/table/checkbox/table-checkbox.svelte.d.ts +13 -0
- package/dist/table/checkbox-indicator/README.md +29 -0
- package/dist/table/checkbox-indicator/table-checkbox-indicator.svelte +22 -0
- package/dist/table/checkbox-indicator/table-checkbox-indicator.svelte.d.ts +10 -0
- package/dist/table/column/README.md +32 -0
- package/dist/table/column/table-column.svelte +108 -0
- package/dist/table/column/table-column.svelte.d.ts +18 -0
- package/dist/table/column-header-cell/README.md +28 -0
- package/dist/table/column-header-cell/table-column-header-cell.svelte +281 -0
- package/dist/table/column-header-cell/table-column-header-cell.svelte.d.ts +9 -0
- package/dist/table/column-resizer/README.md +32 -0
- package/dist/table/column-resizer/table-column-resizer-freeze-layout-test.svelte +51 -0
- package/dist/table/column-resizer/table-column-resizer-freeze-layout-test.svelte.d.ts +3 -0
- package/dist/table/column-resizer/table-column-resizer-selection-column-test.svelte +83 -0
- package/dist/table/column-resizer/table-column-resizer-selection-column-test.svelte.d.ts +3 -0
- package/dist/table/column-resizer/table-column-resizer-test.svelte +75 -0
- package/dist/table/column-resizer/table-column-resizer-test.svelte.d.ts +3 -0
- package/dist/table/column-resizer/table-column-resizer.svelte +616 -0
- package/dist/table/column-resizer/table-column-resizer.svelte.d.ts +11 -0
- package/dist/table/empty-state/README.md +25 -0
- package/dist/table/empty-state/table-empty-state.svelte +38 -0
- package/dist/table/empty-state/table-empty-state.svelte.d.ts +8 -0
- package/dist/table/footer/README.md +24 -0
- package/dist/table/footer/table-footer.svelte +19 -0
- package/dist/table/footer/table-footer.svelte.d.ts +9 -0
- package/dist/table/header/README.md +24 -0
- package/dist/table/header/table-header.svelte +19 -0
- package/dist/table/header/table-header.svelte.d.ts +9 -0
- package/dist/table/index.d.ts +16 -0
- package/dist/table/index.js +16 -0
- package/dist/table/index.parts.d.ts +12 -0
- package/dist/table/index.parts.js +12 -0
- package/dist/table/root/README.md +56 -0
- package/dist/table/root/context.d.ts +198 -0
- package/dist/table/root/context.js +1426 -0
- package/dist/table/root/table-reorder-test.svelte +64 -0
- package/dist/table/root/table-reorder-test.svelte.d.ts +3 -0
- package/dist/table/root/table-root.svelte +410 -0
- package/dist/table/root/table-root.svelte.d.ts +29 -0
- package/dist/table/root/table-test.svelte +165 -0
- package/dist/table/root/table-test.svelte.d.ts +25 -0
- package/dist/table/row/README.md +27 -0
- package/dist/table/row/table-row.svelte +321 -0
- package/dist/table/row/table-row.svelte.d.ts +13 -0
- package/package.json +11 -1
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
<!-- markdownlint-disable MD007 MD010 MD060 -->
|
|
2
|
+
|
|
3
|
+
# Table Plan
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Design and implement a new public `Table` component for the Svelte library, using the React Aria Components usability model as the main reference, but adapting it to the repository conventions: part-based composition, centralized state in `Root`, typed context, Svelte 5 runes, colocated tests for each part, and component/part documentation.
|
|
8
|
+
|
|
9
|
+
## Product Goals
|
|
10
|
+
|
|
11
|
+
- Provide a composable and readable API for headless tables.
|
|
12
|
+
- Prioritize robust and predictable keyboard navigation.
|
|
13
|
+
- Support row selection and sorting in the first version.
|
|
14
|
+
- Keep the initial scope reasonable so the component is not blocked by advanced features.
|
|
15
|
+
- Establish an internal foundation that can be extended later.
|
|
16
|
+
|
|
17
|
+
## Repository References
|
|
18
|
+
|
|
19
|
+
### Patterns to Follow
|
|
20
|
+
|
|
21
|
+
- Namespace-style exports and named exports, as in `ListBox`.
|
|
22
|
+
- Explicit typed context in `root/context.ts`.
|
|
23
|
+
- Controlled/uncontrolled state pattern in `Root`.
|
|
24
|
+
- Colocated interaction and accessibility tests.
|
|
25
|
+
- Base component README plus a README for each public part.
|
|
26
|
+
|
|
27
|
+
### Relevant Components and Utilities
|
|
28
|
+
|
|
29
|
+
- `packages/svelte/src/lib/listbox`
|
|
30
|
+
- `packages/svelte/src/lib/calendar`
|
|
31
|
+
- `packages/svelte/src/lib/primitives/keyboard-navigation.ts`
|
|
32
|
+
- `packages/svelte/src/lib/FOCUS_STATE_CONTRACT.md`
|
|
33
|
+
- `packages/svelte/src/lib/test-utils/focus-contract.ts`
|
|
34
|
+
|
|
35
|
+
## Decisions Already Made
|
|
36
|
+
|
|
37
|
+
- The v1 public API will be static/composable, not dynamic.
|
|
38
|
+
- `Table` must support cell focus and row-derived focus state.
|
|
39
|
+
- `Table.Column` is added to the anatomy to solve column metadata.
|
|
40
|
+
- `Table.Column` is a logical component with no DOM output; it registers column metadata in context. `Table.ColumnHeaderCell` renders the `<th>`.
|
|
41
|
+
- `Table.EmptyState` is a dedicated part for the body empty state.
|
|
42
|
+
- V1 must include:
|
|
43
|
+
- base anatomy
|
|
44
|
+
- `Table.Footer`
|
|
45
|
+
- `Table.EmptyState`
|
|
46
|
+
- keyboard grid navigation
|
|
47
|
+
- row selection
|
|
48
|
+
- sorting
|
|
49
|
+
|
|
50
|
+
## V1 Scope
|
|
51
|
+
|
|
52
|
+
### Proposed Public Anatomy
|
|
53
|
+
|
|
54
|
+
```svelte
|
|
55
|
+
<Table.Root aria-label="Users">
|
|
56
|
+
<Table.Header>
|
|
57
|
+
<Table.Row>
|
|
58
|
+
<!-- Column es lógico (sin DOM), solo registra metadata -->
|
|
59
|
+
<!-- Column is logical (no DOM), it only registers metadata -->
|
|
60
|
+
<Table.Column id="email" isRowHeader>
|
|
61
|
+
<Table.ColumnHeaderCell>Email</Table.ColumnHeaderCell>
|
|
62
|
+
</Table.Column>
|
|
63
|
+
<Table.Column id="group" allowsSorting>
|
|
64
|
+
<Table.ColumnHeaderCell>Group</Table.ColumnHeaderCell>
|
|
65
|
+
</Table.Column>
|
|
66
|
+
</Table.Row>
|
|
67
|
+
</Table.Header>
|
|
68
|
+
|
|
69
|
+
<Table.Body>
|
|
70
|
+
<Table.Row id="danilo">
|
|
71
|
+
<Table.Cell>danilo@example.com</Table.Cell>
|
|
72
|
+
<Table.Cell>Developer</Table.Cell>
|
|
73
|
+
</Table.Row>
|
|
74
|
+
<Table.Row id="zahra">
|
|
75
|
+
<Table.Cell>zahra@example.com</Table.Cell>
|
|
76
|
+
<Table.Cell>Admin</Table.Cell>
|
|
77
|
+
</Table.Row>
|
|
78
|
+
|
|
79
|
+
<!-- Only shown when Body has no rows -->
|
|
80
|
+
<Table.EmptyState>No users found.</Table.EmptyState>
|
|
81
|
+
</Table.Body>
|
|
82
|
+
|
|
83
|
+
<Table.Footer>
|
|
84
|
+
<Table.Row>
|
|
85
|
+
<Table.Cell>Total</Table.Cell>
|
|
86
|
+
<Table.Cell>2 users</Table.Cell>
|
|
87
|
+
</Table.Row>
|
|
88
|
+
</Table.Footer>
|
|
89
|
+
</Table.Root>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Target Public Parts
|
|
93
|
+
|
|
94
|
+
- `Table.Root`
|
|
95
|
+
- `Table.Column` — logical, no DOM
|
|
96
|
+
- `Table.Header`
|
|
97
|
+
- `Table.Body`
|
|
98
|
+
- `Table.EmptyState`
|
|
99
|
+
- `Table.Footer`
|
|
100
|
+
- `Table.Row`
|
|
101
|
+
- `Table.ColumnHeaderCell`
|
|
102
|
+
- `Table.Cell`
|
|
103
|
+
|
|
104
|
+
## Semantics and Accessibility
|
|
105
|
+
|
|
106
|
+
### Semantic Model
|
|
107
|
+
|
|
108
|
+
- The table should behave as an interactive table, not only as a passive `<table>`.
|
|
109
|
+
- The primary reference is a `grid` model with directional navigation.
|
|
110
|
+
- `Table.Root` must require an accessible name through `aria-label` or `aria-labelledby`.
|
|
111
|
+
- There must be support for marking a column as `isRowHeader` to improve screen reader announcements.
|
|
112
|
+
|
|
113
|
+
### Recommended Semantic Rendering
|
|
114
|
+
|
|
115
|
+
- `Table.Root` should render `<table role="grid">`.
|
|
116
|
+
- `Table.Header` should render `<thead role="rowgroup">`.
|
|
117
|
+
- `Table.Body` should render `<tbody role="rowgroup">`.
|
|
118
|
+
- `Table.Footer` should render `<tfoot role="rowgroup">`.
|
|
119
|
+
- `Table.Row` should render `<tr role="row">`.
|
|
120
|
+
- `Table.ColumnHeaderCell` should render `<th role="columnheader">`.
|
|
121
|
+
- `Table.Cell` should render:
|
|
122
|
+
- `<th scope="row" role="rowheader">` when the associated column has `isRowHeader`
|
|
123
|
+
- `<td role="gridcell">` in all other cases
|
|
124
|
+
|
|
125
|
+
This combination preserves real HTML semantics while allowing interactive grid behavior without inventing a structure entirely based on `div`s.
|
|
126
|
+
|
|
127
|
+
### Focus Model
|
|
128
|
+
|
|
129
|
+
Two related levels will be implemented:
|
|
130
|
+
|
|
131
|
+
1. **DOM focus on cell/header cell**
|
|
132
|
+
- `Table.ColumnHeaderCell` and `Table.Cell` are the real navigation targets.
|
|
133
|
+
- This keeps the behavior close to React Aria Components.
|
|
134
|
+
|
|
135
|
+
2. **Row-derived state**
|
|
136
|
+
- `Table.Row` exposes derived states such as focused row, selected row, or disabled row.
|
|
137
|
+
- This helps with styling and simplifies the visual experience.
|
|
138
|
+
|
|
139
|
+
### Target Keyboard Support in V1
|
|
140
|
+
|
|
141
|
+
- `Tab` enters and leaves the grid.
|
|
142
|
+
- Arrow keys navigate between header cells and body cells.
|
|
143
|
+
- `Home` / `End` move to the start/end of the current row.
|
|
144
|
+
- `Ctrl/Cmd + Home` and `Ctrl/Cmd + End` may be evaluated as an enhancement if the 2D engine can support them without extra complexity.
|
|
145
|
+
- `Enter` / `Space` should trigger row selection when appropriate.
|
|
146
|
+
- Sortable headers should respond to keyboard input to change sort order.
|
|
147
|
+
|
|
148
|
+
### Recommended Tabbability Strategy
|
|
149
|
+
|
|
150
|
+
- `Table.Root` should not be tabbable under normal conditions.
|
|
151
|
+
- Only one `Table.ColumnHeaderCell` or `Table.Cell` should have `tabindex="0"` at a time.
|
|
152
|
+
- All other navigable cells should have `tabindex="-1"`.
|
|
153
|
+
- When entering with `Tab` from outside:
|
|
154
|
+
- if there is a previously focused cell, focus is restored there
|
|
155
|
+
- otherwise focus enters the first navigable header cell
|
|
156
|
+
- if no header is navigable, focus enters the first navigable body cell
|
|
157
|
+
- `Shift+Tab` and `Tab` should allow the browser to leave the grid naturally from the active cell.
|
|
158
|
+
|
|
159
|
+
This preserves the roving tabindex pattern and avoids making `Root` compete with cells as a focus target.
|
|
160
|
+
|
|
161
|
+
### Decision for `Footer`
|
|
162
|
+
|
|
163
|
+
- In v1, `Table.Footer` is mainly semantic/structural.
|
|
164
|
+
- It will not participate in the main grid focus flow unless a clear need appears during implementation.
|
|
165
|
+
|
|
166
|
+
## Key Types
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
/** Sort direction */
|
|
170
|
+
type SortDirection = 'ascending' | 'descending';
|
|
171
|
+
|
|
172
|
+
/** Active sort descriptor */
|
|
173
|
+
type SortDescriptor = {
|
|
174
|
+
column: string;
|
|
175
|
+
direction: SortDirection;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** Row selection mode */
|
|
179
|
+
type SelectionMode = 'none' | 'single' | 'multiple';
|
|
180
|
+
|
|
181
|
+
/** Selectable key (Row ids) */
|
|
182
|
+
type SelectionKey = string | number;
|
|
183
|
+
|
|
184
|
+
/** Set of selected keys (Row ids) */
|
|
185
|
+
type SelectionSet = Set<SelectionKey>;
|
|
186
|
+
|
|
187
|
+
/** Coordinate within the 2D grid (global, header row 0 = row 0) */
|
|
188
|
+
type GridCoord = {
|
|
189
|
+
row: number;
|
|
190
|
+
col: number;
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Proposed V1 API
|
|
195
|
+
|
|
196
|
+
### `Table.Root`
|
|
197
|
+
|
|
198
|
+
Responsibilities:
|
|
199
|
+
|
|
200
|
+
- register columns, rows, and cells
|
|
201
|
+
- maintain the 2D focus cursor
|
|
202
|
+
- expose controlled/uncontrolled selection state
|
|
203
|
+
- expose controlled/uncontrolled sorting state
|
|
204
|
+
- coordinate ARIA attributes and focus/selection data attributes
|
|
205
|
+
|
|
206
|
+
Tentative API:
|
|
207
|
+
|
|
208
|
+
- `aria-label` / `aria-labelledby` — required accessible name
|
|
209
|
+
- `selectionMode?: SelectionMode` — default `'none'`
|
|
210
|
+
- `selectedKeys?: SelectionSet`
|
|
211
|
+
- `defaultSelectedKeys?: SelectionSet`
|
|
212
|
+
- `onSelectionChange?: (keys: SelectionSet) => void`
|
|
213
|
+
- `sortDescriptor?: SortDescriptor`
|
|
214
|
+
- `defaultSortDescriptor?: SortDescriptor`
|
|
215
|
+
- `onSortChange?: (descriptor: SortDescriptor) => void`
|
|
216
|
+
- `disabledKeys?: Set<SelectionKey>` — disabled row ids
|
|
217
|
+
- `children`
|
|
218
|
+
|
|
219
|
+
### `Table.Column`
|
|
220
|
+
|
|
221
|
+
> Logical component — does not render its own DOM element. Registers column metadata in `Root` context and wraps `ColumnHeaderCell`.
|
|
222
|
+
|
|
223
|
+
Responsibilities:
|
|
224
|
+
|
|
225
|
+
- define stable column identity
|
|
226
|
+
- register column metadata in context (sorting, row header)
|
|
227
|
+
- register column resize metadata in context when enabled
|
|
228
|
+
- serve as the anchor for sorting and row header semantics
|
|
229
|
+
- serve as the anchor for width/resizing behavior
|
|
230
|
+
|
|
231
|
+
Tentative API:
|
|
232
|
+
|
|
233
|
+
- `id: string` — stable column identity
|
|
234
|
+
- `allowsSorting?: boolean`
|
|
235
|
+
- `allowsResizing?: boolean`
|
|
236
|
+
- `isRowHeader?: boolean`
|
|
237
|
+
- `textValue?: string`
|
|
238
|
+
- `width?: number | string`
|
|
239
|
+
- `defaultWidth?: number | string`
|
|
240
|
+
- `minWidth?: number`
|
|
241
|
+
- `maxWidth?: number`
|
|
242
|
+
|
|
243
|
+
### `Table.Header`
|
|
244
|
+
|
|
245
|
+
Responsibilities:
|
|
246
|
+
|
|
247
|
+
- contain header rows
|
|
248
|
+
- coordinate header semantics
|
|
249
|
+
|
|
250
|
+
### `Table.Body`
|
|
251
|
+
|
|
252
|
+
Responsibilities:
|
|
253
|
+
|
|
254
|
+
- contain data rows
|
|
255
|
+
- coordinate `Table.EmptyState` visibility when there are no rows
|
|
256
|
+
|
|
257
|
+
Tentative API:
|
|
258
|
+
|
|
259
|
+
- `children`
|
|
260
|
+
|
|
261
|
+
> The empty state is handled through the dedicated `Table.EmptyState` part inside `Body`, not through a prop.
|
|
262
|
+
|
|
263
|
+
### `Table.EmptyState`
|
|
264
|
+
|
|
265
|
+
Responsibilities:
|
|
266
|
+
|
|
267
|
+
- render empty-state content when `Body` has no rows
|
|
268
|
+
- hide itself automatically when rows exist
|
|
269
|
+
- not participate in the grid navigation model
|
|
270
|
+
|
|
271
|
+
Tentative API:
|
|
272
|
+
|
|
273
|
+
- `children`
|
|
274
|
+
|
|
275
|
+
Recommended semantics:
|
|
276
|
+
|
|
277
|
+
- `Table.EmptyState` should be a convenience part that internally renders a row and an empty-state cell.
|
|
278
|
+
- Its output should be equivalent to:
|
|
279
|
+
|
|
280
|
+
```svelte
|
|
281
|
+
<tr role="row" data-empty>
|
|
282
|
+
<td role="gridcell" colspan={columnCount}>
|
|
283
|
+
{children}
|
|
284
|
+
</td>
|
|
285
|
+
</tr>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
- `colspan` should be resolved automatically from the number of registered columns.
|
|
289
|
+
- It should not be focusable or participate in 2D navigation.
|
|
290
|
+
- It should only be allowed inside `Table.Body`.
|
|
291
|
+
|
|
292
|
+
This avoids invalid markup inside `<tbody>` and keeps the API convenient for consumers.
|
|
293
|
+
|
|
294
|
+
### `Table.Footer`
|
|
295
|
+
|
|
296
|
+
Responsibilities:
|
|
297
|
+
|
|
298
|
+
- contain summary/metadata rows
|
|
299
|
+
- not interfere with the main v1 navigation model
|
|
300
|
+
|
|
301
|
+
### `Table.Row`
|
|
302
|
+
|
|
303
|
+
Responsibilities:
|
|
304
|
+
|
|
305
|
+
- row identity
|
|
306
|
+
- disabled state
|
|
307
|
+
- derived selection state
|
|
308
|
+
- derived focus/selection styling
|
|
309
|
+
|
|
310
|
+
Tentative API:
|
|
311
|
+
|
|
312
|
+
- `id`
|
|
313
|
+
- `isDisabled?: boolean`
|
|
314
|
+
- `textValue?: string`
|
|
315
|
+
|
|
316
|
+
### `Table.ColumnHeaderCell`
|
|
317
|
+
|
|
318
|
+
Responsibilities:
|
|
319
|
+
|
|
320
|
+
- focus target in the header
|
|
321
|
+
- sorting trigger when the column allows it
|
|
322
|
+
- apply `aria-sort` automatically from `Root.sortDescriptor` and `Column.allowsSorting`
|
|
323
|
+
- `aria-sort` values: `ascending` | `descending` | `none`
|
|
324
|
+
- host the resize affordance when the consumer composes a `Table.ColumnResizer` inside it
|
|
325
|
+
|
|
326
|
+
### `Table.ColumnResizer`
|
|
327
|
+
|
|
328
|
+
Responsibilities:
|
|
329
|
+
|
|
330
|
+
- interactive resize handle for the current `Table.Column`
|
|
331
|
+
- consume column identity from `Table.Column` context rather than matching by visual position
|
|
332
|
+
- support pointer drag and keyboard resizing
|
|
333
|
+
- expose resize state for styling through data attributes
|
|
334
|
+
|
|
335
|
+
Tentative API:
|
|
336
|
+
|
|
337
|
+
- `step?: number` — keyboard delta in px, default `16`
|
|
338
|
+
- `shiftStep?: number` — larger keyboard delta in px, default `48`
|
|
339
|
+
- `children?`
|
|
340
|
+
- `class?`
|
|
341
|
+
|
|
342
|
+
### `Table.Cell`
|
|
343
|
+
|
|
344
|
+
Responsibilities:
|
|
345
|
+
|
|
346
|
+
- focus target within the body
|
|
347
|
+
- reflect derived row selection state
|
|
348
|
+
- support simple textual content in v1
|
|
349
|
+
|
|
350
|
+
## Data Attributes by Part
|
|
351
|
+
|
|
352
|
+
| Part | Attribute | Values | Description |
|
|
353
|
+
| ------------------ | --------------------- | ---------------------------- | ---------------------------------------- |
|
|
354
|
+
| `Root` | `data-selection-mode` | `none`, `single`, `multiple` | Active selection mode |
|
|
355
|
+
| `Header` | `data-table-header` | `''` | Semantic marker |
|
|
356
|
+
| `Body` | `data-table-body` | `''` | Semantic marker |
|
|
357
|
+
| `Body` | `data-empty` | `''` | Present when Body has no rows |
|
|
358
|
+
| `Footer` | `data-table-footer` | `''` | Semantic marker |
|
|
359
|
+
| `Row` (body) | `data-focused` | `''` | The row contains the focused cell |
|
|
360
|
+
| `Row` (body) | `data-selected` | `''` | Selected row |
|
|
361
|
+
| `Row` (body) | `data-disabled` | `''` | Disabled row |
|
|
362
|
+
| `ColumnHeaderCell` | `data-focused` | `''` | Focused header cell |
|
|
363
|
+
| `ColumnHeaderCell` | `data-sortable` | `''` | The column allows sorting |
|
|
364
|
+
| `ColumnHeaderCell` | `data-sort-direction` | `ascending`, `descending` | Active sort direction |
|
|
365
|
+
| `Cell` | `data-focused` | `''` | Focused cell |
|
|
366
|
+
| `Cell` | `data-row-selected` | `''` | The row containing this cell is selected |
|
|
367
|
+
|
|
368
|
+
## ARIA Attributes by Part
|
|
369
|
+
|
|
370
|
+
| Part | Attribute | Value | Description |
|
|
371
|
+
| ------------------ | -------------------------------- | --------------------------------- | ------------------------------------------------ |
|
|
372
|
+
| `Root` | `role` | `grid` | Interactive table |
|
|
373
|
+
| `Root` | `aria-label` / `aria-labelledby` | string | Accessible name (required) |
|
|
374
|
+
| `Root` | `aria-multiselectable` | `true` | Present when `selectionMode='multiple'` |
|
|
375
|
+
| `Header` | `role` | `rowgroup` | Header row group |
|
|
376
|
+
| `Body` | `role` | `rowgroup` | Data row group |
|
|
377
|
+
| `Footer` | `role` | `rowgroup` | Footer row group |
|
|
378
|
+
| `Row` | `role` | `row` | Row |
|
|
379
|
+
| `Row` (body) | `aria-selected` | `true` / `false` | Selection state (when `selectionMode != 'none'`) |
|
|
380
|
+
| `Row` (body) | `aria-disabled` | `true` | Disabled row |
|
|
381
|
+
| `ColumnHeaderCell` | `role` | `columnheader` | Column header |
|
|
382
|
+
| `ColumnHeaderCell` | `aria-sort` | `ascending`, `descending`, `none` | Sort direction (only when `allowsSorting`) |
|
|
383
|
+
| `Cell` | `role` | `gridcell` or `rowheader` | `rowheader` if the column has `isRowHeader` |
|
|
384
|
+
| `EmptyState` | `role` | `row` + internal `gridcell` | Semantic, non-navigable empty-state row |
|
|
385
|
+
|
|
386
|
+
## What V1 Does Not Include
|
|
387
|
+
|
|
388
|
+
### Out of Scope
|
|
389
|
+
|
|
390
|
+
- drag and drop
|
|
391
|
+
- async loading / load more
|
|
392
|
+
- API pública dinámica con `items` y `columns`
|
|
393
|
+
- row actions / row links
|
|
394
|
+
- typeahead
|
|
395
|
+
- focus management para elementos interactivos dentro de `Cell`
|
|
396
|
+
- focus management for interactive elements inside `Cell`
|
|
397
|
+
- nested headers / grouped columns
|
|
398
|
+
- cell selection
|
|
399
|
+
- cell selection
|
|
400
|
+
- virtualización
|
|
401
|
+
- virtualization
|
|
402
|
+
- soporte complejo de `colSpan` / `rowSpan`
|
|
403
|
+
- complex `colSpan` / `rowSpan` support
|
|
404
|
+
- footer navegable como parte del grid principal
|
|
405
|
+
- footer navigation as part of the main grid
|
|
406
|
+
|
|
407
|
+
## Advanced Feature Matrix
|
|
408
|
+
|
|
409
|
+
| Feature | Main Complexity | Risk | Recommendation |
|
|
410
|
+
| ---------------------------------- | ---------------------------------------------------------- | ----------- | ------------------ |
|
|
411
|
+
| Column resizing | width state, handles, pointer + keyboard, persistence | high | next planned phase |
|
|
412
|
+
| Drag and drop | reorder, drop targets, SR + keyboard + pointer | very high | keep out of v1 |
|
|
413
|
+
| Async loading / load more | scroll state, sentinel rows, partial states | high | keep out of v1 |
|
|
414
|
+
| Dynamic `items` / `columns` API | collection, stable ids, render functions, memoization | high | defer |
|
|
415
|
+
| Row actions / row links | conflicts between actions, selection, and HTML limitations | medium/high | defer |
|
|
416
|
+
| Interactive content inside `Cell` | focus handoff between grid and nested controls | very high | keep out of v1 |
|
|
417
|
+
| Typeahead | depends on stable collection and consistent `textValue` | medium | defer |
|
|
418
|
+
| Nested headers / column groups | spans, navigation, and complex semantics | high | keep out of v1 |
|
|
419
|
+
| Cell selection | changes the entire interaction model | high | keep out of v1 |
|
|
420
|
+
| Full `selectionBehavior="replace"` | modifiers and fine-grained focus/selection semantics | medium/high | defer |
|
|
421
|
+
| Virtualization | strong decoupling between collection and DOM | very high | keep out of v1 |
|
|
422
|
+
| Integrated select-all | useful UX but depends on mature selection behavior | medium | phase 2 |
|
|
423
|
+
| Complex `colSpan` / `rowSpan` | breaks the rectangular grid model | high | defer |
|
|
424
|
+
| Navigable footer | adds another region to the focus model | medium | avoid in v1 |
|
|
425
|
+
|
|
426
|
+
## Proposed Internal Architecture
|
|
427
|
+
|
|
428
|
+
## Root State
|
|
429
|
+
|
|
430
|
+
Create `packages/svelte/src/lib/table/root/context.ts` with responsibilities for:
|
|
431
|
+
|
|
432
|
+
- column registration
|
|
433
|
+
- row registration
|
|
434
|
+
- cell registration by coordinate
|
|
435
|
+
- current focus resolution
|
|
436
|
+
- 2D navigation
|
|
437
|
+
- row selection state
|
|
438
|
+
- sorting state
|
|
439
|
+
- derived utilities for each part
|
|
440
|
+
|
|
441
|
+
## 2D Navigation Engine
|
|
442
|
+
|
|
443
|
+
It is not enough to depend only on `createKeyboardNavigation()` because it currently solves linear navigation. `Table` needs a dedicated engine able to:
|
|
444
|
+
|
|
445
|
+
- know rows and columns by index
|
|
446
|
+
- move focus in two dimensions
|
|
447
|
+
- distinguish header and body
|
|
448
|
+
- resolve missing or disabled cells
|
|
449
|
+
- derive the active row from the active cell
|
|
450
|
+
|
|
451
|
+
### Proposed Internal Interface
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
interface GridNavigation {
|
|
455
|
+
/** Currently focused coordinate (global, header row 0 = row 0) */
|
|
456
|
+
focusedCoord: GridCoord;
|
|
457
|
+
|
|
458
|
+
/** Moves focus in the given direction, skipping non-navigable cells */
|
|
459
|
+
move(direction: 'up' | 'down' | 'left' | 'right'): void;
|
|
460
|
+
|
|
461
|
+
/** Moves focus to the first cell in the current row */
|
|
462
|
+
moveToRowStart(): void;
|
|
463
|
+
|
|
464
|
+
/** Moves focus to the last cell in the current row */
|
|
465
|
+
moveToRowEnd(): void;
|
|
466
|
+
|
|
467
|
+
/** Moves focus to the first cell in the grid (header[0][0]) */
|
|
468
|
+
moveToGridStart(): void;
|
|
469
|
+
|
|
470
|
+
/** Moves focus to the last cell in the body */
|
|
471
|
+
moveToGridEnd(): void;
|
|
472
|
+
|
|
473
|
+
/** Registers a navigable cell in the grid */
|
|
474
|
+
register(coord: GridCoord, element: HTMLElement): void;
|
|
475
|
+
|
|
476
|
+
/** Unregisters a cell from the grid */
|
|
477
|
+
unregister(coord: GridCoord): void;
|
|
478
|
+
|
|
479
|
+
/** Checks whether a coordinate is navigable (exists and is not disabled) */
|
|
480
|
+
isNavigable(coord: GridCoord): boolean;
|
|
481
|
+
|
|
482
|
+
/** Returns the body row index from a global coordinate */
|
|
483
|
+
toBodyRowIndex(globalRow: number): number | null;
|
|
484
|
+
|
|
485
|
+
/** Applies DOM focus to the element at the given coordinate */
|
|
486
|
+
focusCell(coord: GridCoord): void;
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Key-to-Action Mapping
|
|
491
|
+
|
|
492
|
+
| Key | Action |
|
|
493
|
+
| ------------------------ | -------------------------------------------------------- |
|
|
494
|
+
| `ArrowUp` | `move('up')` |
|
|
495
|
+
| `ArrowDown` | `move('down')` |
|
|
496
|
+
| `ArrowLeft` | `move('left')` |
|
|
497
|
+
| `ArrowRight` | `move('right')` |
|
|
498
|
+
| `Home` | `moveToRowStart()` |
|
|
499
|
+
| `End` | `moveToRowEnd()` |
|
|
500
|
+
| `Ctrl+Home` / `Cmd+Home` | `moveToGridStart()` |
|
|
501
|
+
| `Ctrl+End` / `Cmd+End` | `moveToGridEnd()` |
|
|
502
|
+
| `Tab` | Leaves the grid (focus to the next tabbable element) |
|
|
503
|
+
| `Shift+Tab` | Leaves the grid (focus to the previous tabbable element) |
|
|
504
|
+
| `Enter` / `Space` | Select row (body) or toggle sort (sortable header) |
|
|
505
|
+
|
|
506
|
+
## Selection
|
|
507
|
+
|
|
508
|
+
- V1 selection is row-based.
|
|
509
|
+
- Focus must not be equivalent to selection.
|
|
510
|
+
- `single` and `multiple` must work in both controlled and uncontrolled mode.
|
|
511
|
+
- Disabled rows must not be selectable.
|
|
512
|
+
|
|
513
|
+
## Sorting
|
|
514
|
+
|
|
515
|
+
- Sort state should live in `Root`.
|
|
516
|
+
- `Column` defines whether a column allows sorting.
|
|
517
|
+
- `ColumnHeaderCell` triggers sort changes.
|
|
518
|
+
- The component reflects state, but does not necessarily mutate data automatically; that remains the consumer's responsibility.
|
|
519
|
+
|
|
520
|
+
## Planned File Structure
|
|
521
|
+
|
|
522
|
+
### New Component Files
|
|
523
|
+
|
|
524
|
+
- `packages/svelte/src/lib/table/index.ts`
|
|
525
|
+
- `packages/svelte/src/lib/table/index.parts.ts`
|
|
526
|
+
- `packages/svelte/src/lib/table/README.md`
|
|
527
|
+
- `packages/svelte/src/lib/table/TODO.md`
|
|
528
|
+
|
|
529
|
+
### Root
|
|
530
|
+
|
|
531
|
+
- `packages/svelte/src/lib/table/root/table-root.svelte`
|
|
532
|
+
- `packages/svelte/src/lib/table/root/context.ts`
|
|
533
|
+
- `packages/svelte/src/lib/table/root/table-root.test.ts`
|
|
534
|
+
- `packages/svelte/src/lib/table/root/table-test.svelte`
|
|
535
|
+
- `packages/svelte/src/lib/table/root/README.md`
|
|
536
|
+
|
|
537
|
+
### Public Parts
|
|
538
|
+
|
|
539
|
+
- `packages/svelte/src/lib/table/column/table-column.svelte`
|
|
540
|
+
- `packages/svelte/src/lib/table/column/README.md`
|
|
541
|
+
- `packages/svelte/src/lib/table/column/table-column.test.ts`
|
|
542
|
+
|
|
543
|
+
## Phase 2: Column Resizing Plan
|
|
544
|
+
|
|
545
|
+
### Resize Goal
|
|
546
|
+
|
|
547
|
+
Add column resizing in a way that follows the React Aria Components mental model while preserving the repository's existing `Table` architecture: logical `Table.Column`, state in `Table.Root`, typed context, native table semantics, and composable parts.
|
|
548
|
+
|
|
549
|
+
### Functional Contract
|
|
550
|
+
|
|
551
|
+
- Resizing must target the column whose composition includes the resize handle.
|
|
552
|
+
- The resize handle must live inside the header composition for that column, not in a parallel list of handlers.
|
|
553
|
+
- The active column is resolved from `Table.Column` context, never by visual index guessing alone.
|
|
554
|
+
- Functional behavior should mirror RAC:
|
|
555
|
+
- a column opts into resizing via column metadata
|
|
556
|
+
- a dedicated resizer part provides the interactive affordance
|
|
557
|
+
- widths can be controlled or uncontrolled
|
|
558
|
+
- pointer and keyboard resizing are both supported
|
|
559
|
+
|
|
560
|
+
### Recommended Public Composition
|
|
561
|
+
|
|
562
|
+
```svelte
|
|
563
|
+
<Table.Root aria-label="Users" bind:columnWidths>
|
|
564
|
+
<Table.Header>
|
|
565
|
+
<Table.Row>
|
|
566
|
+
<Table.Column
|
|
567
|
+
id="email"
|
|
568
|
+
isRowHeader
|
|
569
|
+
allowsSorting
|
|
570
|
+
allowsResizing
|
|
571
|
+
defaultWidth={280}
|
|
572
|
+
minWidth={180}
|
|
573
|
+
>
|
|
574
|
+
<Table.ColumnHeaderCell>
|
|
575
|
+
<span>Email</span>
|
|
576
|
+
<Table.ColumnResizer />
|
|
577
|
+
</Table.ColumnHeaderCell>
|
|
578
|
+
</Table.Column>
|
|
579
|
+
|
|
580
|
+
<Table.Column id="group" allowsSorting allowsResizing defaultWidth={180} minWidth={140}>
|
|
581
|
+
<Table.ColumnHeaderCell>
|
|
582
|
+
<span>Group</span>
|
|
583
|
+
<Table.ColumnResizer />
|
|
584
|
+
</Table.ColumnHeaderCell>
|
|
585
|
+
</Table.Column>
|
|
586
|
+
</Table.Row>
|
|
587
|
+
</Table.Header>
|
|
588
|
+
|
|
589
|
+
<Table.Body>
|
|
590
|
+
<!-- rows -->
|
|
591
|
+
</Table.Body>
|
|
592
|
+
</Table.Root>
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### API Recommendation
|
|
596
|
+
|
|
597
|
+
#### Resize Props on `Table.Column`
|
|
598
|
+
|
|
599
|
+
Add the following props:
|
|
600
|
+
|
|
601
|
+
- `allowsResizing?: boolean`
|
|
602
|
+
- `width?: number | string`
|
|
603
|
+
- `defaultWidth?: number | string`
|
|
604
|
+
- `minWidth?: number`
|
|
605
|
+
- `maxWidth?: number`
|
|
606
|
+
|
|
607
|
+
Notes:
|
|
608
|
+
|
|
609
|
+
- `width` is the controlled width for the column.
|
|
610
|
+
- `defaultWidth` is the uncontrolled initial width.
|
|
611
|
+
- `allowsResizing` is required for resize behavior, even if a `Table.ColumnResizer` is rendered.
|
|
612
|
+
- `Table.ColumnResizer` without `allowsResizing` should be ignored in production and warn in dev.
|
|
613
|
+
|
|
614
|
+
#### Width State on `Table.Root`
|
|
615
|
+
|
|
616
|
+
Add root-level width state APIs:
|
|
617
|
+
|
|
618
|
+
- `columnWidths?: Map<string, number>`
|
|
619
|
+
- `defaultColumnWidths?: Map<string, number>`
|
|
620
|
+
- `onColumnWidthsChange?: (widths: Map<string, number>) => void`
|
|
621
|
+
- `onColumnResizeStart?: (columnId: string) => void`
|
|
622
|
+
- `onColumnResizeEnd?: (widths: Map<string, number>) => void`
|
|
623
|
+
|
|
624
|
+
Notes:
|
|
625
|
+
|
|
626
|
+
- Controlled/uncontrolled width state should mirror the existing `selectedKeys` and `sortDescriptor` contracts.
|
|
627
|
+
- Widths in root state should be normalized to px numbers even if consumer input allows string forms.
|
|
628
|
+
|
|
629
|
+
#### `Table.ColumnResizer` Part
|
|
630
|
+
|
|
631
|
+
Public part to place inside `Table.ColumnHeaderCell`.
|
|
632
|
+
|
|
633
|
+
Tentative props:
|
|
634
|
+
|
|
635
|
+
- `step?: number`
|
|
636
|
+
- `shiftStep?: number`
|
|
637
|
+
- `class?: string`
|
|
638
|
+
- `children?: Snippet`
|
|
639
|
+
|
|
640
|
+
No `columnId` prop should be needed; it must use `Table.Column` context.
|
|
641
|
+
|
|
642
|
+
### Why Not a Parallel `ColumnHandler`
|
|
643
|
+
|
|
644
|
+
This API should explicitly avoid a separate sibling structure like:
|
|
645
|
+
|
|
646
|
+
```svelte
|
|
647
|
+
<Table.Column id="email" />
|
|
648
|
+
<Table.Column id="group" />
|
|
649
|
+
<Table.ColumnHandler index={0} />
|
|
650
|
+
<Table.ColumnHandler index={1} />
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Reasons:
|
|
654
|
+
|
|
655
|
+
- position-based matching becomes fragile with dynamic columns
|
|
656
|
+
- it duplicates the concept of column identity
|
|
657
|
+
- it becomes harder to keep sorting, row-header semantics, and resizing anchored to the same column contract
|
|
658
|
+
- it does not follow the RAC model, where resizing is column-owned and the handle is colocated with the header content
|
|
659
|
+
|
|
660
|
+
### Width Model
|
|
661
|
+
|
|
662
|
+
#### Effective Width Resolution
|
|
663
|
+
|
|
664
|
+
For each registered column, compute the effective width from highest to lowest precedence:
|
|
665
|
+
|
|
666
|
+
1. `Table.Root.columnWidths.get(columnId)`
|
|
667
|
+
2. `Table.Column.width`
|
|
668
|
+
3. `Table.Root.defaultColumnWidths.get(columnId)`
|
|
669
|
+
4. `Table.Column.defaultWidth`
|
|
670
|
+
5. no explicit width
|
|
671
|
+
|
|
672
|
+
Then clamp the result against:
|
|
673
|
+
|
|
674
|
+
- `minWidth`
|
|
675
|
+
- `maxWidth`
|
|
676
|
+
|
|
677
|
+
#### Initial Implementation Constraint
|
|
678
|
+
|
|
679
|
+
For the first resizing implementation, normalize widths to px values.
|
|
680
|
+
|
|
681
|
+
- Accept `number` as px.
|
|
682
|
+
- Optionally accept `"123px"` and normalize it to `123`.
|
|
683
|
+
- Defer `%`, `fr`, and more advanced layout math until the base feature is stable.
|
|
684
|
+
|
|
685
|
+
This keeps the state model simple and reduces layout bugs.
|
|
686
|
+
|
|
687
|
+
### Rendering Strategy
|
|
688
|
+
|
|
689
|
+
The recommended implementation is to generate a `<colgroup>` inside `Table.Root` from the registered columns and the effective widths.
|
|
690
|
+
|
|
691
|
+
Reasons:
|
|
692
|
+
|
|
693
|
+
- widths apply consistently to both header and body cells
|
|
694
|
+
- native table layout remains intact
|
|
695
|
+
- it avoids pushing per-cell width styles into every `Table.Cell`
|
|
696
|
+
- it scales better as the feature grows
|
|
697
|
+
|
|
698
|
+
Planned approach:
|
|
699
|
+
|
|
700
|
+
- `Table.Root` renders a managed `<colgroup>` before children
|
|
701
|
+
- each registered column maps to one `<col>`
|
|
702
|
+
- effective width is applied to the `<col>`
|
|
703
|
+
- body and header cells keep their semantic markup unchanged
|
|
704
|
+
|
|
705
|
+
Fallback if `<colgroup>` proves insufficient for some cases:
|
|
706
|
+
|
|
707
|
+
- apply inline width/min-width styles to header cells and derived styles to body cells by column index
|
|
708
|
+
|
|
709
|
+
But `<colgroup>` should be the default strategy.
|
|
710
|
+
|
|
711
|
+
### Interaction Model
|
|
712
|
+
|
|
713
|
+
#### Pointer
|
|
714
|
+
|
|
715
|
+
- pointer down on `Table.ColumnResizer` starts resizing for its current column
|
|
716
|
+
- movement computes a new width in px relative to the starting header width
|
|
717
|
+
- width updates continuously during drag
|
|
718
|
+
- pointer up finalizes the interaction and calls `onColumnResizeEnd`
|
|
719
|
+
|
|
720
|
+
#### Keyboard
|
|
721
|
+
|
|
722
|
+
- the resizer is focusable
|
|
723
|
+
- `ArrowLeft` reduces width by `step`
|
|
724
|
+
- `ArrowRight` increases width by `step`
|
|
725
|
+
- `Shift+ArrowLeft` / `Shift+ArrowRight` use `shiftStep`
|
|
726
|
+
- resize keyboard handling should not hijack the existing table cell navigation when the resizer itself is not focused
|
|
727
|
+
|
|
728
|
+
### Accessibility Contract
|
|
729
|
+
|
|
730
|
+
`Table.ColumnResizer` should behave like a column separator/resizer control.
|
|
731
|
+
|
|
732
|
+
Recommended attributes:
|
|
733
|
+
|
|
734
|
+
- `role="separator"`
|
|
735
|
+
- `aria-orientation="vertical"`
|
|
736
|
+
- `aria-valuenow`
|
|
737
|
+
- `aria-valuemin`
|
|
738
|
+
- `aria-valuemax`
|
|
739
|
+
- accessible label derived from the current column, for example `Resize Email column`
|
|
740
|
+
|
|
741
|
+
Derived data attributes:
|
|
742
|
+
|
|
743
|
+
- `data-resizing`
|
|
744
|
+
- `data-focused`
|
|
745
|
+
- `data-focus-visible`
|
|
746
|
+
- `data-resizable-direction="right"`
|
|
747
|
+
|
|
748
|
+
### Internal Architecture Additions
|
|
749
|
+
|
|
750
|
+
#### `root/context.ts`
|
|
751
|
+
|
|
752
|
+
Extend column registration to include:
|
|
753
|
+
|
|
754
|
+
- `allowsResizing`
|
|
755
|
+
- `width`
|
|
756
|
+
- `defaultWidth`
|
|
757
|
+
- `minWidth`
|
|
758
|
+
- `maxWidth`
|
|
759
|
+
|
|
760
|
+
Add root-level APIs for:
|
|
761
|
+
|
|
762
|
+
- resolving effective column widths
|
|
763
|
+
- updating a column width by `columnId`
|
|
764
|
+
- starting/ending resize interactions
|
|
765
|
+
- reading resize state for a column
|
|
766
|
+
|
|
767
|
+
#### New Part Context Usage
|
|
768
|
+
|
|
769
|
+
`Table.ColumnResizer` should consume:
|
|
770
|
+
|
|
771
|
+
- `Table.Column` context for `columnId`
|
|
772
|
+
- `Table.Root` context for width state and resize actions
|
|
773
|
+
|
|
774
|
+
It should not require positional props like `index` or `for`.
|
|
775
|
+
|
|
776
|
+
### Planned File Additions
|
|
777
|
+
|
|
778
|
+
- `packages/svelte/src/lib/table/column-resizer/table-column-resizer.svelte`
|
|
779
|
+
- `packages/svelte/src/lib/table/column-resizer/README.md`
|
|
780
|
+
- `packages/svelte/src/lib/table/column-resizer/table-column-resizer.test.ts`
|
|
781
|
+
|
|
782
|
+
Planned touched files:
|
|
783
|
+
|
|
784
|
+
- `packages/svelte/src/lib/table/index.parts.ts`
|
|
785
|
+
- `packages/svelte/src/lib/table/index.ts`
|
|
786
|
+
- `packages/svelte/src/lib/table/root/context.ts`
|
|
787
|
+
- `packages/svelte/src/lib/table/root/table-root.svelte`
|
|
788
|
+
- `packages/svelte/src/lib/table/column/table-column.svelte`
|
|
789
|
+
- `packages/svelte/src/lib/table/column-header-cell/table-column-header-cell.svelte`
|
|
790
|
+
- `packages/svelte/src/lib/table/root/table-root.test.ts`
|
|
791
|
+
- `docs/src/routes/docs/table/+page.svelte`
|
|
792
|
+
|
|
793
|
+
### Testing Plan
|
|
794
|
+
|
|
795
|
+
Minimum regression coverage:
|
|
796
|
+
|
|
797
|
+
- renders a resize handle only for columns composed with `Table.ColumnResizer`
|
|
798
|
+
- dragging the resizer changes the associated column width only
|
|
799
|
+
- resizing one column does not corrupt neighboring column identity
|
|
800
|
+
- controlled `columnWidths` updates are reflected in the DOM
|
|
801
|
+
- uncontrolled `defaultWidth` is honored on mount
|
|
802
|
+
- keyboard resizing updates width in deterministic steps
|
|
803
|
+
- min/max constraints are enforced
|
|
804
|
+
- focus and pointer interactions on the resizer do not break table navigation
|
|
805
|
+
|
|
806
|
+
### Recommended Implementation Order
|
|
807
|
+
|
|
808
|
+
1. Add API fields to `Table.Column` and root context registration.
|
|
809
|
+
2. Add root width state and effective width resolution.
|
|
810
|
+
3. Render managed `<colgroup>` from `Table.Root`.
|
|
811
|
+
4. Add `Table.ColumnResizer` pointer interaction.
|
|
812
|
+
5. Add keyboard and ARIA support for the resizer.
|
|
813
|
+
6. Add docs/demo and controlled/uncontrolled tests.
|
|
814
|
+
|
|
815
|
+
### Non-Goals for the First Resize Release
|
|
816
|
+
|
|
817
|
+
- percentage/fr width math
|
|
818
|
+
- column resize persistence outside consumer-provided state
|
|
819
|
+
- multi-column proportional redistribution
|
|
820
|
+
- double-click auto-fit
|
|
821
|
+
- resize in nested/grouped headers
|
|
822
|
+
- resizable footer-specific behavior
|
|
823
|
+
- `packages/svelte/src/lib/table/header/table-header.svelte`
|
|
824
|
+
- `packages/svelte/src/lib/table/header/README.md`
|
|
825
|
+
- `packages/svelte/src/lib/table/header/table-header.test.ts`
|
|
826
|
+
- `packages/svelte/src/lib/table/body/table-body.svelte`
|
|
827
|
+
- `packages/svelte/src/lib/table/body/README.md`
|
|
828
|
+
- `packages/svelte/src/lib/table/body/table-body.test.ts`
|
|
829
|
+
- `packages/svelte/src/lib/table/empty-state/table-empty-state.svelte`
|
|
830
|
+
- `packages/svelte/src/lib/table/empty-state/README.md`
|
|
831
|
+
- `packages/svelte/src/lib/table/empty-state/table-empty-state.test.ts`
|
|
832
|
+
- `packages/svelte/src/lib/table/footer/table-footer.svelte`
|
|
833
|
+
- `packages/svelte/src/lib/table/footer/README.md`
|
|
834
|
+
- `packages/svelte/src/lib/table/footer/table-footer.test.ts`
|
|
835
|
+
- `packages/svelte/src/lib/table/row/table-row.svelte`
|
|
836
|
+
- `packages/svelte/src/lib/table/row/README.md`
|
|
837
|
+
- `packages/svelte/src/lib/table/row/table-row.test.ts`
|
|
838
|
+
- `packages/svelte/src/lib/table/column-header-cell/table-column-header-cell.svelte`
|
|
839
|
+
- `packages/svelte/src/lib/table/column-header-cell/README.md`
|
|
840
|
+
- `packages/svelte/src/lib/table/column-header-cell/table-column-header-cell.test.ts`
|
|
841
|
+
- `packages/svelte/src/lib/table/cell/table-cell.svelte`
|
|
842
|
+
- `packages/svelte/src/lib/table/cell/README.md`
|
|
843
|
+
- `packages/svelte/src/lib/table/cell/table-cell.test.ts`
|
|
844
|
+
|
|
845
|
+
### Package Integrations
|
|
846
|
+
|
|
847
|
+
- `packages/svelte/src/lib/index.ts`
|
|
848
|
+
- `packages/svelte/package.json`
|
|
849
|
+
- `docs/src/routes/docs/table/+page.svelte`
|
|
850
|
+
- `README.md`
|
|
851
|
+
- `.changeset/*`
|
|
852
|
+
|
|
853
|
+
## Implementation Strategy
|
|
854
|
+
|
|
855
|
+
### Phase 1: Public Contract
|
|
856
|
+
|
|
857
|
+
- finalize the public anatomy
|
|
858
|
+
- finalize prop naming
|
|
859
|
+
- define exact responsibilities for each part
|
|
860
|
+
- document minimal examples
|
|
861
|
+
|
|
862
|
+
### Phase 2: State and Navigation
|
|
863
|
+
|
|
864
|
+
- implement root context
|
|
865
|
+
- implement column/row/cell registration
|
|
866
|
+
- build 2D navigation
|
|
867
|
+
- add data attributes and focus contract
|
|
868
|
+
|
|
869
|
+
### Phase 3: Selection and Sorting
|
|
870
|
+
|
|
871
|
+
- add controlled/uncontrolled selection
|
|
872
|
+
- add disabled rows
|
|
873
|
+
- add per-column sorting
|
|
874
|
+
- validate keyboard behavior
|
|
875
|
+
|
|
876
|
+
### Phase 4: Docs and Tests
|
|
877
|
+
|
|
878
|
+
- tests for each public part
|
|
879
|
+
- integral harness
|
|
880
|
+
- base README and per-part READMEs
|
|
881
|
+
- docs demo page
|
|
882
|
+
- changeset
|
|
883
|
+
|
|
884
|
+
## Resize Testing Plan
|
|
885
|
+
|
|
886
|
+
### Minimum Cases
|
|
887
|
+
|
|
888
|
+
- correct semantic rendering
|
|
889
|
+
- required accessible label
|
|
890
|
+
- arrow-key navigation between cells
|
|
891
|
+
- row-level `Home` / `End`
|
|
892
|
+
- entering/leaving the grid with `Tab`
|
|
893
|
+
- correct focus-visible and focus attributes
|
|
894
|
+
- single selection
|
|
895
|
+
- multiple selection
|
|
896
|
+
- disabled rows
|
|
897
|
+
- sorting via keyboard and pointer
|
|
898
|
+
- empty state
|
|
899
|
+
- `Footer` renders without breaking main navigation
|
|
900
|
+
|
|
901
|
+
### Test Inspirations
|
|
902
|
+
|
|
903
|
+
- `ListBox` tests
|
|
904
|
+
- `Calendar` tests
|
|
905
|
+
- focus contract tests
|
|
906
|
+
|
|
907
|
+
## Main Risks
|
|
908
|
+
|
|
909
|
+
- without a clear 2D engine, the component may end up stuck between a passive table and an interactive grid
|
|
910
|
+
- mixing cell focus with row selection requires very explicit rules
|
|
911
|
+
- if `Footer` joins the focus flow too early, the model becomes unnecessarily complex
|
|
912
|
+
- allowing interactive content inside cells in v1 may break the keyboard experience
|
|
913
|
+
|
|
914
|
+
## V1 Success Criteria
|
|
915
|
+
|
|
916
|
+
- the public API is clear and consistent with the rest of the library
|
|
917
|
+
- keyboard navigation feels close to React Aria Components in the core cases
|
|
918
|
+
- sorting and selection work without ambiguity
|
|
919
|
+
- tests cover critical behavior
|
|
920
|
+
- the implementation leaves real room for future phases without breaking the API
|
|
921
|
+
|
|
922
|
+
## Recommended Next Step
|
|
923
|
+
|
|
924
|
+
Turn this plan into a more concrete API specification, part by part and prop by prop, before creating implementation files.
|