@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.
Files changed (27) hide show
  1. package/dist/table/PLAN.md +6 -6
  2. package/dist/table/README.md +4 -2
  3. package/dist/table/body/table-body.svelte +5 -0
  4. package/dist/table/cell/table-cell.svelte +17 -0
  5. package/dist/table/checkbox/README.md +1 -1
  6. package/dist/table/checkbox/table-checkbox.svelte +2 -15
  7. package/dist/table/checkbox-indicator/README.md +1 -1
  8. package/dist/table/column/README.md +11 -11
  9. package/dist/table/column-header-cell/table-column-header-cell.svelte +19 -16
  10. package/dist/table/column-resizer/table-column-resizer-fixed-width-test.svelte +57 -0
  11. package/dist/table/column-resizer/table-column-resizer-fixed-width-test.svelte.d.ts +3 -0
  12. package/dist/table/column-resizer/table-column-resizer-freeze-layout-test.svelte +2 -1
  13. package/dist/table/column-resizer/table-column-resizer-overflow-test.svelte +64 -0
  14. package/dist/table/column-resizer/table-column-resizer-overflow-test.svelte.d.ts +3 -0
  15. package/dist/table/column-resizer/table-column-resizer-padded-container-test.svelte +67 -0
  16. package/dist/table/column-resizer/table-column-resizer-padded-container-test.svelte.d.ts +3 -0
  17. package/dist/table/column-resizer/table-column-resizer-selection-column-test.svelte +2 -1
  18. package/dist/table/column-resizer/table-column-resizer-test.svelte +3 -3
  19. package/dist/table/column-resizer/table-column-resizer-three-column-relative-test.svelte +64 -0
  20. package/dist/table/column-resizer/table-column-resizer-three-column-relative-test.svelte.d.ts +3 -0
  21. package/dist/table/column-resizer/table-column-resizer.svelte +47 -54
  22. package/dist/table/root/README.md +12 -12
  23. package/dist/table/root/context.d.ts +13 -7
  24. package/dist/table/root/context.js +363 -38
  25. package/dist/table/root/table-root.svelte +113 -9
  26. package/dist/table/types.d.ts +4 -4
  27. package/package.json +1 -1
@@ -1,6 +1,11 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy } from 'svelte';
3
- import { getTableCellContext, useTableColumnContext, useTableContext } from '../root/context';
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) ?? 75;
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 latestClientX = startX;
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
- const direction = isRTL ? -1 : 1;
324
- const delta = (latestClientX - startX) * positionScale * direction;
325
- const nextWidth = clampWidth(startWidth + delta);
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
- updateWidth(startWidth);
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
- updateWidth(startWidth);
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="separator"
549
+ role={isResizable ? 'separator' : undefined}
559
550
  tabindex={isResizable ? 0 : undefined}
560
551
  class={className}
561
- aria-label={accessibleLabel}
562
- aria-orientation="vertical"
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, number>` | `undefined` | Controlled column width state in px. Supports `bind:columnWidths`. |
27
- | `defaultColumnWidths` | `Iterable<[string, number]>` | `undefined` | Initial uncontrolled column widths in px. |
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, number>) => void` | `undefined` | Called when resize interactions update column widths. |
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, number>) => void` | `undefined` | Called when a column resize drag ends. |
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 is normalized to px values and 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. |
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, number]>;
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, number>) => void;
68
+ onColumnWidthsChange?: (widths: Map<string, TableColumnWidth>) => void;
68
69
  onHiddenColumnsChange?: (columnIds: string[]) => void;
69
70
  onColumnResizeStart?: (columnId: string) => void;
70
- onColumnResizeEnd?: (widths: Map<string, number>) => void;
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, number>;
106
- getVisibleColumnWidths: () => Map<string, number>;
107
- setColumnWidths: (widths?: Iterable<readonly [string, number]>) => void;
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;