@human-kit/svelte-components 1.0.0-alpha.15 → 1.0.0-alpha.17
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/table/PLAN.md +6 -6
- package/dist/table/README.md +4 -2
- package/dist/table/body/table-body.svelte +5 -0
- package/dist/table/cell/table-cell.svelte +17 -0
- package/dist/table/checkbox/README.md +1 -1
- package/dist/table/checkbox/table-checkbox.svelte +2 -15
- package/dist/table/checkbox-indicator/README.md +1 -1
- package/dist/table/column/README.md +11 -11
- package/dist/table/column-header-cell/table-column-header-cell.svelte +19 -16
- package/dist/table/column-resizer/table-column-resizer-fixed-width-test.svelte +57 -0
- package/dist/table/column-resizer/table-column-resizer-fixed-width-test.svelte.d.ts +3 -0
- package/dist/table/column-resizer/table-column-resizer-freeze-layout-test.svelte +2 -1
- package/dist/table/column-resizer/table-column-resizer-overflow-test.svelte +64 -0
- package/dist/table/column-resizer/table-column-resizer-overflow-test.svelte.d.ts +3 -0
- package/dist/table/column-resizer/table-column-resizer-padded-container-test.svelte +67 -0
- package/dist/table/column-resizer/table-column-resizer-padded-container-test.svelte.d.ts +3 -0
- package/dist/table/column-resizer/table-column-resizer-selection-column-test.svelte +2 -1
- package/dist/table/column-resizer/table-column-resizer-test.svelte +3 -3
- package/dist/table/column-resizer/table-column-resizer-three-column-relative-test.svelte +64 -0
- package/dist/table/column-resizer/table-column-resizer-three-column-relative-test.svelte.d.ts +3 -0
- package/dist/table/column-resizer/table-column-resizer.svelte +49 -56
- package/dist/table/root/README.md +12 -12
- package/dist/table/root/context.d.ts +13 -7
- package/dist/table/root/context.js +363 -38
- package/dist/table/root/table-root.svelte +116 -17
- package/dist/table/types.d.ts +4 -4
- package/dist/table/utils/visually-hidden-style.d.ts +1 -0
- package/dist/table/utils/visually-hidden-style.js +1 -0
- package/package.json +1 -1
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onDestroy } from 'svelte';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_TABLE_COLUMN_MIN_WIDTH,
|
|
5
|
+
getTableCellContext,
|
|
6
|
+
useTableColumnContext,
|
|
7
|
+
useTableContext
|
|
8
|
+
} from '../root/context';
|
|
4
9
|
import type { TableColumnResizerProps } from '../types.js';
|
|
10
|
+
import { visuallyHiddenStyle } from '../utils/visually-hidden-style';
|
|
5
11
|
import {
|
|
6
12
|
shouldShowFocusVisible,
|
|
7
13
|
trackInteractionModality
|
|
@@ -59,7 +65,7 @@
|
|
|
59
65
|
});
|
|
60
66
|
const minWidth = $derived.by(() => {
|
|
61
67
|
void $widthVersion;
|
|
62
|
-
return table.getColumnMinWidth(column.id) ??
|
|
68
|
+
return table.getColumnMinWidth(column.id) ?? DEFAULT_TABLE_COLUMN_MIN_WIDTH;
|
|
63
69
|
});
|
|
64
70
|
const maxWidth = $derived.by(() => {
|
|
65
71
|
void $widthVersion;
|
|
@@ -267,16 +273,16 @@
|
|
|
267
273
|
suppressNextDoubleClickAutofit = false;
|
|
268
274
|
|
|
269
275
|
stopKeyboardResizeMode();
|
|
270
|
-
table.startColumnResize(column.id);
|
|
271
276
|
|
|
272
|
-
const th = element?.closest('th') as HTMLElement | null;
|
|
273
|
-
const tableEl = th?.closest('table') as HTMLTableElement | null;
|
|
274
|
-
const startX = event.clientX;
|
|
275
277
|
const startWidth = table.getColumnWidth(column.id) ?? getHeaderWidth();
|
|
276
278
|
const pointerId = event.pointerId;
|
|
277
279
|
const isRTL = isRightToLeft();
|
|
280
|
+
const pointerStartClientX = event.clientX;
|
|
278
281
|
let didDrag = false;
|
|
279
|
-
let
|
|
282
|
+
let didStartResize = false;
|
|
283
|
+
let latestClientX = event.clientX;
|
|
284
|
+
let previousClientX = event.clientX;
|
|
285
|
+
let dragWidth = startWidth;
|
|
280
286
|
let animationFrameId: number | null = null;
|
|
281
287
|
|
|
282
288
|
// Capture the pointer so we receive move/up events even if the cursor
|
|
@@ -298,30 +304,27 @@
|
|
|
298
304
|
return clamped;
|
|
299
305
|
}
|
|
300
306
|
|
|
301
|
-
function applyTemporaryWidthToDOM(width: number) {
|
|
302
|
-
if (th) th.style.width = `${width}px`;
|
|
303
|
-
if (tableEl) {
|
|
304
|
-
const allThs = tableEl.querySelectorAll<HTMLElement>('thead th[style*="width"]');
|
|
305
|
-
let total = 0;
|
|
306
|
-
for (const cell of allThs) {
|
|
307
|
-
total += parseFloat(cell.style.width) || 0;
|
|
308
|
-
}
|
|
309
|
-
if (total > 0) {
|
|
310
|
-
tableEl.style.width = `${total}px`;
|
|
311
|
-
tableEl.style.minWidth = '0';
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
307
|
function flushPendingPointerMove() {
|
|
317
308
|
if (animationFrameId !== null) {
|
|
318
309
|
cancelAnimationFrame(animationFrameId);
|
|
319
310
|
animationFrameId = null;
|
|
320
311
|
}
|
|
321
312
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
313
|
+
if (!didStartResize && latestClientX === pointerStartClientX) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!didStartResize) {
|
|
318
|
+
table.startColumnResize(column.id);
|
|
319
|
+
didStartResize = true;
|
|
320
|
+
previousClientX = pointerStartClientX;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const pointerDelta = latestClientX - previousClientX;
|
|
324
|
+
previousClientX = latestClientX;
|
|
325
|
+
const widthDelta = isRTL ? -pointerDelta : pointerDelta;
|
|
326
|
+
dragWidth = clampWidth(dragWidth + widthDelta);
|
|
327
|
+
const nextWidth = dragWidth;
|
|
325
328
|
updateWidth(nextWidth);
|
|
326
329
|
}
|
|
327
330
|
|
|
@@ -333,30 +336,14 @@
|
|
|
333
336
|
});
|
|
334
337
|
}
|
|
335
338
|
|
|
336
|
-
// Measure position compensation factor.
|
|
337
|
-
// In centered/flex layouts, growing a column shifts the table's left edge,
|
|
338
|
-
// so the handle moves less than the mouse delta. We detect this by applying
|
|
339
|
-
// a 1px test change and measuring how much the <th> left edge drifts.
|
|
340
|
-
// NOTE: applyTemporaryWidthToDOM intentionally mutates the DOM synchronously
|
|
341
|
-
// outside Svelte's reactive cycle. The mutation is immediately reverted
|
|
342
|
-
// within the same microtask, so no observer or $effect will see it.
|
|
343
|
-
let positionScale = 1;
|
|
344
|
-
if (th) {
|
|
345
|
-
const leftBefore = th.getBoundingClientRect().left;
|
|
346
|
-
applyTemporaryWidthToDOM(startWidth + 1);
|
|
347
|
-
const leftAfter = th.getBoundingClientRect().left;
|
|
348
|
-
applyTemporaryWidthToDOM(startWidth);
|
|
349
|
-
const drift = leftBefore - leftAfter;
|
|
350
|
-
if (drift > 0.01 && drift < 0.99) {
|
|
351
|
-
positionScale = 1 / (1 - drift);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
339
|
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
356
340
|
if (moveEvent.pointerId !== pointerId) return;
|
|
357
341
|
moveEvent.preventDefault();
|
|
358
|
-
didDrag = true;
|
|
359
342
|
latestClientX = moveEvent.clientX;
|
|
343
|
+
if (!didStartResize && latestClientX === pointerStartClientX) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
didDrag = true;
|
|
360
347
|
schedulePointerMove();
|
|
361
348
|
};
|
|
362
349
|
|
|
@@ -385,7 +372,9 @@
|
|
|
385
372
|
}
|
|
386
373
|
// Treat system-initiated cancellation the same as Escape:
|
|
387
374
|
// restore the width the column had before the drag started.
|
|
388
|
-
|
|
375
|
+
if (didStartResize) {
|
|
376
|
+
updateWidth(startWidth);
|
|
377
|
+
}
|
|
389
378
|
cleanupPointerListeners();
|
|
390
379
|
};
|
|
391
380
|
|
|
@@ -398,7 +387,9 @@
|
|
|
398
387
|
cancelAnimationFrame(animationFrameId);
|
|
399
388
|
animationFrameId = null;
|
|
400
389
|
}
|
|
401
|
-
|
|
390
|
+
if (didStartResize) {
|
|
391
|
+
updateWidth(startWidth);
|
|
392
|
+
}
|
|
402
393
|
cleanupPointerListeners();
|
|
403
394
|
};
|
|
404
395
|
|
|
@@ -428,6 +419,7 @@
|
|
|
428
419
|
}
|
|
429
420
|
|
|
430
421
|
function handleClick(event: MouseEvent) {
|
|
422
|
+
if (!isResizable) return;
|
|
431
423
|
event.preventDefault();
|
|
432
424
|
event.stopPropagation();
|
|
433
425
|
}
|
|
@@ -554,18 +546,19 @@
|
|
|
554
546
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
555
547
|
<div
|
|
556
548
|
bind:this={element}
|
|
557
|
-
role=
|
|
549
|
+
role={isResizable ? 'separator' : undefined}
|
|
558
550
|
tabindex={isResizable ? 0 : undefined}
|
|
559
551
|
class={className}
|
|
560
|
-
aria-label={accessibleLabel}
|
|
561
|
-
aria-orientation=
|
|
562
|
-
aria-valuenow={currentWidth ?? undefined}
|
|
563
|
-
aria-valuemin={minWidth}
|
|
564
|
-
aria-valuemax={maxWidth}
|
|
565
|
-
aria-valuetext={accessibleValueText}
|
|
552
|
+
aria-label={isResizable ? accessibleLabel : undefined}
|
|
553
|
+
aria-orientation={isResizable ? 'vertical' : undefined}
|
|
554
|
+
aria-valuenow={isResizable ? (currentWidth ?? undefined) : undefined}
|
|
555
|
+
aria-valuemin={isResizable ? minWidth : undefined}
|
|
556
|
+
aria-valuemax={isResizable ? maxWidth : undefined}
|
|
557
|
+
aria-valuetext={isResizable ? accessibleValueText : undefined}
|
|
566
558
|
data-focused={isFocused ? 'true' : undefined}
|
|
567
559
|
data-focus-visible={isFocusVisible ? 'true' : undefined}
|
|
568
560
|
data-resizing={isResizing ? 'true' : undefined}
|
|
561
|
+
data-resizable={isResizable ? 'true' : undefined}
|
|
569
562
|
data-table-column-resizer="true"
|
|
570
563
|
data-resizable-direction="right"
|
|
571
564
|
style:position="absolute"
|
|
@@ -579,6 +572,7 @@
|
|
|
579
572
|
style:align-items="center"
|
|
580
573
|
style:justify-content="center"
|
|
581
574
|
style:user-select="none"
|
|
575
|
+
style:pointer-events={isResizable ? 'auto' : 'none'}
|
|
582
576
|
style:touch-action="none"
|
|
583
577
|
onpointerdown={handlePointerDown}
|
|
584
578
|
ondblclick={handleDoubleClick}
|
|
@@ -601,8 +595,7 @@
|
|
|
601
595
|
role="status"
|
|
602
596
|
aria-live="polite"
|
|
603
597
|
aria-atomic="true"
|
|
604
|
-
style=
|
|
605
|
-
>{resizeAnnouncement}</span
|
|
598
|
+
style={visuallyHiddenStyle}>{resizeAnnouncement}</span
|
|
606
599
|
>
|
|
607
600
|
</div>
|
|
608
601
|
{/if}
|
|
@@ -23,16 +23,16 @@ Public prop type: `TableRootProps`
|
|
|
23
23
|
| `defaultSelectedKeys` | `Iterable<string \| number>` | `undefined` | Initial selected row ids for uncontrolled usage. When `selectionMode` later becomes `none`, the internal selection is cleared. |
|
|
24
24
|
| `sortDescriptor` | `{ column: string; direction: 'ascending' \| 'descending' }` | `undefined` | Controlled sort state. Supports `bind:sortDescriptor`. Setting it back to `undefined` clears the sort. |
|
|
25
25
|
| `defaultSortDescriptor` | `{ column: string; direction: 'ascending' \| 'descending' }` | `undefined` | Initial sort state for uncontrolled usage. |
|
|
26
|
-
| `columnWidths` | `Map<string,
|
|
27
|
-
| `defaultColumnWidths` | `Iterable<[string,
|
|
26
|
+
| `columnWidths` | `Map<string, TableColumnWidth>` | `undefined` | Controlled column width state. Supports px, `%`, and `fr` specs via `bind:columnWidths`. |
|
|
27
|
+
| `defaultColumnWidths` | `Iterable<[string, TableColumnWidth]>` | `undefined` | Initial uncontrolled column widths using px, `%`, or `fr` specs. |
|
|
28
28
|
| `disabledKeys` | `Iterable<string \| number>` | `undefined` | Row ids that should be non-selectable. |
|
|
29
29
|
| `onRowAction` | `(id: string \| number) => void` | `undefined` | Called when a row action is triggered for an actionable row. |
|
|
30
30
|
| `onSelectionChange` | `(keys: Set<string \| number>) => void` | `undefined` | Called when row selection changes. |
|
|
31
31
|
| `onSortChange` | `(descriptor) => void` | `undefined` | Called when sortable header state changes. |
|
|
32
|
-
| `onColumnWidthsChange` | `(widths: Map<string,
|
|
32
|
+
| `onColumnWidthsChange` | `(widths: Map<string, TableColumnWidth>) => void` | `undefined` | Called when resize interactions update managed column widths. |
|
|
33
33
|
| `onHiddenColumnsChange` | `(columnIds: string[]) => void` | `undefined` | Called when hidden column state changes. |
|
|
34
34
|
| `onColumnResizeStart` | `(columnId: string) => void` | `undefined` | Called when a column resize drag starts. |
|
|
35
|
-
| `onColumnResizeEnd` | `(widths: Map<string,
|
|
35
|
+
| `onColumnResizeEnd` | `(widths: Map<string, TableColumnWidth>) => void` | `undefined` | Called when a column resize drag ends. |
|
|
36
36
|
| `aria-label` | `string` | `undefined` | Accessible name when no external label is present. |
|
|
37
37
|
| `aria-labelledby` | `string` | `undefined` | Id reference for an external label. |
|
|
38
38
|
| `class` | `string` | `''` | Class names for the root table element. |
|
|
@@ -45,14 +45,14 @@ Public prop type: `TableRootProps`
|
|
|
45
45
|
Name: Selection and sorting notes
|
|
46
46
|
Description: Current v1 interaction constraints that affect consumer expectations.
|
|
47
47
|
|
|
48
|
-
| Topic | Behavior
|
|
49
|
-
| ----------------------- |
|
|
50
|
-
| `selectionMode="none"` | Clears existing row selection internally and prevents further row selection until another mode is set.
|
|
51
|
-
| Text selection and copy | Browser-native text selection and `Ctrl+C` behavior are preserved; the component does not implement custom clipboard logic.
|
|
52
|
-
| `replace` mode blur | Clicking or tabbing outside the table clears focus state but does not clear row selection.
|
|
53
|
-
| Sort announcements | Sort changes are mirrored into a polite live region. Use `Table.Column.textValue` when the announced label should differ from the column `id`.
|
|
54
|
-
| Column resizing | Width state
|
|
55
|
-
| Row edge focus | In body rows, `ArrowLeft` before the first cell and `ArrowRight` after the last cell move focus onto the row itself; `ArrowUp` / `ArrowDown` keep that row-edge focus aligned across rows, repeating the same horizontal arrow loops back into the opposite edge cell, and `Home` / `End` jump to the first or last focusable body row while row focus is active.
|
|
48
|
+
| Topic | Behavior |
|
|
49
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `selectionMode="none"` | Clears existing row selection internally and prevents further row selection until another mode is set. |
|
|
51
|
+
| Text selection and copy | Browser-native text selection and `Ctrl+C` behavior are preserved; the component does not implement custom clipboard logic. |
|
|
52
|
+
| `replace` mode blur | Clicking or tabbing outside the table clears focus state but does not clear row selection. |
|
|
53
|
+
| Sort announcements | Sort changes are mirrored into a polite live region. Use `Table.Column.textValue` when the announced label should differ from the column `id`. |
|
|
54
|
+
| Column resizing | Width state accepts px, `%`, and `fr` specs. Before interaction, unspecified columns behave like implicit flexible space. On the first real resize, visible columns are converted to px; the trailing column absorbs the delta until its minimum width, and additional growth overflows the table horizontally. A `Table.ColumnResizer` only affects the `Table.Column` it is composed within. |
|
|
55
|
+
| Row edge focus | In body rows, `ArrowLeft` before the first cell and `ArrowRight` after the last cell move focus onto the row itself; `ArrowUp` / `ArrowDown` keep that row-edge focus aligned across rows, repeating the same horizontal arrow loops back into the opposite edge cell, and `Home` / `End` jump to the first or last focusable body row while row focus is active. |
|
|
56
56
|
|
|
57
57
|
### Context utilities
|
|
58
58
|
|
|
@@ -17,7 +17,8 @@ export type TableSortDescriptor = {
|
|
|
17
17
|
column: string;
|
|
18
18
|
direction: TableSortDirection;
|
|
19
19
|
};
|
|
20
|
-
export type TableColumnWidth = number | `${number}px`;
|
|
20
|
+
export type TableColumnWidth = number | `${number}px` | `${number}%` | `${number}fr`;
|
|
21
|
+
export declare const DEFAULT_TABLE_COLUMN_MIN_WIDTH = 60;
|
|
21
22
|
export type TableGridCoord = {
|
|
22
23
|
row: number;
|
|
23
24
|
col: number;
|
|
@@ -58,16 +59,16 @@ export type CreateTableContextOptions = {
|
|
|
58
59
|
disallowEmptySelection?: boolean;
|
|
59
60
|
initialSelectedKeys?: Iterable<TableSelectionKey>;
|
|
60
61
|
initialSortDescriptor?: TableSortDescriptor;
|
|
61
|
-
initialColumnWidths?: Iterable<readonly [string,
|
|
62
|
+
initialColumnWidths?: Iterable<readonly [string, TableColumnWidth]>;
|
|
62
63
|
initialHiddenColumns?: Iterable<string>;
|
|
63
64
|
disabledKeys?: Iterable<TableSelectionKey>;
|
|
64
65
|
onRowAction?: TableRowActionHandler;
|
|
65
66
|
onSelectionChange?: (keys: Set<TableSelectionKey>) => void;
|
|
66
67
|
onSortChange?: (descriptor: TableSortDescriptor | undefined) => void;
|
|
67
|
-
onColumnWidthsChange?: (widths: Map<string,
|
|
68
|
+
onColumnWidthsChange?: (widths: Map<string, TableColumnWidth>) => void;
|
|
68
69
|
onHiddenColumnsChange?: (columnIds: string[]) => void;
|
|
69
70
|
onColumnResizeStart?: (columnId: string) => void;
|
|
70
|
-
onColumnResizeEnd?: (widths: Map<string,
|
|
71
|
+
onColumnResizeEnd?: (widths: Map<string, TableColumnWidth>) => void;
|
|
71
72
|
};
|
|
72
73
|
export type TableContext = {
|
|
73
74
|
layoutVersion: Readable<number>;
|
|
@@ -98,13 +99,17 @@ export type TableContext = {
|
|
|
98
99
|
getVisibleColumnIndexByToken: (token: string) => number;
|
|
99
100
|
getColumnTextValue: (columnId: string) => string | undefined;
|
|
100
101
|
getColumnWidth: (columnId: string) => number | undefined;
|
|
102
|
+
getColumnWidthStyle: (columnId: string) => string | undefined;
|
|
103
|
+
hasAuthoredColumnWidthSpec: (columnId: string) => boolean;
|
|
101
104
|
getColumnMinWidth: (columnId: string) => number | undefined;
|
|
102
105
|
getColumnMaxWidth: (columnId: string) => number | undefined;
|
|
103
106
|
isColumnHidden: (columnId: string) => boolean;
|
|
104
107
|
isColumnResizable: (columnId: string) => boolean;
|
|
105
|
-
getColumnWidths: () => Map<string,
|
|
106
|
-
getVisibleColumnWidths: () => Map<string,
|
|
107
|
-
|
|
108
|
+
getColumnWidths: () => Map<string, TableColumnWidth>;
|
|
109
|
+
getVisibleColumnWidths: () => Map<string, TableColumnWidth>;
|
|
110
|
+
getResolvedVisibleColumnWidths: () => Map<string, number>;
|
|
111
|
+
hasRelativeVisibleColumnWidths: () => boolean;
|
|
112
|
+
setColumnWidths: (widths?: Iterable<readonly [string, TableColumnWidth]>) => void;
|
|
108
113
|
setColumnWidth: (columnId: string, width: number) => void;
|
|
109
114
|
setHiddenColumns: (columnIds?: Iterable<string>) => void;
|
|
110
115
|
measureColumnContentWidth: (columnId: string) => number | undefined;
|
|
@@ -115,6 +120,7 @@ export type TableContext = {
|
|
|
115
120
|
hasResizableColumns: () => boolean;
|
|
116
121
|
registerRow: (row: TableRowRegistration) => void;
|
|
117
122
|
unregisterRow: (token: string) => void;
|
|
123
|
+
markBodyRowsInitialized: () => void;
|
|
118
124
|
getHeaderRowCount: () => number;
|
|
119
125
|
getBodyRowCount: () => number;
|
|
120
126
|
isRowSelected: (id: TableSelectionKey | undefined) => boolean;
|