@human-kit/svelte-components 1.0.0-alpha.13 → 1.0.0-alpha.15

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 (57) hide show
  1. package/dist/checkbox/root/checkbox-root.svelte +22 -2
  2. package/dist/checkbox/root/checkbox-root.svelte.d.ts +4 -1
  3. package/dist/combobox/root/combobox.svelte +1 -0
  4. package/dist/listbox/item/listbox-item.svelte +13 -0
  5. package/dist/listbox/root/listbox.svelte +7 -1
  6. package/dist/table/IMPLEMENTATION_NOTES.md +2 -1
  7. package/dist/table/PLAN.md +445 -33
  8. package/dist/table/README.md +11 -0
  9. package/dist/table/TODO.md +40 -2
  10. package/dist/table/body/README.md +2 -0
  11. package/dist/table/body/table-body.svelte +1 -7
  12. package/dist/table/body/table-body.svelte.d.ts +1 -6
  13. package/dist/table/cell/README.md +2 -0
  14. package/dist/table/cell/table-cell.svelte +87 -86
  15. package/dist/table/cell/table-cell.svelte.d.ts +1 -6
  16. package/dist/table/checkbox/README.md +2 -0
  17. package/dist/table/checkbox/table-checkbox-test.svelte +7 -0
  18. package/dist/table/checkbox/table-checkbox-test.svelte.d.ts +3 -1
  19. package/dist/table/checkbox/table-checkbox.svelte +56 -52
  20. package/dist/table/checkbox/table-checkbox.svelte.d.ts +1 -10
  21. package/dist/table/checkbox-indicator/README.md +2 -0
  22. package/dist/table/checkbox-indicator/table-checkbox-indicator.svelte +1 -8
  23. package/dist/table/checkbox-indicator/table-checkbox-indicator.svelte.d.ts +1 -7
  24. package/dist/table/column/README.md +17 -14
  25. package/dist/table/column/table-column.svelte +2 -26
  26. package/dist/table/column/table-column.svelte.d.ts +1 -15
  27. package/dist/table/column-header-cell/README.md +2 -0
  28. package/dist/table/column-header-cell/table-column-header-cell.svelte +1 -7
  29. package/dist/table/column-header-cell/table-column-header-cell.svelte.d.ts +1 -6
  30. package/dist/table/column-resizer/README.md +2 -1
  31. package/dist/table/column-resizer/table-column-resizer.svelte +1 -9
  32. package/dist/table/column-resizer/table-column-resizer.svelte.d.ts +1 -8
  33. package/dist/table/empty-state/README.md +2 -0
  34. package/dist/table/empty-state/table-empty-state.svelte +1 -6
  35. package/dist/table/empty-state/table-empty-state.svelte.d.ts +1 -5
  36. package/dist/table/footer/README.md +2 -0
  37. package/dist/table/footer/table-footer.svelte +1 -7
  38. package/dist/table/footer/table-footer.svelte.d.ts +1 -6
  39. package/dist/table/header/README.md +2 -0
  40. package/dist/table/header/table-header.svelte +1 -7
  41. package/dist/table/header/table-header.svelte.d.ts +1 -6
  42. package/dist/table/index.d.ts +2 -1
  43. package/dist/table/root/README.md +31 -21
  44. package/dist/table/root/context.d.ts +19 -6
  45. package/dist/table/root/context.js +203 -29
  46. package/dist/table/root/table-root.svelte +30 -33
  47. package/dist/table/root/table-root.svelte.d.ts +1 -26
  48. package/dist/table/root/table-test.svelte +29 -0
  49. package/dist/table/root/table-test.svelte.d.ts +5 -1
  50. package/dist/table/row/README.md +2 -0
  51. package/dist/table/row/table-row.svelte +46 -83
  52. package/dist/table/row/table-row.svelte.d.ts +1 -10
  53. package/dist/table/types.d.ts +90 -0
  54. package/dist/table/types.js +1 -0
  55. package/dist/table/utils/handle-body-keydown.d.ts +13 -0
  56. package/dist/table/utils/handle-body-keydown.js +67 -0
  57. package/package.json +1 -1
@@ -46,7 +46,7 @@ Ship a stable `Table` v1 with keyboard navigation, row selection, sorting, docum
46
46
  - [x] [M][P1][Area: DX][Owner: Unassigned][Target: Done] Break the controlled `selectedKeys` feedback loop — the `$effect` that syncs `selectedKeys` to `ctx.setSelection` also fires after internal selection changes via `onSelectionChange`, causing a redundant `notifySelection`.
47
47
  - [x] [S][P2][Area: DX][Owner: Unassigned][Target: Done] Remove inline `style="outline: none;"` from Cell and ColumnHeaderCell — it overrides consumer inline styles; let consumers handle focus-visible styling via `data-focus-visible` / `data-focused` attributes instead.
48
48
  - [x] [S][P2][Area: DX][Owner: Unassigned][Target: Done] Document `defaultSelectedKeys` and `defaultSortDescriptor` props in the README and docs page.
49
- - [ ] [C][P2][Area: DX][Owner: Unassigned][Target: TBD] Export component prop types (`TableRootProps`, `TableRowProps`, `TableCellProps`, etc.) so consumers can type wrapper components.
49
+ - [x] [C][P2][Area: DX][Owner: Unassigned][Target: Done] Export component prop types (`TableRootProps`, `TableRowProps`, `TableCellProps`, etc.) so consumers can type wrapper components.
50
50
  - [ ] [C][P2][Area: DX][Owner: Unassigned][Target: TBD] Evaluate whether exposing `context` as a `$bindable` prop on `Table.Root` is necessary or if a narrower public API would be safer.
51
51
  - [ ] [C][P3][Area: DX][Owner: Unassigned][Target: TBD] Extract a shared registration helper to eliminate the duplicated sync-then-effect pattern across Column, Row, Cell, and ColumnHeaderCell.
52
52
 
@@ -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
- - [ ] [C][P3][Area: Code Quality][Owner: Unassigned][Target: TBD] Remove unused keyboard/click handlers from `Table.Cell` when rendering in footer scope — currently handlers are bound but short-circuit via guards.
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`.
@@ -9,6 +9,8 @@
9
9
  Name: `Table.Body`
10
10
  Description: Body rowgroup for table data rows. It also exposes empty-state markers when no body rows are registered.
11
11
 
12
+ Public prop type: `TableBodyProps`
13
+
12
14
  | Prop | Type | Default | Description |
13
15
  | ---------- | --------- | ----------- | ------------------------------------------ |
14
16
  | `class` | `string` | `''` | Class names for the `tbody` element. |
@@ -1,12 +1,6 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import type { HTMLAttributes } from 'svelte/elements';
4
2
  import { setTableSectionContext, useTableContext } from '../root/context';
5
-
6
- type TableBodyProps = Omit<HTMLAttributes<HTMLTableSectionElement>, 'children'> & {
7
- children?: Snippet;
8
- class?: string;
9
- };
3
+ import type { TableBodyProps } from '../types.js';
10
4
 
11
5
  let { children, class: className = '', ...restProps }: TableBodyProps = $props();
12
6
  setTableSectionContext({ section: 'body' });
@@ -1,9 +1,4 @@
1
- import type { Snippet } from 'svelte';
2
- import type { HTMLAttributes } from 'svelte/elements';
3
- type TableBodyProps = Omit<HTMLAttributes<HTMLTableSectionElement>, 'children'> & {
4
- children?: Snippet;
5
- class?: string;
6
- };
1
+ import type { TableBodyProps } from '../types.js';
7
2
  declare const TableBody: import("svelte").Component<TableBodyProps, {}, "">;
8
3
  type TableBody = ReturnType<typeof TableBody>;
9
4
  export default TableBody;
@@ -9,6 +9,8 @@
9
9
  Name: `Table.Cell`
10
10
  Description: Table data cell part. In body scope it participates in roving focus and row selection. In footer scope it renders semantic summary cells only.
11
11
 
12
+ Public prop type: `TableCellProps`
13
+
12
14
  | Prop | Type | Default | Description |
13
15
  | ---------- | --------- | ----------- | ------------------------------------------ |
14
16
  | `class` | `string` | `''` | Class names for the rendered `td` or `th`. |
@@ -1,22 +1,17 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy } from 'svelte';
3
- import type { Snippet } from 'svelte';
4
- import type { HTMLAttributes } from 'svelte/elements';
5
3
  import {
6
4
  setTableCellContext,
7
5
  useTableContext,
8
6
  useTableRowContext,
9
7
  type TableSelectionKey
10
8
  } from '../root/context';
9
+ import type { TableCellProps } from '../types.js';
11
10
  import {
12
11
  shouldShowFocusVisible,
13
12
  trackInteractionModality
14
13
  } from '../../primitives/input-modality';
15
-
16
- type TableCellProps = Omit<HTMLAttributes<HTMLTableCellElement>, 'children'> & {
17
- children?: Snippet;
18
- class?: string;
19
- };
14
+ import { handleTableBodyKeydown } from '../utils/handle-body-keydown';
20
15
 
21
16
  let { children, class: className = '', ...restProps }: TableCellProps = $props();
22
17
 
@@ -109,6 +104,27 @@
109
104
  void $selectionVersion;
110
105
  return row.section === 'body' ? table.isRowDisabled(row.rowId, row.isDisabled) : row.isDisabled;
111
106
  });
107
+ const isRowSelectionDisabled = $derived.by(() => {
108
+ void $selectionVersion;
109
+ return row.section === 'body'
110
+ ? table.isRowSelectionDisabled(row.rowId, row.isDisabled)
111
+ : row.isDisabled;
112
+ });
113
+ const isRowActionable = $derived.by(() => {
114
+ void $selectionVersion;
115
+ return row.section === 'body' ? table.isRowActionable(row.rowId, row.isDisabled) : false;
116
+ });
117
+ const selectionUnavailableDescription = $derived.by(() => {
118
+ return row.section === 'body' &&
119
+ table.selectionMode !== 'none' &&
120
+ !isRowDisabled &&
121
+ isRowSelectionDisabled
122
+ ? 'Selection unavailable for this row.'
123
+ : undefined;
124
+ });
125
+ const selectionUnavailableDescriptionId = $derived(
126
+ selectionUnavailableDescription ? table.selectionUnavailableDescriptionId : undefined
127
+ );
112
128
  const isCellFocusable = $derived(row.section !== 'body' || !isRowDisabled);
113
129
  const cellTabIndex = $derived.by(() => {
114
130
  if (row.section !== 'body') return undefined;
@@ -128,12 +144,29 @@
128
144
  if (row.section !== 'body') return;
129
145
  if (isRowDisabled) return;
130
146
  table.focusCellByKey(key);
131
- table.pressRow(row.rowId as TableSelectionKey | undefined, {
132
- shiftKey: event.shiftKey,
133
- ctrlKey: event.ctrlKey,
134
- metaKey: event.metaKey,
135
- altKey: event.altKey
136
- });
147
+ table.pressRow(
148
+ row.rowId as TableSelectionKey | undefined,
149
+ 'pointer',
150
+ {
151
+ shiftKey: event.shiftKey,
152
+ ctrlKey: event.ctrlKey,
153
+ metaKey: event.metaKey,
154
+ altKey: event.altKey
155
+ },
156
+ row.isDisabled
157
+ );
158
+ }
159
+
160
+ function handleDoubleClick() {
161
+ if (row.section !== 'body') return;
162
+ if (isRowDisabled) return;
163
+ table.focusCellByKey(key);
164
+ table.pressRow(
165
+ row.rowId as TableSelectionKey | undefined,
166
+ 'pointer-double',
167
+ {},
168
+ row.isDisabled
169
+ );
137
170
  }
138
171
 
139
172
  function handleMouseDown(event: MouseEvent) {
@@ -143,79 +176,38 @@
143
176
 
144
177
  function handleKeyDown(event: KeyboardEvent) {
145
178
  if (row.section !== 'body') return;
146
- trackInteractionModality(event, element ?? null);
147
-
148
- if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
149
- if (table.selectionMode === 'multiple') {
150
- event.preventDefault();
151
- table.selectAllRows();
152
- }
153
- return;
154
- }
155
-
156
- if ((event.ctrlKey || event.metaKey) && event.key === 'Home') {
157
- event.preventDefault();
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, {
179
+ handleTableBodyKeydown({
180
+ event,
181
+ table,
182
+ focusTarget: element,
183
+ isDisabled: isRowDisabled,
184
+ onHome: () => table.moveToRowStart(),
185
+ onEnd: () => table.moveToRowEnd(),
186
+ onEnter: () =>
187
+ table.pressRow(
188
+ row.rowId as TableSelectionKey | undefined,
189
+ 'keyboard-enter',
190
+ {
211
191
  shiftKey: event.shiftKey,
212
192
  ctrlKey: event.ctrlKey,
213
193
  metaKey: event.metaKey,
214
194
  altKey: event.altKey
215
- });
216
- }
217
- return;
218
- }
195
+ },
196
+ row.isDisabled
197
+ ),
198
+ onSpace: () =>
199
+ table.pressRow(
200
+ row.rowId as TableSelectionKey | undefined,
201
+ 'keyboard-space',
202
+ {
203
+ shiftKey: event.shiftKey,
204
+ ctrlKey: event.ctrlKey,
205
+ metaKey: event.metaKey,
206
+ altKey: event.altKey
207
+ },
208
+ row.isDisabled
209
+ )
210
+ });
219
211
  }
220
212
  </script>
221
213
 
@@ -228,17 +220,26 @@
228
220
  scope={row.section === 'body' && column?.isRowHeader ? 'row' : undefined}
229
221
  aria-colindex={!isColumnHidden && visibleColumnIndex >= 0 ? visibleColumnIndex + 1 : undefined}
230
222
  aria-hidden={isColumnHidden ? true : undefined}
223
+ aria-describedby={selectionUnavailableDescriptionId}
231
224
  aria-disabled={row.section === 'body' && isRowDisabled ? true : undefined}
232
225
  data-focused={isFocused ? 'true' : undefined}
233
226
  data-focus-visible={isFocusVisible ? 'true' : undefined}
227
+ data-actionable={isRowActionable ? 'true' : undefined}
234
228
  data-row-selected={isRowSelected ? 'true' : undefined}
229
+ data-selection-disabled={row.section === 'body' &&
230
+ table.selectionMode !== 'none' &&
231
+ !isRowDisabled &&
232
+ isRowSelectionDisabled
233
+ ? 'true'
234
+ : undefined}
235
235
  data-disabled={isRowDisabled || undefined}
236
236
  data-column-index={visibleColumnIndex >= 0 ? visibleColumnIndex : undefined}
237
237
  style:display={isColumnHidden ? 'none' : undefined}
238
- onfocus={handleFocus}
239
- onclick={handleClick}
240
- onmousedown={handleMouseDown}
241
- onkeydown={handleKeyDown}
238
+ onfocus={row.section === 'body' ? handleFocus : undefined}
239
+ onclick={row.section === 'body' ? handleClick : undefined}
240
+ ondblclick={row.section === 'body' ? handleDoubleClick : undefined}
241
+ onmousedown={row.section === 'body' ? handleMouseDown : undefined}
242
+ onkeydown={row.section === 'body' ? handleKeyDown : undefined}
242
243
  {...restProps}
243
244
  >
244
245
  {#if children}
@@ -1,9 +1,4 @@
1
- import type { Snippet } from 'svelte';
2
- import type { HTMLAttributes } from 'svelte/elements';
3
- type TableCellProps = Omit<HTMLAttributes<HTMLTableCellElement>, 'children'> & {
4
- children?: Snippet;
5
- class?: string;
6
- };
1
+ import type { TableCellProps } from '../types.js';
7
2
  declare const TableCell: import("svelte").Component<TableCellProps, {}, "">;
8
3
  type TableCell = ReturnType<typeof TableCell>;
9
4
  export default TableCell;
@@ -7,6 +7,8 @@
7
7
  Name: `Table.Checkbox`
8
8
  Description: Headless selection-aware checkbox root for tables. In body cells it toggles the owning row. In header cells it becomes a select-all checkbox for multiple selection mode.
9
9
 
10
+ Public prop type: `TableCheckboxProps`
11
+
10
12
  | Prop | Type | Default | Description |
11
13
  | ----------------- | ------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------- |
12
14
  | `id` | `string` | `undefined` | Optional id forwarded to the composed checkbox root. |
@@ -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
  };
@@ -1,6 +1,4 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import type { HTMLAttributes } from 'svelte/elements';
4
2
  import { Checkbox } from '../../checkbox';
5
3
  import {
6
4
  useTableCellContext,
@@ -8,31 +6,12 @@
8
6
  useTableRowContext,
9
7
  useTableSectionContext
10
8
  } from '../root/context';
9
+ import type { TableCheckboxProps } from '../types.js';
11
10
  import {
12
11
  shouldShowFocusVisible,
13
12
  trackInteractionModality
14
13
  } from '../../primitives/input-modality';
15
14
 
16
- type TableCheckboxProps = Omit<
17
- HTMLAttributes<HTMLSpanElement>,
18
- | 'children'
19
- | 'class'
20
- | 'id'
21
- | 'role'
22
- | 'tabindex'
23
- | 'aria-checked'
24
- | 'aria-disabled'
25
- | 'onclick'
26
- | 'onkeydown'
27
- > & {
28
- id?: string;
29
- title?: string;
30
- children?: Snippet;
31
- class?: string;
32
- 'aria-label'?: string;
33
- 'aria-labelledby'?: string;
34
- };
35
-
36
15
  let {
37
16
  id,
38
17
  title,
@@ -50,7 +29,9 @@
50
29
  const selectionVersion = table.selectionVersion;
51
30
  const layoutVersion = table.layoutVersion;
52
31
 
53
- let wrapperElement = $state<HTMLElement | undefined>(undefined);
32
+ let checkboxElement = $state<HTMLSpanElement | null>(null);
33
+ let checkboxChecked = $state(false);
34
+ let checkboxIndeterminate = $state(false);
54
35
 
55
36
  const isVisible = $derived.by(() => {
56
37
  if (table.selectionMode === 'none') return false;
@@ -83,7 +64,7 @@
83
64
  return !table.hasSelectableRows();
84
65
  }
85
66
  if (section.section === 'body') {
86
- return table.isRowDisabled(row.rowId, row.isDisabled) || row.rowId === undefined;
67
+ return table.isRowSelectionDisabled(row.rowId, row.isDisabled) || row.rowId === undefined;
87
68
  }
88
69
  return true;
89
70
  });
@@ -101,9 +82,20 @@
101
82
  });
102
83
 
103
84
  function getCheckboxRootElement() {
104
- return wrapperElement?.querySelector<HTMLElement>('[data-checkbox-root="true"]') ?? undefined;
85
+ return checkboxElement ?? undefined;
105
86
  }
106
87
 
88
+ $effect(() => {
89
+ void $selectionVersion;
90
+ checkboxChecked = isChecked;
91
+ });
92
+
93
+ $effect(() => {
94
+ void $selectionVersion;
95
+ void $layoutVersion;
96
+ checkboxIndeterminate = isIndeterminate;
97
+ });
98
+
107
99
  $effect(() => {
108
100
  if (!isVisible || isDisabled) {
109
101
  cell.unregisterFocusDelegate();
@@ -129,6 +121,27 @@
129
121
  checkboxElement.tabIndex = tabIndex;
130
122
  });
131
123
 
124
+ $effect(() => {
125
+ const checkboxElement = getCheckboxRootElement();
126
+ if (!checkboxElement) return;
127
+
128
+ const handleElementFocus = (event: FocusEvent) => {
129
+ handleFocusIn(event);
130
+ };
131
+
132
+ const handleElementMouseDown = (event: MouseEvent) => {
133
+ handleMouseDown(event);
134
+ };
135
+
136
+ checkboxElement.addEventListener('focus', handleElementFocus);
137
+ checkboxElement.addEventListener('mousedown', handleElementMouseDown);
138
+
139
+ return () => {
140
+ checkboxElement.removeEventListener('focus', handleElementFocus);
141
+ checkboxElement.removeEventListener('mousedown', handleElementMouseDown);
142
+ };
143
+ });
144
+
132
145
  function applySelection(nextChecked: boolean) {
133
146
  if (isDisabled) return;
134
147
 
@@ -152,11 +165,6 @@
152
165
  table.setFocusVisible(shouldShowFocusVisible(target ?? null));
153
166
  }
154
167
 
155
- function handleFocusOut(event: FocusEvent) {
156
- const nextFocused = event.relatedTarget;
157
- if (nextFocused instanceof Node && wrapperElement?.contains(nextFocused)) return;
158
- }
159
-
160
168
  function handleMouseDown(event: MouseEvent) {
161
169
  trackInteractionModality(event, getCheckboxRootElement() ?? null);
162
170
  table.setFocusVisible(false);
@@ -166,6 +174,9 @@
166
174
  function handleClick(event: MouseEvent) {
167
175
  event.stopPropagation();
168
176
  if (!isVisible || isDisabled) return;
177
+ event.preventDefault();
178
+ const nextChecked = section.section === 'header' ? checkboxState !== 'all' : !isChecked;
179
+ applySelection(nextChecked);
169
180
  table.focusCellByKey(cell.cellKey);
170
181
  }
171
182
 
@@ -246,29 +257,22 @@
246
257
  </script>
247
258
 
248
259
  {#if isVisible}
249
- <div
250
- bind:this={wrapperElement}
251
- role="presentation"
252
- onfocusin={handleFocusIn}
253
- onfocusout={handleFocusOut}
254
- onmousedown={handleMouseDown}
260
+ <Checkbox.Root
261
+ {id}
262
+ bind:element={checkboxElement}
263
+ bind:isChecked={checkboxChecked}
264
+ bind:isIndeterminate={checkboxIndeterminate}
265
+ {isDisabled}
266
+ onCheckedChange={applySelection}
267
+ {title}
268
+ aria-label={accessibleLabel}
269
+ aria-labelledby={ariaLabelledby}
270
+ data-table-checkbox="true"
255
271
  onclick={handleClick}
256
272
  onkeydown={handleKeyDown}
273
+ class={className}
274
+ {...restProps}
257
275
  >
258
- <Checkbox.Root
259
- {id}
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>
276
+ {@render children?.()}
277
+ </Checkbox.Root>
274
278
  {/if}
@@ -1,13 +1,4 @@
1
- import type { Snippet } from 'svelte';
2
- import type { HTMLAttributes } from 'svelte/elements';
3
- type TableCheckboxProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'class' | 'id' | 'role' | 'tabindex' | 'aria-checked' | 'aria-disabled' | 'onclick' | 'onkeydown'> & {
4
- id?: string;
5
- title?: string;
6
- children?: Snippet;
7
- class?: string;
8
- 'aria-label'?: string;
9
- 'aria-labelledby'?: string;
10
- };
1
+ import type { TableCheckboxProps } from '../types.js';
11
2
  declare const TableCheckbox: import("svelte").Component<TableCheckboxProps, {}, "">;
12
3
  type TableCheckbox = ReturnType<typeof TableCheckbox>;
13
4
  export default TableCheckbox;
@@ -7,6 +7,8 @@
7
7
  Name: `Table.CheckboxIndicator`
8
8
  Description: Headless presence wrapper for indicator content inside `Table.Checkbox`. It renders when the checkbox is checked or indeterminate.
9
9
 
10
+ Public prop type: `TableCheckboxIndicatorProps`
11
+
10
12
  | Prop | Type | Default | Description |
11
13
  | -------------- | --------------------------------- | ----------- | ------------------------------------------------------------------------------- |
12
14
  | `keepMounted` | `boolean` | `false` | Keeps the indicator mounted while hidden when the checkbox is unchecked. |
@@ -1,13 +1,6 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import type { HTMLAttributes } from 'svelte/elements';
4
2
  import { Checkbox } from '../../checkbox';
5
-
6
- type TableCheckboxIndicatorProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'class'> & {
7
- keepMounted?: boolean;
8
- children?: Snippet;
9
- class?: string;
10
- };
3
+ import type { TableCheckboxIndicatorProps } from '../types.js';
11
4
 
12
5
  let {
13
6
  keepMounted = false,
@@ -1,10 +1,4 @@
1
- import type { Snippet } from 'svelte';
2
- import type { HTMLAttributes } from 'svelte/elements';
3
- type TableCheckboxIndicatorProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'class'> & {
4
- keepMounted?: boolean;
5
- children?: Snippet;
6
- class?: string;
7
- };
1
+ import type { TableCheckboxIndicatorProps } from '../types.js';
8
2
  declare const TableCheckboxIndicator: import("svelte").Component<TableCheckboxIndicatorProps, {}, "">;
9
3
  type TableCheckboxIndicator = ReturnType<typeof TableCheckboxIndicator>;
10
4
  export default TableCheckboxIndicator;