@human-kit/svelte-components 1.0.0-alpha.20 → 1.0.0-alpha.21

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 (30) hide show
  1. package/dist/button/root/button-root.svelte +32 -1
  2. package/dist/dialog/trigger/dialog-trigger.svelte +3 -0
  3. package/dist/popover/content/popover-content-controlled-close-test.svelte +30 -0
  4. package/dist/popover/content/popover-content-controlled-close-test.svelte.d.ts +3 -0
  5. package/dist/popover/content/popover-content.svelte +38 -0
  6. package/dist/popover/trigger/popover-trigger-button-root-test.svelte +17 -0
  7. package/dist/popover/trigger/popover-trigger-button-root-test.svelte.d.ts +18 -0
  8. package/dist/popover/trigger/popover-trigger-button.svelte +1 -0
  9. package/dist/popover/trigger/popover-trigger.svelte +3 -0
  10. package/dist/table/body/README.md +18 -5
  11. package/dist/table/body/table-body-items-test.svelte +45 -0
  12. package/dist/table/body/table-body-items-test.svelte.d.ts +18 -0
  13. package/dist/table/body/table-body.svelte +153 -6
  14. package/dist/table/body/table-body.svelte.d.ts +43 -2
  15. package/dist/table/cell/table-cell.svelte +5 -17
  16. package/dist/table/checkbox/table-checkbox-test.svelte +116 -74
  17. package/dist/table/checkbox/table-checkbox-test.svelte.d.ts +4 -0
  18. package/dist/table/empty-state/table-empty-state.svelte +1 -1
  19. package/dist/table/index.d.ts +1 -1
  20. package/dist/table/root/context.d.ts +5 -0
  21. package/dist/table/root/context.js +51 -9
  22. package/dist/table/root/table-root.svelte +17 -9
  23. package/dist/table/root/table-ssr-wrapper-column.svelte +48 -0
  24. package/dist/table/root/table-ssr-wrapper-column.svelte.d.ts +4 -0
  25. package/dist/table/root/table-ssr-wrapper-context.d.ts +11 -0
  26. package/dist/table/root/table-ssr-wrapper-context.js +13 -0
  27. package/dist/table/root/table-ssr-wrapper-test.svelte +57 -0
  28. package/dist/table/root/table-ssr-wrapper-test.svelte.d.ts +3 -0
  29. package/dist/table/types.d.ts +20 -3
  30. package/package.json +3 -2
@@ -6,6 +6,7 @@
6
6
  TableSelectionKey,
7
7
  TableSelectionMode
8
8
  } from '../root/context';
9
+ import type { TableBodyVirtualizer } from '../types.js';
9
10
 
10
11
  type DemoRow = {
11
12
  id: string;
@@ -32,6 +33,9 @@
32
33
  disallowEmptySelection?: boolean;
33
34
  disabledKeys?: Iterable<TableSelectionKey>;
34
35
  initialSelectedKeys?: Iterable<TableSelectionKey>;
36
+ useItemsMode?: boolean;
37
+ virtualizer?: TableBodyVirtualizer;
38
+ containerHeight?: string;
35
39
  };
36
40
 
37
41
  let {
@@ -41,7 +45,10 @@
41
45
  disabledBehavior = 'all',
42
46
  disallowEmptySelection = false,
43
47
  disabledKeys,
44
- initialSelectedKeys
48
+ initialSelectedKeys,
49
+ useItemsMode = false,
50
+ virtualizer,
51
+ containerHeight = '160px'
45
52
  }: CheckboxTestProps = $props();
46
53
 
47
54
  let currentSelectedKeys = $state<Set<TableSelectionKey>>(
@@ -49,80 +56,115 @@
49
56
  );
50
57
  </script>
51
58
 
52
- <Table.Root
53
- aria-label="Users table"
54
- {selectionMode}
55
- {selectionBehavior}
56
- {disabledBehavior}
57
- {disallowEmptySelection}
58
- bind:selectedKeys={currentSelectedKeys}
59
- {disabledKeys}
60
- >
61
- <Table.Header>
62
- <Table.Row>
63
- <Table.Column id="selection" textValue="Selection">
64
- <Table.ColumnHeaderCell data-testid="selection-header-cell">
65
- <Table.Checkbox style={checkboxStyle} data-testid="header-checkbox">
66
- <Table.CheckboxIndicator style={indicatorStyle}>
67
- <svg aria-hidden="true" viewBox="0 0 16 16" class="h-3.5 w-3.5">
68
- <path
69
- d="M3.75 8.5 6.75 11.5 12.25 5.5"
70
- fill="none"
71
- stroke="currentColor"
72
- stroke-linecap="round"
73
- stroke-linejoin="round"
74
- stroke-width="2"
75
- />
76
- </svg>
77
- </Table.CheckboxIndicator>
78
- </Table.Checkbox>
79
- </Table.ColumnHeaderCell>
80
- </Table.Column>
81
- <Table.Column id="email" isRowHeader textValue="Email">
82
- <Table.ColumnHeaderCell>Email</Table.ColumnHeaderCell>
83
- </Table.Column>
84
- <Table.Column id="group" textValue="Group">
85
- <Table.ColumnHeaderCell>Group</Table.ColumnHeaderCell>
86
- </Table.Column>
87
- </Table.Row>
88
- </Table.Header>
89
-
90
- <Table.Body>
91
- {#each rows as row (row.id)}
92
- <Table.Row
93
- id={row.id}
94
- isDisabled={disabledKeys ? Array.from(disabledKeys).includes(row.id) : false}
95
- >
96
- <Table.Cell data-testid={`selection-cell-${row.id}`}>
97
- <Table.Checkbox style={checkboxStyle} data-testid={`row-checkbox-${row.id}`}>
98
- <Table.CheckboxIndicator style={indicatorStyle}>
99
- <svg aria-hidden="true" viewBox="0 0 16 16" class="h-3.5 w-3.5">
100
- <path
101
- d="M3.75 8.5 6.75 11.5 12.25 5.5"
102
- fill="none"
103
- stroke="currentColor"
104
- stroke-linecap="round"
105
- stroke-linejoin="round"
106
- stroke-width="2"
107
- />
108
- </svg>
109
- </Table.CheckboxIndicator>
110
- </Table.Checkbox>
111
- </Table.Cell>
112
- <Table.Cell data-testid={`email-cell-${row.id}`}>{row.email}</Table.Cell>
113
- <Table.Cell data-testid={`group-cell-${row.id}`}>{row.group}</Table.Cell>
59
+ <div style={useItemsMode ? `max-height:${containerHeight};overflow:auto;` : undefined}>
60
+ <Table.Root
61
+ aria-label="Users table"
62
+ {selectionMode}
63
+ {selectionBehavior}
64
+ {disabledBehavior}
65
+ {disallowEmptySelection}
66
+ bind:selectedKeys={currentSelectedKeys}
67
+ {disabledKeys}
68
+ >
69
+ <Table.Header>
70
+ <Table.Row>
71
+ <Table.Column id="selection" textValue="Selection">
72
+ <Table.ColumnHeaderCell data-testid="selection-header-cell">
73
+ <Table.Checkbox style={checkboxStyle} data-testid="header-checkbox">
74
+ <Table.CheckboxIndicator style={indicatorStyle}>
75
+ <svg aria-hidden="true" viewBox="0 0 16 16" class="h-3.5 w-3.5">
76
+ <path
77
+ d="M3.75 8.5 6.75 11.5 12.25 5.5"
78
+ fill="none"
79
+ stroke="currentColor"
80
+ stroke-linecap="round"
81
+ stroke-linejoin="round"
82
+ stroke-width="2"
83
+ />
84
+ </svg>
85
+ </Table.CheckboxIndicator>
86
+ </Table.Checkbox>
87
+ </Table.ColumnHeaderCell>
88
+ </Table.Column>
89
+ <Table.Column id="email" isRowHeader textValue="Email">
90
+ <Table.ColumnHeaderCell>Email</Table.ColumnHeaderCell>
91
+ </Table.Column>
92
+ <Table.Column id="group" textValue="Group">
93
+ <Table.ColumnHeaderCell>Group</Table.ColumnHeaderCell>
94
+ </Table.Column>
114
95
  </Table.Row>
115
- {/each}
116
- <Table.EmptyState>No users found.</Table.EmptyState>
117
- </Table.Body>
96
+ </Table.Header>
97
+
98
+ {#if useItemsMode}
99
+ <Table.Body items={rows} {virtualizer}>
100
+ {#snippet children(row)}
101
+ <Table.Row
102
+ id={row.id}
103
+ isDisabled={disabledKeys ? Array.from(disabledKeys).includes(row.id) : false}
104
+ >
105
+ <Table.Cell data-testid={`selection-cell-${row.id}`}>
106
+ <Table.Checkbox style={checkboxStyle} data-testid={`row-checkbox-${row.id}`}>
107
+ <Table.CheckboxIndicator style={indicatorStyle}>
108
+ <svg aria-hidden="true" viewBox="0 0 16 16" class="h-3.5 w-3.5">
109
+ <path
110
+ d="M3.75 8.5 6.75 11.5 12.25 5.5"
111
+ fill="none"
112
+ stroke="currentColor"
113
+ stroke-linecap="round"
114
+ stroke-linejoin="round"
115
+ stroke-width="2"
116
+ />
117
+ </svg>
118
+ </Table.CheckboxIndicator>
119
+ </Table.Checkbox>
120
+ </Table.Cell>
121
+ <Table.Cell data-testid={`email-cell-${row.id}`}>{row.email}</Table.Cell>
122
+ <Table.Cell data-testid={`group-cell-${row.id}`}>{row.group}</Table.Cell>
123
+ </Table.Row>
124
+ {/snippet}
125
+ {#snippet empty()}
126
+ <Table.EmptyState>No users found.</Table.EmptyState>
127
+ {/snippet}
128
+ </Table.Body>
129
+ {:else}
130
+ <Table.Body>
131
+ {#each rows as row (row.id)}
132
+ <Table.Row
133
+ id={row.id}
134
+ isDisabled={disabledKeys ? Array.from(disabledKeys).includes(row.id) : false}
135
+ >
136
+ <Table.Cell data-testid={`selection-cell-${row.id}`}>
137
+ <Table.Checkbox style={checkboxStyle} data-testid={`row-checkbox-${row.id}`}>
138
+ <Table.CheckboxIndicator style={indicatorStyle}>
139
+ <svg aria-hidden="true" viewBox="0 0 16 16" class="h-3.5 w-3.5">
140
+ <path
141
+ d="M3.75 8.5 6.75 11.5 12.25 5.5"
142
+ fill="none"
143
+ stroke="currentColor"
144
+ stroke-linecap="round"
145
+ stroke-linejoin="round"
146
+ stroke-width="2"
147
+ />
148
+ </svg>
149
+ </Table.CheckboxIndicator>
150
+ </Table.Checkbox>
151
+ </Table.Cell>
152
+ <Table.Cell data-testid={`email-cell-${row.id}`}>{row.email}</Table.Cell>
153
+ <Table.Cell data-testid={`group-cell-${row.id}`}>{row.group}</Table.Cell>
154
+ </Table.Row>
155
+ {/each}
156
+ <Table.EmptyState>No users found.</Table.EmptyState>
157
+ </Table.Body>
158
+ {/if}
118
159
 
119
- <Table.Footer>
120
- <Table.Row>
121
- <Table.Cell />
122
- <Table.Cell>Total</Table.Cell>
123
- <Table.Cell>{rows.length} users</Table.Cell>
124
- </Table.Row>
125
- </Table.Footer>
126
- </Table.Root>
160
+ <Table.Footer>
161
+ <Table.Row>
162
+ <Table.Cell />
163
+ <Table.Cell>Total</Table.Cell>
164
+ <Table.Cell>{rows.length} users</Table.Cell>
165
+ </Table.Row>
166
+ </Table.Footer>
167
+ </Table.Root>
168
+ </div>
127
169
 
128
170
  <output data-testid="selected-keys">{JSON.stringify([...currentSelectedKeys])}</output>
@@ -1,4 +1,5 @@
1
1
  import type { TableDisabledBehavior, TableSelectionBehavior, TableSelectionKey, TableSelectionMode } from '../root/context';
2
+ import type { TableBodyVirtualizer } from '../types.js';
2
3
  type DemoRow = {
3
4
  id: string;
4
5
  email: string;
@@ -12,6 +13,9 @@ type CheckboxTestProps = {
12
13
  disallowEmptySelection?: boolean;
13
14
  disabledKeys?: Iterable<TableSelectionKey>;
14
15
  initialSelectedKeys?: Iterable<TableSelectionKey>;
16
+ useItemsMode?: boolean;
17
+ virtualizer?: TableBodyVirtualizer;
18
+ containerHeight?: string;
15
19
  };
16
20
  declare const TableCheckboxTest: import("svelte").Component<CheckboxTestProps, {}, "">;
17
21
  type TableCheckboxTest = ReturnType<typeof TableCheckboxTest>;
@@ -13,7 +13,7 @@
13
13
  const layoutVersion = table.layoutVersion;
14
14
  const isVisible = $derived.by(() => {
15
15
  void $layoutVersion;
16
- return table.getBodyRowCount() === 0;
16
+ return table.getLogicalBodyRowCount() === 0;
17
17
  });
18
18
  const columnCount = $derived.by(() => {
19
19
  void $layoutVersion;
@@ -1,5 +1,5 @@
1
1
  export * as Table from './index.parts.js';
2
- export type { TableBodyProps, TableCellProps, TableCheckboxIndicatorProps, TableCheckboxProps, TableColumnHeaderCellProps, TableColumnProps, TableColumnResizerProps, TableSortTriggerRenderState, TableSortTriggerProps, TableEmptyStateProps, TableFooterProps, TableHeaderProps, TableRowProps, TableRootProps } from './types.js';
2
+ export type { TableBodyProps, TableBodyVirtualizer, TableCellProps, TableCheckboxIndicatorProps, TableCheckboxProps, TableColumnHeaderCellProps, TableColumnProps, TableColumnResizerProps, TableSortTriggerRenderState, TableSortTriggerProps, TableEmptyStateProps, TableFooterProps, TableHeaderProps, TableRowProps, TableRootProps } from './types.js';
3
3
  export { default as TableRoot } from './root/table-root.svelte';
4
4
  export { default as TableColumn } from './column/table-column.svelte';
5
5
  export { default as TableHeader } from './header/table-header.svelte';
@@ -1,5 +1,8 @@
1
1
  import { type Readable } from 'svelte/store';
2
2
  export type TableSelectionKey = string | number;
3
+ export type TableRowItem = Record<string, unknown> & {
4
+ id: TableSelectionKey;
5
+ };
3
6
  export type TableSelectionMode = 'none' | 'single' | 'multiple';
4
7
  export type TableSelectionBehavior = 'toggle' | 'replace';
5
8
  export type TableDisabledBehavior = 'selection' | 'all';
@@ -132,9 +135,11 @@ export type TableContext = {
132
135
  hasResizableColumns: () => boolean;
133
136
  registerRow: (row: TableRowRegistration) => void;
134
137
  unregisterRow: (token: string) => void;
138
+ setLogicalBodyRows: (ids?: Iterable<TableSelectionKey>) => void;
135
139
  markBodyRowsInitialized: () => void;
136
140
  getHeaderRowCount: () => number;
137
141
  getBodyRowCount: () => number;
142
+ getLogicalBodyRowCount: () => number;
138
143
  isRowSelected: (id: TableSelectionKey | undefined) => boolean;
139
144
  isRowFocused: (token: string) => boolean;
140
145
  isRowFocusTarget: (token: string) => boolean;
@@ -75,6 +75,7 @@ export function createTableContext(options = {}) {
75
75
  const bodyRowOrder = [];
76
76
  let bodyRowsInitialized = false;
77
77
  let selectableBodyRowCount = 0;
78
+ let logicalBodyRowIds = null;
78
79
  const cells = new Map();
79
80
  const cellOrder = [];
80
81
  let orderedRowTokensCache = {
@@ -86,6 +87,8 @@ export function createTableContext(options = {}) {
86
87
  let columnWidthsCache = null;
87
88
  let visibleColumnWidthsCache = null;
88
89
  let resolvedVisibleColumnWidthsCache = null;
90
+ let measuredTableWidthCache;
91
+ let hasMeasuredTableWidthCache = false;
89
92
  let navigableCellsCache = null;
90
93
  let rowsWithCellsCache = null;
91
94
  const layoutVersion = writable(0);
@@ -109,6 +112,8 @@ export function createTableContext(options = {}) {
109
112
  columnWidthsCache = null;
110
113
  visibleColumnWidthsCache = null;
111
114
  resolvedVisibleColumnWidthsCache = null;
115
+ measuredTableWidthCache = undefined;
116
+ hasMeasuredTableWidthCache = false;
112
117
  navigableCellsCache = null;
113
118
  rowsWithCellsCache = null;
114
119
  }
@@ -153,6 +158,8 @@ export function createTableContext(options = {}) {
153
158
  columnWidthsCache = null;
154
159
  visibleColumnWidthsCache = null;
155
160
  resolvedVisibleColumnWidthsCache = null;
161
+ measuredTableWidthCache = undefined;
162
+ hasMeasuredTableWidthCache = false;
156
163
  if (!widthNotifyScheduled) {
157
164
  widthNotifyScheduled = true;
158
165
  queueMicrotask(() => {
@@ -165,6 +172,8 @@ export function createTableContext(options = {}) {
165
172
  columnWidthsCache = null;
166
173
  visibleColumnWidthsCache = null;
167
174
  resolvedVisibleColumnWidthsCache = null;
175
+ measuredTableWidthCache = undefined;
176
+ hasMeasuredTableWidthCache = false;
168
177
  flushSync(() => {
169
178
  widthVersion.update((value) => value + 1);
170
179
  });
@@ -259,6 +268,9 @@ export function createTableContext(options = {}) {
259
268
  return resizerLayoutReady && columnsWithResizers.size > 0 ? '1fr' : undefined;
260
269
  }
261
270
  function getMeasuredTableWidth() {
271
+ if (hasMeasuredTableWidthCache) {
272
+ return measuredTableWidthCache;
273
+ }
262
274
  const tableCell = Array.from(cells.values()).find((cell) => cell.element)?.element;
263
275
  const tableElement = tableCell?.closest('table');
264
276
  const tableWidth = tableElement?.getBoundingClientRect().width;
@@ -277,9 +289,13 @@ export function createTableContext(options = {}) {
277
289
  // the actual available space the table should fill.
278
290
  const width = containerWidth ?? tableWidth;
279
291
  if (width === undefined || width <= 0 || !Number.isFinite(width)) {
292
+ hasMeasuredTableWidthCache = true;
293
+ measuredTableWidthCache = undefined;
280
294
  return undefined;
281
295
  }
282
- return Math.round(width);
296
+ measuredTableWidthCache = Math.round(width);
297
+ hasMeasuredTableWidthCache = true;
298
+ return measuredTableWidthCache;
283
299
  }
284
300
  function getColumnWidthBounds(columnId) {
285
301
  const registration = getColumnRegistrationById(columnId);
@@ -1066,6 +1082,27 @@ export function createTableContext(options = {}) {
1066
1082
  }
1067
1083
  notifyLayout();
1068
1084
  }
1085
+ function hasSameLogicalBodyRows(nextIds) {
1086
+ if (logicalBodyRowIds === nextIds)
1087
+ return true;
1088
+ if (logicalBodyRowIds === null || nextIds === null)
1089
+ return false;
1090
+ if (logicalBodyRowIds.length !== nextIds.length)
1091
+ return false;
1092
+ for (let index = 0; index < logicalBodyRowIds.length; index += 1) {
1093
+ if (logicalBodyRowIds[index] !== nextIds[index])
1094
+ return false;
1095
+ }
1096
+ return true;
1097
+ }
1098
+ function setLogicalBodyRows(ids) {
1099
+ const nextIds = ids ? [...ids] : null;
1100
+ if (hasSameLogicalBodyRows(nextIds))
1101
+ return;
1102
+ logicalBodyRowIds = nextIds;
1103
+ notifyLayout();
1104
+ notifySelection();
1105
+ }
1069
1106
  function unregisterRow(token) {
1070
1107
  const row = rows.get(token);
1071
1108
  const previousSelectableBodyRowCount = selectableBodyRowCount;
@@ -1107,7 +1144,7 @@ export function createTableContext(options = {}) {
1107
1144
  return;
1108
1145
  const optimisticHasSelectableRows = selectionMode === 'multiple' || selectedKeys.size > 0;
1109
1146
  bodyRowsInitialized = true;
1110
- const actualHasSelectableRows = selectableBodyRowCount > 0 || selectedKeys.size > 0;
1147
+ const actualHasSelectableRows = hasSelectableRows();
1111
1148
  if (optimisticHasSelectableRows !== actualHasSelectableRows) {
1112
1149
  notifySelection();
1113
1150
  }
@@ -1115,6 +1152,9 @@ export function createTableContext(options = {}) {
1115
1152
  function getBodyRowCount() {
1116
1153
  return getOrderedRowTokens('body').length;
1117
1154
  }
1155
+ function getLogicalBodyRowCount() {
1156
+ return logicalBodyRowIds?.length ?? getBodyRowCount();
1157
+ }
1118
1158
  function getHeaderRowCount() {
1119
1159
  return getOrderedRowTokens('header').length;
1120
1160
  }
@@ -1143,6 +1183,9 @@ export function createTableContext(options = {}) {
1143
1183
  return sorted;
1144
1184
  }
1145
1185
  function getOrderedSelectableRowIds() {
1186
+ if (logicalBodyRowIds) {
1187
+ return logicalBodyRowIds.filter((id) => !isRowSelectionDisabled(id));
1188
+ }
1146
1189
  const rowIds = [];
1147
1190
  for (const token of getOrderedRowTokens('body')) {
1148
1191
  const row = rows.get(token);
@@ -1213,6 +1256,9 @@ export function createTableContext(options = {}) {
1213
1256
  if (!bodyRowsInitialized) {
1214
1257
  return selectionMode === 'multiple' || selectedKeys.size > 0;
1215
1258
  }
1259
+ if (logicalBodyRowIds) {
1260
+ return getOrderedSelectableRowIds().length > 0 || selectedKeys.size > 0;
1261
+ }
1216
1262
  return selectableBodyRowCount > 0 || selectedKeys.size > 0;
1217
1263
  }
1218
1264
  function isRowFocused(token) {
@@ -1811,13 +1857,7 @@ export function createTableContext(options = {}) {
1811
1857
  function selectAllRows() {
1812
1858
  if (selectionMode !== 'multiple')
1813
1859
  return;
1814
- const next = new Set();
1815
- for (const token of getOrderedRowTokens('body')) {
1816
- const row = rows.get(token);
1817
- if (!row?.id || isRowSelectionDisabled(row.id, row.disabled))
1818
- continue;
1819
- next.add(row.id);
1820
- }
1860
+ const next = new Set(getOrderedSelectableRowIds());
1821
1861
  setSelectedKeys(next, next.values().next().value ?? null);
1822
1862
  emitSelectionChange();
1823
1863
  }
@@ -1992,9 +2032,11 @@ export function createTableContext(options = {}) {
1992
2032
  hasResizableColumns,
1993
2033
  registerRow,
1994
2034
  unregisterRow,
2035
+ setLogicalBodyRows,
1995
2036
  markBodyRowsInitialized,
1996
2037
  getHeaderRowCount,
1997
2038
  getBodyRowCount,
2039
+ getLogicalBodyRowCount,
1998
2040
  isRowSelected,
1999
2041
  isRowFocused,
2000
2042
  isRowFocusTarget,
@@ -159,7 +159,7 @@
159
159
  });
160
160
  const ariaRowCount = $derived.by(() => {
161
161
  void $layoutVersion;
162
- const rowCount = ctx.getHeaderRowCount() + ctx.getBodyRowCount();
162
+ const rowCount = ctx.getHeaderRowCount() + ctx.getLogicalBodyRowCount();
163
163
  return rowCount > 0 ? rowCount : undefined;
164
164
  });
165
165
 
@@ -313,27 +313,35 @@
313
313
  if (!tableElement) return;
314
314
 
315
315
  const resizeTarget = tableElement.parentElement ?? tableElement;
316
- const refreshMeasuredLayout = () => {
317
- ctx.refreshMeasuredLayout();
316
+ let refreshFrame = 0;
317
+ const scheduleMeasuredLayoutRefresh = () => {
318
+ if (refreshFrame !== 0) return;
319
+ refreshFrame = window.requestAnimationFrame(() => {
320
+ refreshFrame = 0;
321
+ ctx.refreshMeasuredLayout();
322
+ });
318
323
  };
319
324
 
320
- refreshMeasuredLayout();
325
+ scheduleMeasuredLayoutRefresh();
321
326
 
322
- window.addEventListener('resize', refreshMeasuredLayout);
323
- window.visualViewport?.addEventListener('resize', refreshMeasuredLayout);
327
+ window.addEventListener('resize', scheduleMeasuredLayoutRefresh);
328
+ window.visualViewport?.addEventListener('resize', scheduleMeasuredLayoutRefresh);
324
329
 
325
330
  const resizeObserver =
326
331
  typeof ResizeObserver !== 'undefined'
327
332
  ? new ResizeObserver(() => {
328
- refreshMeasuredLayout();
333
+ scheduleMeasuredLayoutRefresh();
329
334
  })
330
335
  : null;
331
336
 
332
337
  resizeObserver?.observe(resizeTarget);
333
338
 
334
339
  return () => {
335
- window.removeEventListener('resize', refreshMeasuredLayout);
336
- window.visualViewport?.removeEventListener('resize', refreshMeasuredLayout);
340
+ if (refreshFrame !== 0) {
341
+ window.cancelAnimationFrame(refreshFrame);
342
+ }
343
+ window.removeEventListener('resize', scheduleMeasuredLayoutRefresh);
344
+ window.visualViewport?.removeEventListener('resize', scheduleMeasuredLayoutRefresh);
337
345
  resizeObserver?.disconnect();
338
346
  };
339
347
  });
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import { onDestroy, untrack } from 'svelte';
3
+ import {
4
+ useTableSsrWrapperRegistry,
5
+ type TableSsrWrapperColumn
6
+ } from './table-ssr-wrapper-context';
7
+
8
+ let { id, header }: TableSsrWrapperColumn = $props();
9
+
10
+ const registry = useTableSsrWrapperRegistry();
11
+
12
+ function getToken() {
13
+ return `ssr-column-${id}`;
14
+ }
15
+
16
+ let registeredToken = getToken();
17
+
18
+ function getColumn() {
19
+ return { id, header };
20
+ }
21
+
22
+ function syncRegistration() {
23
+ const nextToken = getToken();
24
+
25
+ if (nextToken !== registeredToken) {
26
+ registry.removeColumn(registeredToken);
27
+ registeredToken = nextToken;
28
+ }
29
+
30
+ registry.upsertColumn(registeredToken, getColumn());
31
+ }
32
+
33
+ untrack(() => {
34
+ syncRegistration();
35
+ });
36
+
37
+ $effect(() => {
38
+ untrack(() => {
39
+ syncRegistration();
40
+ });
41
+ });
42
+
43
+ onDestroy(() => {
44
+ registry.removeColumn(registeredToken);
45
+ });
46
+ </script>
47
+
48
+ <span hidden aria-hidden="true" data-ssr-wrapper-column={id}></span>
@@ -0,0 +1,4 @@
1
+ import { type TableSsrWrapperColumn } from './table-ssr-wrapper-context';
2
+ declare const TableSsrWrapperColumn: any;
3
+ type TableSsrWrapperColumn = ReturnType<typeof TableSsrWrapperColumn>;
4
+ export default TableSsrWrapperColumn;
@@ -0,0 +1,11 @@
1
+ export type TableSsrWrapperColumn = {
2
+ id: string;
3
+ header: string;
4
+ };
5
+ type TableSsrWrapperRegistry = {
6
+ upsertColumn: (token: string, column: TableSsrWrapperColumn) => void;
7
+ removeColumn: (token: string) => void;
8
+ };
9
+ export declare function setTableSsrWrapperRegistry(registry: TableSsrWrapperRegistry): TableSsrWrapperRegistry;
10
+ export declare function useTableSsrWrapperRegistry(): TableSsrWrapperRegistry;
11
+ export {};
@@ -0,0 +1,13 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const TABLE_SSR_WRAPPER_KEY = Symbol('table-ssr-wrapper');
3
+ export function setTableSsrWrapperRegistry(registry) {
4
+ setContext(TABLE_SSR_WRAPPER_KEY, registry);
5
+ return registry;
6
+ }
7
+ export function useTableSsrWrapperRegistry() {
8
+ const registry = getContext(TABLE_SSR_WRAPPER_KEY);
9
+ if (!registry) {
10
+ throw new Error('Table SSR wrapper registry is not available.');
11
+ }
12
+ return registry;
13
+ }
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import TableSsrWrapperColumn from './table-ssr-wrapper-column.svelte';
3
+ import {
4
+ setTableSsrWrapperRegistry,
5
+ type TableSsrWrapperColumn as RegisteredColumn
6
+ } from './table-ssr-wrapper-context';
7
+
8
+ type DemoRow = {
9
+ id: string;
10
+ email: string;
11
+ group: string;
12
+ };
13
+
14
+ const rows: DemoRow[] = [{ id: 'danilo', email: 'danilo@example.com', group: 'Developer' }];
15
+ let registeredColumns = $state<Array<{ token: string; column: RegisteredColumn }>>([]);
16
+
17
+ setTableSsrWrapperRegistry({
18
+ upsertColumn(token, column) {
19
+ const index = registeredColumns.findIndex((entry) => entry.token === token);
20
+
21
+ if (index === -1) {
22
+ registeredColumns = [...registeredColumns, { token, column }];
23
+ return;
24
+ }
25
+
26
+ registeredColumns = registeredColumns.map((entry) =>
27
+ entry.token === token ? { token, column } : entry
28
+ );
29
+ },
30
+ removeColumn(token) {
31
+ registeredColumns = registeredColumns.filter((entry) => entry.token !== token);
32
+ }
33
+ });
34
+
35
+ function getResolvedColumns() {
36
+ return registeredColumns.map((entry) => entry.column);
37
+ }
38
+ </script>
39
+
40
+ <TableSsrWrapperColumn id="email" header="Email" />
41
+ <TableSsrWrapperColumn id="group" header="Group" />
42
+
43
+ <div data-testid="ssr-column-count">{(() => getResolvedColumns().length)()}</div>
44
+
45
+ <div data-testid="ssr-headers">
46
+ {#each (() => getResolvedColumns())() as column (column.id)}
47
+ <span>{column.header}</span>
48
+ {/each}
49
+ </div>
50
+
51
+ <div data-testid="ssr-body-cells">
52
+ {#each rows as row (row.id)}
53
+ {#each (() => getResolvedColumns())() as column (column.id)}
54
+ <span>{row[column.id as keyof DemoRow]}</span>
55
+ {/each}
56
+ {/each}
57
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const TableSsrWrapperTest: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type TableSsrWrapperTest = ReturnType<typeof TableSsrWrapperTest>;
3
+ export default TableSsrWrapperTest;
@@ -1,6 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
- import type { TableColumnWidth, TableContext, TableDisabledBehavior, TableRowActionHandler, TableSelectionBehavior, TableSelectionKey, TableSelectionMode, TableSortDirection, TableSortDescriptor } from './root/context.js';
3
+ import type { TableColumnWidth, TableContext, TableDisabledBehavior, TableRowActionHandler, TableRowItem, TableSelectionBehavior, TableSelectionKey, TableSelectionMode, TableSortDirection, TableSortDescriptor } from './root/context.js';
4
4
  export type TableColumnProps = {
5
5
  id: string;
6
6
  isRowHeader?: boolean;
@@ -15,10 +15,26 @@ export type TableHeaderProps = Omit<HTMLAttributes<HTMLTableSectionElement>, 'ch
15
15
  children?: Snippet;
16
16
  class?: string;
17
17
  };
18
- export type TableBodyProps = Omit<HTMLAttributes<HTMLTableSectionElement>, 'children'> & {
19
- children?: Snippet;
18
+ export type TableBodyVirtualizer = {
19
+ rowHeight: number;
20
+ overscan?: number;
21
+ };
22
+ type TableBodyBaseProps = Omit<HTMLAttributes<HTMLTableSectionElement>, 'children'> & {
20
23
  class?: string;
21
24
  };
25
+ export type TableBodyManualProps = TableBodyBaseProps & {
26
+ items?: undefined;
27
+ virtualizer?: undefined;
28
+ children?: Snippet;
29
+ empty?: undefined;
30
+ };
31
+ export type TableBodyItemsProps<T extends TableRowItem = TableRowItem> = TableBodyBaseProps & {
32
+ items: readonly T[];
33
+ virtualizer?: TableBodyVirtualizer;
34
+ children?: Snippet<[T]>;
35
+ empty?: Snippet;
36
+ };
37
+ export type TableBodyProps<T extends TableRowItem = TableRowItem> = TableBodyManualProps | TableBodyItemsProps<T>;
22
38
  export type TableFooterProps = Omit<HTMLAttributes<HTMLTableSectionElement>, 'children'> & {
23
39
  children?: Snippet;
24
40
  class?: string;
@@ -93,3 +109,4 @@ export type TableCheckboxIndicatorProps = Omit<HTMLAttributes<HTMLSpanElement>,
93
109
  children?: Snippet;
94
110
  class?: string;
95
111
  };
112
+ export {};