@human-kit/svelte-components 1.0.0-alpha.19 → 1.0.0-alpha.20
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/popover/trigger/popover-trigger.svelte +13 -4
- package/dist/table/README.md +19 -5
- package/dist/table/column/README.md +13 -12
- package/dist/table/column/table-column.svelte +0 -5
- package/dist/table/column-header-cell/README.md +1 -1
- package/dist/table/column-header-cell/table-column-header-cell.svelte +8 -15
- package/dist/table/column-resizer/table-column-resizer-test.svelte +4 -2
- package/dist/table/column-resizer/table-column-resizer.svelte +21 -12
- package/dist/table/index.d.ts +2 -1
- package/dist/table/index.js +1 -0
- package/dist/table/index.parts.d.ts +1 -0
- package/dist/table/index.parts.js +1 -0
- package/dist/table/root/context.d.ts +2 -2
- package/dist/table/root/context.js +18 -2
- package/dist/table/root/table-test.svelte +14 -2
- package/dist/table/sort-trigger/README.md +45 -0
- package/dist/table/sort-trigger/table-sort-trigger.svelte +183 -0
- package/dist/table/sort-trigger/table-sort-trigger.svelte.d.ts +4 -0
- package/dist/table/types.d.ts +7 -2
- package/package.json +1 -1
|
@@ -26,15 +26,25 @@
|
|
|
26
26
|
let wrapperRef: HTMLElement | null = $state(null);
|
|
27
27
|
let activeTrigger: HTMLElement | null = null;
|
|
28
28
|
|
|
29
|
+
function syncTriggerState(button: HTMLElement) {
|
|
30
|
+
button.setAttribute('aria-haspopup', 'dialog');
|
|
31
|
+
button.setAttribute('aria-expanded', String(popoverCtx.isOpen));
|
|
32
|
+
if (popoverCtx.isOpen) {
|
|
33
|
+
button.dataset.pressed = 'true';
|
|
34
|
+
} else {
|
|
35
|
+
delete button.dataset.pressed;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
function setActiveTrigger(button: HTMLElement) {
|
|
30
40
|
if (activeTrigger && activeTrigger !== button) {
|
|
31
41
|
activeTrigger.setAttribute('aria-expanded', 'false');
|
|
42
|
+
delete activeTrigger.dataset.pressed;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
activeTrigger = button;
|
|
35
46
|
popoverCtx.setTriggerRef(button);
|
|
36
|
-
button
|
|
37
|
-
button.setAttribute('aria-expanded', String(popoverCtx.isOpen));
|
|
47
|
+
syncTriggerState(button);
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
function handleClick(event: MouseEvent) {
|
|
@@ -70,8 +80,7 @@
|
|
|
70
80
|
if (activeTrigger !== popoverCtx.triggerRef) {
|
|
71
81
|
activeTrigger = popoverCtx.triggerRef;
|
|
72
82
|
}
|
|
73
|
-
popoverCtx.triggerRef
|
|
74
|
-
popoverCtx.triggerRef.setAttribute('aria-expanded', String(popoverCtx.isOpen));
|
|
83
|
+
syncTriggerState(popoverCtx.triggerRef);
|
|
75
84
|
}
|
|
76
85
|
});
|
|
77
86
|
</script>
|
package/dist/table/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
## Description
|
|
6
6
|
|
|
7
|
-
`Table` is a headless interactive table primitive with grid-style keyboard navigation, row selection, sortable
|
|
7
|
+
`Table` is a headless interactive table primitive with grid-style keyboard navigation, row selection, explicit sortable header triggers, and a composable part-based API.
|
|
8
8
|
|
|
9
|
-
All public Table part prop types are exported from the table barrel, including `TableRootProps`, `TableColumnProps`, `TableHeaderProps`, `TableBodyProps`, `TableFooterProps`, `TableRowProps`, `TableColumnHeaderCellProps`, `TableColumnResizerProps`, `TableCellProps`, `TableEmptyStateProps`, `TableCheckboxProps`, and `TableCheckboxIndicatorProps`.
|
|
9
|
+
All public Table part prop types are exported from the table barrel, including `TableRootProps`, `TableColumnProps`, `TableHeaderProps`, `TableBodyProps`, `TableFooterProps`, `TableRowProps`, `TableColumnHeaderCellProps`, `TableSortTriggerProps`, `TableColumnResizerProps`, `TableCellProps`, `TableEmptyStateProps`, `TableCheckboxProps`, and `TableCheckboxIndicatorProps`.
|
|
10
10
|
|
|
11
11
|
## Anatomy
|
|
12
12
|
|
|
@@ -26,8 +26,19 @@ All public Table part prop types are exported from the table barrel, including `
|
|
|
26
26
|
<Table.Column id="email" isRowHeader>
|
|
27
27
|
<Table.ColumnHeaderCell>Email</Table.ColumnHeaderCell>
|
|
28
28
|
</Table.Column>
|
|
29
|
-
<Table.Column id="group"
|
|
30
|
-
<Table.ColumnHeaderCell>
|
|
29
|
+
<Table.Column id="group">
|
|
30
|
+
<Table.ColumnHeaderCell>
|
|
31
|
+
<Table.SortTrigger>
|
|
32
|
+
{#snippet children({ sortDirection })}
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
aria-label={`Group sort button. ${sortDirection ?? 'not sorted'}.`}
|
|
36
|
+
>
|
|
37
|
+
Sort group
|
|
38
|
+
</button>
|
|
39
|
+
{/snippet}
|
|
40
|
+
</Table.SortTrigger>
|
|
41
|
+
</Table.ColumnHeaderCell>
|
|
31
42
|
</Table.Column>
|
|
32
43
|
<Table.Column id="size" minWidth={120}>
|
|
33
44
|
<Table.ColumnHeaderCell>
|
|
@@ -71,6 +82,7 @@ All public Table part prop types are exported from the table barrel, including `
|
|
|
71
82
|
- `Table.Footer`
|
|
72
83
|
- `Table.Row`
|
|
73
84
|
- `Table.ColumnHeaderCell`
|
|
85
|
+
- `Table.SortTrigger`
|
|
74
86
|
- `Table.ColumnResizer`
|
|
75
87
|
- `Table.Checkbox`
|
|
76
88
|
- `Table.CheckboxIndicator`
|
|
@@ -112,7 +124,8 @@ All public Table part prop types are exported from the table barrel, including `
|
|
|
112
124
|
|
|
113
125
|
- DOM-rendering parts: `Table.Root`, `Table.Header`, `Table.Body`, `Table.Footer`, `Table.Row`, `Table.Cell`, `Table.ColumnHeaderCell`, `Table.ColumnResizer`, `Table.Checkbox`, `Table.CheckboxIndicator`, and `Table.EmptyState` all render DOM.
|
|
114
126
|
- Metadata-only part: `Table.Column` does not render its own element. It only registers the public column input for the surrounding header composition.
|
|
115
|
-
- Sorting: `Table.
|
|
127
|
+
- Sorting: `Table.SortTrigger` is the public opt-in for sortable columns. Rendering it inside `Table.ColumnHeaderCell` makes the owning `Table.Column` sortable and toggles `Table.Root.sortDescriptor`.
|
|
128
|
+
- `Table.SortTrigger.children` can consume a `sortDirection` render state so the nested button can expose stateful labels or visuals without reading the root descriptor directly.
|
|
116
129
|
- Resizing: `Table.ColumnResizer` is the only public opt-in for resizing. Rendering it inside a `Table.ColumnHeaderCell` enables resizing for the owning `Table.Column`.
|
|
117
130
|
- Public input types: import the `Table*Props` types you need from `@human-kit/svelte-components/table` or the main package barrel instead of deriving contracts from component internals.
|
|
118
131
|
- Internal normalized state: table context stores normalized column metadata internally as `TableColumnMetadata`. That metadata is not the public input contract for wrappers or consumers.
|
|
@@ -125,5 +138,6 @@ All public Table part prop types are exported from the table barrel, including `
|
|
|
125
138
|
- `Table.Checkbox` can receive DOM focus directly while still participating in the table's roving-focus grid.
|
|
126
139
|
- First-column body cells become `rowheader` when their associated column has `isRowHeader`.
|
|
127
140
|
- Disabled rows remain rendered and non-selectable, but are skipped by focus navigation.
|
|
141
|
+
- `Table.SortTrigger` wires a nested trigger button, while the header cell remains the roving-focus target for arrow-key grid navigation.
|
|
128
142
|
- Sort changes are mirrored into a polite live region so screen readers announce direction changes more reliably than `aria-sort` alone.
|
|
129
143
|
- Column resize handles are keyboard accessible separators. Press `Enter` to enter resize mode, use the horizontal arrow keys to resize, `Home` to jump to the minimum width, `End` to auto-fit to content width, and press `Enter` again to exit resize mode while keeping focus on the handle.
|
|
@@ -7,21 +7,22 @@
|
|
|
7
7
|
### Table.Column
|
|
8
8
|
|
|
9
9
|
Name: `Table.Column`
|
|
10
|
-
Description: Logical metadata wrapper for a header column. It does not render DOM and is used to register stable column identity,
|
|
10
|
+
Description: Logical metadata wrapper for a header column. It does not render DOM and is used to register stable column identity, row-header semantics, and width constraints.
|
|
11
11
|
|
|
12
12
|
Public prop type: `TableColumnProps`
|
|
13
13
|
|
|
14
|
-
| Prop
|
|
15
|
-
|
|
|
16
|
-
| `id`
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
|
|
14
|
+
| Prop | Type | Default | Description |
|
|
15
|
+
| -------------- | --------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| `id` | `string` | `-` | Stable identifier for the column. |
|
|
17
|
+
| `isRowHeader` | `boolean` | `false` | Marks the associated body column as row-header cells. |
|
|
18
|
+
| `textValue` | `string` | `undefined` | Optional spoken label used by `Table.Root` sort announcements when it should differ from `id`. |
|
|
19
|
+
| `width` | `number \| \`${number}px\`` | `undefined` | Fixed column width. When set, the column keeps that width and user resize interactions are disabled. |
|
|
20
|
+
| `defaultWidth` | `number \| \`${number}px\`` | `undefined` | Uncontrolled initial width hint for the column. The column remains user-resizable when a resizer is composed. |
|
|
21
|
+
| `minWidth` | `number` | `undefined` | Minimum width in px enforced during resize interactions. |
|
|
22
|
+
| `maxWidth` | `number` | `undefined` | Maximum width in px enforced during resize interactions. |
|
|
23
|
+
| `children` | `Snippet` | `undefined` | Usually a single `Table.ColumnHeaderCell`. |
|
|
24
|
+
|
|
25
|
+
`Table.SortTrigger` is the public sorting opt-in. Compose it inside `Table.ColumnHeaderCell` when the owning column should be sortable.
|
|
25
26
|
|
|
26
27
|
`Table.ColumnResizer` is the public resize opt-in. Compose it inside `Table.ColumnHeaderCell` when the owning column should be resizable.
|
|
27
28
|
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
let {
|
|
7
7
|
id,
|
|
8
|
-
allowsSorting = false,
|
|
9
8
|
isRowHeader = false,
|
|
10
9
|
textValue,
|
|
11
10
|
width,
|
|
@@ -28,9 +27,6 @@
|
|
|
28
27
|
get id() {
|
|
29
28
|
return id;
|
|
30
29
|
},
|
|
31
|
-
get allowsSorting() {
|
|
32
|
-
return allowsSorting;
|
|
33
|
-
},
|
|
34
30
|
get isHidden() {
|
|
35
31
|
return table.isColumnHidden(id);
|
|
36
32
|
},
|
|
@@ -58,7 +54,6 @@
|
|
|
58
54
|
table.registerColumn({
|
|
59
55
|
token,
|
|
60
56
|
id,
|
|
61
|
-
allowsSorting,
|
|
62
57
|
isRowHeader,
|
|
63
58
|
textValue,
|
|
64
59
|
width,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
### Table.ColumnHeaderCell
|
|
8
8
|
|
|
9
9
|
Name: `Table.ColumnHeaderCell`
|
|
10
|
-
Description: Focusable header cell for a column. It participates in roving focus
|
|
10
|
+
Description: Focusable header cell for a column. It participates in roving focus, exposes `aria-sort` when a nested `Table.SortTrigger` is present, and can host additional header actions.
|
|
11
11
|
|
|
12
12
|
Public prop type: `TableColumnHeaderCellProps`
|
|
13
13
|
|
|
@@ -109,6 +109,11 @@
|
|
|
109
109
|
void $layoutVersion;
|
|
110
110
|
return table.getVisibleColumnIndexByToken(column.token);
|
|
111
111
|
});
|
|
112
|
+
const isSortable = $derived.by(() => {
|
|
113
|
+
void $layoutVersion;
|
|
114
|
+
void $sortVersion;
|
|
115
|
+
return table.isColumnSortable(column.id);
|
|
116
|
+
});
|
|
112
117
|
const headerTabIndex = $derived.by(() => {
|
|
113
118
|
if (isHidden || focusDelegate) return undefined;
|
|
114
119
|
return table.isCellTabStop(key) ? 0 : -1;
|
|
@@ -173,12 +178,7 @@
|
|
|
173
178
|
|
|
174
179
|
function handleClick() {
|
|
175
180
|
table.focusCellByKey(key);
|
|
176
|
-
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (column.allowsSorting) {
|
|
180
|
-
table.toggleSort(column.id);
|
|
181
|
-
}
|
|
181
|
+
table.consumeHeaderClickSuppression();
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
function handleMouseDown(event: MouseEvent) {
|
|
@@ -230,13 +230,6 @@
|
|
|
230
230
|
event.preventDefault();
|
|
231
231
|
table.moveToRowEnd();
|
|
232
232
|
return;
|
|
233
|
-
case 'Enter':
|
|
234
|
-
case ' ':
|
|
235
|
-
if (!column.allowsSorting) return;
|
|
236
|
-
event.preventDefault();
|
|
237
|
-
if (event.repeat) return;
|
|
238
|
-
table.toggleSort(column.id);
|
|
239
|
-
return;
|
|
240
233
|
}
|
|
241
234
|
}
|
|
242
235
|
</script>
|
|
@@ -248,12 +241,12 @@
|
|
|
248
241
|
tabindex={headerTabIndex}
|
|
249
242
|
aria-colindex={!isHidden && visibleColumnIndex >= 0 ? visibleColumnIndex + 1 : undefined}
|
|
250
243
|
aria-hidden={isHidden ? true : undefined}
|
|
251
|
-
aria-sort={
|
|
244
|
+
aria-sort={isSortable ? (sortDirection ?? 'none') : undefined}
|
|
252
245
|
data-focused={isFocused ? 'true' : undefined}
|
|
253
246
|
data-focus-visible={isFocusVisible ? 'true' : undefined}
|
|
254
247
|
data-focus-within={isFocusWithin ? 'true' : undefined}
|
|
255
248
|
data-focus-visible-within={isFocusVisibleWithin ? 'true' : undefined}
|
|
256
|
-
data-sortable={
|
|
249
|
+
data-sortable={isSortable || undefined}
|
|
257
250
|
data-sort-direction={sortDirection}
|
|
258
251
|
data-column-index={visibleColumnIndex >= 0 ? visibleColumnIndex : undefined}
|
|
259
252
|
style:box-sizing="border-box"
|
|
@@ -37,10 +37,12 @@
|
|
|
37
37
|
</div>
|
|
38
38
|
</Table.ColumnHeaderCell>
|
|
39
39
|
</Table.Column>
|
|
40
|
-
<Table.Column id="group"
|
|
40
|
+
<Table.Column id="group" textValue="Group" minWidth={100} maxWidth={260}>
|
|
41
41
|
<Table.ColumnHeaderCell data-testid="group-header-cell">
|
|
42
42
|
<div class="flex items-center justify-between gap-3">
|
|
43
|
-
<
|
|
43
|
+
<Table.SortTrigger>
|
|
44
|
+
<button type="button" data-testid="group-sort-trigger">Group</button>
|
|
45
|
+
</Table.SortTrigger>
|
|
44
46
|
<Table.ColumnResizer
|
|
45
47
|
data-testid="group-resizer"
|
|
46
48
|
class="inline-flex w-3 cursor-col-resize justify-center"
|
|
@@ -365,6 +365,12 @@
|
|
|
365
365
|
|
|
366
366
|
const handlePointerCancel = (cancelEvent: PointerEvent) => {
|
|
367
367
|
if (cancelEvent.pointerId !== pointerId) return;
|
|
368
|
+
cancelPointerResize();
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
function cancelPointerResize(event?: Event) {
|
|
372
|
+
event?.preventDefault();
|
|
373
|
+
event?.stopPropagation();
|
|
368
374
|
clearRecentClickCandidate();
|
|
369
375
|
if (animationFrameId !== null) {
|
|
370
376
|
cancelAnimationFrame(animationFrameId);
|
|
@@ -376,21 +382,20 @@
|
|
|
376
382
|
updateWidth(startWidth);
|
|
377
383
|
}
|
|
378
384
|
cleanupPointerListeners();
|
|
379
|
-
}
|
|
385
|
+
}
|
|
380
386
|
|
|
381
387
|
const handleWindowKeyDown = (keyEvent: KeyboardEvent) => {
|
|
382
388
|
if (keyEvent.key !== 'Escape') return;
|
|
383
|
-
keyEvent
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
cleanupPointerListeners();
|
|
389
|
+
cancelPointerResize(keyEvent);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const handleContextMenu = (menuEvent: MouseEvent) => {
|
|
393
|
+
if (!didStartResize && !didDrag) return;
|
|
394
|
+
cancelPointerResize(menuEvent);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const handleWindowBlur = () => {
|
|
398
|
+
cancelPointerResize();
|
|
394
399
|
};
|
|
395
400
|
|
|
396
401
|
// With pointer capture active the browser routes all pointer events for
|
|
@@ -401,6 +406,8 @@
|
|
|
401
406
|
window.addEventListener('pointerup', handlePointerUp);
|
|
402
407
|
window.addEventListener('pointercancel', handlePointerCancel);
|
|
403
408
|
window.addEventListener('keydown', handleWindowKeyDown, true);
|
|
409
|
+
window.addEventListener('contextmenu', handleContextMenu, true);
|
|
410
|
+
window.addEventListener('blur', handleWindowBlur);
|
|
404
411
|
removeListeners = () => {
|
|
405
412
|
if (animationFrameId !== null) {
|
|
406
413
|
cancelAnimationFrame(animationFrameId);
|
|
@@ -415,6 +422,8 @@
|
|
|
415
422
|
window.removeEventListener('pointerup', handlePointerUp);
|
|
416
423
|
window.removeEventListener('pointercancel', handlePointerCancel);
|
|
417
424
|
window.removeEventListener('keydown', handleWindowKeyDown, true);
|
|
425
|
+
window.removeEventListener('contextmenu', handleContextMenu, true);
|
|
426
|
+
window.removeEventListener('blur', handleWindowBlur);
|
|
418
427
|
};
|
|
419
428
|
}
|
|
420
429
|
|
package/dist/table/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * as Table from './index.parts.js';
|
|
2
|
-
export type { TableBodyProps, TableCellProps, TableCheckboxIndicatorProps, TableCheckboxProps, TableColumnHeaderCellProps, TableColumnProps, TableColumnResizerProps, TableEmptyStateProps, TableFooterProps, TableHeaderProps, TableRowProps, TableRootProps } from './types.js';
|
|
2
|
+
export type { TableBodyProps, TableCellProps, TableCheckboxIndicatorProps, TableCheckboxProps, TableColumnHeaderCellProps, TableColumnProps, TableColumnResizerProps, TableSortTriggerRenderState, TableSortTriggerProps, TableEmptyStateProps, TableFooterProps, TableHeaderProps, TableRowProps, TableRootProps } from './types.js';
|
|
3
3
|
export { default as TableRoot } from './root/table-root.svelte';
|
|
4
4
|
export { default as TableColumn } from './column/table-column.svelte';
|
|
5
5
|
export { default as TableHeader } from './header/table-header.svelte';
|
|
@@ -8,6 +8,7 @@ export { default as TableEmptyState } from './empty-state/table-empty-state.svel
|
|
|
8
8
|
export { default as TableFooter } from './footer/table-footer.svelte';
|
|
9
9
|
export { default as TableRow } from './row/table-row.svelte';
|
|
10
10
|
export { default as TableColumnHeaderCell } from './column-header-cell/table-column-header-cell.svelte';
|
|
11
|
+
export { default as TableSortTrigger } from './sort-trigger/table-sort-trigger.svelte';
|
|
11
12
|
export { default as TableColumnResizer } from './column-resizer/table-column-resizer.svelte';
|
|
12
13
|
export { default as TableCheckbox } from './checkbox/table-checkbox.svelte';
|
|
13
14
|
export { default as TableCheckboxIndicator } from './checkbox-indicator/table-checkbox-indicator.svelte';
|
package/dist/table/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { default as TableEmptyState } from './empty-state/table-empty-state.svel
|
|
|
7
7
|
export { default as TableFooter } from './footer/table-footer.svelte';
|
|
8
8
|
export { default as TableRow } from './row/table-row.svelte';
|
|
9
9
|
export { default as TableColumnHeaderCell } from './column-header-cell/table-column-header-cell.svelte';
|
|
10
|
+
export { default as TableSortTrigger } from './sort-trigger/table-sort-trigger.svelte';
|
|
10
11
|
export { default as TableColumnResizer } from './column-resizer/table-column-resizer.svelte';
|
|
11
12
|
export { default as TableCheckbox } from './checkbox/table-checkbox.svelte';
|
|
12
13
|
export { default as TableCheckboxIndicator } from './checkbox-indicator/table-checkbox-indicator.svelte';
|
|
@@ -6,6 +6,7 @@ export { default as EmptyState } from './empty-state/table-empty-state.svelte';
|
|
|
6
6
|
export { default as Footer } from './footer/table-footer.svelte';
|
|
7
7
|
export { default as Row } from './row/table-row.svelte';
|
|
8
8
|
export { default as ColumnHeaderCell } from './column-header-cell/table-column-header-cell.svelte';
|
|
9
|
+
export { default as SortTrigger } from './sort-trigger/table-sort-trigger.svelte';
|
|
9
10
|
export { default as ColumnResizer } from './column-resizer/table-column-resizer.svelte';
|
|
10
11
|
export { default as Checkbox } from './checkbox/table-checkbox.svelte';
|
|
11
12
|
export { default as CheckboxIndicator } from './checkbox-indicator/table-checkbox-indicator.svelte';
|
|
@@ -6,6 +6,7 @@ export { default as EmptyState } from './empty-state/table-empty-state.svelte';
|
|
|
6
6
|
export { default as Footer } from './footer/table-footer.svelte';
|
|
7
7
|
export { default as Row } from './row/table-row.svelte';
|
|
8
8
|
export { default as ColumnHeaderCell } from './column-header-cell/table-column-header-cell.svelte';
|
|
9
|
+
export { default as SortTrigger } from './sort-trigger/table-sort-trigger.svelte';
|
|
9
10
|
export { default as ColumnResizer } from './column-resizer/table-column-resizer.svelte';
|
|
10
11
|
export { default as Checkbox } from './checkbox/table-checkbox.svelte';
|
|
11
12
|
export { default as CheckboxIndicator } from './checkbox-indicator/table-checkbox-indicator.svelte';
|
|
@@ -37,7 +37,6 @@ export type TableRowFocusEdge = 'start' | 'end';
|
|
|
37
37
|
type TableColumnMetadata = {
|
|
38
38
|
token: string;
|
|
39
39
|
id: string;
|
|
40
|
-
allowsSorting: boolean;
|
|
41
40
|
isRowHeader: boolean;
|
|
42
41
|
textValue?: string;
|
|
43
42
|
width?: TableColumnWidth;
|
|
@@ -113,6 +112,8 @@ export type TableContext = {
|
|
|
113
112
|
hasAuthoredColumnWidthSpec: (columnId: string) => boolean;
|
|
114
113
|
getColumnMinWidth: (columnId: string) => number | undefined;
|
|
115
114
|
getColumnMaxWidth: (columnId: string) => number | undefined;
|
|
115
|
+
registerColumnSortTrigger: (columnToken: string) => void;
|
|
116
|
+
unregisterColumnSortTrigger: (columnToken: string) => void;
|
|
116
117
|
isColumnHidden: (columnId: string) => boolean;
|
|
117
118
|
isColumnResizable: (columnId: string) => boolean;
|
|
118
119
|
getColumnWidths: () => Map<string, TableColumnWidth>;
|
|
@@ -193,7 +194,6 @@ export type TableRowContext = {
|
|
|
193
194
|
export type TableColumnContext = {
|
|
194
195
|
token: string;
|
|
195
196
|
id: string;
|
|
196
|
-
allowsSorting: boolean;
|
|
197
197
|
isHidden: boolean;
|
|
198
198
|
isRowHeader: boolean;
|
|
199
199
|
textValue?: string;
|
|
@@ -66,6 +66,7 @@ export function createTableContext(options = {}) {
|
|
|
66
66
|
const columns = new Map();
|
|
67
67
|
const columnIds = new Map();
|
|
68
68
|
const columnOrder = [];
|
|
69
|
+
const columnsWithSortTriggers = new Set();
|
|
69
70
|
const columnsWithResizers = new Set();
|
|
70
71
|
let resizerLayoutReady = false;
|
|
71
72
|
const columnWidths = new Map(options.initialColumnWidths ?? []);
|
|
@@ -95,6 +96,7 @@ export function createTableContext(options = {}) {
|
|
|
95
96
|
const resizeVersion = writable(0);
|
|
96
97
|
const instanceCounters = new Map();
|
|
97
98
|
const selectionUnavailableDescriptionId = createInstanceToken('selection-unavailable');
|
|
99
|
+
setSelectedKeys(new Set(selectedKeys), selectionAnchorKey);
|
|
98
100
|
function createInstanceToken(prefix) {
|
|
99
101
|
const nextCount = (instanceCounters.get(prefix) ?? 0) + 1;
|
|
100
102
|
instanceCounters.set(prefix, nextCount);
|
|
@@ -351,7 +353,6 @@ export function createTableContext(options = {}) {
|
|
|
351
353
|
function sameColumnMetadata(left, right) {
|
|
352
354
|
return (left.token === right.token &&
|
|
353
355
|
left.id === right.id &&
|
|
354
|
-
left.allowsSorting === right.allowsSorting &&
|
|
355
356
|
left.isRowHeader === right.isRowHeader &&
|
|
356
357
|
left.textValue === right.textValue &&
|
|
357
358
|
left.width === right.width &&
|
|
@@ -396,6 +397,7 @@ export function createTableContext(options = {}) {
|
|
|
396
397
|
columnIds.delete(column.id);
|
|
397
398
|
}
|
|
398
399
|
columns.delete(token);
|
|
400
|
+
columnsWithSortTriggers.delete(token);
|
|
399
401
|
columnsWithResizers.delete(token);
|
|
400
402
|
const index = columnOrder.indexOf(token);
|
|
401
403
|
if (index >= 0) {
|
|
@@ -403,6 +405,17 @@ export function createTableContext(options = {}) {
|
|
|
403
405
|
}
|
|
404
406
|
notifyLayout();
|
|
405
407
|
}
|
|
408
|
+
function registerColumnSortTrigger(columnToken) {
|
|
409
|
+
if (columnsWithSortTriggers.has(columnToken))
|
|
410
|
+
return;
|
|
411
|
+
columnsWithSortTriggers.add(columnToken);
|
|
412
|
+
notifyLayout();
|
|
413
|
+
}
|
|
414
|
+
function unregisterColumnSortTrigger(columnToken) {
|
|
415
|
+
if (!columnsWithSortTriggers.delete(columnToken))
|
|
416
|
+
return;
|
|
417
|
+
notifyLayout();
|
|
418
|
+
}
|
|
406
419
|
function getOrderedColumnTokens() {
|
|
407
420
|
if (orderedColumnTokensCache)
|
|
408
421
|
return orderedColumnTokensCache;
|
|
@@ -1888,7 +1901,8 @@ export function createTableContext(options = {}) {
|
|
|
1888
1901
|
notifySort();
|
|
1889
1902
|
}
|
|
1890
1903
|
function isColumnSortable(columnId) {
|
|
1891
|
-
|
|
1904
|
+
const token = columnIds.get(columnId);
|
|
1905
|
+
return token ? columnsWithSortTriggers.has(token) : false;
|
|
1892
1906
|
}
|
|
1893
1907
|
function toggleSort(columnId) {
|
|
1894
1908
|
if (!isColumnSortable(columnId))
|
|
@@ -1945,6 +1959,8 @@ export function createTableContext(options = {}) {
|
|
|
1945
1959
|
},
|
|
1946
1960
|
registerColumn,
|
|
1947
1961
|
unregisterColumn,
|
|
1962
|
+
registerColumnSortTrigger,
|
|
1963
|
+
unregisterColumnSortTrigger,
|
|
1948
1964
|
registerColumnResizer,
|
|
1949
1965
|
unregisterColumnResizer,
|
|
1950
1966
|
getColumnCount,
|
|
@@ -117,8 +117,20 @@
|
|
|
117
117
|
<Table.Column id="email" isRowHeader textValue="Email">
|
|
118
118
|
<Table.ColumnHeaderCell>Email</Table.ColumnHeaderCell>
|
|
119
119
|
</Table.Column>
|
|
120
|
-
<Table.Column id="group"
|
|
121
|
-
<Table.ColumnHeaderCell>
|
|
120
|
+
<Table.Column id="group" textValue="Group">
|
|
121
|
+
<Table.ColumnHeaderCell>
|
|
122
|
+
<Table.SortTrigger>
|
|
123
|
+
{#snippet children({ sortDirection })}
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
data-testid="group-sort-trigger"
|
|
127
|
+
data-sort-direction-state={sortDirection ?? 'none'}
|
|
128
|
+
>
|
|
129
|
+
Group
|
|
130
|
+
</button>
|
|
131
|
+
{/snippet}
|
|
132
|
+
</Table.SortTrigger>
|
|
133
|
+
</Table.ColumnHeaderCell>
|
|
122
134
|
</Table.Column>
|
|
123
135
|
</Table.Row>
|
|
124
136
|
</Table.Header>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!-- markdownlint-disable MD024 -->
|
|
2
|
+
|
|
3
|
+
# Table.SortTrigger
|
|
4
|
+
|
|
5
|
+
## API reference
|
|
6
|
+
|
|
7
|
+
### Table.SortTrigger
|
|
8
|
+
|
|
9
|
+
Name: `Table.SortTrigger`
|
|
10
|
+
Description: Headless wrapper that makes the owning `Table.Column` sortable. It must be composed inside `Table.ColumnHeaderCell` and contain a `button` or `[role="button"]` child that acts as the actual trigger element.
|
|
11
|
+
|
|
12
|
+
Public prop type: `TableSortTriggerProps`
|
|
13
|
+
|
|
14
|
+
| Prop | Type | Default | Description |
|
|
15
|
+
| ---------- | --------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| `children` | `Snippet<[TableSortTriggerRenderState]> \| Snippet` | `undefined` | Child content that includes the actual trigger button or role=button UI. The snippet receives the current `sortDirection`. |
|
|
17
|
+
|
|
18
|
+
## Usage notes
|
|
19
|
+
|
|
20
|
+
- `Table.SortTrigger` must be used inside `Table.ColumnHeaderCell`.
|
|
21
|
+
- Rendering `Table.SortTrigger` inside `Table.ColumnHeaderCell` is enough to make the owning `Table.Column` sortable.
|
|
22
|
+
- The trigger resolves the active column from `Table.Column` context. It does not accept a separate `columnId` prop.
|
|
23
|
+
- The wrapper finds the first nested `button` or `[role="button"]` and wires sorting behavior plus sort-state data attributes onto that element.
|
|
24
|
+
- `children` can read the current `sortDirection` render state to adjust accessible labels or icons without reaching into `Table.Root.sortDescriptor`.
|
|
25
|
+
- The header cell remains the roving-focus target for arrow-key grid navigation; use `Tab` to move into the nested trigger button.
|
|
26
|
+
- Use separate controls for secondary header actions such as filter popovers or menus.
|
|
27
|
+
|
|
28
|
+
```svelte
|
|
29
|
+
<Table.Column id="group" textValue="Group">
|
|
30
|
+
<Table.ColumnHeaderCell>
|
|
31
|
+
<Table.SortTrigger>
|
|
32
|
+
{#snippet children({ sortDirection })}
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
class="inline-flex items-center gap-2 rounded-sm"
|
|
36
|
+
aria-label={`Group sort button. ${sortDirection ?? 'not sorted'}.`}
|
|
37
|
+
>
|
|
38
|
+
<span>Sort group</span>
|
|
39
|
+
<SortIcon data-direction={sortDirection} />
|
|
40
|
+
</button>
|
|
41
|
+
{/snippet}
|
|
42
|
+
</Table.SortTrigger>
|
|
43
|
+
</Table.ColumnHeaderCell>
|
|
44
|
+
</Table.Column>
|
|
45
|
+
```
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { onDestroy, onMount } from 'svelte';
|
|
4
|
+
import { useTableCellContext, useTableColumnContext, useTableContext } from '../root/context';
|
|
5
|
+
import type { TableSortTriggerProps, TableSortTriggerRenderState } from '../types.js';
|
|
6
|
+
import {
|
|
7
|
+
shouldShowFocusVisible,
|
|
8
|
+
trackInteractionModality
|
|
9
|
+
} from '../../primitives/input-modality';
|
|
10
|
+
|
|
11
|
+
let { children }: TableSortTriggerProps = $props();
|
|
12
|
+
|
|
13
|
+
const table = useTableContext();
|
|
14
|
+
const column = useTableColumnContext();
|
|
15
|
+
const cell = useTableCellContext();
|
|
16
|
+
const sortVersion = table.sortVersion;
|
|
17
|
+
|
|
18
|
+
let wrapperRef = $state<HTMLElement | null>(null);
|
|
19
|
+
let activeTrigger = $state<HTMLElement | null>(null);
|
|
20
|
+
|
|
21
|
+
const sortDirection = $derived.by(() => {
|
|
22
|
+
void $sortVersion;
|
|
23
|
+
return table.getSortDirection(column.id);
|
|
24
|
+
});
|
|
25
|
+
const renderState = $derived.by<TableSortTriggerRenderState>(() => ({
|
|
26
|
+
sortDirection
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
function getTriggerElement() {
|
|
30
|
+
if (!wrapperRef) return null;
|
|
31
|
+
return wrapperRef.querySelector<HTMLElement>('button, [role="button"]');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function syncTriggerMetadata(trigger: HTMLElement | null) {
|
|
35
|
+
if (!trigger) return;
|
|
36
|
+
|
|
37
|
+
if (trigger instanceof HTMLButtonElement && !trigger.hasAttribute('type')) {
|
|
38
|
+
trigger.type = 'button';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
trigger.setAttribute('data-table-sort-trigger', 'true');
|
|
42
|
+
if (sortDirection) {
|
|
43
|
+
trigger.setAttribute('data-sorted', 'true');
|
|
44
|
+
trigger.setAttribute('data-sort-direction', sortDirection);
|
|
45
|
+
} else {
|
|
46
|
+
trigger.removeAttribute('data-sorted');
|
|
47
|
+
trigger.removeAttribute('data-sort-direction');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function refreshActiveTrigger() {
|
|
52
|
+
activeTrigger = getTriggerElement();
|
|
53
|
+
syncTriggerMetadata(activeTrigger);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$effect(() => {
|
|
57
|
+
table.registerColumnSortTrigger(column.token);
|
|
58
|
+
syncTriggerMetadata(activeTrigger);
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
table.unregisterColumnSortTrigger(column.token);
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function handleFocusIn(event: FocusEvent) {
|
|
66
|
+
const target = event.target instanceof HTMLElement ? event.target : activeTrigger;
|
|
67
|
+
table.setFocusedCell(cell.cellKey);
|
|
68
|
+
table.setFocusVisible(shouldShowFocusVisible(target ?? null));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleMouseDown(event: MouseEvent) {
|
|
72
|
+
const target = event.target as HTMLElement | null;
|
|
73
|
+
const trigger = target?.closest('button, [role="button"]') as HTMLElement | null;
|
|
74
|
+
if (!trigger || !wrapperRef?.contains(trigger)) return;
|
|
75
|
+
|
|
76
|
+
trackInteractionModality(event, trigger);
|
|
77
|
+
table.setFocusVisible(false);
|
|
78
|
+
event.stopPropagation();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleClick(event: MouseEvent) {
|
|
82
|
+
const target = event.target as HTMLElement | null;
|
|
83
|
+
const trigger = target?.closest('button, [role="button"]') as HTMLElement | null;
|
|
84
|
+
if (!trigger || !wrapperRef?.contains(trigger)) return;
|
|
85
|
+
|
|
86
|
+
activeTrigger = trigger;
|
|
87
|
+
syncTriggerMetadata(activeTrigger);
|
|
88
|
+
event.stopPropagation();
|
|
89
|
+
table.toggleSort(column.id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
93
|
+
const target = event.target as HTMLElement | null;
|
|
94
|
+
const trigger = target?.closest('button, [role="button"]') as HTMLElement | null;
|
|
95
|
+
if (!trigger || !wrapperRef?.contains(trigger)) return;
|
|
96
|
+
|
|
97
|
+
activeTrigger = trigger;
|
|
98
|
+
syncTriggerMetadata(activeTrigger);
|
|
99
|
+
trackInteractionModality(event, trigger);
|
|
100
|
+
|
|
101
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'Home') {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
event.stopPropagation();
|
|
104
|
+
table.moveToGridStart();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'End') {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
event.stopPropagation();
|
|
111
|
+
table.moveToGridEnd();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
switch (event.key) {
|
|
116
|
+
case 'ArrowUp':
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
event.stopPropagation();
|
|
119
|
+
table.moveFocus('up', {
|
|
120
|
+
shiftKey: event.shiftKey,
|
|
121
|
+
ctrlKey: event.ctrlKey,
|
|
122
|
+
metaKey: event.metaKey,
|
|
123
|
+
altKey: event.altKey
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
case 'ArrowDown':
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
event.stopPropagation();
|
|
129
|
+
table.moveFocus('down', {
|
|
130
|
+
shiftKey: event.shiftKey,
|
|
131
|
+
ctrlKey: event.ctrlKey,
|
|
132
|
+
metaKey: event.metaKey,
|
|
133
|
+
altKey: event.altKey
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
case 'ArrowLeft':
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
event.stopPropagation();
|
|
139
|
+
table.moveFocus('left');
|
|
140
|
+
return;
|
|
141
|
+
case 'ArrowRight':
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
event.stopPropagation();
|
|
144
|
+
table.moveFocus('right');
|
|
145
|
+
return;
|
|
146
|
+
case 'Home':
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
event.stopPropagation();
|
|
149
|
+
table.moveToRowStart();
|
|
150
|
+
return;
|
|
151
|
+
case 'End':
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
event.stopPropagation();
|
|
154
|
+
table.moveToRowEnd();
|
|
155
|
+
return;
|
|
156
|
+
case 'Enter':
|
|
157
|
+
case ' ':
|
|
158
|
+
event.stopPropagation();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
onMount(() => {
|
|
164
|
+
refreshActiveTrigger();
|
|
165
|
+
wrapperRef?.addEventListener('focusin', handleFocusIn);
|
|
166
|
+
wrapperRef?.addEventListener('mousedown', handleMouseDown);
|
|
167
|
+
wrapperRef?.addEventListener('click', handleClick);
|
|
168
|
+
wrapperRef?.addEventListener('keydown', handleKeyDown);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
onDestroy(() => {
|
|
172
|
+
wrapperRef?.removeEventListener('focusin', handleFocusIn);
|
|
173
|
+
wrapperRef?.removeEventListener('mousedown', handleMouseDown);
|
|
174
|
+
wrapperRef?.removeEventListener('click', handleClick);
|
|
175
|
+
wrapperRef?.removeEventListener('keydown', handleKeyDown);
|
|
176
|
+
});
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<div bind:this={wrapperRef} style="display: contents;">
|
|
180
|
+
{#if children}
|
|
181
|
+
{@render (children as Snippet<[TableSortTriggerRenderState]>)(renderState)}
|
|
182
|
+
{/if}
|
|
183
|
+
</div>
|
package/dist/table/types.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
-
import type { TableColumnWidth, TableContext, TableDisabledBehavior, TableRowActionHandler, TableSelectionBehavior, TableSelectionKey, TableSelectionMode, TableSortDescriptor } from './root/context.js';
|
|
3
|
+
import type { TableColumnWidth, TableContext, TableDisabledBehavior, TableRowActionHandler, TableSelectionBehavior, TableSelectionKey, TableSelectionMode, TableSortDirection, TableSortDescriptor } from './root/context.js';
|
|
4
4
|
export type TableColumnProps = {
|
|
5
5
|
id: string;
|
|
6
|
-
allowsSorting?: boolean;
|
|
7
6
|
isRowHeader?: boolean;
|
|
8
7
|
textValue?: string;
|
|
9
8
|
width?: TableColumnWidth;
|
|
@@ -61,6 +60,12 @@ export type TableColumnHeaderCellProps = Omit<HTMLAttributes<HTMLTableCellElemen
|
|
|
61
60
|
children?: Snippet;
|
|
62
61
|
class?: string;
|
|
63
62
|
};
|
|
63
|
+
export type TableSortTriggerRenderState = {
|
|
64
|
+
sortDirection: TableSortDirection | undefined;
|
|
65
|
+
};
|
|
66
|
+
export type TableSortTriggerProps = {
|
|
67
|
+
children?: Snippet<[TableSortTriggerRenderState]> | Snippet;
|
|
68
|
+
};
|
|
64
69
|
export type TableColumnResizerProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
65
70
|
step?: number;
|
|
66
71
|
shiftStep?: number;
|