@human-kit/svelte-components 1.0.0-alpha.16 → 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 +47 -54
- 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 +113 -9
- package/dist/table/types.d.ts +4 -4
- package/package.json +1 -1
|
@@ -1,6 +1,11 @@
|
|
|
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';
|
|
5
10
|
import { visuallyHiddenStyle } from '../utils/visually-hidden-style';
|
|
6
11
|
import {
|
|
@@ -60,7 +65,7 @@
|
|
|
60
65
|
});
|
|
61
66
|
const minWidth = $derived.by(() => {
|
|
62
67
|
void $widthVersion;
|
|
63
|
-
return table.getColumnMinWidth(column.id) ??
|
|
68
|
+
return table.getColumnMinWidth(column.id) ?? DEFAULT_TABLE_COLUMN_MIN_WIDTH;
|
|
64
69
|
});
|
|
65
70
|
const maxWidth = $derived.by(() => {
|
|
66
71
|
void $widthVersion;
|
|
@@ -268,16 +273,16 @@
|
|
|
268
273
|
suppressNextDoubleClickAutofit = false;
|
|
269
274
|
|
|
270
275
|
stopKeyboardResizeMode();
|
|
271
|
-
table.startColumnResize(column.id);
|
|
272
276
|
|
|
273
|
-
const th = element?.closest('th') as HTMLElement | null;
|
|
274
|
-
const tableEl = th?.closest('table') as HTMLTableElement | null;
|
|
275
|
-
const startX = event.clientX;
|
|
276
277
|
const startWidth = table.getColumnWidth(column.id) ?? getHeaderWidth();
|
|
277
278
|
const pointerId = event.pointerId;
|
|
278
279
|
const isRTL = isRightToLeft();
|
|
280
|
+
const pointerStartClientX = event.clientX;
|
|
279
281
|
let didDrag = false;
|
|
280
|
-
let
|
|
282
|
+
let didStartResize = false;
|
|
283
|
+
let latestClientX = event.clientX;
|
|
284
|
+
let previousClientX = event.clientX;
|
|
285
|
+
let dragWidth = startWidth;
|
|
281
286
|
let animationFrameId: number | null = null;
|
|
282
287
|
|
|
283
288
|
// Capture the pointer so we receive move/up events even if the cursor
|
|
@@ -299,30 +304,27 @@
|
|
|
299
304
|
return clamped;
|
|
300
305
|
}
|
|
301
306
|
|
|
302
|
-
function applyTemporaryWidthToDOM(width: number) {
|
|
303
|
-
if (th) th.style.width = `${width}px`;
|
|
304
|
-
if (tableEl) {
|
|
305
|
-
const allThs = tableEl.querySelectorAll<HTMLElement>('thead th[style*="width"]');
|
|
306
|
-
let total = 0;
|
|
307
|
-
for (const cell of allThs) {
|
|
308
|
-
total += parseFloat(cell.style.width) || 0;
|
|
309
|
-
}
|
|
310
|
-
if (total > 0) {
|
|
311
|
-
tableEl.style.width = `${total}px`;
|
|
312
|
-
tableEl.style.minWidth = '0';
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
307
|
function flushPendingPointerMove() {
|
|
318
308
|
if (animationFrameId !== null) {
|
|
319
309
|
cancelAnimationFrame(animationFrameId);
|
|
320
310
|
animationFrameId = null;
|
|
321
311
|
}
|
|
322
312
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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;
|
|
326
328
|
updateWidth(nextWidth);
|
|
327
329
|
}
|
|
328
330
|
|
|
@@ -334,30 +336,14 @@
|
|
|
334
336
|
});
|
|
335
337
|
}
|
|
336
338
|
|
|
337
|
-
// Measure position compensation factor.
|
|
338
|
-
// In centered/flex layouts, growing a column shifts the table's left edge,
|
|
339
|
-
// so the handle moves less than the mouse delta. We detect this by applying
|
|
340
|
-
// a 1px test change and measuring how much the <th> left edge drifts.
|
|
341
|
-
// NOTE: applyTemporaryWidthToDOM intentionally mutates the DOM synchronously
|
|
342
|
-
// outside Svelte's reactive cycle. The mutation is immediately reverted
|
|
343
|
-
// within the same microtask, so no observer or $effect will see it.
|
|
344
|
-
let positionScale = 1;
|
|
345
|
-
if (th) {
|
|
346
|
-
const leftBefore = th.getBoundingClientRect().left;
|
|
347
|
-
applyTemporaryWidthToDOM(startWidth + 1);
|
|
348
|
-
const leftAfter = th.getBoundingClientRect().left;
|
|
349
|
-
applyTemporaryWidthToDOM(startWidth);
|
|
350
|
-
const drift = leftBefore - leftAfter;
|
|
351
|
-
if (drift > 0.01 && drift < 0.99) {
|
|
352
|
-
positionScale = 1 / (1 - drift);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
339
|
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
357
340
|
if (moveEvent.pointerId !== pointerId) return;
|
|
358
341
|
moveEvent.preventDefault();
|
|
359
|
-
didDrag = true;
|
|
360
342
|
latestClientX = moveEvent.clientX;
|
|
343
|
+
if (!didStartResize && latestClientX === pointerStartClientX) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
didDrag = true;
|
|
361
347
|
schedulePointerMove();
|
|
362
348
|
};
|
|
363
349
|
|
|
@@ -386,7 +372,9 @@
|
|
|
386
372
|
}
|
|
387
373
|
// Treat system-initiated cancellation the same as Escape:
|
|
388
374
|
// restore the width the column had before the drag started.
|
|
389
|
-
|
|
375
|
+
if (didStartResize) {
|
|
376
|
+
updateWidth(startWidth);
|
|
377
|
+
}
|
|
390
378
|
cleanupPointerListeners();
|
|
391
379
|
};
|
|
392
380
|
|
|
@@ -399,7 +387,9 @@
|
|
|
399
387
|
cancelAnimationFrame(animationFrameId);
|
|
400
388
|
animationFrameId = null;
|
|
401
389
|
}
|
|
402
|
-
|
|
390
|
+
if (didStartResize) {
|
|
391
|
+
updateWidth(startWidth);
|
|
392
|
+
}
|
|
403
393
|
cleanupPointerListeners();
|
|
404
394
|
};
|
|
405
395
|
|
|
@@ -429,6 +419,7 @@
|
|
|
429
419
|
}
|
|
430
420
|
|
|
431
421
|
function handleClick(event: MouseEvent) {
|
|
422
|
+
if (!isResizable) return;
|
|
432
423
|
event.preventDefault();
|
|
433
424
|
event.stopPropagation();
|
|
434
425
|
}
|
|
@@ -555,18 +546,19 @@
|
|
|
555
546
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
556
547
|
<div
|
|
557
548
|
bind:this={element}
|
|
558
|
-
role=
|
|
549
|
+
role={isResizable ? 'separator' : undefined}
|
|
559
550
|
tabindex={isResizable ? 0 : undefined}
|
|
560
551
|
class={className}
|
|
561
|
-
aria-label={accessibleLabel}
|
|
562
|
-
aria-orientation=
|
|
563
|
-
aria-valuenow={currentWidth ?? undefined}
|
|
564
|
-
aria-valuemin={minWidth}
|
|
565
|
-
aria-valuemax={maxWidth}
|
|
566
|
-
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}
|
|
567
558
|
data-focused={isFocused ? 'true' : undefined}
|
|
568
559
|
data-focus-visible={isFocusVisible ? 'true' : undefined}
|
|
569
560
|
data-resizing={isResizing ? 'true' : undefined}
|
|
561
|
+
data-resizable={isResizable ? 'true' : undefined}
|
|
570
562
|
data-table-column-resizer="true"
|
|
571
563
|
data-resizable-direction="right"
|
|
572
564
|
style:position="absolute"
|
|
@@ -580,6 +572,7 @@
|
|
|
580
572
|
style:align-items="center"
|
|
581
573
|
style:justify-content="center"
|
|
582
574
|
style:user-select="none"
|
|
575
|
+
style:pointer-events={isResizable ? 'auto' : 'none'}
|
|
583
576
|
style:touch-action="none"
|
|
584
577
|
onpointerdown={handlePointerDown}
|
|
585
578
|
ondblclick={handleDoubleClick}
|
|
@@ -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;
|