@human-kit/svelte-components 1.0.0-alpha.17 → 1.0.0-alpha.19

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,76 @@
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
+ </script>
7
+
8
+ <div data-testid="narrow-min-width-container" style="width: 240px; overflow-x: auto;">
9
+ <Table.Root
10
+ aria-label="Narrow min width table"
11
+ bind:columnWidths={currentColumnWidths}
12
+ class="min-w-full border-collapse text-left"
13
+ >
14
+ <Table.Header>
15
+ <Table.Row>
16
+ <Table.Column id="request" isRowHeader textValue="Request" minWidth={75}>
17
+ <Table.ColumnHeaderCell data-testid="narrow-request-header-cell">
18
+ <div class="flex items-center justify-between gap-3">
19
+ <span>Request</span>
20
+ <Table.ColumnResizer
21
+ data-testid="narrow-request-resizer"
22
+ class="inline-flex w-3 cursor-col-resize justify-center"
23
+ />
24
+ </div>
25
+ </Table.ColumnHeaderCell>
26
+ </Table.Column>
27
+ <Table.Column id="requester" textValue="Requester" minWidth={140}>
28
+ <Table.ColumnHeaderCell data-testid="narrow-requester-header-cell">
29
+ <div class="flex items-center justify-between gap-3">
30
+ <span>Requester</span>
31
+ <Table.ColumnResizer class="inline-flex w-3 cursor-col-resize justify-center" />
32
+ </div>
33
+ </Table.ColumnHeaderCell>
34
+ </Table.Column>
35
+ <Table.Column id="area" textValue="Area">
36
+ <Table.ColumnHeaderCell data-testid="narrow-area-header-cell">
37
+ <div class="flex items-center justify-between gap-3">
38
+ <span>Area</span>
39
+ <Table.ColumnResizer class="inline-flex w-3 cursor-col-resize justify-center" />
40
+ </div>
41
+ </Table.ColumnHeaderCell>
42
+ </Table.Column>
43
+ <Table.Column id="status" textValue="Status">
44
+ <Table.ColumnHeaderCell data-testid="narrow-status-header-cell">
45
+ <div class="flex items-center justify-between gap-3">
46
+ <span>Status</span>
47
+ <Table.ColumnResizer class="inline-flex w-3 cursor-col-resize justify-center" />
48
+ </div>
49
+ </Table.ColumnHeaderCell>
50
+ </Table.Column>
51
+ <Table.Column id="total" textValue="Total">
52
+ <Table.ColumnHeaderCell data-testid="narrow-total-header-cell">
53
+ <div class="flex items-center justify-between gap-3">
54
+ <span>Total</span>
55
+ <Table.ColumnResizer class="inline-flex w-3 cursor-col-resize justify-center" />
56
+ </div>
57
+ </Table.ColumnHeaderCell>
58
+ </Table.Column>
59
+ </Table.Row>
60
+ </Table.Header>
61
+
62
+ <Table.Body>
63
+ <Table.Row id="pr-001">
64
+ <Table.Cell>PR-001</Table.Cell>
65
+ <Table.Cell>Ana Gomez</Table.Cell>
66
+ <Table.Cell>Ops</Table.Cell>
67
+ <Table.Cell>Pending</Table.Cell>
68
+ <Table.Cell>$1,520</Table.Cell>
69
+ </Table.Row>
70
+ </Table.Body>
71
+ </Table.Root>
72
+ </div>
73
+
74
+ <output data-testid="narrow-min-width-widths"
75
+ >{JSON.stringify(Object.fromEntries(currentColumnWidths ?? new Map()))}</output
76
+ >
@@ -0,0 +1,3 @@
1
+ declare const TableColumnResizerNarrowMinWidthTest: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type TableColumnResizerNarrowMinWidthTest = ReturnType<typeof TableColumnResizerNarrowMinWidthTest>;
3
+ export default TableColumnResizerNarrowMinWidthTest;
@@ -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
  }
@@ -163,7 +213,16 @@ export function createTableContext(options = {}) {
163
213
  return hiddenColumnIds.has(columnId);
164
214
  }
165
215
  function getColumnMinWidth(columnId) {
166
- return getColumnRegistrationById(columnId)?.minWidth;
216
+ const registration = getColumnRegistrationById(columnId);
217
+ if (!registration)
218
+ return undefined;
219
+ if (registration.minWidth !== undefined)
220
+ return registration.minWidth;
221
+ if (!isColumnResizable(columnId) &&
222
+ !isRelativeColumnWidth(getEffectiveColumnWidthSpec(columnId))) {
223
+ return undefined;
224
+ }
225
+ return getColumnWidthBounds(columnId).minWidth;
167
226
  }
168
227
  function getColumnMaxWidth(columnId) {
169
228
  return getColumnRegistrationById(columnId)?.maxWidth;
@@ -220,10 +279,20 @@ export function createTableContext(options = {}) {
220
279
  }
221
280
  return Math.round(width);
222
281
  }
223
- function clampColumnWidth(columnId, width) {
282
+ function getColumnWidthBounds(columnId) {
224
283
  const registration = getColumnRegistrationById(columnId);
225
- const minWidth = registration?.minWidth ?? DEFAULT_TABLE_COLUMN_MIN_WIDTH;
226
- const maxWidth = registration?.maxWidth;
284
+ const fixedWidth = parseColumnWidth(registration?.width);
285
+ const minWidth = registration?.minWidth ??
286
+ (fixedWidth?.unit === 'px'
287
+ ? Math.min(fixedWidth.value, DEFAULT_TABLE_COLUMN_MIN_WIDTH)
288
+ : DEFAULT_TABLE_COLUMN_MIN_WIDTH);
289
+ return {
290
+ minWidth,
291
+ maxWidth: registration?.maxWidth
292
+ };
293
+ }
294
+ function clampColumnWidth(columnId, width) {
295
+ const { minWidth, maxWidth } = getColumnWidthBounds(columnId);
227
296
  let next = Math.round(width);
228
297
  if (Number.isNaN(next) || !Number.isFinite(next)) {
229
298
  next = minWidth;
@@ -234,6 +303,19 @@ export function createTableContext(options = {}) {
234
303
  }
235
304
  return next;
236
305
  }
306
+ function resolveRelativeColumnWidthAllocations(entries, targetTotal) {
307
+ const allocations = entries.map((entry) => {
308
+ const { minWidth, maxWidth } = getColumnWidthBounds(entry.columnId);
309
+ return {
310
+ ...entry,
311
+ width: clampColumnWidth(entry.columnId, entry.exactWidth),
312
+ minWidth,
313
+ maxWidth,
314
+ remainder: entry.exactWidth - Math.floor(entry.exactWidth)
315
+ };
316
+ });
317
+ return distributeRoundedWidths(allocations, targetTotal);
318
+ }
237
319
  function hasResizableColumns() {
238
320
  for (const column of columns.values()) {
239
321
  if (isColumnResizable(column.id))
@@ -410,6 +492,8 @@ export function createTableContext(options = {}) {
410
492
  let fixedPxTotal = 0;
411
493
  let percentTotal = 0;
412
494
  let totalFr = 0;
495
+ const resolvedVisibleWidths = getResolvedVisibleColumnWidths();
496
+ const tableWidth = getMeasuredTableWidth();
413
497
  for (const token of getVisibleOrderedColumnTokens()) {
414
498
  const column = columns.get(token);
415
499
  if (!column)
@@ -440,6 +524,25 @@ export function createTableContext(options = {}) {
440
524
  const availableWidthExpression = availableWidthTerms.length === 1
441
525
  ? availableWidthTerms[0]
442
526
  : `(${availableWidthTerms.join(' - ')})`;
527
+ if (tableWidth !== undefined) {
528
+ const targetRelativeTotal = Math.max(tableWidth - fixedPxTotal, 0);
529
+ let actualRelativeTotal = 0;
530
+ for (const token of getVisibleOrderedColumnTokens()) {
531
+ const column = columns.get(token);
532
+ if (!column)
533
+ continue;
534
+ const spec = parseColumnWidth(getEffectiveColumnWidthSpec(column.id));
535
+ if (!spec || spec.unit === 'px')
536
+ continue;
537
+ actualRelativeTotal += resolvedVisibleWidths.get(column.id) ?? 0;
538
+ }
539
+ if (Math.abs(actualRelativeTotal - targetRelativeTotal) > 1) {
540
+ const constrainedWidth = resolvedVisibleWidths.get(columnId);
541
+ if (constrainedWidth !== undefined) {
542
+ return `${constrainedWidth}px`;
543
+ }
544
+ }
545
+ }
443
546
  if (frShare === 1) {
444
547
  return `calc(${availableWidthExpression})`;
445
548
  }
@@ -497,10 +600,12 @@ export function createTableContext(options = {}) {
497
600
  return resolvedVisibleColumnWidthsCache;
498
601
  const widths = new Map();
499
602
  const flexibleColumns = [];
603
+ const relativeColumns = [];
500
604
  const tableWidth = getMeasuredTableWidth();
501
- let remainingWidth = tableWidth;
605
+ let fixedWidthTotal = 0;
606
+ let exactRelativeWidthTotal = 0;
502
607
  let totalFr = 0;
503
- for (const token of getVisibleOrderedColumnTokens()) {
608
+ for (const [index, token] of getVisibleOrderedColumnTokens().entries()) {
504
609
  const column = columns.get(token);
505
610
  if (!column)
506
611
  continue;
@@ -510,29 +615,34 @@ export function createTableContext(options = {}) {
510
615
  if (parsed.unit === 'px') {
511
616
  const nextWidth = clampColumnWidth(column.id, parsed.value);
512
617
  widths.set(column.id, nextWidth);
513
- if (remainingWidth !== undefined) {
514
- remainingWidth -= nextWidth;
515
- }
618
+ fixedWidthTotal += nextWidth;
516
619
  continue;
517
620
  }
518
621
  if (parsed.unit === '%') {
519
622
  if (tableWidth === undefined)
520
623
  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
- }
624
+ const exactWidth = (tableWidth * parsed.value) / 100;
625
+ relativeColumns.push({ columnId: column.id, index, exactWidth });
626
+ exactRelativeWidthTotal += exactWidth;
526
627
  continue;
527
628
  }
528
- flexibleColumns.push({ columnId: column.id, fr: parsed.value });
629
+ flexibleColumns.push({ columnId: column.id, fr: parsed.value, index });
529
630
  totalFr += parsed.value;
530
631
  }
531
632
  if (tableWidth !== undefined && flexibleColumns.length > 0 && totalFr > 0) {
532
- const distributableWidth = Math.max(remainingWidth ?? tableWidth, 0);
633
+ const distributableWidth = Math.max(tableWidth - fixedWidthTotal - exactRelativeWidthTotal, 0);
533
634
  for (const entry of flexibleColumns) {
534
- const nextWidth = clampColumnWidth(entry.columnId, (distributableWidth * entry.fr) / totalFr);
535
- widths.set(entry.columnId, nextWidth);
635
+ relativeColumns.push({
636
+ columnId: entry.columnId,
637
+ index: entry.index,
638
+ exactWidth: (distributableWidth * entry.fr) / totalFr
639
+ });
640
+ }
641
+ }
642
+ if (tableWidth !== undefined && relativeColumns.length > 0) {
643
+ const targetRelativeTotal = Math.max(tableWidth - fixedWidthTotal, 0);
644
+ for (const entry of resolveRelativeColumnWidthAllocations(relativeColumns, targetRelativeTotal)) {
645
+ widths.set(entry.columnId, entry.width);
536
646
  }
537
647
  }
538
648
  resolvedVisibleColumnWidthsCache = widths;
@@ -546,6 +656,22 @@ export function createTableContext(options = {}) {
546
656
  }
547
657
  return Math.round(width);
548
658
  }
659
+ function getFixedVisibleColumnWidthTotal() {
660
+ let total = 0;
661
+ for (const token of getVisibleOrderedColumnTokens()) {
662
+ const column = columns.get(token);
663
+ if (!column)
664
+ continue;
665
+ if (getFixedColumnWidthSpec(column.id) === undefined)
666
+ continue;
667
+ const measuredWidth = getMeasuredHeaderWidth(token);
668
+ const resolvedWidth = getColumnWidth(column.id) ?? measuredWidth;
669
+ if (resolvedWidth === undefined)
670
+ continue;
671
+ total += clampColumnWidth(column.id, resolvedWidth);
672
+ }
673
+ return total;
674
+ }
549
675
  function getResizableRelativeTailColumnId(activeColumnId) {
550
676
  return getVisibleOrderedColumnTokens()
551
677
  .map((token) => columns.get(token))
@@ -567,6 +693,7 @@ export function createTableContext(options = {}) {
567
693
  ? normalizeColumnWidth(getEffectiveColumnWidthSpec(preservedFlexibleColumnId))
568
694
  : undefined;
569
695
  const measuredTableWidth = getMeasuredTableWidth();
696
+ const fixedVisibleColumnWidthTotal = getFixedVisibleColumnWidthTotal();
570
697
  for (const token of getVisibleOrderedColumnTokens()) {
571
698
  const column = columns.get(token);
572
699
  if (!column)
@@ -606,7 +733,9 @@ export function createTableContext(options = {}) {
606
733
  ? preservedFlexibleEffectiveWidth
607
734
  : undefined,
608
735
  baselineWidths,
609
- baselineTableWidth: measuredTableWidth ?? baselineTotalWidth,
736
+ baselineAvailableTableWidth: measuredTableWidth !== undefined
737
+ ? Math.max(measuredTableWidth - fixedVisibleColumnWidthTotal, 0)
738
+ : baselineTotalWidth,
610
739
  baselineTotalWidth
611
740
  };
612
741
  options.onColumnWidthsChange?.(getColumnWidths());
@@ -655,7 +784,7 @@ export function createTableContext(options = {}) {
655
784
  const baselineTailWidth = resizeSession.baselineWidths.get(resizeSession.flexibleTailColumnId);
656
785
  if (baselineActiveWidth !== undefined && baselineTailWidth !== undefined) {
657
786
  const widthDelta = nextWidth - baselineActiveWidth;
658
- const overflowWidth = Math.max(resizeSession.baselineTotalWidth - resizeSession.baselineTableWidth, 0);
787
+ const overflowWidth = Math.max(resizeSession.baselineTotalWidth - resizeSession.baselineAvailableTableWidth, 0);
659
788
  const tailTargetWidth = widthDelta >= 0
660
789
  ? baselineTailWidth - widthDelta
661
790
  : baselineTailWidth + Math.max(-widthDelta - overflowWidth, 0);
@@ -892,6 +1021,7 @@ export function createTableContext(options = {}) {
892
1021
  }
893
1022
  function registerRow(row) {
894
1023
  const existing = rows.get(row.token);
1024
+ const previousSelectableBodyRowCount = selectableBodyRowCount;
895
1025
  const targetOrder = row.section === 'header' ? headerRowOrder : row.section === 'body' ? bodyRowOrder : null;
896
1026
  const alreadyOrdered = targetOrder ? targetOrder.includes(row.token) : false;
897
1027
  const wasInHeader = headerRowOrder.includes(row.token);
@@ -907,16 +1037,29 @@ export function createTableContext(options = {}) {
907
1037
  if (wasInBody) {
908
1038
  bodyRowOrder.splice(bodyRowOrder.indexOf(row.token), 1);
909
1039
  }
1040
+ const previousSelectableBodyRow = isSelectableBodyRow(existing);
910
1041
  rows.set(row.token, row);
911
1042
  if (targetOrder && !targetOrder.includes(row.token)) {
912
1043
  targetOrder.push(row.token);
913
1044
  }
914
- notifySelection();
1045
+ const nextSelectableBodyRow = isSelectableBodyRow(row);
1046
+ if (previousSelectableBodyRow !== nextSelectableBodyRow) {
1047
+ selectableBodyRowCount += nextSelectableBodyRow ? 1 : -1;
1048
+ }
1049
+ if ((selectedKeys.size > 0 && (existing?.section === 'body' || row.section === 'body')) ||
1050
+ (bodyRowsInitialized &&
1051
+ (previousSelectableBodyRowCount === 0) !== (selectableBodyRowCount === 0))) {
1052
+ notifySelection();
1053
+ }
915
1054
  notifyLayout();
916
1055
  }
917
1056
  function unregisterRow(token) {
918
1057
  const row = rows.get(token);
1058
+ const previousSelectableBodyRowCount = selectableBodyRowCount;
919
1059
  rows.delete(token);
1060
+ if (isSelectableBodyRow(row)) {
1061
+ selectableBodyRowCount = Math.max(0, selectableBodyRowCount - 1);
1062
+ }
920
1063
  if (focusedRowTarget?.rowToken === token) {
921
1064
  focusedRowTarget = null;
922
1065
  notifyFocus();
@@ -939,7 +1082,11 @@ export function createTableContext(options = {}) {
939
1082
  notifyFocus();
940
1083
  }
941
1084
  }
942
- notifySelection();
1085
+ if ((selectedKeys.size > 0 && row?.section === 'body') ||
1086
+ (bodyRowsInitialized &&
1087
+ (previousSelectableBodyRowCount === 0) !== (selectableBodyRowCount === 0))) {
1088
+ notifySelection();
1089
+ }
943
1090
  notifyLayout();
944
1091
  }
945
1092
  function markBodyRowsInitialized() {
@@ -947,7 +1094,7 @@ export function createTableContext(options = {}) {
947
1094
  return;
948
1095
  const optimisticHasSelectableRows = selectionMode === 'multiple' || selectedKeys.size > 0;
949
1096
  bodyRowsInitialized = true;
950
- const actualHasSelectableRows = getOrderedSelectableRowIds().length > 0 || selectedKeys.size > 0;
1097
+ const actualHasSelectableRows = selectableBodyRowCount > 0 || selectedKeys.size > 0;
951
1098
  if (optimisticHasSelectableRows !== actualHasSelectableRows) {
952
1099
  notifySelection();
953
1100
  }
@@ -1001,6 +1148,20 @@ export function createTableContext(options = {}) {
1001
1148
  return false;
1002
1149
  return disabledKeys.has(id);
1003
1150
  }
1151
+ function isSelectableBodyRow(row) {
1152
+ return (row?.section === 'body' &&
1153
+ row.id !== undefined &&
1154
+ !isRowSelectionDisabled(row.id, row.disabled));
1155
+ }
1156
+ function recomputeSelectableBodyRowCount() {
1157
+ let nextSelectableBodyRowCount = 0;
1158
+ for (const token of bodyRowOrder) {
1159
+ if (isSelectableBodyRow(rows.get(token))) {
1160
+ nextSelectableBodyRowCount += 1;
1161
+ }
1162
+ }
1163
+ selectableBodyRowCount = nextSelectableBodyRowCount;
1164
+ }
1004
1165
  function isRowDisabled(id, localDisabled = false) {
1005
1166
  return disabledBehavior === 'all' && isRowSelectionDisabled(id, localDisabled);
1006
1167
  }
@@ -1039,7 +1200,7 @@ export function createTableContext(options = {}) {
1039
1200
  if (!bodyRowsInitialized) {
1040
1201
  return selectionMode === 'multiple' || selectedKeys.size > 0;
1041
1202
  }
1042
- return getOrderedSelectableRowIds().length > 0 || selectedKeys.size > 0;
1203
+ return selectableBodyRowCount > 0 || selectedKeys.size > 0;
1043
1204
  }
1044
1205
  function isRowFocused(token) {
1045
1206
  if (focusedRowTarget?.rowToken === token)
@@ -1064,6 +1225,7 @@ export function createTableContext(options = {}) {
1064
1225
  cellOrder.push(cell.key);
1065
1226
  }
1066
1227
  notifyLayout();
1228
+ notifyWidth();
1067
1229
  }
1068
1230
  function unregisterCell(key) {
1069
1231
  cells.delete(key);
@@ -1076,6 +1238,7 @@ export function createTableContext(options = {}) {
1076
1238
  notifyFocus();
1077
1239
  }
1078
1240
  notifyLayout();
1241
+ notifyWidth();
1079
1242
  }
1080
1243
  function isCellFocused(key) {
1081
1244
  return focusedCellKey === key;
@@ -1698,16 +1861,22 @@ export function createTableContext(options = {}) {
1698
1861
  notifySelection();
1699
1862
  }
1700
1863
  function setDisabledKeys(keys) {
1864
+ const previousSelectableBodyRowCount = selectableBodyRowCount;
1701
1865
  disabledKeys.clear();
1702
1866
  if (keys) {
1703
1867
  for (const key of keys) {
1704
1868
  disabledKeys.add(key);
1705
1869
  }
1706
1870
  }
1871
+ recomputeSelectableBodyRowCount();
1707
1872
  invalidateLayoutCaches();
1708
1873
  reconcileFocusAfterDisabledStateChange();
1709
1874
  notifyLayout();
1710
- notifySelection();
1875
+ if (selectedKeys.size > 0 ||
1876
+ (bodyRowsInitialized &&
1877
+ (previousSelectableBodyRowCount === 0) !== (selectableBodyRowCount === 0))) {
1878
+ notifySelection();
1879
+ }
1711
1880
  }
1712
1881
  function setRowActionHandler(handler) {
1713
1882
  onRowAction = handler;
@@ -1795,6 +1964,7 @@ export function createTableContext(options = {}) {
1795
1964
  getVisibleColumnWidths,
1796
1965
  getResolvedVisibleColumnWidths,
1797
1966
  hasRelativeVisibleColumnWidths,
1967
+ refreshMeasuredLayout,
1798
1968
  setColumnWidths,
1799
1969
  setColumnWidth,
1800
1970
  setHiddenColumns,
@@ -210,6 +210,20 @@
210
210
  return columns;
211
211
  });
212
212
 
213
+ const minimumTableWidth = $derived.by(() => {
214
+ if (!hasResizable && !ctx.hasRelativeVisibleColumnWidths()) return undefined;
215
+
216
+ let total = 0;
217
+ let hasMinimumWidth = false;
218
+ for (const column of layoutColumns) {
219
+ if (column.minWidth === undefined) continue;
220
+ total += column.minWidth;
221
+ hasMinimumWidth = true;
222
+ }
223
+
224
+ return hasMinimumWidth ? total : undefined;
225
+ });
226
+
213
227
  const explicitManagedTableWidth = $derived.by(() => {
214
228
  void $layoutVersion;
215
229
  if (ctx.hasRelativeVisibleColumnWidths()) return undefined;
@@ -295,6 +309,35 @@
295
309
  element = tableElement;
296
310
  });
297
311
 
312
+ $effect(() => {
313
+ if (!tableElement) return;
314
+
315
+ const resizeTarget = tableElement.parentElement ?? tableElement;
316
+ const refreshMeasuredLayout = () => {
317
+ ctx.refreshMeasuredLayout();
318
+ };
319
+
320
+ refreshMeasuredLayout();
321
+
322
+ window.addEventListener('resize', refreshMeasuredLayout);
323
+ window.visualViewport?.addEventListener('resize', refreshMeasuredLayout);
324
+
325
+ const resizeObserver =
326
+ typeof ResizeObserver !== 'undefined'
327
+ ? new ResizeObserver(() => {
328
+ refreshMeasuredLayout();
329
+ })
330
+ : null;
331
+
332
+ resizeObserver?.observe(resizeTarget);
333
+
334
+ return () => {
335
+ window.removeEventListener('resize', refreshMeasuredLayout);
336
+ window.visualViewport?.removeEventListener('resize', refreshMeasuredLayout);
337
+ resizeObserver?.disconnect();
338
+ };
339
+ });
340
+
298
341
  $effect(() => {
299
342
  ctx.setSelectionMode(selectionMode);
300
343
  });
@@ -448,7 +491,11 @@
448
491
  style:width={resolvedTableWidth !== undefined
449
492
  ? `${resolvedTableWidth}px`
450
493
  : fallbackRelativeTableWidth}
451
- style:min-width={resolvedTableWidth !== undefined ? '0' : undefined}
494
+ style:min-width={minimumTableWidth !== undefined
495
+ ? `${minimumTableWidth}px`
496
+ : resolvedTableWidth !== undefined
497
+ ? '0'
498
+ : undefined}
452
499
  aria-label={ariaLabel}
453
500
  aria-labelledby={ariaLabelledby}
454
501
  aria-colcount={ariaColCount}
@@ -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.19",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",