@human-kit/svelte-components 1.0.0-alpha.16 → 1.0.0-alpha.18

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 (32) hide show
  1. package/dist/checkbox/root/checkbox-root.svelte +9 -4
  2. package/dist/checkbox/root/checkbox-root.svelte.d.ts +4 -1
  3. package/dist/table/PLAN.md +6 -6
  4. package/dist/table/README.md +4 -2
  5. package/dist/table/body/table-body.svelte +5 -0
  6. package/dist/table/cell/table-cell.svelte +18 -1
  7. package/dist/table/checkbox/README.md +1 -1
  8. package/dist/table/checkbox/table-checkbox.svelte +5 -48
  9. package/dist/table/checkbox-indicator/README.md +1 -1
  10. package/dist/table/column/README.md +11 -11
  11. package/dist/table/column-header-cell/table-column-header-cell.svelte +20 -17
  12. package/dist/table/column-resizer/table-column-resizer-fixed-width-test.svelte +57 -0
  13. package/dist/table/column-resizer/table-column-resizer-fixed-width-test.svelte.d.ts +3 -0
  14. package/dist/table/column-resizer/table-column-resizer-freeze-layout-test.svelte +2 -1
  15. package/dist/table/column-resizer/table-column-resizer-overflow-test.svelte +64 -0
  16. package/dist/table/column-resizer/table-column-resizer-overflow-test.svelte.d.ts +3 -0
  17. package/dist/table/column-resizer/table-column-resizer-padded-container-test.svelte +67 -0
  18. package/dist/table/column-resizer/table-column-resizer-padded-container-test.svelte.d.ts +3 -0
  19. package/dist/table/column-resizer/table-column-resizer-sandbox-overflow-test.svelte +87 -0
  20. package/dist/table/column-resizer/table-column-resizer-sandbox-overflow-test.svelte.d.ts +3 -0
  21. package/dist/table/column-resizer/table-column-resizer-selection-column-test.svelte +2 -1
  22. package/dist/table/column-resizer/table-column-resizer-test.svelte +3 -3
  23. package/dist/table/column-resizer/table-column-resizer-three-column-relative-test.svelte +64 -0
  24. package/dist/table/column-resizer/table-column-resizer-three-column-relative-test.svelte.d.ts +3 -0
  25. package/dist/table/column-resizer/table-column-resizer.svelte +47 -54
  26. package/dist/table/root/README.md +12 -12
  27. package/dist/table/root/context.d.ts +24 -7
  28. package/dist/table/root/context.js +506 -43
  29. package/dist/table/root/table-root.svelte +140 -9
  30. package/dist/table/row/table-row.svelte +20 -60
  31. package/dist/table/types.d.ts +4 -4
  32. package/package.json +1 -1
@@ -21,6 +21,7 @@
21
21
  import {
22
22
  createTableContext,
23
23
  setTableContext,
24
+ type TableColumnWidth,
24
25
  type TableSelectionKey,
25
26
  type TableSortDescriptor
26
27
  } from './context';
@@ -60,7 +61,7 @@
60
61
  let focusVisible = $state(false);
61
62
  let pendingControlledHiddenColumns = $state<string[] | null>(null);
62
63
  let pendingControlledSelection = $state<Set<TableSelectionKey> | null>(null);
63
- let pendingControlledColumnWidths = $state<Map<string, number> | null>(null);
64
+ let pendingControlledColumnWidths = $state<Map<string, TableColumnWidth> | null>(null);
64
65
  let sortAnnouncement = $state('');
65
66
  let hasObservedSortState = $state(false);
66
67
  let hasInitializedSortSync = $state(false);
@@ -137,7 +138,10 @@
137
138
  return true;
138
139
  }
139
140
 
140
- function hasSameColumnWidths(left: Map<string, number>, right: Map<string, number>) {
141
+ function hasSameColumnWidths(
142
+ left: Map<string, TableColumnWidth>,
143
+ right: Map<string, TableColumnWidth>
144
+ ) {
141
145
  if (left.size !== right.size) return false;
142
146
  for (const [key, value] of left) {
143
147
  if (right.get(key) !== value) return false;
@@ -164,7 +168,52 @@
164
168
  return ctx.hasResizableColumns();
165
169
  });
166
170
 
171
+ const hasDefinedColumnWidths = $derived.by(() => {
172
+ void $layoutVersion;
173
+ for (let index = 0; index < ctx.getColumnCount(); index += 1) {
174
+ const column = ctx.getColumnAt(index);
175
+ if (!column || ctx.isColumnHidden(column.id)) continue;
176
+ if (column.width !== undefined || column.defaultWidth !== undefined) {
177
+ return true;
178
+ }
179
+ }
180
+ return false;
181
+ });
182
+
183
+ const layoutColumns = $derived.by(() => {
184
+ void $layoutVersion;
185
+ void $widthVersion;
186
+
187
+ const columns: Array<{
188
+ id: string;
189
+ width: number | undefined;
190
+ widthStyle: string | undefined;
191
+ implicitWidth: boolean;
192
+ minWidth: number | undefined;
193
+ maxWidth: number | undefined;
194
+ }> = [];
195
+
196
+ for (let index = 0; index < ctx.getColumnCount(); index += 1) {
197
+ const column = ctx.getColumnAt(index);
198
+ if (!column || ctx.isColumnHidden(column.id)) continue;
199
+
200
+ columns.push({
201
+ id: column.id,
202
+ width: ctx.getColumnWidth(column.id),
203
+ widthStyle: ctx.getColumnWidthStyle(column.id),
204
+ implicitWidth: !ctx.hasAuthoredColumnWidthSpec(column.id),
205
+ minWidth: ctx.getColumnMinWidth(column.id),
206
+ maxWidth: ctx.getColumnMaxWidth(column.id)
207
+ });
208
+ }
209
+
210
+ return columns;
211
+ });
212
+
167
213
  const explicitManagedTableWidth = $derived.by(() => {
214
+ void $layoutVersion;
215
+ if (ctx.hasRelativeVisibleColumnWidths()) return undefined;
216
+
168
217
  const widths = columnWidths ?? defaultColumnWidths;
169
218
  if (!widths) return undefined;
170
219
 
@@ -172,8 +221,10 @@
172
221
  let hasAnyWidth = false;
173
222
  for (const [columnId, width] of widths) {
174
223
  if (ctx.isColumnHidden(columnId)) continue;
175
- if (!Number.isFinite(width)) continue;
176
- total += width;
224
+ if (typeof width === 'string' && !width.trim().endsWith('px')) return undefined;
225
+ const numericWidth = typeof width === 'number' ? width : Number.parseFloat(width);
226
+ if (!Number.isFinite(numericWidth)) return undefined;
227
+ total += numericWidth;
177
228
  hasAnyWidth = true;
178
229
  }
179
230
 
@@ -183,16 +234,37 @@
183
234
  const managedTableWidth = $derived.by(() => {
184
235
  void $widthVersion;
185
236
  void $layoutVersion;
186
- if (!hasResizable) return undefined;
187
- const widths = ctx.getVisibleColumnWidths();
237
+ const widths = ctx.getResolvedVisibleColumnWidths();
238
+ const columnCount = ctx.getVisibleColumnCount();
239
+ if (widths.size === 0 || widths.size < columnCount) return undefined;
240
+ if (ctx.hasRelativeVisibleColumnWidths()) return undefined;
241
+ let total = 0;
242
+ for (const w of widths.values()) total += w;
243
+ return total;
244
+ });
245
+
246
+ const relativeResolvedTableWidth = $derived.by(() => {
247
+ void $widthVersion;
248
+ void $layoutVersion;
249
+ if (!ctx.hasRelativeVisibleColumnWidths()) return undefined;
250
+
251
+ const widths = ctx.getResolvedVisibleColumnWidths();
188
252
  const columnCount = ctx.getVisibleColumnCount();
189
253
  if (widths.size === 0 || widths.size < columnCount) return undefined;
254
+
190
255
  let total = 0;
191
256
  for (const w of widths.values()) total += w;
192
257
  return total;
193
258
  });
194
259
 
195
- const resolvedTableWidth = $derived(managedTableWidth ?? explicitManagedTableWidth);
260
+ const fallbackRelativeTableWidth = $derived.by(() => {
261
+ void $layoutVersion;
262
+ return ctx.hasRelativeVisibleColumnWidths() ? '100%' : undefined;
263
+ });
264
+
265
+ const resolvedTableWidth = $derived(
266
+ managedTableWidth ?? explicitManagedTableWidth ?? relativeResolvedTableWidth
267
+ );
196
268
 
197
269
  context = ctx;
198
270
 
@@ -223,6 +295,33 @@
223
295
  element = tableElement;
224
296
  });
225
297
 
298
+ $effect(() => {
299
+ if (!tableElement) return;
300
+
301
+ const resizeTarget = tableElement.parentElement ?? tableElement;
302
+ const refreshMeasuredLayout = () => {
303
+ ctx.refreshMeasuredLayout();
304
+ };
305
+
306
+ window.addEventListener('resize', refreshMeasuredLayout);
307
+ window.visualViewport?.addEventListener('resize', refreshMeasuredLayout);
308
+
309
+ const resizeObserver =
310
+ typeof ResizeObserver !== 'undefined'
311
+ ? new ResizeObserver(() => {
312
+ refreshMeasuredLayout();
313
+ })
314
+ : null;
315
+
316
+ resizeObserver?.observe(resizeTarget);
317
+
318
+ return () => {
319
+ window.removeEventListener('resize', refreshMeasuredLayout);
320
+ window.visualViewport?.removeEventListener('resize', refreshMeasuredLayout);
321
+ resizeObserver?.disconnect();
322
+ };
323
+ });
324
+
226
325
  $effect(() => {
227
326
  ctx.setSelectionMode(selectionMode);
228
327
  });
@@ -369,8 +468,13 @@
369
468
  bind:this={tableElement}
370
469
  role="grid"
371
470
  class={className}
372
- style:table-layout={hasResizable || resolvedTableWidth !== undefined ? 'fixed' : undefined}
373
- style:width={resolvedTableWidth !== undefined ? `${resolvedTableWidth}px` : undefined}
471
+ style:--table-visible-column-count={ariaColCount ? `${ariaColCount}` : undefined}
472
+ style:table-layout={hasResizable || hasDefinedColumnWidths || resolvedTableWidth !== undefined
473
+ ? 'fixed'
474
+ : undefined}
475
+ style:width={resolvedTableWidth !== undefined
476
+ ? `${resolvedTableWidth}px`
477
+ : fallbackRelativeTableWidth}
374
478
  style:min-width={resolvedTableWidth !== undefined ? '0' : undefined}
375
479
  aria-label={ariaLabel}
376
480
  aria-labelledby={ariaLabelledby}
@@ -388,6 +492,19 @@
388
492
  onkeydown={handleKeyDown}
389
493
  {...restProps}
390
494
  >
495
+ {#if layoutColumns.length > 0}
496
+ <colgroup>
497
+ {#each layoutColumns as column (column.id)}
498
+ <col
499
+ data-table-implicit-width={column.implicitWidth ? 'true' : undefined}
500
+ style:width={column.widthStyle}
501
+ style:min-width={column.minWidth !== undefined ? `${column.minWidth}px` : undefined}
502
+ style:max-width={column.maxWidth !== undefined ? `${column.maxWidth}px` : undefined}
503
+ />
504
+ {/each}
505
+ </colgroup>
506
+ {/if}
507
+
391
508
  {#if children}
392
509
  {@render children()}
393
510
  {/if}
@@ -400,3 +517,17 @@
400
517
  <span id={ctx.selectionUnavailableDescriptionId} style={visuallyHiddenStyle}
401
518
  >Selection unavailable for this row.</span
402
519
  >
520
+
521
+ <style>
522
+ :global(table[role='grid']:has([data-table-column-resizer='true'])) {
523
+ table-layout: fixed;
524
+ width: 100%;
525
+ min-width: 0;
526
+ }
527
+
528
+ :global(
529
+ table[role='grid']:has([data-table-column-resizer='true']) col[data-table-implicit-width='true']
530
+ ) {
531
+ width: calc(100% / var(--table-visible-column-count, 1));
532
+ }
533
+ </style>
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy } from 'svelte';
3
- import { SvelteMap } from 'svelte/reactivity';
4
3
  import { writable } from 'svelte/store';
5
4
  import { setTableRowContext, useTableContext, useTableSectionContext } from '../root/context';
6
5
  import type { TableRowProps } from '../types.js';
@@ -22,14 +21,12 @@
22
21
  const section = useTableSectionContext();
23
22
  const rowToken = table.createInstanceToken('row');
24
23
  const cellOrder: string[] = [];
25
- const cellElements = new SvelteMap<string, () => HTMLElement | undefined>();
24
+ const cellElements: Record<string, () => HTMLElement | undefined> = {};
26
25
  const cellOrderVersion = writable(0);
27
26
 
28
27
  let rowElement = $state<HTMLTableRowElement | undefined>(undefined);
29
- let childListObserver: MutationObserver | null = null;
30
28
 
31
29
  function notifyCellOrderChange() {
32
- cellIndexCache = null;
33
30
  cellOrderVersion.update((value) => value + 1);
34
31
  }
35
32
 
@@ -39,12 +36,12 @@
39
36
  notifyCellOrderChange();
40
37
  }
41
38
  if (getElement) {
42
- cellElements.set(token, getElement);
39
+ cellElements[token] = getElement;
43
40
  }
44
41
  }
45
42
 
46
43
  function unregisterCellToken(token: string) {
47
- cellElements.delete(token);
44
+ delete cellElements[token];
48
45
  const index = cellOrder.indexOf(token);
49
46
  if (index >= 0) {
50
47
  cellOrder.splice(index, 1);
@@ -52,40 +49,12 @@
52
49
  }
53
50
  }
54
51
 
55
- let cellIndexCache: Map<string, number> | null = null;
56
-
57
- function buildCellIndexCache(): Map<string, number> {
58
- if (cellIndexCache) return cellIndexCache;
59
- // eslint-disable-next-line svelte/prefer-svelte-reactivity -- plain cache, not reactive state
60
- const cache = new Map<string, number>();
61
- if (rowElement) {
62
- const directCells = rowElement.children;
63
- // eslint-disable-next-line svelte/prefer-svelte-reactivity -- plain cache, not reactive state
64
- const elementToIndex = new Map<HTMLElement, number>();
65
- for (let i = 0; i < directCells.length; i += 1) {
66
- const child = directCells[i];
67
- if (child instanceof HTMLElement) {
68
- elementToIndex.set(child, i);
69
- }
70
- }
71
- for (const registeredToken of cellOrder) {
72
- const element = cellElements.get(registeredToken)?.();
73
- if (element) {
74
- const idx = elementToIndex.get(element);
75
- if (idx !== undefined) {
76
- cache.set(registeredToken, idx);
77
- }
78
- }
79
- }
80
- }
81
- cellIndexCache = cache;
82
- return cache;
83
- }
84
-
85
52
  function getCellIndex(token: string) {
86
- const cache = buildCellIndexCache();
87
- const cached = cache.get(token);
88
- if (cached !== undefined) return cached;
53
+ const element = cellElements[token]?.();
54
+ if (rowElement && element) {
55
+ const index = Array.from(rowElement.children).indexOf(element);
56
+ if (index >= 0) return index;
57
+ }
89
58
  return cellOrder.indexOf(token);
90
59
  }
91
60
 
@@ -120,23 +89,7 @@
120
89
  syncRowRegistration();
121
90
  });
122
91
 
123
- let pendingCellOrderNotify = false;
124
-
125
- function scheduleCellOrderNotify() {
126
- if (!pendingCellOrderNotify) {
127
- pendingCellOrderNotify = true;
128
- queueMicrotask(() => {
129
- pendingCellOrderNotify = false;
130
- cellIndexCache = null;
131
- notifyCellOrderChange();
132
- });
133
- }
134
- }
135
-
136
92
  $effect(() => {
137
- childListObserver?.disconnect();
138
- childListObserver = null;
139
-
140
93
  if (!rowElement) {
141
94
  return;
142
95
  }
@@ -145,18 +98,25 @@
145
98
  scheduleCellOrderNotify();
146
99
  });
147
100
  observer.observe(rowElement, { childList: true });
148
- childListObserver = observer;
149
101
 
150
102
  return () => {
151
103
  observer.disconnect();
152
- if (childListObserver === observer) {
153
- childListObserver = null;
154
- }
155
104
  };
156
105
  });
157
106
 
107
+ let pendingCellOrderNotify = false;
108
+
109
+ function scheduleCellOrderNotify() {
110
+ if (!pendingCellOrderNotify) {
111
+ pendingCellOrderNotify = true;
112
+ queueMicrotask(() => {
113
+ pendingCellOrderNotify = false;
114
+ notifyCellOrderChange();
115
+ });
116
+ }
117
+ }
118
+
158
119
  onDestroy(() => {
159
- childListObserver?.disconnect();
160
120
  table.unregisterRow(rowToken);
161
121
  });
162
122
 
@@ -35,16 +35,16 @@ export type TableRootProps = Omit<HTMLAttributes<HTMLTableElement>, 'children'>
35
35
  defaultSelectedKeys?: Iterable<TableSelectionKey>;
36
36
  sortDescriptor?: TableSortDescriptor;
37
37
  defaultSortDescriptor?: TableSortDescriptor;
38
- columnWidths?: Map<string, number>;
39
- defaultColumnWidths?: Iterable<readonly [string, number]>;
38
+ columnWidths?: Map<string, TableColumnWidth>;
39
+ defaultColumnWidths?: Iterable<readonly [string, TableColumnWidth]>;
40
40
  disabledKeys?: Iterable<TableSelectionKey>;
41
41
  onRowAction?: TableRowActionHandler;
42
42
  onSelectionChange?: (keys: Set<TableSelectionKey>) => void;
43
43
  onSortChange?: (descriptor: TableSortDescriptor | undefined) => void;
44
- onColumnWidthsChange?: (widths: Map<string, number>) => void;
44
+ onColumnWidthsChange?: (widths: Map<string, TableColumnWidth>) => void;
45
45
  onHiddenColumnsChange?: (columnIds: string[]) => void;
46
46
  onColumnResizeStart?: (columnId: string) => void;
47
- onColumnResizeEnd?: (widths: Map<string, number>) => void;
47
+ onColumnResizeEnd?: (widths: Map<string, TableColumnWidth>) => void;
48
48
  children?: Snippet;
49
49
  class?: string;
50
50
  context?: TableContext;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@human-kit/svelte-components",
3
- "version": "1.0.0-alpha.16",
3
+ "version": "1.0.0-alpha.18",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",