@human-kit/svelte-components 1.0.0-alpha.17 → 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.
@@ -14,7 +14,6 @@
14
14
  | 'class'
15
15
  | 'id'
16
16
  | 'role'
17
- | 'tabindex'
18
17
  | 'aria-checked'
19
18
  | 'aria-disabled'
20
19
  | 'aria-readonly'
@@ -40,8 +39,11 @@
40
39
  class?: string;
41
40
  'aria-label'?: string;
42
41
  'aria-labelledby'?: string;
42
+ tabindex?: number;
43
43
  onclick?: HTMLAttributes<HTMLSpanElement>['onclick'];
44
44
  onkeydown?: HTMLAttributes<HTMLSpanElement>['onkeydown'];
45
+ onfocus?: HTMLAttributes<HTMLSpanElement>['onfocus'];
46
+ onmousedown?: HTMLAttributes<HTMLSpanElement>['onmousedown'];
45
47
  };
46
48
 
47
49
  function composeEventHandlers<TEvent extends Event>(
@@ -85,8 +87,11 @@
85
87
  class: className = '',
86
88
  'aria-label': ariaLabel,
87
89
  'aria-labelledby': ariaLabelledby,
90
+ tabindex,
88
91
  onclick: onClickExternal,
89
92
  onkeydown: onKeyDownExternal,
93
+ onfocus: onFocusExternal,
94
+ onmousedown: onMouseDownExternal,
90
95
  ...restProps
91
96
  }: CheckboxRootProps = $props();
92
97
 
@@ -329,7 +334,7 @@
329
334
  bind:this={rootRef}
330
335
  id={rootId}
331
336
  role="checkbox"
332
- tabindex={isDisabled ? undefined : 0}
337
+ tabindex={isDisabled ? undefined : (tabindex ?? 0)}
333
338
  aria-checked={currentIndeterminate ? 'mixed' : currentChecked ? 'true' : 'false'}
334
339
  aria-disabled={isDisabled || undefined}
335
340
  aria-readonly={isReadOnly || undefined}
@@ -350,8 +355,8 @@
350
355
  onkeydown={composeEventHandlers(handleKeyDown, onKeyDownExternal ?? undefined)}
351
356
  onkeyup={handleKeyUp}
352
357
  onpointerdown={handlePointerDown}
353
- onmousedown={handlePointerDown}
354
- onfocus={handleFocus}
358
+ onmousedown={composeEventHandlers(onMouseDownExternal ?? undefined, handlePointerDown)}
359
+ onfocus={composeEventHandlers(handleFocus, onFocusExternal ?? undefined)}
355
360
  onblur={handleBlur}
356
361
  class={cn(
357
362
  'relative inline-flex shrink-0 items-center justify-center align-middle outline-none',
@@ -1,6 +1,6 @@
1
1
  import { type Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
- type CheckboxRootProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'class' | 'id' | 'role' | 'tabindex' | 'aria-checked' | 'aria-disabled' | 'aria-readonly' | 'aria-required' | 'onclick' | 'onkeydown' | 'value'> & {
3
+ type CheckboxRootProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'class' | 'id' | 'role' | 'aria-checked' | 'aria-disabled' | 'aria-readonly' | 'aria-required' | 'onclick' | 'onkeydown' | 'value'> & {
4
4
  id?: string;
5
5
  element?: HTMLSpanElement | null;
6
6
  name?: string;
@@ -18,8 +18,11 @@ type CheckboxRootProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'cla
18
18
  class?: string;
19
19
  'aria-label'?: string;
20
20
  'aria-labelledby'?: string;
21
+ tabindex?: number;
21
22
  onclick?: HTMLAttributes<HTMLSpanElement>['onclick'];
22
23
  onkeydown?: HTMLAttributes<HTMLSpanElement>['onkeydown'];
24
+ onfocus?: HTMLAttributes<HTMLSpanElement>['onfocus'];
25
+ onmousedown?: HTMLAttributes<HTMLSpanElement>['onmousedown'];
23
26
  };
24
27
  declare const CheckboxRoot: import("svelte").Component<CheckboxRootProps, {}, "element" | "isChecked" | "isIndeterminate">;
25
28
  type CheckboxRoot = ReturnType<typeof CheckboxRoot>;
@@ -251,7 +251,7 @@
251
251
  style:width={columnWidthStyle}
252
252
  style:min-width={columnMinWidth !== undefined ? `${columnMinWidth}px` : undefined}
253
253
  style:max-width={columnMaxWidth !== undefined ? `${columnMaxWidth}px` : undefined}
254
- style:display={isColumnHidden ? 'none' : undefined}
254
+ style:display={isColumnHidden ? 'none' : 'table-cell'}
255
255
  onfocus={row.section === 'body' ? handleFocus : undefined}
256
256
  onclick={row.section === 'body' ? handleClick : undefined}
257
257
  ondblclick={row.section === 'body' ? handleDoubleClick : undefined}
@@ -96,39 +96,6 @@
96
96
  };
97
97
  });
98
98
 
99
- $effect(() => {
100
- const checkboxElement = getCheckboxRootElement();
101
- if (!checkboxElement) return;
102
-
103
- if (!isVisible || isDisabled || tabIndex === undefined) {
104
- checkboxElement.removeAttribute('tabindex');
105
- return;
106
- }
107
-
108
- checkboxElement.tabIndex = tabIndex;
109
- });
110
-
111
- $effect(() => {
112
- const checkboxElement = getCheckboxRootElement();
113
- if (!checkboxElement) return;
114
-
115
- const handleElementFocus = (event: FocusEvent) => {
116
- handleFocusIn(event);
117
- };
118
-
119
- const handleElementMouseDown = (event: MouseEvent) => {
120
- handleMouseDown(event);
121
- };
122
-
123
- checkboxElement.addEventListener('focus', handleElementFocus);
124
- checkboxElement.addEventListener('mousedown', handleElementMouseDown);
125
-
126
- return () => {
127
- checkboxElement.removeEventListener('focus', handleElementFocus);
128
- checkboxElement.removeEventListener('mousedown', handleElementMouseDown);
129
- };
130
- });
131
-
132
99
  function applySelection(nextChecked: boolean) {
133
100
  if (isDisabled) return;
134
101
 
@@ -255,8 +222,11 @@
255
222
  aria-label={accessibleLabel}
256
223
  aria-labelledby={ariaLabelledby}
257
224
  data-table-checkbox="true"
225
+ tabindex={tabIndex}
258
226
  onclick={handleClick}
259
227
  onkeydown={handleKeyDown}
228
+ onfocus={handleFocusIn}
229
+ onmousedown={handleMouseDown}
260
230
  class={className}
261
231
  {...restProps}
262
232
  >
@@ -262,7 +262,7 @@
262
262
  style:width={columnWidthStyle}
263
263
  style:min-width={columnMinWidth !== undefined ? `${columnMinWidth}px` : undefined}
264
264
  style:max-width={columnMaxWidth !== undefined ? `${columnMaxWidth}px` : undefined}
265
- style:display={isHidden ? 'none' : undefined}
265
+ style:display={isHidden ? 'none' : 'table-cell'}
266
266
  onfocusin={handleFocusIn}
267
267
  onfocusout={handleFocusOut}
268
268
  onfocus={handleFocus}
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ import { Table } from '../index';
3
+ import type { TableColumnWidth } from '../root/context';
4
+
5
+ let currentColumnWidths = $state<Map<string, TableColumnWidth> | undefined>(undefined);
6
+ const rows = Array.from({ length: 24 }, (_, index) => ({
7
+ id: `row-${index + 1}`,
8
+ request: `PR-${String(index + 1).padStart(4, '0')}`,
9
+ requester: ['Ana Gomez', 'Lucas Perez', 'Mara Silva', 'Juan Torres'][index % 4],
10
+ area: ['Production', 'Logistics', 'Maintenance', 'Quality'][index % 4],
11
+ status: ['Pending', 'Review', 'Approved'][index % 3],
12
+ priority: ['Low', 'Medium', 'High'][index % 3],
13
+ total: 850 + index * 137
14
+ }));
15
+ </script>
16
+
17
+ <div
18
+ data-testid="sandbox-overflow-container"
19
+ style="width: 920px; max-height: 320px; overflow: auto;"
20
+ >
21
+ <Table.Root
22
+ aria-label="Sandbox overflow table"
23
+ selectionMode="multiple"
24
+ bind:columnWidths={currentColumnWidths}
25
+ class="min-w-full border-collapse text-left"
26
+ >
27
+ <Table.Header>
28
+ <Table.Row>
29
+ <Table.Column id="selection" textValue="Selection" width={44}>
30
+ <Table.ColumnHeaderCell data-testid="sandbox-selection-header-cell">
31
+ <Table.Checkbox />
32
+ </Table.ColumnHeaderCell>
33
+ </Table.Column>
34
+ <Table.Column id="request" textValue="Request" isRowHeader defaultWidth={350}>
35
+ <Table.ColumnHeaderCell data-testid="sandbox-request-header-cell">
36
+ <div class="flex items-center justify-between gap-3">
37
+ <span>Request</span>
38
+ <Table.ColumnResizer
39
+ data-testid="sandbox-request-resizer"
40
+ class="inline-flex w-3 cursor-col-resize justify-center"
41
+ />
42
+ </div>
43
+ </Table.ColumnHeaderCell>
44
+ </Table.Column>
45
+ <Table.Column id="requester" textValue="Requester">
46
+ <Table.ColumnHeaderCell>Requester</Table.ColumnHeaderCell>
47
+ </Table.Column>
48
+ <Table.Column id="area" textValue="Area">
49
+ <Table.ColumnHeaderCell>Area</Table.ColumnHeaderCell>
50
+ </Table.Column>
51
+ <Table.Column id="status" textValue="Status">
52
+ <Table.ColumnHeaderCell>Status</Table.ColumnHeaderCell>
53
+ </Table.Column>
54
+ <Table.Column id="priority" textValue="Priority">
55
+ <Table.ColumnHeaderCell>Priority</Table.ColumnHeaderCell>
56
+ </Table.Column>
57
+ <Table.Column id="total" textValue="Total">
58
+ <Table.ColumnHeaderCell data-testid="sandbox-total-header-cell">
59
+ <div class="flex w-full justify-end">Total</div>
60
+ </Table.ColumnHeaderCell>
61
+ </Table.Column>
62
+ </Table.Row>
63
+ </Table.Header>
64
+
65
+ <Table.Body>
66
+ {#each rows as row (row.id)}
67
+ <Table.Row id={row.id}>
68
+ <Table.Cell>
69
+ <Table.Checkbox />
70
+ </Table.Cell>
71
+ <Table.Cell>{row.request}</Table.Cell>
72
+ <Table.Cell>{row.requester}</Table.Cell>
73
+ <Table.Cell>{row.area}</Table.Cell>
74
+ <Table.Cell>{row.status}</Table.Cell>
75
+ <Table.Cell>{row.priority}</Table.Cell>
76
+ <Table.Cell>
77
+ <div class="flex w-full justify-end">${row.total.toLocaleString()}</div>
78
+ </Table.Cell>
79
+ </Table.Row>
80
+ {/each}
81
+ </Table.Body>
82
+ </Table.Root>
83
+ </div>
84
+
85
+ <output data-testid="sandbox-overflow-widths"
86
+ >{JSON.stringify(Object.fromEntries(currentColumnWidths ?? new Map()))}</output
87
+ >
@@ -0,0 +1,3 @@
1
+ declare const TableColumnResizerSandboxOverflowTest: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type TableColumnResizerSandboxOverflowTest = ReturnType<typeof TableColumnResizerSandboxOverflowTest>;
3
+ export default TableColumnResizerSandboxOverflowTest;
@@ -19,6 +19,16 @@ export type TableSortDescriptor = {
19
19
  };
20
20
  export type TableColumnWidth = number | `${number}px` | `${number}%` | `${number}fr`;
21
21
  export declare const DEFAULT_TABLE_COLUMN_MIN_WIDTH = 60;
22
+ export type RoundedWidthDistributionEntry = {
23
+ columnId: string;
24
+ index: number;
25
+ exactWidth: number;
26
+ width: number;
27
+ minWidth: number;
28
+ maxWidth?: number;
29
+ remainder: number;
30
+ };
31
+ export declare function distributeRoundedWidths(entries: RoundedWidthDistributionEntry[], targetTotal: number): RoundedWidthDistributionEntry[];
22
32
  export type TableGridCoord = {
23
33
  row: number;
24
34
  col: number;
@@ -109,6 +119,7 @@ export type TableContext = {
109
119
  getVisibleColumnWidths: () => Map<string, TableColumnWidth>;
110
120
  getResolvedVisibleColumnWidths: () => Map<string, number>;
111
121
  hasRelativeVisibleColumnWidths: () => boolean;
122
+ refreshMeasuredLayout: () => void;
112
123
  setColumnWidths: (widths?: Iterable<readonly [string, TableColumnWidth]>) => void;
113
124
  setColumnWidth: (columnId: string, width: number) => void;
114
125
  setHiddenColumns: (columnIds?: Iterable<string>) => void;
@@ -7,6 +7,45 @@ const TABLE_COLUMN_KEY = Symbol('table-column');
7
7
  const TABLE_CELL_KEY = Symbol('table-cell');
8
8
  const IS_BROWSER = typeof window !== 'undefined';
9
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
+ }
10
49
  export function createTableContext(options = {}) {
11
50
  let selectionMode = options.selectionMode ?? 'none';
12
51
  let selectionBehavior = options.selectionBehavior ?? 'toggle';
@@ -34,6 +73,7 @@ export function createTableContext(options = {}) {
34
73
  const headerRowOrder = [];
35
74
  const bodyRowOrder = [];
36
75
  let bodyRowsInitialized = false;
76
+ let selectableBodyRowCount = 0;
37
77
  const cells = new Map();
38
78
  const cellOrder = [];
39
79
  let orderedRowTokensCache = {
@@ -71,6 +111,7 @@ export function createTableContext(options = {}) {
71
111
  rowsWithCellsCache = null;
72
112
  }
73
113
  let layoutNotifyScheduled = false;
114
+ let selectionNotifyScheduled = false;
74
115
  let widthNotifyScheduled = false;
75
116
  function syncResizerLayoutReady(nextReady) {
76
117
  if (resizerLayoutReady === nextReady)
@@ -91,7 +132,13 @@ export function createTableContext(options = {}) {
91
132
  }
92
133
  }
93
134
  function notifySelection() {
94
- selectionVersion.update((value) => value + 1);
135
+ if (!selectionNotifyScheduled) {
136
+ selectionNotifyScheduled = true;
137
+ queueMicrotask(() => {
138
+ selectionNotifyScheduled = false;
139
+ selectionVersion.update((value) => value + 1);
140
+ });
141
+ }
95
142
  }
96
143
  function notifyFocus() {
97
144
  focusVersion.update((value) => value + 1);
@@ -120,6 +167,9 @@ export function createTableContext(options = {}) {
120
167
  widthVersion.update((value) => value + 1);
121
168
  });
122
169
  }
170
+ function refreshMeasuredLayout() {
171
+ notifyWidthImmediately();
172
+ }
123
173
  function notifyResize() {
124
174
  resizeVersion.update((value) => value + 1);
125
175
  }
@@ -220,10 +270,20 @@ export function createTableContext(options = {}) {
220
270
  }
221
271
  return Math.round(width);
222
272
  }
223
- function clampColumnWidth(columnId, width) {
273
+ function getColumnWidthBounds(columnId) {
224
274
  const registration = getColumnRegistrationById(columnId);
225
- const minWidth = registration?.minWidth ?? DEFAULT_TABLE_COLUMN_MIN_WIDTH;
226
- 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);
227
287
  let next = Math.round(width);
228
288
  if (Number.isNaN(next) || !Number.isFinite(next)) {
229
289
  next = minWidth;
@@ -234,6 +294,19 @@ export function createTableContext(options = {}) {
234
294
  }
235
295
  return next;
236
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
+ }
237
310
  function hasResizableColumns() {
238
311
  for (const column of columns.values()) {
239
312
  if (isColumnResizable(column.id))
@@ -497,10 +570,12 @@ export function createTableContext(options = {}) {
497
570
  return resolvedVisibleColumnWidthsCache;
498
571
  const widths = new Map();
499
572
  const flexibleColumns = [];
573
+ const relativeColumns = [];
500
574
  const tableWidth = getMeasuredTableWidth();
501
- let remainingWidth = tableWidth;
575
+ let fixedWidthTotal = 0;
576
+ let exactRelativeWidthTotal = 0;
502
577
  let totalFr = 0;
503
- for (const token of getVisibleOrderedColumnTokens()) {
578
+ for (const [index, token] of getVisibleOrderedColumnTokens().entries()) {
504
579
  const column = columns.get(token);
505
580
  if (!column)
506
581
  continue;
@@ -510,29 +585,34 @@ export function createTableContext(options = {}) {
510
585
  if (parsed.unit === 'px') {
511
586
  const nextWidth = clampColumnWidth(column.id, parsed.value);
512
587
  widths.set(column.id, nextWidth);
513
- if (remainingWidth !== undefined) {
514
- remainingWidth -= nextWidth;
515
- }
588
+ fixedWidthTotal += nextWidth;
516
589
  continue;
517
590
  }
518
591
  if (parsed.unit === '%') {
519
592
  if (tableWidth === undefined)
520
593
  continue;
521
- const nextWidth = clampColumnWidth(column.id, (tableWidth * parsed.value) / 100);
522
- widths.set(column.id, nextWidth);
523
- if (remainingWidth !== undefined) {
524
- remainingWidth -= nextWidth;
525
- }
594
+ const exactWidth = (tableWidth * parsed.value) / 100;
595
+ relativeColumns.push({ columnId: column.id, index, exactWidth });
596
+ exactRelativeWidthTotal += exactWidth;
526
597
  continue;
527
598
  }
528
- flexibleColumns.push({ columnId: column.id, fr: parsed.value });
599
+ flexibleColumns.push({ columnId: column.id, fr: parsed.value, index });
529
600
  totalFr += parsed.value;
530
601
  }
531
602
  if (tableWidth !== undefined && flexibleColumns.length > 0 && totalFr > 0) {
532
- const distributableWidth = Math.max(remainingWidth ?? tableWidth, 0);
603
+ const distributableWidth = Math.max(tableWidth - fixedWidthTotal - exactRelativeWidthTotal, 0);
533
604
  for (const entry of flexibleColumns) {
534
- const nextWidth = clampColumnWidth(entry.columnId, (distributableWidth * entry.fr) / totalFr);
535
- widths.set(entry.columnId, nextWidth);
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);
536
616
  }
537
617
  }
538
618
  resolvedVisibleColumnWidthsCache = widths;
@@ -546,6 +626,22 @@ export function createTableContext(options = {}) {
546
626
  }
547
627
  return Math.round(width);
548
628
  }
629
+ function getFixedVisibleColumnWidthTotal() {
630
+ let total = 0;
631
+ for (const token of getVisibleOrderedColumnTokens()) {
632
+ const column = columns.get(token);
633
+ if (!column)
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
+ }
549
645
  function getResizableRelativeTailColumnId(activeColumnId) {
550
646
  return getVisibleOrderedColumnTokens()
551
647
  .map((token) => columns.get(token))
@@ -567,6 +663,7 @@ export function createTableContext(options = {}) {
567
663
  ? normalizeColumnWidth(getEffectiveColumnWidthSpec(preservedFlexibleColumnId))
568
664
  : undefined;
569
665
  const measuredTableWidth = getMeasuredTableWidth();
666
+ const fixedVisibleColumnWidthTotal = getFixedVisibleColumnWidthTotal();
570
667
  for (const token of getVisibleOrderedColumnTokens()) {
571
668
  const column = columns.get(token);
572
669
  if (!column)
@@ -606,7 +703,9 @@ export function createTableContext(options = {}) {
606
703
  ? preservedFlexibleEffectiveWidth
607
704
  : undefined,
608
705
  baselineWidths,
609
- baselineTableWidth: measuredTableWidth ?? baselineTotalWidth,
706
+ baselineAvailableTableWidth: measuredTableWidth !== undefined
707
+ ? Math.max(measuredTableWidth - fixedVisibleColumnWidthTotal, 0)
708
+ : baselineTotalWidth,
610
709
  baselineTotalWidth
611
710
  };
612
711
  options.onColumnWidthsChange?.(getColumnWidths());
@@ -655,7 +754,7 @@ export function createTableContext(options = {}) {
655
754
  const baselineTailWidth = resizeSession.baselineWidths.get(resizeSession.flexibleTailColumnId);
656
755
  if (baselineActiveWidth !== undefined && baselineTailWidth !== undefined) {
657
756
  const widthDelta = nextWidth - baselineActiveWidth;
658
- const overflowWidth = Math.max(resizeSession.baselineTotalWidth - resizeSession.baselineTableWidth, 0);
757
+ const overflowWidth = Math.max(resizeSession.baselineTotalWidth - resizeSession.baselineAvailableTableWidth, 0);
659
758
  const tailTargetWidth = widthDelta >= 0
660
759
  ? baselineTailWidth - widthDelta
661
760
  : baselineTailWidth + Math.max(-widthDelta - overflowWidth, 0);
@@ -892,6 +991,7 @@ export function createTableContext(options = {}) {
892
991
  }
893
992
  function registerRow(row) {
894
993
  const existing = rows.get(row.token);
994
+ const previousSelectableBodyRowCount = selectableBodyRowCount;
895
995
  const targetOrder = row.section === 'header' ? headerRowOrder : row.section === 'body' ? bodyRowOrder : null;
896
996
  const alreadyOrdered = targetOrder ? targetOrder.includes(row.token) : false;
897
997
  const wasInHeader = headerRowOrder.includes(row.token);
@@ -907,16 +1007,29 @@ export function createTableContext(options = {}) {
907
1007
  if (wasInBody) {
908
1008
  bodyRowOrder.splice(bodyRowOrder.indexOf(row.token), 1);
909
1009
  }
1010
+ const previousSelectableBodyRow = isSelectableBodyRow(existing);
910
1011
  rows.set(row.token, row);
911
1012
  if (targetOrder && !targetOrder.includes(row.token)) {
912
1013
  targetOrder.push(row.token);
913
1014
  }
914
- notifySelection();
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
+ }
915
1024
  notifyLayout();
916
1025
  }
917
1026
  function unregisterRow(token) {
918
1027
  const row = rows.get(token);
1028
+ const previousSelectableBodyRowCount = selectableBodyRowCount;
919
1029
  rows.delete(token);
1030
+ if (isSelectableBodyRow(row)) {
1031
+ selectableBodyRowCount = Math.max(0, selectableBodyRowCount - 1);
1032
+ }
920
1033
  if (focusedRowTarget?.rowToken === token) {
921
1034
  focusedRowTarget = null;
922
1035
  notifyFocus();
@@ -939,7 +1052,11 @@ export function createTableContext(options = {}) {
939
1052
  notifyFocus();
940
1053
  }
941
1054
  }
942
- notifySelection();
1055
+ if ((selectedKeys.size > 0 && row?.section === 'body') ||
1056
+ (bodyRowsInitialized &&
1057
+ (previousSelectableBodyRowCount === 0) !== (selectableBodyRowCount === 0))) {
1058
+ notifySelection();
1059
+ }
943
1060
  notifyLayout();
944
1061
  }
945
1062
  function markBodyRowsInitialized() {
@@ -947,7 +1064,7 @@ export function createTableContext(options = {}) {
947
1064
  return;
948
1065
  const optimisticHasSelectableRows = selectionMode === 'multiple' || selectedKeys.size > 0;
949
1066
  bodyRowsInitialized = true;
950
- const actualHasSelectableRows = getOrderedSelectableRowIds().length > 0 || selectedKeys.size > 0;
1067
+ const actualHasSelectableRows = selectableBodyRowCount > 0 || selectedKeys.size > 0;
951
1068
  if (optimisticHasSelectableRows !== actualHasSelectableRows) {
952
1069
  notifySelection();
953
1070
  }
@@ -1001,6 +1118,20 @@ export function createTableContext(options = {}) {
1001
1118
  return false;
1002
1119
  return disabledKeys.has(id);
1003
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
+ }
1004
1135
  function isRowDisabled(id, localDisabled = false) {
1005
1136
  return disabledBehavior === 'all' && isRowSelectionDisabled(id, localDisabled);
1006
1137
  }
@@ -1039,7 +1170,7 @@ export function createTableContext(options = {}) {
1039
1170
  if (!bodyRowsInitialized) {
1040
1171
  return selectionMode === 'multiple' || selectedKeys.size > 0;
1041
1172
  }
1042
- return getOrderedSelectableRowIds().length > 0 || selectedKeys.size > 0;
1173
+ return selectableBodyRowCount > 0 || selectedKeys.size > 0;
1043
1174
  }
1044
1175
  function isRowFocused(token) {
1045
1176
  if (focusedRowTarget?.rowToken === token)
@@ -1698,16 +1829,22 @@ export function createTableContext(options = {}) {
1698
1829
  notifySelection();
1699
1830
  }
1700
1831
  function setDisabledKeys(keys) {
1832
+ const previousSelectableBodyRowCount = selectableBodyRowCount;
1701
1833
  disabledKeys.clear();
1702
1834
  if (keys) {
1703
1835
  for (const key of keys) {
1704
1836
  disabledKeys.add(key);
1705
1837
  }
1706
1838
  }
1839
+ recomputeSelectableBodyRowCount();
1707
1840
  invalidateLayoutCaches();
1708
1841
  reconcileFocusAfterDisabledStateChange();
1709
1842
  notifyLayout();
1710
- notifySelection();
1843
+ if (selectedKeys.size > 0 ||
1844
+ (bodyRowsInitialized &&
1845
+ (previousSelectableBodyRowCount === 0) !== (selectableBodyRowCount === 0))) {
1846
+ notifySelection();
1847
+ }
1711
1848
  }
1712
1849
  function setRowActionHandler(handler) {
1713
1850
  onRowAction = handler;
@@ -1795,6 +1932,7 @@ export function createTableContext(options = {}) {
1795
1932
  getVisibleColumnWidths,
1796
1933
  getResolvedVisibleColumnWidths,
1797
1934
  hasRelativeVisibleColumnWidths,
1935
+ refreshMeasuredLayout,
1798
1936
  setColumnWidths,
1799
1937
  setColumnWidth,
1800
1938
  setHiddenColumns,
@@ -295,6 +295,33 @@
295
295
  element = tableElement;
296
296
  });
297
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
+
298
325
  $effect(() => {
299
326
  ctx.setSelectionMode(selectionMode);
300
327
  });
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@human-kit/svelte-components",
3
- "version": "1.0.0-alpha.17",
3
+ "version": "1.0.0-alpha.18",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",