@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
@@ -1,10 +1,51 @@
1
- import { getContext, setContext } from 'svelte';
1
+ import { flushSync, getContext, setContext } from 'svelte';
2
2
  import { writable } from 'svelte/store';
3
3
  const TABLE_KEY = Symbol('table');
4
4
  const TABLE_SECTION_KEY = Symbol('table-section');
5
5
  const TABLE_ROW_KEY = Symbol('table-row');
6
6
  const TABLE_COLUMN_KEY = Symbol('table-column');
7
7
  const TABLE_CELL_KEY = Symbol('table-cell');
8
+ const IS_BROWSER = typeof window !== 'undefined';
9
+ export const DEFAULT_TABLE_COLUMN_MIN_WIDTH = 60;
10
+ export function distributeRoundedWidths(entries, targetTotal) {
11
+ let delta = targetTotal - entries.reduce((total, entry) => total + entry.width, 0);
12
+ if (delta === 0)
13
+ return entries;
14
+ const prioritizedEntries = [...entries].sort((left, right) => {
15
+ if (delta > 0) {
16
+ if (right.remainder !== left.remainder)
17
+ return right.remainder - left.remainder;
18
+ return right.index - left.index;
19
+ }
20
+ if (left.remainder !== right.remainder)
21
+ return left.remainder - right.remainder;
22
+ return left.index - right.index;
23
+ });
24
+ while (delta !== 0) {
25
+ let updated = false;
26
+ for (const entry of prioritizedEntries) {
27
+ if (delta > 0) {
28
+ if (entry.maxWidth !== undefined && entry.width >= entry.maxWidth)
29
+ continue;
30
+ entry.width += 1;
31
+ delta -= 1;
32
+ updated = true;
33
+ }
34
+ else {
35
+ if (entry.width <= entry.minWidth)
36
+ continue;
37
+ entry.width -= 1;
38
+ delta += 1;
39
+ updated = true;
40
+ }
41
+ if (delta === 0)
42
+ break;
43
+ }
44
+ if (!updated)
45
+ break;
46
+ }
47
+ return entries;
48
+ }
8
49
  export function createTableContext(options = {}) {
9
50
  let selectionMode = options.selectionMode ?? 'none';
10
51
  let selectionBehavior = options.selectionBehavior ?? 'toggle';
@@ -20,15 +61,19 @@ export function createTableContext(options = {}) {
20
61
  const disabledKeys = new Set(options.disabledKeys ?? []);
21
62
  const hiddenColumnIds = new Set(options.initialHiddenColumns ?? []);
22
63
  let resizingColumnId = null;
64
+ let resizeSession = null;
23
65
  let suppressNextHeaderClick = false;
24
66
  const columns = new Map();
25
67
  const columnIds = new Map();
26
68
  const columnOrder = [];
27
69
  const columnsWithResizers = new Set();
70
+ let resizerLayoutReady = false;
28
71
  const columnWidths = new Map(options.initialColumnWidths ?? []);
29
72
  const rows = new Map();
30
73
  const headerRowOrder = [];
31
74
  const bodyRowOrder = [];
75
+ let bodyRowsInitialized = false;
76
+ let selectableBodyRowCount = 0;
32
77
  const cells = new Map();
33
78
  const cellOrder = [];
34
79
  let orderedRowTokensCache = {
@@ -39,6 +84,7 @@ export function createTableContext(options = {}) {
39
84
  let visibleOrderedColumnTokensCache = null;
40
85
  let columnWidthsCache = null;
41
86
  let visibleColumnWidthsCache = null;
87
+ let resolvedVisibleColumnWidthsCache = null;
42
88
  let navigableCellsCache = null;
43
89
  let rowsWithCellsCache = null;
44
90
  const layoutVersion = writable(0);
@@ -60,11 +106,21 @@ export function createTableContext(options = {}) {
60
106
  visibleOrderedColumnTokensCache = null;
61
107
  columnWidthsCache = null;
62
108
  visibleColumnWidthsCache = null;
109
+ resolvedVisibleColumnWidthsCache = null;
63
110
  navigableCellsCache = null;
64
111
  rowsWithCellsCache = null;
65
112
  }
66
113
  let layoutNotifyScheduled = false;
114
+ let selectionNotifyScheduled = false;
67
115
  let widthNotifyScheduled = false;
116
+ function syncResizerLayoutReady(nextReady) {
117
+ if (resizerLayoutReady === nextReady)
118
+ return;
119
+ resizerLayoutReady = nextReady;
120
+ invalidateLayoutCaches();
121
+ layoutVersion.update((value) => value + 1);
122
+ notifyWidth();
123
+ }
68
124
  function notifyLayout() {
69
125
  invalidateLayoutCaches();
70
126
  if (!layoutNotifyScheduled) {
@@ -76,7 +132,13 @@ export function createTableContext(options = {}) {
76
132
  }
77
133
  }
78
134
  function notifySelection() {
79
- selectionVersion.update((value) => value + 1);
135
+ if (!selectionNotifyScheduled) {
136
+ selectionNotifyScheduled = true;
137
+ queueMicrotask(() => {
138
+ selectionNotifyScheduled = false;
139
+ selectionVersion.update((value) => value + 1);
140
+ });
141
+ }
80
142
  }
81
143
  function notifyFocus() {
82
144
  focusVersion.update((value) => value + 1);
@@ -88,6 +150,7 @@ export function createTableContext(options = {}) {
88
150
  function notifyWidth() {
89
151
  columnWidthsCache = null;
90
152
  visibleColumnWidthsCache = null;
153
+ resolvedVisibleColumnWidthsCache = null;
91
154
  if (!widthNotifyScheduled) {
92
155
  widthNotifyScheduled = true;
93
156
  queueMicrotask(() => {
@@ -96,22 +159,52 @@ export function createTableContext(options = {}) {
96
159
  });
97
160
  }
98
161
  }
162
+ function notifyWidthImmediately() {
163
+ columnWidthsCache = null;
164
+ visibleColumnWidthsCache = null;
165
+ resolvedVisibleColumnWidthsCache = null;
166
+ flushSync(() => {
167
+ widthVersion.update((value) => value + 1);
168
+ });
169
+ }
170
+ function refreshMeasuredLayout() {
171
+ notifyWidthImmediately();
172
+ }
99
173
  function notifyResize() {
100
174
  resizeVersion.update((value) => value + 1);
101
175
  }
102
- function normalizeColumnWidth(width) {
176
+ function parseColumnWidth(width) {
103
177
  if (typeof width === 'number') {
104
- return Number.isFinite(width) ? width : undefined;
178
+ return Number.isFinite(width) ? { unit: 'px', value: width } : undefined;
105
179
  }
106
180
  if (typeof width === 'string') {
107
- const match = width.trim().match(/^(\d+(?:\.\d+)?)px$/i);
181
+ const match = width.trim().match(/^(\d+(?:\.\d+)?)(px|%|fr)$/i);
108
182
  if (!match)
109
183
  return undefined;
110
184
  const next = Number(match[1]);
111
- return Number.isFinite(next) ? next : undefined;
185
+ if (!Number.isFinite(next))
186
+ return undefined;
187
+ const unit = match[2].toLowerCase();
188
+ return { unit, value: next };
112
189
  }
113
190
  return undefined;
114
191
  }
192
+ function normalizeColumnWidth(width) {
193
+ const parsed = parseColumnWidth(width);
194
+ if (!parsed)
195
+ return undefined;
196
+ if (parsed.unit === 'px') {
197
+ return typeof width === 'number' ? width : `${parsed.value}px`;
198
+ }
199
+ if (parsed.unit === '%') {
200
+ return `${parsed.value}%`;
201
+ }
202
+ return `${parsed.value}fr`;
203
+ }
204
+ function isRelativeColumnWidth(width) {
205
+ const parsed = parseColumnWidth(width);
206
+ return parsed ? parsed.unit !== 'px' : false;
207
+ }
115
208
  function getColumnRegistrationById(columnId) {
116
209
  const token = columnIds.get(columnId);
117
210
  return token ? columns.get(token) : undefined;
@@ -125,10 +218,72 @@ export function createTableContext(options = {}) {
125
218
  function getColumnMaxWidth(columnId) {
126
219
  return getColumnRegistrationById(columnId)?.maxWidth;
127
220
  }
128
- function clampColumnWidth(columnId, width) {
221
+ function getFixedColumnWidthSpec(columnId) {
222
+ return normalizeColumnWidth(getColumnRegistrationById(columnId)?.width);
223
+ }
224
+ function getManagedColumnWidthSpec(columnId) {
225
+ return normalizeColumnWidth(columnWidths.get(columnId));
226
+ }
227
+ function getDefaultColumnWidthSpec(columnId) {
228
+ return normalizeColumnWidth(getColumnRegistrationById(columnId)?.defaultWidth);
229
+ }
230
+ function hasAuthoredColumnWidthSpec(columnId) {
231
+ return (getFixedColumnWidthSpec(columnId) !== undefined ||
232
+ getManagedColumnWidthSpec(columnId) !== undefined ||
233
+ getDefaultColumnWidthSpec(columnId) !== undefined);
234
+ }
235
+ function getEffectiveColumnWidthSpec(columnId) {
236
+ const fixedWidth = getFixedColumnWidthSpec(columnId);
237
+ if (fixedWidth !== undefined) {
238
+ return fixedWidth;
239
+ }
240
+ const managedWidth = getManagedColumnWidthSpec(columnId);
241
+ if (managedWidth !== undefined) {
242
+ return managedWidth;
243
+ }
244
+ const defaultWidth = getDefaultColumnWidthSpec(columnId);
245
+ if (defaultWidth !== undefined) {
246
+ return defaultWidth;
247
+ }
248
+ return resizerLayoutReady && columnsWithResizers.size > 0 ? '1fr' : undefined;
249
+ }
250
+ function getMeasuredTableWidth() {
251
+ const tableCell = Array.from(cells.values()).find((cell) => cell.element)?.element;
252
+ const tableElement = tableCell?.closest('table');
253
+ const tableWidth = tableElement?.getBoundingClientRect().width;
254
+ const containerElement = tableElement?.parentElement;
255
+ const containerClientWidth = containerElement?.clientWidth;
256
+ const containerStyle = containerElement ? getComputedStyle(containerElement) : undefined;
257
+ const containerPaddingLeft = Number.parseFloat(containerStyle?.paddingLeft ?? '0');
258
+ const containerPaddingRight = Number.parseFloat(containerStyle?.paddingRight ?? '0');
259
+ const containerWidth = containerClientWidth !== undefined && Number.isFinite(containerClientWidth)
260
+ ? containerClientWidth - containerPaddingLeft - containerPaddingRight
261
+ : undefined;
262
+ // Use the container width as the stable reference for fr/% resolution.
263
+ // Using Math.max(table, container) causes a feedback loop during resize:
264
+ // the table grows → measurement returns more → fr columns get more space →
265
+ // the table grows further. The container width is stable and represents
266
+ // the actual available space the table should fill.
267
+ const width = containerWidth ?? tableWidth;
268
+ if (width === undefined || width <= 0 || !Number.isFinite(width)) {
269
+ return undefined;
270
+ }
271
+ return Math.round(width);
272
+ }
273
+ function getColumnWidthBounds(columnId) {
129
274
  const registration = getColumnRegistrationById(columnId);
130
- const minWidth = registration?.minWidth ?? 75;
131
- const maxWidth = registration?.maxWidth;
275
+ const fixedWidth = parseColumnWidth(registration?.width);
276
+ const minWidth = registration?.minWidth ??
277
+ (fixedWidth?.unit === 'px'
278
+ ? Math.min(fixedWidth.value, DEFAULT_TABLE_COLUMN_MIN_WIDTH)
279
+ : DEFAULT_TABLE_COLUMN_MIN_WIDTH);
280
+ return {
281
+ minWidth,
282
+ maxWidth: registration?.maxWidth
283
+ };
284
+ }
285
+ function clampColumnWidth(columnId, width) {
286
+ const { minWidth, maxWidth } = getColumnWidthBounds(columnId);
132
287
  let next = Math.round(width);
133
288
  if (Number.isNaN(next) || !Number.isFinite(next)) {
134
289
  next = minWidth;
@@ -139,11 +294,22 @@ export function createTableContext(options = {}) {
139
294
  }
140
295
  return next;
141
296
  }
297
+ function resolveRelativeColumnWidthAllocations(entries, targetTotal) {
298
+ const allocations = entries.map((entry) => {
299
+ const { minWidth, maxWidth } = getColumnWidthBounds(entry.columnId);
300
+ return {
301
+ ...entry,
302
+ width: clampColumnWidth(entry.columnId, entry.exactWidth),
303
+ minWidth,
304
+ maxWidth,
305
+ remainder: entry.exactWidth - Math.floor(entry.exactWidth)
306
+ };
307
+ });
308
+ return distributeRoundedWidths(allocations, targetTotal);
309
+ }
142
310
  function hasResizableColumns() {
143
311
  for (const column of columns.values()) {
144
- if (isColumnHidden(column.id))
145
- continue;
146
- if (columnsWithResizers.has(column.token))
312
+ if (isColumnResizable(column.id))
147
313
  return true;
148
314
  }
149
315
  return false;
@@ -152,12 +318,26 @@ export function createTableContext(options = {}) {
152
318
  if (columnsWithResizers.has(columnToken))
153
319
  return;
154
320
  columnsWithResizers.add(columnToken);
155
- notifyLayout();
321
+ if (IS_BROWSER) {
322
+ syncResizerLayoutReady(true);
323
+ return;
324
+ }
325
+ resizerLayoutReady = false;
326
+ invalidateLayoutCaches();
327
+ layoutVersion.update((value) => value + 1);
328
+ notifyWidth();
156
329
  }
157
330
  function unregisterColumnResizer(columnToken) {
158
331
  if (!columnsWithResizers.delete(columnToken))
159
332
  return;
160
- notifyLayout();
333
+ if (IS_BROWSER) {
334
+ syncResizerLayoutReady(columnsWithResizers.size > 0);
335
+ return;
336
+ }
337
+ resizerLayoutReady = false;
338
+ invalidateLayoutCaches();
339
+ layoutVersion.update((value) => value + 1);
340
+ notifyWidth();
161
341
  }
162
342
  function sameColumnMetadata(left, right) {
163
343
  return (left.token === right.token &&
@@ -271,15 +451,72 @@ export function createTableContext(options = {}) {
271
451
  return getColumnRegistrationById(columnId)?.textValue;
272
452
  }
273
453
  function getColumnWidth(columnId) {
274
- const managedWidth = columnWidths.get(columnId);
275
- if (managedWidth !== undefined) {
276
- return clampColumnWidth(columnId, managedWidth);
454
+ const parsed = parseColumnWidth(getEffectiveColumnWidthSpec(columnId));
455
+ if (!parsed)
456
+ return undefined;
457
+ if (!isColumnHidden(columnId)) {
458
+ if (parsed.unit !== 'px') {
459
+ return undefined;
460
+ }
461
+ return getResolvedVisibleColumnWidths().get(columnId);
277
462
  }
278
- const registration = getColumnRegistrationById(columnId);
279
- if (!registration)
463
+ return parsed?.unit === 'px' ? clampColumnWidth(columnId, parsed.value) : undefined;
464
+ }
465
+ function formatCssLength(length) {
466
+ const rounded = Math.round(length * 1000) / 1000;
467
+ return Number.isInteger(rounded) ? `${rounded}` : `${rounded}`;
468
+ }
469
+ function getColumnWidthStyle(columnId) {
470
+ const resolvedWidth = getColumnWidth(columnId);
471
+ if (resolvedWidth !== undefined) {
472
+ return `${resolvedWidth}px`;
473
+ }
474
+ const parsed = parseColumnWidth(getEffectiveColumnWidthSpec(columnId));
475
+ if (!parsed)
280
476
  return undefined;
281
- const nextWidth = normalizeColumnWidth(registration.width) ?? normalizeColumnWidth(registration.defaultWidth);
282
- return nextWidth !== undefined ? clampColumnWidth(columnId, nextWidth) : undefined;
477
+ if (parsed.unit === 'px') {
478
+ return `${clampColumnWidth(columnId, parsed.value)}px`;
479
+ }
480
+ if (parsed.unit === '%') {
481
+ return `${parsed.value}%`;
482
+ }
483
+ let fixedPxTotal = 0;
484
+ let percentTotal = 0;
485
+ let totalFr = 0;
486
+ for (const token of getVisibleOrderedColumnTokens()) {
487
+ const column = columns.get(token);
488
+ if (!column)
489
+ continue;
490
+ const spec = parseColumnWidth(getEffectiveColumnWidthSpec(column.id));
491
+ if (!spec)
492
+ continue;
493
+ if (spec.unit === 'px') {
494
+ fixedPxTotal += clampColumnWidth(column.id, spec.value);
495
+ continue;
496
+ }
497
+ if (spec.unit === '%') {
498
+ percentTotal += spec.value;
499
+ continue;
500
+ }
501
+ totalFr += spec.value;
502
+ }
503
+ if (totalFr <= 0)
504
+ return undefined;
505
+ const frShare = parsed.value / totalFr;
506
+ const availableWidthTerms = ['100%'];
507
+ if (fixedPxTotal > 0) {
508
+ availableWidthTerms.push(`${formatCssLength(fixedPxTotal)}px`);
509
+ }
510
+ if (percentTotal > 0) {
511
+ availableWidthTerms.push(`${formatCssLength(percentTotal)}%`);
512
+ }
513
+ const availableWidthExpression = availableWidthTerms.length === 1
514
+ ? availableWidthTerms[0]
515
+ : `(${availableWidthTerms.join(' - ')})`;
516
+ if (frShare === 1) {
517
+ return `calc(${availableWidthExpression})`;
518
+ }
519
+ return `calc(${availableWidthExpression} * ${formatCssLength(frShare)})`;
283
520
  }
284
521
  function isColumnResizable(columnId) {
285
522
  const column = getColumnRegistrationById(columnId);
@@ -297,7 +534,7 @@ export function createTableContext(options = {}) {
297
534
  const column = columns.get(token);
298
535
  if (!column)
299
536
  continue;
300
- const width = getColumnWidth(column.id);
537
+ const width = getManagedColumnWidthSpec(column.id);
301
538
  if (width !== undefined) {
302
539
  widths.set(column.id, width);
303
540
  }
@@ -317,6 +554,70 @@ export function createTableContext(options = {}) {
317
554
  visibleColumnWidthsCache = widths;
318
555
  return widths;
319
556
  }
557
+ function hasRelativeVisibleColumnWidths() {
558
+ for (const token of getVisibleOrderedColumnTokens()) {
559
+ const column = columns.get(token);
560
+ if (!column)
561
+ continue;
562
+ if (isRelativeColumnWidth(getEffectiveColumnWidthSpec(column.id))) {
563
+ return true;
564
+ }
565
+ }
566
+ return false;
567
+ }
568
+ function getResolvedVisibleColumnWidths() {
569
+ if (resolvedVisibleColumnWidthsCache)
570
+ return resolvedVisibleColumnWidthsCache;
571
+ const widths = new Map();
572
+ const flexibleColumns = [];
573
+ const relativeColumns = [];
574
+ const tableWidth = getMeasuredTableWidth();
575
+ let fixedWidthTotal = 0;
576
+ let exactRelativeWidthTotal = 0;
577
+ let totalFr = 0;
578
+ for (const [index, token] of getVisibleOrderedColumnTokens().entries()) {
579
+ const column = columns.get(token);
580
+ if (!column)
581
+ continue;
582
+ const parsed = parseColumnWidth(getEffectiveColumnWidthSpec(column.id));
583
+ if (!parsed)
584
+ continue;
585
+ if (parsed.unit === 'px') {
586
+ const nextWidth = clampColumnWidth(column.id, parsed.value);
587
+ widths.set(column.id, nextWidth);
588
+ fixedWidthTotal += nextWidth;
589
+ continue;
590
+ }
591
+ if (parsed.unit === '%') {
592
+ if (tableWidth === undefined)
593
+ continue;
594
+ const exactWidth = (tableWidth * parsed.value) / 100;
595
+ relativeColumns.push({ columnId: column.id, index, exactWidth });
596
+ exactRelativeWidthTotal += exactWidth;
597
+ continue;
598
+ }
599
+ flexibleColumns.push({ columnId: column.id, fr: parsed.value, index });
600
+ totalFr += parsed.value;
601
+ }
602
+ if (tableWidth !== undefined && flexibleColumns.length > 0 && totalFr > 0) {
603
+ const distributableWidth = Math.max(tableWidth - fixedWidthTotal - exactRelativeWidthTotal, 0);
604
+ for (const entry of flexibleColumns) {
605
+ relativeColumns.push({
606
+ columnId: entry.columnId,
607
+ index: entry.index,
608
+ exactWidth: (distributableWidth * entry.fr) / totalFr
609
+ });
610
+ }
611
+ }
612
+ if (tableWidth !== undefined && relativeColumns.length > 0) {
613
+ const targetRelativeTotal = Math.max(tableWidth - fixedWidthTotal, 0);
614
+ for (const entry of resolveRelativeColumnWidthAllocations(relativeColumns, targetRelativeTotal)) {
615
+ widths.set(entry.columnId, entry.width);
616
+ }
617
+ }
618
+ resolvedVisibleColumnWidthsCache = widths;
619
+ return widths;
620
+ }
320
621
  function getMeasuredHeaderWidth(columnToken) {
321
622
  const headerCell = Array.from(cells.values()).find((cell) => cell.section === 'header' && cell.columnToken === columnToken && cell.element);
322
623
  const width = headerCell?.element?.getBoundingClientRect().width;
@@ -325,42 +626,110 @@ export function createTableContext(options = {}) {
325
626
  }
326
627
  return Math.round(width);
327
628
  }
328
- function freezeColumnWidthsFromLayout() {
329
- const next = new Map();
330
- let changed = false;
331
- for (const token of columnOrder) {
629
+ function getFixedVisibleColumnWidthTotal() {
630
+ let total = 0;
631
+ for (const token of getVisibleOrderedColumnTokens()) {
332
632
  const column = columns.get(token);
333
633
  if (!column)
334
634
  continue;
635
+ if (getFixedColumnWidthSpec(column.id) === undefined)
636
+ continue;
637
+ const measuredWidth = getMeasuredHeaderWidth(token);
638
+ const resolvedWidth = getColumnWidth(column.id) ?? measuredWidth;
639
+ if (resolvedWidth === undefined)
640
+ continue;
641
+ total += clampColumnWidth(column.id, resolvedWidth);
642
+ }
643
+ return total;
644
+ }
645
+ function getResizableRelativeTailColumnId(activeColumnId) {
646
+ return getVisibleOrderedColumnTokens()
647
+ .map((token) => columns.get(token))
648
+ .filter((column) => !!column)
649
+ .filter((column) => {
650
+ if (column.id === activeColumnId)
651
+ return false;
652
+ if (getFixedColumnWidthSpec(column.id) !== undefined)
653
+ return false;
654
+ return isRelativeColumnWidth(getEffectiveColumnWidthSpec(column.id));
655
+ })
656
+ .at(-1)?.id;
657
+ }
658
+ function prepareColumnWidthsForResize(activeColumnId) {
659
+ const next = new Map(columnWidths);
660
+ const baselineWidths = new Map();
661
+ const preservedFlexibleColumnId = getResizableRelativeTailColumnId(activeColumnId);
662
+ const preservedFlexibleEffectiveWidth = preservedFlexibleColumnId
663
+ ? normalizeColumnWidth(getEffectiveColumnWidthSpec(preservedFlexibleColumnId))
664
+ : undefined;
665
+ const measuredTableWidth = getMeasuredTableWidth();
666
+ const fixedVisibleColumnWidthTotal = getFixedVisibleColumnWidthTotal();
667
+ for (const token of getVisibleOrderedColumnTokens()) {
668
+ const column = columns.get(token);
669
+ if (!column)
670
+ continue;
671
+ if (getFixedColumnWidthSpec(column.id) !== undefined)
672
+ continue;
335
673
  const measuredWidth = getMeasuredHeaderWidth(token);
336
- const resolvedWidth = measuredWidth ?? getColumnWidth(column.id);
674
+ const resolvedWidth = getColumnWidth(column.id) ?? measuredWidth;
337
675
  if (resolvedWidth === undefined)
338
676
  continue;
339
- const clampedWidth = clampColumnWidth(column.id, resolvedWidth);
340
- next.set(column.id, clampedWidth);
341
- if (columnWidths.get(column.id) !== clampedWidth) {
342
- changed = true;
677
+ const frozenWidth = clampColumnWidth(column.id, resolvedWidth);
678
+ next.set(column.id, frozenWidth);
679
+ baselineWidths.set(column.id, frozenWidth);
680
+ }
681
+ let changed = next.size !== columnWidths.size;
682
+ if (!changed) {
683
+ for (const [columnId, width] of next) {
684
+ if (columnWidths.get(columnId) !== width) {
685
+ changed = true;
686
+ break;
687
+ }
343
688
  }
344
689
  }
345
- if (!changed || next.size === 0)
690
+ if (!changed)
346
691
  return;
347
692
  columnWidths.clear();
348
693
  for (const [columnId, width] of next) {
349
694
  columnWidths.set(columnId, width);
350
695
  }
351
696
  columnWidthsCache = null;
697
+ visibleColumnWidthsCache = null;
698
+ const baselineTotalWidth = Array.from(baselineWidths.values()).reduce((total, current) => total + current, 0);
699
+ resizeSession = {
700
+ activeColumnId: activeColumnId,
701
+ flexibleTailColumnId: preservedFlexibleColumnId,
702
+ flexibleTailRestoreWidth: preservedFlexibleColumnId && isRelativeColumnWidth(preservedFlexibleEffectiveWidth)
703
+ ? preservedFlexibleEffectiveWidth
704
+ : undefined,
705
+ baselineWidths,
706
+ baselineAvailableTableWidth: measuredTableWidth !== undefined
707
+ ? Math.max(measuredTableWidth - fixedVisibleColumnWidthTotal, 0)
708
+ : baselineTotalWidth,
709
+ baselineTotalWidth
710
+ };
352
711
  options.onColumnWidthsChange?.(getColumnWidths());
353
712
  notifyWidth();
354
713
  }
714
+ function shouldPrepareColumnWidthsForResize(columnId) {
715
+ if (getFixedColumnWidthSpec(columnId) !== undefined)
716
+ return false;
717
+ if (!hasRelativeVisibleColumnWidths())
718
+ return false;
719
+ return resizeSession?.activeColumnId !== columnId;
720
+ }
355
721
  function setColumnWidths(widths) {
356
722
  const next = new Map();
723
+ const incomingWidths = widths ? new Map(widths) : undefined;
357
724
  for (const token of columnOrder) {
358
725
  const column = columns.get(token);
359
726
  if (!column)
360
727
  continue;
361
- const incomingWidth = widths ? new Map(widths).get(column.id) : undefined;
728
+ if (getFixedColumnWidthSpec(column.id) !== undefined)
729
+ continue;
730
+ const incomingWidth = normalizeColumnWidth(incomingWidths?.get(column.id));
362
731
  if (incomingWidth !== undefined) {
363
- next.set(column.id, clampColumnWidth(column.id, incomingWidth));
732
+ next.set(column.id, incomingWidth);
364
733
  }
365
734
  }
366
735
  columnWidths.clear();
@@ -370,13 +739,36 @@ export function createTableContext(options = {}) {
370
739
  notifyWidth();
371
740
  }
372
741
  function setColumnWidth(columnId, width) {
742
+ if (getFixedColumnWidthSpec(columnId) !== undefined)
743
+ return;
373
744
  if (!isColumnResizable(columnId))
374
745
  return;
746
+ if (shouldPrepareColumnWidthsForResize(columnId)) {
747
+ prepareColumnWidthsForResize(columnId);
748
+ }
375
749
  const nextWidth = clampColumnWidth(columnId, width);
376
- if (columnWidths.get(columnId) === nextWidth)
750
+ const currentWidth = columnWidths.get(columnId);
751
+ if (resizeSession?.activeColumnId === columnId &&
752
+ resizeSession.flexibleTailColumnId !== undefined) {
753
+ const baselineActiveWidth = resizeSession.baselineWidths.get(columnId);
754
+ const baselineTailWidth = resizeSession.baselineWidths.get(resizeSession.flexibleTailColumnId);
755
+ if (baselineActiveWidth !== undefined && baselineTailWidth !== undefined) {
756
+ const widthDelta = nextWidth - baselineActiveWidth;
757
+ const overflowWidth = Math.max(resizeSession.baselineTotalWidth - resizeSession.baselineAvailableTableWidth, 0);
758
+ const tailTargetWidth = widthDelta >= 0
759
+ ? baselineTailWidth - widthDelta
760
+ : baselineTailWidth + Math.max(-widthDelta - overflowWidth, 0);
761
+ const tailWidth = clampColumnWidth(resizeSession.flexibleTailColumnId, tailTargetWidth);
762
+ if (columnWidths.get(resizeSession.flexibleTailColumnId) !== tailWidth) {
763
+ columnWidths.set(resizeSession.flexibleTailColumnId, tailWidth);
764
+ }
765
+ }
766
+ }
767
+ if (currentWidth === nextWidth)
377
768
  return;
378
769
  columnWidths.set(columnId, nextWidth);
379
770
  columnWidthsCache = null;
771
+ visibleColumnWidthsCache = null;
380
772
  options.onColumnWidthsChange?.(getColumnWidths());
381
773
  notifyWidth();
382
774
  }
@@ -502,9 +894,7 @@ export function createTableContext(options = {}) {
502
894
  notifyWidth();
503
895
  }
504
896
  function measureIntrinsicElementWidth(cell) {
505
- const target = cell.querySelector('[data-table-header-content]') ??
506
- cell.firstElementChild ??
507
- cell;
897
+ const target = cell;
508
898
  const clone = target.cloneNode(true);
509
899
  for (const separator of clone.querySelectorAll('[role="separator"]')) {
510
900
  separator.remove();
@@ -563,9 +953,7 @@ export function createTableContext(options = {}) {
563
953
  function startColumnResize(columnId) {
564
954
  if (!isColumnResizable(columnId) || resizingColumnId === columnId)
565
955
  return;
566
- if (getVisibleColumnWidths().size < getVisibleColumnCount()) {
567
- freezeColumnWidthsFromLayout();
568
- }
956
+ resizeSession = null;
569
957
  resizingColumnId = columnId;
570
958
  options.onColumnResizeStart?.(columnId);
571
959
  notifyResize();
@@ -573,8 +961,24 @@ export function createTableContext(options = {}) {
573
961
  function endColumnResize() {
574
962
  if (!resizingColumnId)
575
963
  return;
964
+ let restoredFlexibleTail = false;
965
+ if (resizeSession?.flexibleTailColumnId !== undefined &&
966
+ resizeSession.activeColumnId !== resizeSession.flexibleTailColumnId &&
967
+ resizeSession.flexibleTailRestoreWidth !== undefined) {
968
+ const tailColumnId = resizeSession.flexibleTailColumnId;
969
+ columnWidths.set(tailColumnId, resizeSession.flexibleTailRestoreWidth);
970
+ columnWidthsCache = null;
971
+ visibleColumnWidthsCache = null;
972
+ resolvedVisibleColumnWidthsCache = null;
973
+ restoredFlexibleTail = true;
974
+ }
576
975
  resizingColumnId = null;
976
+ if (restoredFlexibleTail) {
977
+ options.onColumnWidthsChange?.(getColumnWidths());
978
+ }
979
+ resizeSession = null;
577
980
  options.onColumnResizeEnd?.(getColumnWidths());
981
+ notifyWidthImmediately();
578
982
  notifyResize();
579
983
  }
580
984
  function suppressHeaderClickOnce() {
@@ -587,6 +991,7 @@ export function createTableContext(options = {}) {
587
991
  }
588
992
  function registerRow(row) {
589
993
  const existing = rows.get(row.token);
994
+ const previousSelectableBodyRowCount = selectableBodyRowCount;
590
995
  const targetOrder = row.section === 'header' ? headerRowOrder : row.section === 'body' ? bodyRowOrder : null;
591
996
  const alreadyOrdered = targetOrder ? targetOrder.includes(row.token) : false;
592
997
  const wasInHeader = headerRowOrder.includes(row.token);
@@ -602,15 +1007,29 @@ export function createTableContext(options = {}) {
602
1007
  if (wasInBody) {
603
1008
  bodyRowOrder.splice(bodyRowOrder.indexOf(row.token), 1);
604
1009
  }
1010
+ const previousSelectableBodyRow = isSelectableBodyRow(existing);
605
1011
  rows.set(row.token, row);
606
1012
  if (targetOrder && !targetOrder.includes(row.token)) {
607
1013
  targetOrder.push(row.token);
608
1014
  }
1015
+ const nextSelectableBodyRow = isSelectableBodyRow(row);
1016
+ if (previousSelectableBodyRow !== nextSelectableBodyRow) {
1017
+ selectableBodyRowCount += nextSelectableBodyRow ? 1 : -1;
1018
+ }
1019
+ if ((selectedKeys.size > 0 && (existing?.section === 'body' || row.section === 'body')) ||
1020
+ (bodyRowsInitialized &&
1021
+ (previousSelectableBodyRowCount === 0) !== (selectableBodyRowCount === 0))) {
1022
+ notifySelection();
1023
+ }
609
1024
  notifyLayout();
610
1025
  }
611
1026
  function unregisterRow(token) {
612
1027
  const row = rows.get(token);
1028
+ const previousSelectableBodyRowCount = selectableBodyRowCount;
613
1029
  rows.delete(token);
1030
+ if (isSelectableBodyRow(row)) {
1031
+ selectableBodyRowCount = Math.max(0, selectableBodyRowCount - 1);
1032
+ }
614
1033
  if (focusedRowTarget?.rowToken === token) {
615
1034
  focusedRowTarget = null;
616
1035
  notifyFocus();
@@ -633,8 +1052,23 @@ export function createTableContext(options = {}) {
633
1052
  notifyFocus();
634
1053
  }
635
1054
  }
1055
+ if ((selectedKeys.size > 0 && row?.section === 'body') ||
1056
+ (bodyRowsInitialized &&
1057
+ (previousSelectableBodyRowCount === 0) !== (selectableBodyRowCount === 0))) {
1058
+ notifySelection();
1059
+ }
636
1060
  notifyLayout();
637
1061
  }
1062
+ function markBodyRowsInitialized() {
1063
+ if (bodyRowsInitialized)
1064
+ return;
1065
+ const optimisticHasSelectableRows = selectionMode === 'multiple' || selectedKeys.size > 0;
1066
+ bodyRowsInitialized = true;
1067
+ const actualHasSelectableRows = selectableBodyRowCount > 0 || selectedKeys.size > 0;
1068
+ if (optimisticHasSelectableRows !== actualHasSelectableRows) {
1069
+ notifySelection();
1070
+ }
1071
+ }
638
1072
  function getBodyRowCount() {
639
1073
  return getOrderedRowTokens('body').length;
640
1074
  }
@@ -684,6 +1118,20 @@ export function createTableContext(options = {}) {
684
1118
  return false;
685
1119
  return disabledKeys.has(id);
686
1120
  }
1121
+ function isSelectableBodyRow(row) {
1122
+ return (row?.section === 'body' &&
1123
+ row.id !== undefined &&
1124
+ !isRowSelectionDisabled(row.id, row.disabled));
1125
+ }
1126
+ function recomputeSelectableBodyRowCount() {
1127
+ let nextSelectableBodyRowCount = 0;
1128
+ for (const token of bodyRowOrder) {
1129
+ if (isSelectableBodyRow(rows.get(token))) {
1130
+ nextSelectableBodyRowCount += 1;
1131
+ }
1132
+ }
1133
+ selectableBodyRowCount = nextSelectableBodyRowCount;
1134
+ }
687
1135
  function isRowDisabled(id, localDisabled = false) {
688
1136
  return disabledBehavior === 'all' && isRowSelectionDisabled(id, localDisabled);
689
1137
  }
@@ -719,7 +1167,10 @@ export function createTableContext(options = {}) {
719
1167
  return 'some';
720
1168
  }
721
1169
  function hasSelectableRows() {
722
- return getOrderedSelectableRowIds().length > 0 || selectedKeys.size > 0;
1170
+ if (!bodyRowsInitialized) {
1171
+ return selectionMode === 'multiple' || selectedKeys.size > 0;
1172
+ }
1173
+ return selectableBodyRowCount > 0 || selectedKeys.size > 0;
723
1174
  }
724
1175
  function isRowFocused(token) {
725
1176
  if (focusedRowTarget?.rowToken === token)
@@ -1378,16 +1829,22 @@ export function createTableContext(options = {}) {
1378
1829
  notifySelection();
1379
1830
  }
1380
1831
  function setDisabledKeys(keys) {
1832
+ const previousSelectableBodyRowCount = selectableBodyRowCount;
1381
1833
  disabledKeys.clear();
1382
1834
  if (keys) {
1383
1835
  for (const key of keys) {
1384
1836
  disabledKeys.add(key);
1385
1837
  }
1386
1838
  }
1839
+ recomputeSelectableBodyRowCount();
1387
1840
  invalidateLayoutCaches();
1388
1841
  reconcileFocusAfterDisabledStateChange();
1389
1842
  notifyLayout();
1390
- notifySelection();
1843
+ if (selectedKeys.size > 0 ||
1844
+ (bodyRowsInitialized &&
1845
+ (previousSelectableBodyRowCount === 0) !== (selectableBodyRowCount === 0))) {
1846
+ notifySelection();
1847
+ }
1391
1848
  }
1392
1849
  function setRowActionHandler(handler) {
1393
1850
  onRowAction = handler;
@@ -1465,12 +1922,17 @@ export function createTableContext(options = {}) {
1465
1922
  getVisibleColumnIndexByToken,
1466
1923
  getColumnTextValue,
1467
1924
  getColumnWidth,
1925
+ getColumnWidthStyle,
1926
+ hasAuthoredColumnWidthSpec,
1468
1927
  getColumnMinWidth,
1469
1928
  getColumnMaxWidth,
1470
1929
  isColumnHidden,
1471
1930
  isColumnResizable,
1472
1931
  getColumnWidths,
1473
1932
  getVisibleColumnWidths,
1933
+ getResolvedVisibleColumnWidths,
1934
+ hasRelativeVisibleColumnWidths,
1935
+ refreshMeasuredLayout,
1474
1936
  setColumnWidths,
1475
1937
  setColumnWidth,
1476
1938
  setHiddenColumns,
@@ -1482,6 +1944,7 @@ export function createTableContext(options = {}) {
1482
1944
  hasResizableColumns,
1483
1945
  registerRow,
1484
1946
  unregisterRow,
1947
+ markBodyRowsInitialized,
1485
1948
  getHeaderRowCount,
1486
1949
  getBodyRowCount,
1487
1950
  isRowSelected,