@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.
- 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 +445 -33
- package/dist/table/README.md +11 -0
- package/dist/table/TODO.md +40 -2
- package/dist/table/body/README.md +2 -0
- package/dist/table/body/table-body.svelte +1 -7
- package/dist/table/body/table-body.svelte.d.ts +1 -6
- package/dist/table/cell/README.md +2 -0
- package/dist/table/cell/table-cell.svelte +87 -86
- package/dist/table/cell/table-cell.svelte.d.ts +1 -6
- package/dist/table/checkbox/README.md +2 -0
- 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 +56 -52
- package/dist/table/checkbox/table-checkbox.svelte.d.ts +1 -10
- package/dist/table/checkbox-indicator/README.md +2 -0
- package/dist/table/checkbox-indicator/table-checkbox-indicator.svelte +1 -8
- package/dist/table/checkbox-indicator/table-checkbox-indicator.svelte.d.ts +1 -7
- package/dist/table/column/README.md +17 -14
- package/dist/table/column/table-column.svelte +2 -26
- package/dist/table/column/table-column.svelte.d.ts +1 -15
- package/dist/table/column-header-cell/README.md +2 -0
- package/dist/table/column-header-cell/table-column-header-cell.svelte +1 -7
- package/dist/table/column-header-cell/table-column-header-cell.svelte.d.ts +1 -6
- package/dist/table/column-resizer/README.md +2 -1
- package/dist/table/column-resizer/table-column-resizer.svelte +1 -9
- package/dist/table/column-resizer/table-column-resizer.svelte.d.ts +1 -8
- package/dist/table/empty-state/README.md +2 -0
- package/dist/table/empty-state/table-empty-state.svelte +1 -6
- package/dist/table/empty-state/table-empty-state.svelte.d.ts +1 -5
- package/dist/table/footer/README.md +2 -0
- package/dist/table/footer/table-footer.svelte +1 -7
- package/dist/table/footer/table-footer.svelte.d.ts +1 -6
- package/dist/table/header/README.md +2 -0
- package/dist/table/header/table-header.svelte +1 -7
- package/dist/table/header/table-header.svelte.d.ts +1 -6
- package/dist/table/index.d.ts +2 -1
- package/dist/table/root/README.md +31 -21
- package/dist/table/root/context.d.ts +19 -6
- package/dist/table/root/context.js +203 -29
- package/dist/table/root/table-root.svelte +30 -33
- package/dist/table/root/table-root.svelte.d.ts +1 -26
- package/dist/table/root/table-test.svelte +29 -0
- package/dist/table/root/table-test.svelte.d.ts +5 -1
- package/dist/table/row/README.md +2 -0
- package/dist/table/row/table-row.svelte +46 -83
- package/dist/table/row/table-row.svelte.d.ts +1 -10
- package/dist/table/types.d.ts +90 -0
- package/dist/table/types.js +1 -0
- 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
|
@@ -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
|
-
- [
|
|
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
|
-
- [
|
|
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 {
|
|
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(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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 {
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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>
|
|
276
|
+
{@render children?.()}
|
|
277
|
+
</Checkbox.Root>
|
|
274
278
|
{/if}
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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 {
|
|
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;
|