@human-kit/svelte-components 1.0.0-alpha.13 → 1.0.0-alpha.14
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/root/checkbox-root.svelte +22 -2
- package/dist/checkbox/root/checkbox-root.svelte.d.ts +4 -1
- package/dist/combobox/root/combobox.svelte +1 -0
- package/dist/listbox/item/listbox-item.svelte +13 -0
- package/dist/listbox/root/listbox.svelte +7 -1
- package/dist/table/IMPLEMENTATION_NOTES.md +2 -1
- package/dist/table/PLAN.md +440 -17
- package/dist/table/TODO.md +39 -1
- package/dist/table/cell/table-cell.svelte +86 -79
- package/dist/table/checkbox/table-checkbox-test.svelte +7 -0
- package/dist/table/checkbox/table-checkbox-test.svelte.d.ts +3 -1
- package/dist/table/checkbox/table-checkbox.svelte +55 -30
- package/dist/table/index.d.ts +1 -1
- package/dist/table/root/context.d.ts +16 -1
- package/dist/table/root/context.js +199 -24
- package/dist/table/root/table-root.svelte +30 -0
- package/dist/table/root/table-root.svelte.d.ts +4 -1
- package/dist/table/root/table-test.svelte +29 -0
- package/dist/table/root/table-test.svelte.d.ts +5 -1
- package/dist/table/row/table-row.svelte +44 -67
- package/dist/table/utils/handle-body-keydown.d.ts +13 -0
- package/dist/table/utils/handle-body-keydown.js +67 -0
- package/package.json +1 -1
package/dist/table/TODO.md
CHANGED
|
@@ -61,6 +61,9 @@ Ship a stable `Table` v1 with keyboard navigation, row selection, sorting, docum
|
|
|
61
61
|
- [x] [S][P2][Area: Behavior][Owner: Unassigned][Target: Done] Confirm whether disabled body rows should remain keyboard-focusable or be skipped by navigation.
|
|
62
62
|
- [ ] [S][P2][Area: API][Owner: Unassigned][Target: TBD] Decide whether `Table.Column` should hard-enforce a single `Table.ColumnHeaderCell` child.
|
|
63
63
|
- [ ] [S][P2][Area: API][Owner: Unassigned][Target: TBD] Decide whether clipboard-related behavior should remain fully browser-native in v1 or be deferred behind a future explicit cell-selection model.
|
|
64
|
+
- [x] [C][P1][Area: API][Owner: Unassigned][Target: Done] Add `onRowAction?: (id: TableSelectionKey) => void` to `Table.Root` and document its collection-level semantics across `selectionMode` and `selectionBehavior`.
|
|
65
|
+
- [x] [C][P1][Area: API][Owner: Unassigned][Target: Done] Add `disabledBehavior?: 'selection' | 'all'` to `Table.Root` with default `'all'`, and document that `'selection'` disables selection while preserving focus and row actions.
|
|
66
|
+
- [x] [S][P1][Area: API][Owner: Unassigned][Target: Done] Treat stable `selectionBehavior?: 'toggle' | 'replace'` support in `Table.Root` as an explicit prerequisite for the row-actions phase rather than an implicit assumption.
|
|
64
67
|
|
|
65
68
|
### Features
|
|
66
69
|
|
|
@@ -74,6 +77,7 @@ Ship a stable `Table` v1 with keyboard navigation, row selection, sorting, docum
|
|
|
74
77
|
- [ ] [C][P2][Area: Features][Owner: Unassigned][Target: TBD] Add inline cell and row editing primitives — define an opt-in editing model that can coexist with current focus, selection, and keyboard navigation contracts.
|
|
75
78
|
- [ ] [M][P2][Area: Features][Owner: Unassigned][Target: TBD] Add column action primitives — expose a path for header menus, quick sort/filter actions, and future column management UI without requiring consumers to hand-roll header action composition every time.
|
|
76
79
|
- [ ] [M][P3][Area: Features][Owner: Unassigned][Target: TBD] Add row actions patterns — document or expose a composable pattern for common trailing actions columns so selection, row press, and nested interactive controls do not conflict.
|
|
80
|
+
- [x] [C][P1][Area: Features][Owner: Unassigned][Target: Done] Implement collection-level row actions in `Table.Root` so body rows can trigger `onRowAction` using RAC-style interaction rules without introducing a separate `rowPressBehavior` prop.
|
|
77
81
|
|
|
78
82
|
### Tests
|
|
79
83
|
|
|
@@ -81,12 +85,21 @@ Ship a stable `Table` v1 with keyboard navigation, row selection, sorting, docum
|
|
|
81
85
|
- [x] [S][P2][Area: Tests][Owner: Unassigned][Target: Done] Add test for full sort cycle via keyboard (ascending → descending) and verify no way to clear sort with keyboard is intentional.
|
|
82
86
|
- [x] [S][P2][Area: Tests][Owner: Unassigned][Target: Done] Add test verifying disabled rows are not selected when arrow-navigated in `replace` mode.
|
|
83
87
|
- [x] [S][P2][Area: Tests][Owner: Unassigned][Target: Done] Add tests covering `selectionMode` transitions after mount, including collapsing multiple selections to one on `single`.
|
|
88
|
+
- [x] [C][P1][Area: Tests][Owner: Unassigned][Target: Done] Add the full row action matrix to `Table.Root` tests: click/action in `selectionMode="none"`, toggle-mode action when selection is empty, toggle-mode selection when selection is active, and replace-mode double-click actions.
|
|
89
|
+
- [x] [M][P1][Area: Tests][Owner: Unassigned][Target: Done] Add keyboard interaction tests proving `Enter` triggers `onRowAction` and `Space` triggers selection when both behaviors are available.
|
|
90
|
+
- [x] [M][P1][Area: Tests][Owner: Unassigned][Target: Done] Add `disabledBehavior` regression tests covering `'selection'` vs `'all'` for row focusability, checkbox disabled state, row action availability, and selection blocking.
|
|
91
|
+
- [x] [S][P1][Area: Tests][Owner: Unassigned][Target: Done] Add an explicit edge-case test proving a disabled row under `disabledBehavior="selection"` still fires `onRowAction` on `Enter` while remaining non-selectable.
|
|
92
|
+
- [x] [S][P1][Area: Tests][Owner: Unassigned][Target: Done] Add a callback-ordering test proving `selectionBehavior="replace"` double click fires `onSelectionChange` before `onRowAction`.
|
|
84
93
|
|
|
85
94
|
### Docs
|
|
86
95
|
|
|
87
96
|
- [ ] [C][P2][Area: Docs][Owner: Unassigned][Target: TBD] Add richer styling examples and sorting guidance to the docs page.
|
|
88
97
|
- [x] [C][P3][Area: Docs][Owner: Unassigned][Target: Done] Document that `Table.Column` is a logical-only wrapper (no DOM output) prominently in the README anatomy section.
|
|
89
98
|
- [x] [S][P2][Area: Docs][Owner: Unassigned][Target: Done] Document `selectionMode="none"` normalization behavior and clarify that selection is cleared internally when selection is disabled.
|
|
99
|
+
- [ ] [C][P1][Area: Docs][Owner: Unassigned][Target: TBD] Document row actions vs row selection in the Table README and docs page, including the different pointer rules for `toggle` and `replace` when `onRowAction` is provided.
|
|
100
|
+
- [ ] [M][P1][Area: Docs][Owner: Unassigned][Target: TBD] Document `disabledBehavior="selection"` and clarify that it disables selection only, not row actions or focus.
|
|
101
|
+
- [ ] [S][P1][Area: Docs][Owner: Unassigned][Target: TBD] Call out explicitly that `toggle` mode changes row-click behavior dynamically once an active selection exists, and treat this as an intentional RAC-style model rather than an accidental inconsistency.
|
|
102
|
+
- [ ] [S][P1][Area: Docs][Owner: Unassigned][Target: TBD] Document callback ordering for `replace`-mode double click: `onSelectionChange` fires before `onRowAction`.
|
|
90
103
|
|
|
91
104
|
### Selection Checkbox
|
|
92
105
|
|
|
@@ -94,7 +107,32 @@ Ship a stable `Table` v1 with keyboard navigation, row selection, sorting, docum
|
|
|
94
107
|
- [ ] [M][P1][Area: Accessibility][Owner: Unassigned][Target: TBD] Validate `Table.Checkbox` screen reader announcements across NVDA and VoiceOver — verify that `aria-checked="mixed"` transitions in the header checkbox and row selection toggles are announced correctly.
|
|
95
108
|
- [ ] [M][P1][Area: Correctness][Owner: Unassigned][Target: TBD] Document or resolve `Table.Checkbox` bypass of `pressRow()` — the checkbox always calls `toggleRowSelection()` directly, ignoring `selectionBehavior="replace"` semantics (Shift+click range, Ctrl+click toggle). This is intentional for checkbox UX but undocumented and inconsistent with row-click behavior.
|
|
96
109
|
- [ ] [M][P1][Area: Correctness][Owner: Unassigned][Target: TBD] Add dev-time structural validation for `Table.Checkbox` placement — warn when header includes a selection checkbox column but body rows do not, or when the checkbox is placed in a footer cell where it has no behavior.
|
|
110
|
+
- [x] [M][P1][Area: Correctness][Owner: Unassigned][Target: Done] Ensure `Table.Checkbox` respects `disabledBehavior="selection"` by disabling explicit selection controls without suppressing row actions on the rest of the row.
|
|
111
|
+
|
|
112
|
+
### Row Actions
|
|
113
|
+
|
|
114
|
+
- [x] [C][P1][Area: Correctness][Owner: Unassigned][Target: Done] Refactor the current `pressRow()` pipeline so selection and row actions are distinct internal pathways rather than a single selection-oriented press abstraction.
|
|
115
|
+
- [x] [C][P1][Area: Correctness][Owner: Unassigned][Target: Done] Split row disabled-state helpers into selection-disabled vs action-disabled semantics so `disabledBehavior="selection"` does not incorrectly suppress focus or actions.
|
|
116
|
+
- [x] [M][P1][Area: Behavior][Owner: Unassigned][Target: Done] Preserve existing `replace`-mode selection extension (`Shift+Arrow`, `Ctrl/Cmd+Space`, click replace) while layering `onRowAction` on top.
|
|
117
|
+
- [x] [M][P1][Area: Accessibility][Owner: Unassigned][Target: Done] Revisit `aria-disabled` semantics for rows under `disabledBehavior="selection"` so actionable rows are not announced as fully disabled.
|
|
118
|
+
- [x] [M][P1][Area: DX][Owner: Unassigned][Target: Done] Evaluate whether row/cell data attributes should expose actionable and selection-disabled states for styling and debugging once `onRowAction` ships.
|
|
119
|
+
- [x] [S][P1][Area: DX][Owner: Unassigned][Target: Done] Make `data-actionable` part of the required styling contract for actionable rows so consumers can reliably style cursor and affordances.
|
|
97
120
|
|
|
98
121
|
### Code Quality
|
|
99
122
|
|
|
100
|
-
- [
|
|
123
|
+
- [x] [C][P3][Area: Code Quality][Owner: Unassigned][Target: Done] Remove unused keyboard/click handlers from `Table.Cell` when rendering in footer scope — currently handlers are bound but short-circuit via guards.
|
|
124
|
+
- [x] [M][P2][Area: Code Quality][Owner: Unassigned][Target: Done] Extract shared keyboard handler between `Table.Row` and `Table.Cell` — both components duplicate ~60 lines of nearly identical `handleKeyDown` logic (ArrowUp/Down/Left/Right, Home/End, Ctrl+Home/End, Ctrl+A, Enter, Space) with only minor differences in Home/End targets.
|
|
125
|
+
- [x] [S][P2][Area: Code Quality][Owner: Unassigned][Target: Done] Add a defensive `isRowDisabled` guard inside `pressRow()` in context — currently callers (Row, Cell) check disabled state before calling, but `pressRow` itself does not validate, so a direct call bypasses the check.
|
|
126
|
+
|
|
127
|
+
### Bugs / Phase 3
|
|
128
|
+
|
|
129
|
+
- [x] [M][P1][Area: Correctness][Owner: Unassigned][Target: Done] Fix `keyboard-space` ignoring interaction modifiers in `pressRow()` — the `keyboard-space` branch calls `toggleRowSelection(id)` directly, discarding `shiftKey`/`ctrlKey`/`metaKey`. This breaks `Shift+Space` (should extend selection) and `Ctrl+Space` semantics in `replace` + `multiple` mode. Should route through `pressRowSelection(id, interaction)` instead.
|
|
130
|
+
|
|
131
|
+
### Accessibility / Phase 3
|
|
132
|
+
|
|
133
|
+
- [x] [S][P2][Area: Accessibility][Owner: Unassigned][Target: Done] Clarify ARIA semantics for rows under `disabledBehavior="selection"` without a checkbox — a selection-disabled but actionable row has no `aria-disabled` (correct) but also no ARIA hint that selection is unavailable; the checkbox disabled state announces it, but rows without a checkbox column have no signal for assistive technology.
|
|
134
|
+
|
|
135
|
+
### Performance / Phase 3
|
|
136
|
+
|
|
137
|
+
- [x] [S][P2][Area: Performance][Owner: Unassigned][Target: Done] Use O(1) id-to-token lookup in `isColumnSortable()` — currently iterates all columns with `Array.from(columns.values()).some(...)` instead of using `getColumnRegistrationById(columnId)?.allowsSorting`.
|
|
138
|
+
- [x] [S][P2][Area: Performance][Owner: Unassigned][Target: Done] Cache `getVisibleColumnWidths()` result — it filters `getColumnWidths()` on every call but is used in the render path of `Table.Root` for `managedTableWidth` derivation; should share a cache invalidated alongside `columnWidthsCache`.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
shouldShowFocusVisible,
|
|
13
13
|
trackInteractionModality
|
|
14
14
|
} from '../../primitives/input-modality';
|
|
15
|
+
import { handleTableBodyKeydown } from '../utils/handle-body-keydown';
|
|
15
16
|
|
|
16
17
|
type TableCellProps = Omit<HTMLAttributes<HTMLTableCellElement>, 'children'> & {
|
|
17
18
|
children?: Snippet;
|
|
@@ -109,6 +110,27 @@
|
|
|
109
110
|
void $selectionVersion;
|
|
110
111
|
return row.section === 'body' ? table.isRowDisabled(row.rowId, row.isDisabled) : row.isDisabled;
|
|
111
112
|
});
|
|
113
|
+
const isRowSelectionDisabled = $derived.by(() => {
|
|
114
|
+
void $selectionVersion;
|
|
115
|
+
return row.section === 'body'
|
|
116
|
+
? table.isRowSelectionDisabled(row.rowId, row.isDisabled)
|
|
117
|
+
: row.isDisabled;
|
|
118
|
+
});
|
|
119
|
+
const isRowActionable = $derived.by(() => {
|
|
120
|
+
void $selectionVersion;
|
|
121
|
+
return row.section === 'body' ? table.isRowActionable(row.rowId, row.isDisabled) : false;
|
|
122
|
+
});
|
|
123
|
+
const selectionUnavailableDescription = $derived.by(() => {
|
|
124
|
+
return row.section === 'body' &&
|
|
125
|
+
table.selectionMode !== 'none' &&
|
|
126
|
+
!isRowDisabled &&
|
|
127
|
+
isRowSelectionDisabled
|
|
128
|
+
? 'Selection unavailable for this row.'
|
|
129
|
+
: undefined;
|
|
130
|
+
});
|
|
131
|
+
const selectionUnavailableDescriptionId = $derived(
|
|
132
|
+
selectionUnavailableDescription ? table.selectionUnavailableDescriptionId : undefined
|
|
133
|
+
);
|
|
112
134
|
const isCellFocusable = $derived(row.section !== 'body' || !isRowDisabled);
|
|
113
135
|
const cellTabIndex = $derived.by(() => {
|
|
114
136
|
if (row.section !== 'body') return undefined;
|
|
@@ -128,12 +150,29 @@
|
|
|
128
150
|
if (row.section !== 'body') return;
|
|
129
151
|
if (isRowDisabled) return;
|
|
130
152
|
table.focusCellByKey(key);
|
|
131
|
-
table.pressRow(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
table.pressRow(
|
|
154
|
+
row.rowId as TableSelectionKey | undefined,
|
|
155
|
+
'pointer',
|
|
156
|
+
{
|
|
157
|
+
shiftKey: event.shiftKey,
|
|
158
|
+
ctrlKey: event.ctrlKey,
|
|
159
|
+
metaKey: event.metaKey,
|
|
160
|
+
altKey: event.altKey
|
|
161
|
+
},
|
|
162
|
+
row.isDisabled
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function handleDoubleClick() {
|
|
167
|
+
if (row.section !== 'body') return;
|
|
168
|
+
if (isRowDisabled) return;
|
|
169
|
+
table.focusCellByKey(key);
|
|
170
|
+
table.pressRow(
|
|
171
|
+
row.rowId as TableSelectionKey | undefined,
|
|
172
|
+
'pointer-double',
|
|
173
|
+
{},
|
|
174
|
+
row.isDisabled
|
|
175
|
+
);
|
|
137
176
|
}
|
|
138
177
|
|
|
139
178
|
function handleMouseDown(event: MouseEvent) {
|
|
@@ -143,79 +182,38 @@
|
|
|
143
182
|
|
|
144
183
|
function handleKeyDown(event: KeyboardEvent) {
|
|
145
184
|
if (row.section !== 'body') return;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
table.moveToGridStart();
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if ((event.ctrlKey || event.metaKey) && event.key === 'End') {
|
|
163
|
-
event.preventDefault();
|
|
164
|
-
table.moveToGridEnd();
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
switch (event.key) {
|
|
169
|
-
case 'ArrowUp':
|
|
170
|
-
event.preventDefault();
|
|
171
|
-
table.moveFocus('up', {
|
|
172
|
-
shiftKey: event.shiftKey,
|
|
173
|
-
ctrlKey: event.ctrlKey,
|
|
174
|
-
metaKey: event.metaKey,
|
|
175
|
-
altKey: event.altKey
|
|
176
|
-
});
|
|
177
|
-
return;
|
|
178
|
-
case 'ArrowDown':
|
|
179
|
-
event.preventDefault();
|
|
180
|
-
table.moveFocus('down', {
|
|
181
|
-
shiftKey: event.shiftKey,
|
|
182
|
-
ctrlKey: event.ctrlKey,
|
|
183
|
-
metaKey: event.metaKey,
|
|
184
|
-
altKey: event.altKey
|
|
185
|
-
});
|
|
186
|
-
return;
|
|
187
|
-
case 'ArrowLeft':
|
|
188
|
-
event.preventDefault();
|
|
189
|
-
table.moveFocus('left');
|
|
190
|
-
return;
|
|
191
|
-
case 'ArrowRight':
|
|
192
|
-
event.preventDefault();
|
|
193
|
-
table.moveFocus('right');
|
|
194
|
-
return;
|
|
195
|
-
case 'Home':
|
|
196
|
-
event.preventDefault();
|
|
197
|
-
table.moveToRowStart();
|
|
198
|
-
return;
|
|
199
|
-
case 'End':
|
|
200
|
-
event.preventDefault();
|
|
201
|
-
table.moveToRowEnd();
|
|
202
|
-
return;
|
|
203
|
-
case 'Enter':
|
|
204
|
-
case ' ':
|
|
205
|
-
event.preventDefault();
|
|
206
|
-
if (event.repeat) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
if (!isRowDisabled) {
|
|
210
|
-
table.pressRow(row.rowId as TableSelectionKey | undefined, {
|
|
185
|
+
handleTableBodyKeydown({
|
|
186
|
+
event,
|
|
187
|
+
table,
|
|
188
|
+
focusTarget: element,
|
|
189
|
+
isDisabled: isRowDisabled,
|
|
190
|
+
onHome: () => table.moveToRowStart(),
|
|
191
|
+
onEnd: () => table.moveToRowEnd(),
|
|
192
|
+
onEnter: () =>
|
|
193
|
+
table.pressRow(
|
|
194
|
+
row.rowId as TableSelectionKey | undefined,
|
|
195
|
+
'keyboard-enter',
|
|
196
|
+
{
|
|
211
197
|
shiftKey: event.shiftKey,
|
|
212
198
|
ctrlKey: event.ctrlKey,
|
|
213
199
|
metaKey: event.metaKey,
|
|
214
200
|
altKey: event.altKey
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
201
|
+
},
|
|
202
|
+
row.isDisabled
|
|
203
|
+
),
|
|
204
|
+
onSpace: () =>
|
|
205
|
+
table.pressRow(
|
|
206
|
+
row.rowId as TableSelectionKey | undefined,
|
|
207
|
+
'keyboard-space',
|
|
208
|
+
{
|
|
209
|
+
shiftKey: event.shiftKey,
|
|
210
|
+
ctrlKey: event.ctrlKey,
|
|
211
|
+
metaKey: event.metaKey,
|
|
212
|
+
altKey: event.altKey
|
|
213
|
+
},
|
|
214
|
+
row.isDisabled
|
|
215
|
+
)
|
|
216
|
+
});
|
|
219
217
|
}
|
|
220
218
|
</script>
|
|
221
219
|
|
|
@@ -228,17 +226,26 @@
|
|
|
228
226
|
scope={row.section === 'body' && column?.isRowHeader ? 'row' : undefined}
|
|
229
227
|
aria-colindex={!isColumnHidden && visibleColumnIndex >= 0 ? visibleColumnIndex + 1 : undefined}
|
|
230
228
|
aria-hidden={isColumnHidden ? true : undefined}
|
|
229
|
+
aria-describedby={selectionUnavailableDescriptionId}
|
|
231
230
|
aria-disabled={row.section === 'body' && isRowDisabled ? true : undefined}
|
|
232
231
|
data-focused={isFocused ? 'true' : undefined}
|
|
233
232
|
data-focus-visible={isFocusVisible ? 'true' : undefined}
|
|
233
|
+
data-actionable={isRowActionable ? 'true' : undefined}
|
|
234
234
|
data-row-selected={isRowSelected ? 'true' : undefined}
|
|
235
|
+
data-selection-disabled={row.section === 'body' &&
|
|
236
|
+
table.selectionMode !== 'none' &&
|
|
237
|
+
!isRowDisabled &&
|
|
238
|
+
isRowSelectionDisabled
|
|
239
|
+
? 'true'
|
|
240
|
+
: undefined}
|
|
235
241
|
data-disabled={isRowDisabled || undefined}
|
|
236
242
|
data-column-index={visibleColumnIndex >= 0 ? visibleColumnIndex : undefined}
|
|
237
243
|
style:display={isColumnHidden ? 'none' : undefined}
|
|
238
|
-
onfocus={handleFocus}
|
|
239
|
-
onclick={handleClick}
|
|
240
|
-
|
|
241
|
-
|
|
244
|
+
onfocus={row.section === 'body' ? handleFocus : undefined}
|
|
245
|
+
onclick={row.section === 'body' ? handleClick : undefined}
|
|
246
|
+
ondblclick={row.section === 'body' ? handleDoubleClick : undefined}
|
|
247
|
+
onmousedown={row.section === 'body' ? handleMouseDown : undefined}
|
|
248
|
+
onkeydown={row.section === 'body' ? handleKeyDown : undefined}
|
|
242
249
|
{...restProps}
|
|
243
250
|
>
|
|
244
251
|
{#if children}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Table } from '../index';
|
|
3
3
|
import type {
|
|
4
|
+
TableDisabledBehavior,
|
|
4
5
|
TableSelectionBehavior,
|
|
5
6
|
TableSelectionKey,
|
|
6
7
|
TableSelectionMode
|
|
@@ -27,6 +28,8 @@
|
|
|
27
28
|
rows?: DemoRow[];
|
|
28
29
|
selectionMode?: TableSelectionMode;
|
|
29
30
|
selectionBehavior?: TableSelectionBehavior;
|
|
31
|
+
disabledBehavior?: TableDisabledBehavior;
|
|
32
|
+
disallowEmptySelection?: boolean;
|
|
30
33
|
disabledKeys?: Iterable<TableSelectionKey>;
|
|
31
34
|
initialSelectedKeys?: Iterable<TableSelectionKey>;
|
|
32
35
|
};
|
|
@@ -35,6 +38,8 @@
|
|
|
35
38
|
rows = defaultRows,
|
|
36
39
|
selectionMode = 'multiple',
|
|
37
40
|
selectionBehavior = 'toggle',
|
|
41
|
+
disabledBehavior = 'all',
|
|
42
|
+
disallowEmptySelection = false,
|
|
38
43
|
disabledKeys,
|
|
39
44
|
initialSelectedKeys
|
|
40
45
|
}: CheckboxTestProps = $props();
|
|
@@ -48,6 +53,8 @@
|
|
|
48
53
|
aria-label="Users table"
|
|
49
54
|
{selectionMode}
|
|
50
55
|
{selectionBehavior}
|
|
56
|
+
{disabledBehavior}
|
|
57
|
+
{disallowEmptySelection}
|
|
51
58
|
bind:selectedKeys={currentSelectedKeys}
|
|
52
59
|
{disabledKeys}
|
|
53
60
|
>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TableSelectionBehavior, TableSelectionKey, TableSelectionMode } from '../root/context';
|
|
1
|
+
import type { TableDisabledBehavior, TableSelectionBehavior, TableSelectionKey, TableSelectionMode } from '../root/context';
|
|
2
2
|
type DemoRow = {
|
|
3
3
|
id: string;
|
|
4
4
|
email: string;
|
|
@@ -8,6 +8,8 @@ type CheckboxTestProps = {
|
|
|
8
8
|
rows?: DemoRow[];
|
|
9
9
|
selectionMode?: TableSelectionMode;
|
|
10
10
|
selectionBehavior?: TableSelectionBehavior;
|
|
11
|
+
disabledBehavior?: TableDisabledBehavior;
|
|
12
|
+
disallowEmptySelection?: boolean;
|
|
11
13
|
disabledKeys?: Iterable<TableSelectionKey>;
|
|
12
14
|
initialSelectedKeys?: Iterable<TableSelectionKey>;
|
|
13
15
|
};
|
|
@@ -50,7 +50,9 @@
|
|
|
50
50
|
const selectionVersion = table.selectionVersion;
|
|
51
51
|
const layoutVersion = table.layoutVersion;
|
|
52
52
|
|
|
53
|
-
let
|
|
53
|
+
let checkboxElement = $state<HTMLSpanElement | null>(null);
|
|
54
|
+
let checkboxChecked = $state(false);
|
|
55
|
+
let checkboxIndeterminate = $state(false);
|
|
54
56
|
|
|
55
57
|
const isVisible = $derived.by(() => {
|
|
56
58
|
if (table.selectionMode === 'none') return false;
|
|
@@ -83,7 +85,7 @@
|
|
|
83
85
|
return !table.hasSelectableRows();
|
|
84
86
|
}
|
|
85
87
|
if (section.section === 'body') {
|
|
86
|
-
return table.
|
|
88
|
+
return table.isRowSelectionDisabled(row.rowId, row.isDisabled) || row.rowId === undefined;
|
|
87
89
|
}
|
|
88
90
|
return true;
|
|
89
91
|
});
|
|
@@ -101,9 +103,20 @@
|
|
|
101
103
|
});
|
|
102
104
|
|
|
103
105
|
function getCheckboxRootElement() {
|
|
104
|
-
return
|
|
106
|
+
return checkboxElement ?? undefined;
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
$effect(() => {
|
|
110
|
+
void $selectionVersion;
|
|
111
|
+
checkboxChecked = isChecked;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
$effect(() => {
|
|
115
|
+
void $selectionVersion;
|
|
116
|
+
void $layoutVersion;
|
|
117
|
+
checkboxIndeterminate = isIndeterminate;
|
|
118
|
+
});
|
|
119
|
+
|
|
107
120
|
$effect(() => {
|
|
108
121
|
if (!isVisible || isDisabled) {
|
|
109
122
|
cell.unregisterFocusDelegate();
|
|
@@ -129,6 +142,27 @@
|
|
|
129
142
|
checkboxElement.tabIndex = tabIndex;
|
|
130
143
|
});
|
|
131
144
|
|
|
145
|
+
$effect(() => {
|
|
146
|
+
const checkboxElement = getCheckboxRootElement();
|
|
147
|
+
if (!checkboxElement) return;
|
|
148
|
+
|
|
149
|
+
const handleElementFocus = (event: FocusEvent) => {
|
|
150
|
+
handleFocusIn(event);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const handleElementMouseDown = (event: MouseEvent) => {
|
|
154
|
+
handleMouseDown(event);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
checkboxElement.addEventListener('focus', handleElementFocus);
|
|
158
|
+
checkboxElement.addEventListener('mousedown', handleElementMouseDown);
|
|
159
|
+
|
|
160
|
+
return () => {
|
|
161
|
+
checkboxElement.removeEventListener('focus', handleElementFocus);
|
|
162
|
+
checkboxElement.removeEventListener('mousedown', handleElementMouseDown);
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
|
|
132
166
|
function applySelection(nextChecked: boolean) {
|
|
133
167
|
if (isDisabled) return;
|
|
134
168
|
|
|
@@ -152,11 +186,6 @@
|
|
|
152
186
|
table.setFocusVisible(shouldShowFocusVisible(target ?? null));
|
|
153
187
|
}
|
|
154
188
|
|
|
155
|
-
function handleFocusOut(event: FocusEvent) {
|
|
156
|
-
const nextFocused = event.relatedTarget;
|
|
157
|
-
if (nextFocused instanceof Node && wrapperElement?.contains(nextFocused)) return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
189
|
function handleMouseDown(event: MouseEvent) {
|
|
161
190
|
trackInteractionModality(event, getCheckboxRootElement() ?? null);
|
|
162
191
|
table.setFocusVisible(false);
|
|
@@ -166,6 +195,9 @@
|
|
|
166
195
|
function handleClick(event: MouseEvent) {
|
|
167
196
|
event.stopPropagation();
|
|
168
197
|
if (!isVisible || isDisabled) return;
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
const nextChecked = section.section === 'header' ? checkboxState !== 'all' : !isChecked;
|
|
200
|
+
applySelection(nextChecked);
|
|
169
201
|
table.focusCellByKey(cell.cellKey);
|
|
170
202
|
}
|
|
171
203
|
|
|
@@ -246,29 +278,22 @@
|
|
|
246
278
|
</script>
|
|
247
279
|
|
|
248
280
|
{#if isVisible}
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
281
|
+
<Checkbox.Root
|
|
282
|
+
{id}
|
|
283
|
+
bind:element={checkboxElement}
|
|
284
|
+
bind:isChecked={checkboxChecked}
|
|
285
|
+
bind:isIndeterminate={checkboxIndeterminate}
|
|
286
|
+
{isDisabled}
|
|
287
|
+
onCheckedChange={applySelection}
|
|
288
|
+
{title}
|
|
289
|
+
aria-label={accessibleLabel}
|
|
290
|
+
aria-labelledby={ariaLabelledby}
|
|
291
|
+
data-table-checkbox="true"
|
|
255
292
|
onclick={handleClick}
|
|
256
293
|
onkeydown={handleKeyDown}
|
|
294
|
+
class={className}
|
|
295
|
+
{...restProps}
|
|
257
296
|
>
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
{isChecked}
|
|
261
|
-
{isIndeterminate}
|
|
262
|
-
{isDisabled}
|
|
263
|
-
onCheckedChange={applySelection}
|
|
264
|
-
{title}
|
|
265
|
-
aria-label={accessibleLabel}
|
|
266
|
-
aria-labelledby={ariaLabelledby}
|
|
267
|
-
data-table-checkbox="true"
|
|
268
|
-
class={className}
|
|
269
|
-
{...restProps}
|
|
270
|
-
>
|
|
271
|
-
{@render children?.()}
|
|
272
|
-
</Checkbox.Root>
|
|
273
|
-
</div>
|
|
297
|
+
{@render children?.()}
|
|
298
|
+
</Checkbox.Root>
|
|
274
299
|
{/if}
|
package/dist/table/index.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ export { default as TableColumnResizer } from './column-resizer/table-column-res
|
|
|
11
11
|
export { default as TableCheckbox } from './checkbox/table-checkbox.svelte';
|
|
12
12
|
export { default as TableCheckboxIndicator } from './checkbox-indicator/table-checkbox-indicator.svelte';
|
|
13
13
|
export { default as TableCell } from './cell/table-cell.svelte';
|
|
14
|
-
export { createTableContext, getTableContext, setTableContext, useTableContext, getTableSectionContext, setTableSectionContext, useTableSectionContext, getTableRowContext, setTableRowContext, useTableRowContext, getTableColumnContext, setTableColumnContext, useTableColumnContext, type TableContext, type TableSelectionBehavior, type TableSelectionCheckboxState, type TableSelectionKey, type TableSelectionMode, type TableSortDirection, type TableSortDescriptor, type TableColumnWidth, type TableGridCoord, type TableColumnRegistration, type TableSectionKind, type TableSectionContext, type TableRowContext, type TableColumnContext, type CreateTableContextOptions } from './root/context.js';
|
|
14
|
+
export { createTableContext, getTableContext, setTableContext, useTableContext, getTableSectionContext, setTableSectionContext, useTableSectionContext, getTableRowContext, setTableRowContext, useTableRowContext, getTableColumnContext, setTableColumnContext, useTableColumnContext, type TableContext, type TableDisabledBehavior, type TableSelectionBehavior, type TableSelectionCheckboxState, type TableSelectionKey, type TableSelectionMode, type TableRowActionHandler, type TableSortDirection, type TableSortDescriptor, type TableColumnWidth, type TableGridCoord, type TableColumnRegistration, type TableSectionKind, type TableSectionContext, type TableRowContext, type TableColumnContext, type CreateTableContextOptions } from './root/context.js';
|
|
15
15
|
import * as TableParts from './index.parts.js';
|
|
16
16
|
export default TableParts;
|
|
@@ -2,14 +2,17 @@ import { type Readable } from 'svelte/store';
|
|
|
2
2
|
export type TableSelectionKey = string | number;
|
|
3
3
|
export type TableSelectionMode = 'none' | 'single' | 'multiple';
|
|
4
4
|
export type TableSelectionBehavior = 'toggle' | 'replace';
|
|
5
|
+
export type TableDisabledBehavior = 'selection' | 'all';
|
|
5
6
|
export type TableSortDirection = 'ascending' | 'descending';
|
|
6
7
|
export type TableSectionKind = 'header' | 'body' | 'footer';
|
|
8
|
+
export type TableRowActionHandler = (id: TableSelectionKey) => void;
|
|
7
9
|
type TableSelectionInteraction = {
|
|
8
10
|
shiftKey?: boolean;
|
|
9
11
|
ctrlKey?: boolean;
|
|
10
12
|
metaKey?: boolean;
|
|
11
13
|
altKey?: boolean;
|
|
12
14
|
};
|
|
15
|
+
type TableRowPressSource = 'pointer' | 'pointer-double' | 'keyboard-enter' | 'keyboard-space';
|
|
13
16
|
export type TableSortDescriptor = {
|
|
14
17
|
column: string;
|
|
15
18
|
direction: TableSortDirection;
|
|
@@ -52,11 +55,14 @@ type TableCellRegistration = {
|
|
|
52
55
|
export type CreateTableContextOptions = {
|
|
53
56
|
selectionMode?: TableSelectionMode;
|
|
54
57
|
selectionBehavior?: TableSelectionBehavior;
|
|
58
|
+
disabledBehavior?: TableDisabledBehavior;
|
|
59
|
+
disallowEmptySelection?: boolean;
|
|
55
60
|
initialSelectedKeys?: Iterable<TableSelectionKey>;
|
|
56
61
|
initialSortDescriptor?: TableSortDescriptor;
|
|
57
62
|
initialColumnWidths?: Iterable<readonly [string, number]>;
|
|
58
63
|
initialHiddenColumns?: Iterable<string>;
|
|
59
64
|
disabledKeys?: Iterable<TableSelectionKey>;
|
|
65
|
+
onRowAction?: TableRowActionHandler;
|
|
60
66
|
onSelectionChange?: (keys: Set<TableSelectionKey>) => void;
|
|
61
67
|
onSortChange?: (descriptor: TableSortDescriptor | undefined) => void;
|
|
62
68
|
onColumnWidthsChange?: (widths: Map<string, number>) => void;
|
|
@@ -74,6 +80,9 @@ export type TableContext = {
|
|
|
74
80
|
createInstanceToken: (prefix: string) => string;
|
|
75
81
|
selectionMode: TableSelectionMode;
|
|
76
82
|
selectionBehavior: TableSelectionBehavior;
|
|
83
|
+
disabledBehavior: TableDisabledBehavior;
|
|
84
|
+
disallowEmptySelection: boolean;
|
|
85
|
+
selectionUnavailableDescriptionId: string;
|
|
77
86
|
disabledKeys: Set<TableSelectionKey>;
|
|
78
87
|
focusedCellKey: string | null;
|
|
79
88
|
focusVisible: boolean;
|
|
@@ -114,6 +123,9 @@ export type TableContext = {
|
|
|
114
123
|
isRowFocusTarget: (token: string) => boolean;
|
|
115
124
|
getRowFocusEdge: (token: string) => TableRowFocusEdge | null;
|
|
116
125
|
isRowDisabled: (id: TableSelectionKey | undefined, localDisabled?: boolean) => boolean;
|
|
126
|
+
isRowSelectionDisabled: (id: TableSelectionKey | undefined, localDisabled?: boolean) => boolean;
|
|
127
|
+
isRowActionDisabled: (id: TableSelectionKey | undefined, localDisabled?: boolean) => boolean;
|
|
128
|
+
isRowActionable: (id: TableSelectionKey | undefined, localDisabled?: boolean) => boolean;
|
|
117
129
|
hasSelectableRows: () => boolean;
|
|
118
130
|
getSelectionCheckboxState: () => TableSelectionCheckboxState;
|
|
119
131
|
registerCell: (cell: TableCellRegistration) => void;
|
|
@@ -123,7 +135,7 @@ export type TableContext = {
|
|
|
123
135
|
isRowTabStop: (token: string) => boolean;
|
|
124
136
|
focusCellByKey: (key: string | null) => void;
|
|
125
137
|
focusRowByToken: (token: string, edge: TableRowFocusEdge) => void;
|
|
126
|
-
pressRow: (id: TableSelectionKey | undefined, interaction?: TableSelectionInteraction) => void;
|
|
138
|
+
pressRow: (id: TableSelectionKey | undefined, source: TableRowPressSource, interaction?: TableSelectionInteraction, localDisabled?: boolean) => void;
|
|
127
139
|
setFocusedCell: (key: string | null) => void;
|
|
128
140
|
setFocusedRow: (token: string | null, edge?: TableRowFocusEdge) => void;
|
|
129
141
|
setFocusVisible: (visible: boolean) => void;
|
|
@@ -140,7 +152,10 @@ export type TableContext = {
|
|
|
140
152
|
setSelection: (keys: Iterable<TableSelectionKey>) => void;
|
|
141
153
|
setSelectionMode: (mode: TableSelectionMode) => void;
|
|
142
154
|
setSelectionBehavior: (behavior: TableSelectionBehavior) => void;
|
|
155
|
+
setDisabledBehavior: (behavior: TableDisabledBehavior) => void;
|
|
156
|
+
setDisallowEmptySelection: (disallow: boolean) => void;
|
|
143
157
|
setDisabledKeys: (keys?: Iterable<TableSelectionKey>) => void;
|
|
158
|
+
setRowActionHandler: (handler?: TableRowActionHandler) => void;
|
|
144
159
|
setSortDescriptor: (descriptor: TableSortDescriptor | undefined) => void;
|
|
145
160
|
toggleSort: (columnId: string) => void;
|
|
146
161
|
isColumnSortable: (columnId: string) => boolean;
|