@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.
@@ -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`.
@@ -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(row.rowId as TableSelectionKey | undefined, {
132
- shiftKey: event.shiftKey,
133
- ctrlKey: event.ctrlKey,
134
- metaKey: event.metaKey,
135
- altKey: event.altKey
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
- 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, {
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
- return;
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
- onmousedown={handleMouseDown}
241
- onkeydown={handleKeyDown}
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 wrapperElement = $state<HTMLElement | undefined>(undefined);
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.isRowDisabled(row.rowId, row.isDisabled) || row.rowId === undefined;
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 wrapperElement?.querySelector<HTMLElement>('[data-checkbox-root="true"]') ?? undefined;
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
- <div
250
- bind:this={wrapperElement}
251
- role="presentation"
252
- onfocusin={handleFocusIn}
253
- onfocusout={handleFocusOut}
254
- onmousedown={handleMouseDown}
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
- <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>
297
+ {@render children?.()}
298
+ </Checkbox.Root>
274
299
  {/if}
@@ -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;