@lavalogic/scoria 0.40.13 → 0.40.15

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 (143) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +35 -46
  3. package/dist/Components/AccordionGroupButton.svelte +68 -76
  4. package/dist/Components/Action.svelte +103 -135
  5. package/dist/Components/AlertModal.svelte +171 -190
  6. package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +614 -697
  7. package/dist/Components/Base/BaseModal.svelte +90 -116
  8. package/dist/Components/Base/ContextWrapper.svelte +67 -72
  9. package/dist/Components/Base/Pagination.svelte +173 -196
  10. package/dist/Components/Base/Snippets.svelte +34 -36
  11. package/dist/Components/BigRadioSet.svelte +36 -38
  12. package/dist/Components/Bubble.svelte +78 -101
  13. package/dist/Components/Button.svelte +175 -208
  14. package/dist/Components/Checkbox.svelte +75 -86
  15. package/dist/Components/CollapsibleCard.svelte +72 -87
  16. package/dist/Components/CollapsibleMenuGroup.svelte +53 -57
  17. package/dist/Components/ConditionBuilder/ConditionBuilder.svelte +39 -64
  18. package/dist/Components/Contexts/ContextMenu.svelte +141 -156
  19. package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
  20. package/dist/Components/Contexts/ContextMenuOption.svelte +53 -60
  21. package/dist/Components/Contexts/Toast.svelte +81 -86
  22. package/dist/Components/Contexts/ToastContainer.svelte +58 -62
  23. package/dist/Components/Contexts/Tooltip.svelte +36 -37
  24. package/dist/Components/DataMatrixIcon.svelte +95 -108
  25. package/dist/Components/DateInput.svelte +118 -146
  26. package/dist/Components/DatePicker.svelte +255 -294
  27. package/dist/Components/DesktopModal.svelte +70 -90
  28. package/dist/Components/Dial.svelte +167 -197
  29. package/dist/Components/DimensionInput.svelte +124 -169
  30. package/dist/Components/DragMenu/DragMenuHolder.svelte +98 -114
  31. package/dist/Components/DragMenu/DraggableCard.svelte +64 -76
  32. package/dist/Components/FileCard.svelte +21 -24
  33. package/dist/Components/FileUpload.svelte +190 -225
  34. package/dist/Components/GridInput.svelte +159 -196
  35. package/dist/Components/GridInputSet.svelte +24 -29
  36. package/dist/Components/HorizontalTabGroup.svelte +73 -89
  37. package/dist/Components/HorizontalTabGroupButton.svelte +40 -44
  38. package/dist/Components/Icon.svelte +99 -128
  39. package/dist/Components/Icons.js +357 -357
  40. package/dist/Components/InfoAlert.svelte +35 -38
  41. package/dist/Components/LoadingAnimation.svelte +691 -755
  42. package/dist/Components/LoadingModal.svelte +36 -44
  43. package/dist/Components/LoadingOverlay.svelte +22 -26
  44. package/dist/Components/Modal.svelte +204 -254
  45. package/dist/Components/MultiSelect.svelte +186 -216
  46. package/dist/Components/Nav/Nav.svelte +94 -110
  47. package/dist/Components/Nav/NavServiceMenuList.svelte +57 -61
  48. package/dist/Components/Nav/NavTab.svelte +58 -62
  49. package/dist/Components/Nav/ServiceMenuItem.svelte +103 -111
  50. package/dist/Components/Nav/ServicesNav.svelte +72 -79
  51. package/dist/Components/NumberInput.svelte +121 -155
  52. package/dist/Components/OptionCards.svelte +62 -73
  53. package/dist/Components/PageHeading.svelte +23 -25
  54. package/dist/Components/PasswordInput.svelte +69 -88
  55. package/dist/Components/PopoverButton.svelte +151 -0
  56. package/dist/Components/PopoverButton.svelte.d.ts +9 -0
  57. package/dist/Components/PopoverButtonProps.d.ts +21 -0
  58. package/dist/Components/PopoverButtonProps.js +1 -0
  59. package/dist/Components/Portal.svelte +29 -35
  60. package/dist/Components/ProgressBubble.svelte +91 -106
  61. package/dist/Components/QuerySelect.svelte +260 -346
  62. package/dist/Components/SidebarPanel.svelte +34 -36
  63. package/dist/Components/SingleSelect.svelte +196 -244
  64. package/dist/Components/StepButton.svelte +65 -70
  65. package/dist/Components/StepForm.svelte +115 -135
  66. package/dist/Components/Switch.svelte +57 -69
  67. package/dist/Components/Table/ARCHITECTURE.md +298 -298
  68. package/dist/Components/Table/Body/QuickSearchCell.svelte +578 -655
  69. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +42 -51
  70. package/dist/Components/Table/Body/Rows/ColumnList.svelte +42 -58
  71. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +403 -442
  72. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +51 -78
  73. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +165 -184
  74. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +110 -123
  75. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +134 -160
  76. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +190 -231
  77. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +82 -104
  78. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +52 -87
  79. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +122 -150
  80. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +132 -159
  81. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +190 -236
  82. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +173 -211
  83. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +204 -244
  84. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +140 -177
  85. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +188 -217
  86. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +173 -193
  87. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +166 -187
  88. package/dist/Components/Table/Body/Rows/TableRow.svelte +245 -288
  89. package/dist/Components/Table/Body/TableBody.svelte +141 -154
  90. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +35 -41
  91. package/dist/Components/Table/Headers/SelectAllHeader.svelte +24 -30
  92. package/dist/Components/Table/Headers/TableHead.svelte +348 -402
  93. package/dist/Components/Table/Misc/ColumnPanel.svelte +37 -39
  94. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +35 -39
  95. package/dist/Components/Table/Misc/FilterInput.svelte +556 -619
  96. package/dist/Components/Table/Misc/FilterPanel.svelte +170 -183
  97. package/dist/Components/Table/Misc/FilterPanelModal.svelte +25 -31
  98. package/dist/Components/Table/Misc/ShareViewModal.svelte +252 -281
  99. package/dist/Components/Table/Misc/SidePanel.svelte +32 -35
  100. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +574 -629
  101. package/dist/Components/Table/Misc/TableFooter.svelte +326 -332
  102. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +134 -140
  103. package/dist/Components/Table/Misc/TableSidebar.svelte +183 -198
  104. package/dist/Components/Table/Misc/TableViewDropdown.svelte +83 -103
  105. package/dist/Components/Table/Misc/ToolboxPanel.svelte +83 -100
  106. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +88 -107
  107. package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +163 -190
  108. package/dist/Components/Table/NestedTable.svelte +102 -128
  109. package/dist/Components/Table/Table.svelte +531 -663
  110. package/dist/Components/Test/TestContextConsumer.svelte +11 -13
  111. package/dist/Components/TextArea.svelte +95 -130
  112. package/dist/Components/TextInput.svelte +125 -176
  113. package/dist/Components/ThreeStateRadio.svelte +111 -130
  114. package/dist/Components/Touch/BoolCard.svelte +33 -45
  115. package/dist/Components/Touch/DateModal.svelte +164 -204
  116. package/dist/Components/Touch/EditCard.svelte +48 -59
  117. package/dist/Components/Touch/InfoCard.svelte +28 -31
  118. package/dist/Components/Touch/InfoTextCard.svelte +26 -28
  119. package/dist/Components/Touch/ModalExplanation.svelte +15 -17
  120. package/dist/Components/Touch/NumberKeyboard.svelte +181 -218
  121. package/dist/Components/Touch/NumberModal.svelte +59 -79
  122. package/dist/Components/Touch/PlusMinusCard.svelte +43 -46
  123. package/dist/Components/Touch/SelectModal.svelte +111 -126
  124. package/dist/Components/Touch/SummaryCard.svelte +80 -93
  125. package/dist/Components/Touch/TextAreaModal.svelte +72 -94
  126. package/dist/Components/Touch/TextModal.svelte +77 -105
  127. package/dist/Components/Touch/TouchCard.svelte +31 -33
  128. package/dist/Components/Touch/TouchModal.svelte +214 -270
  129. package/dist/Components/Touch/UtilityButton.svelte +88 -109
  130. package/dist/Components/VerticalTabGroup.svelte +60 -78
  131. package/dist/Components/VerticalTabGroupButton.svelte +54 -58
  132. package/dist/Helpers/Datamatrix.d.ts +19 -19
  133. package/dist/Helpers/Datamatrix.js +291 -291
  134. package/dist/Types/Examples/ExampleModal.svelte +31 -35
  135. package/dist/index.d.ts +2 -0
  136. package/dist/index.js +1 -0
  137. package/dist/scss/_basics.scss +12 -12
  138. package/dist/scss/_colours.scss +134 -134
  139. package/dist/scss/_mixins.scss +3392 -3392
  140. package/dist/scss/_motion.scss +57 -57
  141. package/dist/scss/_sizing.scss +85 -85
  142. package/dist/scss/_transitions.scss +8 -8
  143. package/package.json +1 -1
@@ -1,694 +1,562 @@
1
- <svelte:options runes />
2
-
3
- <script
4
- lang="ts"
5
- module
6
- >
7
- import {
8
- ContextMenuContext,
9
- type IContextMenuOption,
10
- } from '../../Contexts/ContextMenuContext.svelte';
11
- import { cssLength, devCatch, refWrapper } from '../../Helpers/Helpers.svelte';
12
- import { getContext, onMount, setContext, tick, untrack } from 'svelte';
13
- import LoadingOverlay from '../LoadingOverlay.svelte';
14
- import TableBody from './Body/TableBody.svelte';
15
- import TableHead from './Headers/TableHead.svelte';
16
- import TableFooter from './Misc/TableFooter.svelte';
17
- import TableHorizontalBar from './Misc/TableHorizontalBar.svelte';
18
- import TableSidebar from './Misc/TableSidebar.svelte';
19
- import { TableContext } from './Types/Context/TableContext.svelte';
20
- import type { Primitive } from './Types/Context/TableInitOptions.js';
21
- import { PaginationType } from './Types/Pagination/PaginationType.js';
22
- import { ToolbarPosition } from './Types/Toolbar/ToolbarPosition.js';
23
-
24
- declare global {
25
- interface HTMLElementEventMap {
26
- edit: CustomEvent<{
27
- selectAll: boolean;
28
- }>;
29
- }
30
- }
31
-
32
- /**
33
- * Top-level orchestrator props. Accepts either:
34
- *
35
- * - `table`: the structured `Table` instance returned by `createTable(...)`.
36
- * This is the consumer-facing form.
37
- * - `tableContext`: the underlying `TableContext`. Used internally by
38
- * `NestedTable` which constructs a context directly for each
39
- * expansion. Not intended for consumer use.
40
- *
41
- * Exactly one of the two must be supplied; a runtime guard inside the
42
- * component throws if both are missing. The reference passed in is
43
- * captured at mount via `setContext` and should not be reassigned
44
- * afterwards.
45
- */
46
- export interface TableProps<T extends object, IdType extends Primitive> {
47
- readonly table?: { readonly _context: TableContext<T, IdType> };
48
- readonly tableContext?: TableContext<T, IdType>;
49
- nested?: boolean;
1
+ <svelte:options runes />
2
+
3
+ <script
4
+ lang="ts"
5
+ module
6
+ >import { ContextMenuContext } from "../../Contexts/ContextMenuContext.svelte";
7
+ import { cssLength, devCatch, refWrapper } from "../../Helpers/Helpers.svelte";
8
+ import { getContext, onMount, setContext, tick, untrack } from "svelte";
9
+ import LoadingOverlay from "../LoadingOverlay.svelte";
10
+ import TableBody from "./Body/TableBody.svelte";
11
+ import TableHead from "./Headers/TableHead.svelte";
12
+ import TableFooter from "./Misc/TableFooter.svelte";
13
+ import TableHorizontalBar from "./Misc/TableHorizontalBar.svelte";
14
+ import TableSidebar from "./Misc/TableSidebar.svelte";
15
+ import { TableContext } from "./Types/Context/TableContext.svelte";
16
+ import { PaginationType } from "./Types/Pagination/PaginationType.js";
17
+ import { ToolbarPosition } from "./Types/Toolbar/ToolbarPosition.js";
18
+ /**
19
+ * Symbol used to share the currently-open quick-filter select across
20
+ * every header in a single Table tree. Stored via `setContext` on
21
+ * the same component that hosts the Table tree so multiple
22
+ * independent tables on a page each own their own popup state.
23
+ */
24
+ export const quickFilterContextIdentifier = Symbol();
25
+ const openedQuickFilterSelect = refWrapper(null);
26
+ </script>
27
+
28
+ <script
29
+ lang="ts"
30
+ generics="T extends object,IdType extends Primitive"
31
+ >const { nested = false, table, tableContext: tableContextProp } = $props();
32
+ // Resolve the underlying TableContext from whichever prop was passed:
33
+ // consumers pass `table`; internal `NestedTable` passes `tableContext`
34
+ // directly. Exactly one must be present — throw cleanly here rather
35
+ // than letting it surface as a downstream null-deref.
36
+ const tableContext = (() => {
37
+ const ctx = table?._context ?? tableContextProp;
38
+ if (!ctx) {
39
+ throw new Error("scoria: <Table /> requires either a `table` prop (the structured instance returned by createTable) or a `tableContext` prop (used internally by NestedTable).");
50
40
  }
51
-
52
- /**
53
- * Symbol used to share the currently-open quick-filter select across
54
- * every header in a single Table tree. Stored via `setContext` on
55
- * the same component that hosts the Table tree so multiple
56
- * independent tables on a page each own their own popup state.
57
- */
58
- export const quickFilterContextIdentifier: symbol = Symbol();
59
-
60
- const openedQuickFilterSelect = refWrapper<symbol | null>(null);
61
- </script>
62
-
63
- <script
64
- lang="ts"
65
- generics="T extends object,IdType extends Primitive"
66
- >
67
- const { nested = false, table, tableContext: tableContextProp }: TableProps<T, IdType> = $props();
68
-
69
- // Resolve the underlying TableContext from whichever prop was passed:
70
- // consumers pass `table`; internal `NestedTable` passes `tableContext`
71
- // directly. Exactly one must be present throw cleanly here rather
72
- // than letting it surface as a downstream null-deref.
73
- const tableContext: TableContext<T, IdType> = (() => {
74
- const ctx = table?._context ?? tableContextProp;
75
- if (!ctx) {
76
- throw new Error(
77
- 'scoria: <Table /> requires either a `table` prop (the structured instance returned by createTable) or a `tableContext` prop (used internally by NestedTable).'
78
- );
79
- }
80
- return ctx;
81
- })();
82
-
83
- // NOTE: known limitation. This is passed as a wrapper, and they should never require reassignment. It is deeply reactive
84
- setContext(TableContext.identifier, tableContext);
85
-
86
- const { openContextMenu: _openContextMenu } = getContext<ContextMenuContext>(
87
- ContextMenuContext.identifier
88
- );
89
-
90
- let ref: HTMLDivElement | undefined = $state();
91
- let unfocusRef: HTMLDivElement | undefined = $state();
92
-
93
- setContext(quickFilterContextIdentifier, openedQuickFilterSelect);
94
-
95
- // Focus-restore on focusStart change. The DOM work runs inside `untrack`
96
- // so a future selector tweak that reads reactive state (e.g.
97
- // `document.activeElement`) does not retrigger on every focus change.
98
- $effect(() => {
99
- void tableContext.focusStart;
100
- void ref;
101
- untrack(() => {
102
- if (tableContext.focusStart && ref) {
103
- const { row, column: index } = tableContext.focusStart;
104
- // `columnDefs[index]` can be undefined when the focus
105
- // coordinate references a slot that no longer exists
106
- // (e.g. an inner expand table remounted with a different
107
- // column count, or a preset hydration raced column init).
108
- // Guard so out-of-range degrades to "no focus restored".
109
- const def = tableContext.columnDefs[index];
110
- if (!def) {
111
- return;
112
- }
113
- let element = ref.querySelector(
114
- `#${tableContext.tableName}-cell-${row}-${def.id}`
115
- ) as HTMLElement | undefined;
116
-
117
- if (element?.tabIndex == -1) {
118
- const td = element;
119
- element = td.querySelector('button') as HTMLElement | undefined;
120
- if (!element) {
121
- element = td.querySelector('input') as HTMLElement | undefined;
122
- }
123
- }
124
-
125
- element?.focus({ preventScroll: true });
126
- // Skip horizontal scroll-into-view for pinned cells (see the
127
- // focusEnd effect for the rationale).
128
- const startCell = element?.closest('[role="cell"]');
129
- const isStartPinned =
130
- startCell?.classList.contains('pinned-left') ||
131
- startCell?.classList.contains('pinned-right');
132
- element?.scrollIntoView({
133
- behavior: 'instant',
134
- block: 'nearest',
135
- inline: isStartPinned ? undefined : 'nearest',
136
- });
41
+ return ctx;
42
+ })();
43
+ // NOTE: known limitation. This is passed as a wrapper, and they should never require reassignment. It is deeply reactive
44
+ setContext(TableContext.identifier, tableContext);
45
+ const { openContextMenu: _openContextMenu } = getContext(ContextMenuContext.identifier);
46
+ let ref = $state();
47
+ let unfocusRef = $state();
48
+ setContext(quickFilterContextIdentifier, openedQuickFilterSelect);
49
+ // Focus-restore on focusStart change. The DOM work runs inside `untrack`
50
+ // so a future selector tweak that reads reactive state (e.g.
51
+ // `document.activeElement`) does not retrigger on every focus change.
52
+ $effect(() => {
53
+ void tableContext.focusStart;
54
+ void ref;
55
+ untrack(() => {
56
+ if (tableContext.focusStart && ref) {
57
+ const { row, column: index } = tableContext.focusStart;
58
+ // `columnDefs[index]` can be undefined when the focus
59
+ // coordinate references a slot that no longer exists
60
+ // (e.g. an inner expand table remounted with a different
61
+ // column count, or a preset hydration raced column init).
62
+ // Guard so out-of-range degrades to "no focus restored".
63
+ const def = tableContext.columnDefs[index];
64
+ if (!def) {
65
+ return;
137
66
  }
138
- });
139
- });
140
-
141
- // Scroll-into-view on focusEnd change. DOM work wrapped in `untrack` so
142
- // reading layout properties never re-establishes a reactive dependency.
143
- $effect(() => {
144
- void tableContext.focusEnd;
145
- void ref;
146
- untrack(() => {
147
- if (tableContext.focusEnd && ref) {
148
- const { row, column: index } = tableContext.focusEnd;
149
- const def = tableContext.columnDefs[index];
150
- if (!def) {
151
- return;
67
+ let element = ref.querySelector(`#${tableContext.tableName}-cell-${row}-${def.id}`);
68
+ if (element?.tabIndex == -1) {
69
+ const td = element;
70
+ element = td.querySelector("button");
71
+ if (!element) {
72
+ element = td.querySelector("input");
152
73
  }
153
- const cell = ref.querySelector(
154
- `#${tableContext.tableName}-cell-${row}-${def.id}`
155
- ) as HTMLElement | undefined;
156
-
157
- // Pinned cells are visually always on screen, but their true
158
- // layout position is at their real column track — which for
159
- // a right-pinned column in a wider-than-viewport table is
160
- // offscreen. Calling `scrollIntoView({inline:'nearest'})`
161
- // would then scroll the table sideways to surface the cell's
162
- // underlying position, which the user sees as the table
163
- // jumping when they click a pinned Actions button. The
164
- // vertical (block) axis still scrolls normally because the
165
- // row's vertical position is the user's real scroll target.
166
- const isPinned =
167
- cell?.classList.contains('pinned-left') ||
168
- cell?.classList.contains('pinned-right');
169
- cell?.scrollIntoView({
170
- behavior: 'instant',
171
- block: 'nearest',
172
- inline: isPinned ? undefined : 'nearest',
173
- });
174
74
  }
175
- });
176
- });
177
-
178
- /**
179
- * Routes a right-click into either a "focus the cell that was
180
- * clicked" path (if the click landed on a cell other than the
181
- * current focus) or a "show copy options" context menu (when the
182
- * Table is configured with `allowCopy` and the platform exposes the
183
- * Clipboard API).
184
- */
185
- function openContextMenu(e: MouseEvent) {
186
- e.stopPropagation();
187
-
188
- if (
189
- (e.target as HTMLElement).tagName === 'INPUT' ||
190
- (e.target as HTMLElement).closest('.select-wrapper') != null
191
- ) {
192
- return;
75
+ element?.focus({ preventScroll: true });
76
+ // Skip horizontal scroll-into-view for pinned cells (see the
77
+ // focusEnd effect for the rationale).
78
+ const startCell = element?.closest("[role=\"cell\"]");
79
+ const isStartPinned = startCell?.classList.contains("pinned-left") || startCell?.classList.contains("pinned-right");
80
+ element?.scrollIntoView({
81
+ behavior: "instant",
82
+ block: "nearest",
83
+ inline: isStartPinned ? undefined : "nearest"
84
+ });
193
85
  }
194
-
195
- const clickedElement = document.activeElement?.id.split('-cell-')[1]?.split('-');
196
-
197
- if (clickedElement) {
198
- const [rowString, columnId] = clickedElement;
199
- const row = parseInt(rowString);
200
-
201
- if (
202
- !tableContext.focusedColumnIds.has(columnId) ||
203
- !tableContext.focusedRowIndices.has(row)
204
- ) {
205
- const columnIndex = tableContext.columnDefs.find((it) => it.id === columnId)?.index;
206
-
207
- // Use `!= null` rather than a truthy check so a column at
208
- // memory index 0 (the leftmost column) is not silently
209
- // rejected. The previous truthy check broke right-click
210
- // copy on the first column.
211
- // `!= null` (not truthy) so an index of 0 (leftmost column) is
212
- // not silently rejected.
213
- if (columnIndex != null) {
214
- tableContext.moveFocus({
215
- column: columnIndex,
216
- row,
217
- id: columnId,
218
- });
219
- }
86
+ });
87
+ });
88
+ // Scroll-into-view on focusEnd change. DOM work wrapped in `untrack` so
89
+ // reading layout properties never re-establishes a reactive dependency.
90
+ $effect(() => {
91
+ void tableContext.focusEnd;
92
+ void ref;
93
+ untrack(() => {
94
+ if (tableContext.focusEnd && ref) {
95
+ const { row, column: index } = tableContext.focusEnd;
96
+ const def = tableContext.columnDefs[index];
97
+ if (!def) {
98
+ return;
220
99
  }
100
+ const cell = ref.querySelector(`#${tableContext.tableName}-cell-${row}-${def.id}`);
101
+ // Pinned cells are visually always on screen, but their true
102
+ // layout position is at their real column track — which for
103
+ // a right-pinned column in a wider-than-viewport table is
104
+ // offscreen. Calling `scrollIntoView({inline:'nearest'})`
105
+ // would then scroll the table sideways to surface the cell's
106
+ // underlying position, which the user sees as the table
107
+ // jumping when they click a pinned Actions button. The
108
+ // vertical (block) axis still scrolls normally because the
109
+ // row's vertical position is the user's real scroll target.
110
+ const isPinned = cell?.classList.contains("pinned-left") || cell?.classList.contains("pinned-right");
111
+ cell?.scrollIntoView({
112
+ behavior: "instant",
113
+ block: "nearest",
114
+ inline: isPinned ? undefined : "nearest"
115
+ });
221
116
  }
222
-
223
- // `window.Clipboard` may be absent in non-HTTPS contexts.
224
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
225
- if (tableContext.allowCopy && window.Clipboard) {
226
- e.preventDefault();
227
-
228
- const options: Array<IContextMenuOption> = [
229
- {
230
- callback: () => {
231
- tableContext.copyDataToClipboard().catch(devCatch);
232
- },
233
- text: 'Copy Selection',
234
- },
235
- ];
236
-
237
- if (tableContext.hasHighlightedItems) {
238
- options.push({
239
- callback: () => {
240
- tableContext.copyHighlightedRowsToClipboard().catch(devCatch);
241
- },
242
- text: 'Copy Highlighted Rows',
117
+ });
118
+ });
119
+ /**
120
+ * Routes a right-click into either a "focus the cell that was
121
+ * clicked" path (if the click landed on a cell other than the
122
+ * current focus) or a "show copy options" context menu (when the
123
+ * Table is configured with `allowCopy` and the platform exposes the
124
+ * Clipboard API).
125
+ */
126
+ function openContextMenu(e) {
127
+ e.stopPropagation();
128
+ if (e.target.tagName === "INPUT" || e.target.closest(".select-wrapper") != null) {
129
+ return;
130
+ }
131
+ const clickedElement = document.activeElement?.id.split("-cell-")[1]?.split("-");
132
+ if (clickedElement) {
133
+ const [rowString, columnId] = clickedElement;
134
+ const row = parseInt(rowString);
135
+ if (!tableContext.focusedColumnIds.has(columnId) || !tableContext.focusedRowIndices.has(row)) {
136
+ const columnIndex = tableContext.columnDefs.find((it) => it.id === columnId)?.index;
137
+ // Use `!= null` rather than a truthy check so a column at
138
+ // memory index 0 (the leftmost column) is not silently
139
+ // rejected. The previous truthy check broke right-click
140
+ // copy on the first column.
141
+ // `!= null` (not truthy) so an index of 0 (leftmost column) is
142
+ // not silently rejected.
143
+ if (columnIndex != null) {
144
+ tableContext.moveFocus({
145
+ column: columnIndex,
146
+ row,
147
+ id: columnId
243
148
  });
244
149
  }
245
-
246
- _openContextMenu(e, options).catch(devCatch);
247
150
  }
248
151
  }
249
-
250
- /**
251
- * Closes the quick-filter popup and the actions-cell popup whenever
252
- * the user clicks somewhere that is not inside either floating UI.
253
- */
254
- function handleClickOutside(e: MouseEvent) {
255
- const target = e.target as HTMLElement;
256
- if (target.isConnected) {
257
- // A click inside an open Svelecte dropdown (marked with
258
- // `data-scoria-portal-dropdown` by `dropdownPortal.ts`) is a
259
- // logical extension of the trigger inside the quick-filter
260
- // popup, so it must not close either floating popover.
261
- const insidePortalDropdown = target.closest('[data-scoria-portal-dropdown]') != null;
262
- if (
263
- !insidePortalDropdown &&
264
- target.closest('div.floating') == null &&
265
- target.closest('div.stick-right') == null
266
- ) {
267
- openedQuickFilterSelect.value = null;
268
- }
269
-
270
- if (!insidePortalDropdown && target.closest('div.slot-wrapper') == null) {
271
- tableContext.shownActionsPopup = undefined;
272
- }
152
+ // `window.Clipboard` may be absent in non-HTTPS contexts.
153
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
154
+ if (tableContext.allowCopy && window.Clipboard) {
155
+ e.preventDefault();
156
+ const options = [{
157
+ callback: () => {
158
+ tableContext.copyDataToClipboard().catch(devCatch);
159
+ },
160
+ text: "Copy Selection"
161
+ }];
162
+ if (tableContext.hasHighlightedItems) {
163
+ options.push({
164
+ callback: () => {
165
+ tableContext.copyHighlightedRowsToClipboard().catch(devCatch);
166
+ },
167
+ text: "Copy Highlighted Rows"
168
+ });
273
169
  }
170
+ _openContextMenu(e, options).catch(devCatch);
274
171
  }
275
-
276
- // nested table resizing
277
- let isResizing = $state(false);
278
- let remPxRatio = $state(0);
279
- const minNestedHeight = $derived(20 * remPxRatio);
280
-
281
- let nestedHeight = $state(0);
282
- $effect.pre(() => {
283
- if (!isResizing) {
284
- nestedHeight = minNestedHeight;
172
+ }
173
+ /**
174
+ * Closes the quick-filter popup and the actions-cell popup whenever
175
+ * the user clicks somewhere that is not inside either floating UI.
176
+ */
177
+ function handleClickOutside(e) {
178
+ const target = e.target;
179
+ if (target.isConnected) {
180
+ // A click inside an open Svelecte dropdown (marked with
181
+ // `data-scoria-portal-dropdown` by `dropdownPortal.ts`) is a
182
+ // logical extension of the trigger inside the quick-filter
183
+ // popup, so it must not close either floating popover.
184
+ const insidePortalDropdown = target.closest("[data-scoria-portal-dropdown]") != null;
185
+ if (!insidePortalDropdown && target.closest("div.floating") == null && target.closest("div.stick-right") == null) {
186
+ openedQuickFilterSelect.value = null;
285
187
  }
286
- });
287
-
288
- let tableRef: HTMLDivElement | undefined = $state();
289
- let mousedDownEmptySpace = $state(false);
290
-
291
- onMount(() => {
292
- remPxRatio = parseFloat(getComputedStyle(document.documentElement).fontSize);
293
- });
294
-
295
- $effect(() => {
296
- if (!isResizing) {
297
- return;
188
+ if (!insidePortalDropdown && target.closest("div.slot-wrapper") == null) {
189
+ tableContext.shownActionsPopup = undefined;
298
190
  }
299
- const onResize = (e: MouseEvent) => {
300
- const sliderHeight = tableRef ? e.clientY - tableRef.getBoundingClientRect().top : 0;
301
- nestedHeight = Math.max(sliderHeight, minNestedHeight);
302
- };
303
- window.addEventListener('mousemove', onResize);
304
- return () => {
305
- window.removeEventListener('mousemove', onResize);
306
- };
307
- });
308
-
309
- function addOverrideResizeHandler() {
310
- isResizing = true;
311
191
  }
312
-
313
- function removeOverrideResizeHandler() {
314
- isResizing = false;
192
+ }
193
+ // nested table resizing
194
+ let isResizing = $state(false);
195
+ let remPxRatio = $state(0);
196
+ const minNestedHeight = $derived(20 * remPxRatio);
197
+ let nestedHeight = $state(0);
198
+ $effect.pre(() => {
199
+ if (!isResizing) {
200
+ nestedHeight = minNestedHeight;
315
201
  }
316
-
317
- const usesLoading = $derived(
318
- tableContext.paginationRepo &&
319
- tableContext.paginationRepo.paginationType === PaginationType.Remote
320
- );
321
-
322
- const shouldUnfocusRemote = $derived(
323
- usesLoading &&
324
- tableContext.paginationRepo?.isLoading === false &&
325
- tableContext.paginationRepo.paginationState.pageIndex !==
326
- tableContext.paginationRepo.previouslyViewedPage
327
- );
328
-
329
- const shouldUnfocusLocal = $derived(
330
- !usesLoading && tableContext.paginationRepo?.paginationState.pageIndex
331
- );
332
-
333
- $effect(() => {
334
- if ((shouldUnfocusRemote || shouldUnfocusLocal) && unfocusRef) {
335
- const el = unfocusRef;
336
- untrack(() => {
337
- tableContext.unfocus();
338
- el.scrollTop = 0;
339
- });
340
- } else if (usesLoading && tableContext.paginationRepo?.isLoading === false) {
341
- // refocus same element after a reload
342
- // this does not break mutifocus
343
- untrack(() => {
344
- const fs = tableContext.focusStart;
345
- if (!fs) {
346
- return;
347
- }
348
- const idToFocusTo = `#${tableContext.tableName}-cell-${fs.row}-${fs.id}`;
349
-
350
- tick()
351
- .then(() => {
352
- (document.querySelector(idToFocusTo) as HTMLElement | undefined)?.focus();
353
- })
354
- .catch(devCatch);
355
- });
356
- }
357
- });
358
-
359
- const leftColumns = $derived(tableContext.table.getLeftVisibleLeafColumns());
360
- const centreColumns = $derived(tableContext.table.getCenterVisibleLeafColumns());
361
- const rightColumns = $derived(tableContext.table.getRightVisibleLeafColumns());
362
-
363
- // CSS Grid template for the table.
364
- //
365
- // - First track is the 20px highlight column (always present).
366
- // - For each other column, the track depends on its `userResized`
367
- // flag and its declared width/maxWidth:
368
- //
369
- // userResized = true → `${width}px`
370
- // userResized = false, maxWidth set → `minmax(width, maxWidth)`
371
- // userResized = false, widthIsFixed → `${width}px`
372
- // userResized = false, on per-kind default → `minmax(width, 1fr)`
373
- //
374
- // This produces "specify widths for the columns you care about; let
375
- // the rest fill the gap", matching the legacy `<table>` behaviour.
376
- // Pinned and centre columns use the same rule so pinning a column
377
- // does not change its width. Sticky-cell offsets read from
378
- // `_columnRenderedWidths` (populated by TableHead's ResizeObserver).
379
- const colsTemplate = $derived.by(() => {
380
- void tableContext.columnSizing;
381
- const track = (col: {
382
- columnDef: {
383
- width: number;
384
- maxWidth?: number;
385
- userResized: boolean;
386
- widthIsFixed: boolean;
387
- };
388
- }) => {
389
- if (col.columnDef.userResized) {
390
- return `${col.columnDef.width}px`;
391
- }
392
- if (col.columnDef.maxWidth != null) {
393
- return `minmax(${col.columnDef.width}px, ${col.columnDef.maxWidth}px)`;
394
- }
395
- if (col.columnDef.widthIsFixed) {
396
- return `${col.columnDef.width}px`;
202
+ });
203
+ let tableRef = $state();
204
+ let mousedDownEmptySpace = $state(false);
205
+ onMount(() => {
206
+ remPxRatio = parseFloat(getComputedStyle(document.documentElement).fontSize);
207
+ });
208
+ $effect(() => {
209
+ if (!isResizing) {
210
+ return;
211
+ }
212
+ const onResize = (e) => {
213
+ const sliderHeight = tableRef ? e.clientY - tableRef.getBoundingClientRect().top : 0;
214
+ nestedHeight = Math.max(sliderHeight, minNestedHeight);
215
+ };
216
+ window.addEventListener("mousemove", onResize);
217
+ return () => {
218
+ window.removeEventListener("mousemove", onResize);
219
+ };
220
+ });
221
+ function addOverrideResizeHandler() {
222
+ isResizing = true;
223
+ }
224
+ function removeOverrideResizeHandler() {
225
+ isResizing = false;
226
+ }
227
+ const usesLoading = $derived(tableContext.paginationRepo && tableContext.paginationRepo.paginationType === PaginationType.Remote);
228
+ const shouldUnfocusRemote = $derived(usesLoading && tableContext.paginationRepo?.isLoading === false && tableContext.paginationRepo.paginationState.pageIndex !== tableContext.paginationRepo.previouslyViewedPage);
229
+ const shouldUnfocusLocal = $derived(!usesLoading && tableContext.paginationRepo?.paginationState.pageIndex);
230
+ $effect(() => {
231
+ if ((shouldUnfocusRemote || shouldUnfocusLocal) && unfocusRef) {
232
+ const el = unfocusRef;
233
+ untrack(() => {
234
+ tableContext.unfocus();
235
+ el.scrollTop = 0;
236
+ });
237
+ } else if (usesLoading && tableContext.paginationRepo?.isLoading === false) {
238
+ // refocus same element after a reload
239
+ // this does not break mutifocus
240
+ untrack(() => {
241
+ const fs = tableContext.focusStart;
242
+ if (!fs) {
243
+ return;
397
244
  }
398
- return `minmax(${col.columnDef.width}px, 1fr)`;
399
- };
400
- const tracks: Array<string> = ['20px'];
401
- for (const col of leftColumns) {
402
- tracks.push(track(col));
403
- }
404
- for (const col of centreColumns) {
405
- tracks.push(track(col));
245
+ const idToFocusTo = `#${tableContext.tableName}-cell-${fs.row}-${fs.id}`;
246
+ tick().then(() => {
247
+ document.querySelector(idToFocusTo)?.focus();
248
+ }).catch(devCatch);
249
+ });
250
+ }
251
+ });
252
+ const leftColumns = $derived(tableContext.table.getLeftVisibleLeafColumns());
253
+ const centreColumns = $derived(tableContext.table.getCenterVisibleLeafColumns());
254
+ const rightColumns = $derived(tableContext.table.getRightVisibleLeafColumns());
255
+ // CSS Grid template for the table.
256
+ //
257
+ // - First track is the 20px highlight column (always present).
258
+ // - For each other column, the track depends on its `userResized`
259
+ // flag and its declared width/maxWidth:
260
+ //
261
+ // userResized = true → `${width}px`
262
+ // userResized = false, maxWidth set → `minmax(width, maxWidth)`
263
+ // userResized = false, widthIsFixed → `${width}px`
264
+ // userResized = false, on per-kind default → `minmax(width, 1fr)`
265
+ //
266
+ // This produces "specify widths for the columns you care about; let
267
+ // the rest fill the gap", matching the legacy `<table>` behaviour.
268
+ // Pinned and centre columns use the same rule so pinning a column
269
+ // does not change its width. Sticky-cell offsets read from
270
+ // `_columnRenderedWidths` (populated by TableHead's ResizeObserver).
271
+ const colsTemplate = $derived.by(() => {
272
+ void tableContext.columnSizing;
273
+ const track = (col) => {
274
+ if (col.columnDef.userResized) {
275
+ return `${col.columnDef.width}px`;
406
276
  }
407
- for (const col of rightColumns) {
408
- tracks.push(track(col));
277
+ if (col.columnDef.maxWidth != null) {
278
+ return `minmax(${col.columnDef.width}px, ${col.columnDef.maxWidth}px)`;
409
279
  }
410
- return tracks.join(' ');
411
- });
412
-
413
- // Total rendered width of the pinned-left and pinned-right column
414
- // stacks. Driven by `_columnRenderedWidths` so the values track actual
415
- // layout rather than the declared `columnDef.width`. Exposed to CSS
416
- // as `--pinned-left` / `--pinned-right`; the SCSS uses these in
417
- // `scroll-padding-{left,right}` so `scrollIntoView` (called on keyboard
418
- // nav and focus restore) keeps the focused cell out from behind the
419
- // pinned columns. The 20px highlight column is always pinned-left.
420
- const totalPinnedLeftWidth = $derived.by(() => {
421
- void tableContext._columnRenderedWidths.size;
422
- let total = 20;
423
- for (const col of leftColumns) {
424
- total += tableContext._columnRenderedWidths.get(col.id) ?? col.columnDef.width;
280
+ if (col.columnDef.widthIsFixed) {
281
+ return `${col.columnDef.width}px`;
425
282
  }
426
- return total;
427
- });
428
- const totalPinnedRightWidth = $derived.by(() => {
429
- void tableContext._columnRenderedWidths.size;
430
- let total = 0;
431
- for (const col of rightColumns) {
432
- total += tableContext._columnRenderedWidths.get(col.id) ?? col.columnDef.width;
283
+ return `minmax(${col.columnDef.width}px, 1fr)`;
284
+ };
285
+ const tracks = ["20px"];
286
+ for (const col of leftColumns) {
287
+ tracks.push(track(col));
288
+ }
289
+ for (const col of centreColumns) {
290
+ tracks.push(track(col));
291
+ }
292
+ for (const col of rightColumns) {
293
+ tracks.push(track(col));
294
+ }
295
+ return tracks.join(" ");
296
+ });
297
+ // Total rendered width of the pinned-left and pinned-right column
298
+ // stacks. Driven by `_columnRenderedWidths` so the values track actual
299
+ // layout rather than the declared `columnDef.width`. Exposed to CSS
300
+ // as `--pinned-left` / `--pinned-right`; the SCSS uses these in
301
+ // `scroll-padding-{left,right}` so `scrollIntoView` (called on keyboard
302
+ // nav and focus restore) keeps the focused cell out from behind the
303
+ // pinned columns. The 20px highlight column is always pinned-left.
304
+ const totalPinnedLeftWidth = $derived.by(() => {
305
+ void tableContext._columnRenderedWidths.size;
306
+ let total = 20;
307
+ for (const col of leftColumns) {
308
+ total += tableContext._columnRenderedWidths.get(col.id) ?? col.columnDef.width;
309
+ }
310
+ return total;
311
+ });
312
+ const totalPinnedRightWidth = $derived.by(() => {
313
+ void tableContext._columnRenderedWidths.size;
314
+ let total = 0;
315
+ for (const col of rightColumns) {
316
+ total += tableContext._columnRenderedWidths.get(col.id) ?? col.columnDef.width;
317
+ }
318
+ return total;
319
+ });
320
+ // Single scroll listener for the shared scroll container, which is
321
+ // also the host the row virtualiser registers against. The same
322
+ // handler updates `hasScrolledPastHeader` / `hasContentBelowFooter`,
323
+ // which drive the sticky-row drop shadows — shadows appear only when
324
+ // there is actually content scrolled under the sticky strip.
325
+ let hasScrolledPastHeader = $state(false);
326
+ let hasContentBelowFooter = $state(false);
327
+ // Measured rendered height of the sticky strip at the top of
328
+ // `.datatable` (header row + quick-filter row when active). Drives
329
+ // `scroll-padding-top` via `--sticky-top-height`, so `scrollIntoView`
330
+ // (keyboard nav, focus restore) parks the focused cell below the
331
+ // actual strip — a static `calc(var(--height) + Npx)` left a 1-2px
332
+ // gap from sub-pixel rounding and borders, clipping the body row.
333
+ let stickyTopHeight = $state(0);
334
+ function recomputeScrollShadows() {
335
+ if (!tableRef) {
336
+ hasScrolledPastHeader = false;
337
+ hasContentBelowFooter = false;
338
+ return;
339
+ }
340
+ // `.datatable` has `overflow-x: auto`. Per the CSS spec, when one
341
+ // overflow axis is non-visible the other axis becomes auto too,
342
+ // so `.datatable` is itself the vertical scroll container when
343
+ // the table is constrained inside a fixed-height parent (the
344
+ // typical app shell). Detect internal scroll directly.
345
+ const internalScrolledPast = tableRef.scrollTop > 0;
346
+ const internalContentBelow = tableRef.scrollTop + tableRef.clientHeight < tableRef.scrollHeight - 1;
347
+ // Fall back to window-relative checks when the table is not
348
+ // constrained and the page itself is the scroll container.
349
+ const rect = tableRef.getBoundingClientRect();
350
+ const windowScrolledPast = rect.top < 0;
351
+ const windowContentBelow = rect.bottom > window.innerHeight;
352
+ hasScrolledPastHeader = internalScrolledPast || windowScrolledPast;
353
+ hasContentBelowFooter = internalContentBelow || windowContentBelow;
354
+ }
355
+ $effect(() => {
356
+ void tableRef;
357
+ void unfocusRef;
358
+ const handler = (e) => {
359
+ const el = e.currentTarget;
360
+ if (!el) {
361
+ return;
433
362
  }
434
- return total;
363
+ tableContext._virtualisation.registerScrollPosition(el.scrollTop, el.clientHeight);
364
+ // Refresh shadow flags on every internal scroll: when the table
365
+ // is the scroll container, body scroll never reaches the window
366
+ // listener and the shadow flags would otherwise never flip.
367
+ recomputeScrollShadows();
368
+ };
369
+ const elements = [];
370
+ if (tableRef) {
371
+ elements.push(tableRef);
372
+ }
373
+ if (unfocusRef && unfocusRef !== tableRef) {
374
+ elements.push(unfocusRef);
375
+ }
376
+ for (const el of elements) {
377
+ el.addEventListener("scroll", handler, { passive: true });
378
+ }
379
+ const primary = tableRef ?? unfocusRef;
380
+ if (primary) {
381
+ tableContext._virtualisation.registerScrollPosition(primary.scrollTop, primary.clientHeight);
382
+ }
383
+ // Window scroll + resize cover the unconstrained case where the
384
+ // page itself scrolls; `recomputeScrollShadows` handles both.
385
+ const onWindow = () => {
386
+ recomputeScrollShadows();
387
+ };
388
+ window.addEventListener("scroll", onWindow, { passive: true });
389
+ window.addEventListener("resize", onWindow, { passive: true });
390
+ // Content-height changes (row add/remove, pagination, expand
391
+ // toggled) can flip the shadow flags without any user scroll, so
392
+ // re-evaluate on any intrinsic-height change of the table.
393
+ const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
394
+ recomputeScrollShadows();
435
395
  });
436
-
437
- // Single scroll listener for the shared scroll container, which is
438
- // also the host the row virtualiser registers against. The same
439
- // handler updates `hasScrolledPastHeader` / `hasContentBelowFooter`,
440
- // which drive the sticky-row drop shadows shadows appear only when
441
- // there is actually content scrolled under the sticky strip.
442
- let hasScrolledPastHeader = $state(false);
443
- let hasContentBelowFooter = $state(false);
444
-
445
- // Measured rendered height of the sticky strip at the top of
446
- // `.datatable` (header row + quick-filter row when active). Drives
447
- // `scroll-padding-top` via `--sticky-top-height`, so `scrollIntoView`
448
- // (keyboard nav, focus restore) parks the focused cell below the
449
- // actual strip — a static `calc(var(--height) + Npx)` left a 1-2px
450
- // gap from sub-pixel rounding and borders, clipping the body row.
451
- let stickyTopHeight: number = $state(0);
452
-
453
- function recomputeScrollShadows(): void {
396
+ if (ro && tableRef) {
397
+ ro.observe(tableRef);
398
+ }
399
+ // Measure the actual rendered height of the sticky strip (header +
400
+ // quick-filter row when active) so `scroll-padding-top` matches
401
+ // it to the pixel.
402
+ const measureStickyTop = () => {
454
403
  if (!tableRef) {
455
- hasScrolledPastHeader = false;
456
- hasContentBelowFooter = false;
457
404
  return;
458
405
  }
459
- // `.datatable` has `overflow-x: auto`. Per the CSS spec, when one
460
- // overflow axis is non-visible the other axis becomes auto too,
461
- // so `.datatable` is itself the vertical scroll container when
462
- // the table is constrained inside a fixed-height parent (the
463
- // typical app shell). Detect internal scroll directly.
464
- const internalScrolledPast = tableRef.scrollTop > 0;
465
- const internalContentBelow =
466
- tableRef.scrollTop + tableRef.clientHeight < tableRef.scrollHeight - 1;
467
- // Fall back to window-relative checks when the table is not
468
- // constrained and the page itself is the scroll container.
469
- const rect = tableRef.getBoundingClientRect();
470
- const windowScrolledPast = rect.top < 0;
471
- const windowContentBelow = rect.bottom > window.innerHeight;
472
- hasScrolledPastHeader = internalScrolledPast || windowScrolledPast;
473
- hasContentBelowFooter = internalContentBelow || windowContentBelow;
406
+ const headerRow = tableRef.querySelector(".datatable-row.header-row");
407
+ const filterRow = tableRef.querySelector(".datatable-row.quick-filter-row");
408
+ stickyTopHeight = (headerRow?.offsetHeight ?? 0) + (filterRow?.offsetHeight ?? 0);
409
+ };
410
+ const stickyRo = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measureStickyTop);
411
+ if (stickyRo && tableRef) {
412
+ // Observe the scroll host so any change inside (mode
413
+ // toggle, rows added, etc.) re-measures on the next frame.
414
+ stickyRo.observe(tableRef);
474
415
  }
475
-
476
- $effect(() => {
477
- void tableRef;
478
- void unfocusRef;
479
- const handler = (e: Event): void => {
480
- const el = e.currentTarget as HTMLElement | null;
481
- if (!el) {
482
- return;
483
- }
484
- tableContext._virtualisation.registerScrollPosition(el.scrollTop, el.clientHeight);
485
- // Refresh shadow flags on every internal scroll: when the table
486
- // is the scroll container, body scroll never reaches the window
487
- // listener and the shadow flags would otherwise never flip.
488
- recomputeScrollShadows();
489
- };
490
- const elements: Array<HTMLElement> = [];
491
- if (tableRef) {
492
- elements.push(tableRef);
493
- }
494
- if (unfocusRef && unfocusRef !== tableRef) {
495
- elements.push(unfocusRef);
496
- }
416
+ // Initial measurement so the very first focus / arrow-key
417
+ // navigation lands below the strip without waiting for the
418
+ // observer's first frame.
419
+ measureStickyTop();
420
+ recomputeScrollShadows();
421
+ return () => {
497
422
  for (const el of elements) {
498
- el.addEventListener('scroll', handler, { passive: true });
499
- }
500
- const primary = tableRef ?? unfocusRef;
501
- if (primary) {
502
- tableContext._virtualisation.registerScrollPosition(primary.scrollTop, primary.clientHeight);
503
- }
504
-
505
- // Window scroll + resize cover the unconstrained case where the
506
- // page itself scrolls; `recomputeScrollShadows` handles both.
507
- const onWindow = (): void => {
508
- recomputeScrollShadows();
509
- };
510
- window.addEventListener('scroll', onWindow, { passive: true });
511
- window.addEventListener('resize', onWindow, { passive: true });
512
-
513
- // Content-height changes (row add/remove, pagination, expand
514
- // toggled) can flip the shadow flags without any user scroll, so
515
- // re-evaluate on any intrinsic-height change of the table.
516
- const ro =
517
- typeof ResizeObserver === 'undefined'
518
- ? null
519
- : new ResizeObserver(() => {
520
- recomputeScrollShadows();
521
- });
522
- if (ro && tableRef) {
523
- ro.observe(tableRef);
524
- }
525
-
526
- // Measure the actual rendered height of the sticky strip (header +
527
- // quick-filter row when active) so `scroll-padding-top` matches
528
- // it to the pixel.
529
- const measureStickyTop = (): void => {
530
- if (!tableRef) {
531
- return;
532
- }
533
- const headerRow = tableRef.querySelector('.datatable-row.header-row') as HTMLElement | null;
534
- const filterRow = tableRef.querySelector(
535
- '.datatable-row.quick-filter-row'
536
- ) as HTMLElement | null;
537
- stickyTopHeight = (headerRow?.offsetHeight ?? 0) + (filterRow?.offsetHeight ?? 0);
538
- };
539
- const stickyRo =
540
- typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measureStickyTop);
541
- if (stickyRo && tableRef) {
542
- // Observe the scroll host so any change inside (mode
543
- // toggle, rows added, etc.) re-measures on the next frame.
544
- stickyRo.observe(tableRef);
423
+ el.removeEventListener("scroll", handler);
545
424
  }
546
- // Initial measurement so the very first focus / arrow-key
547
- // navigation lands below the strip without waiting for the
548
- // observer's first frame.
549
- measureStickyTop();
550
-
551
- recomputeScrollShadows();
552
-
553
- return () => {
554
- for (const el of elements) {
555
- el.removeEventListener('scroll', handler);
556
- }
557
- window.removeEventListener('scroll', onWindow);
558
- window.removeEventListener('resize', onWindow);
559
- ro?.disconnect();
560
- stickyRo?.disconnect();
561
- };
562
- });
563
- </script>
564
-
565
- <svelte:window
566
- onmouseup={(e: MouseEvent) => {
567
- if (
568
- // if it's not a click on an item in the context menu
569
- !(e.target as HTMLElement | undefined)?.classList.contains('context-menu-item') &&
570
- // if it's outside the table or inside the empty space of the table
571
- (e.target === unfocusRef ||
572
- ((e.target as HTMLElement | undefined)?.closest('.table-wrapper') == null &&
573
- mousedDownEmptySpace))
574
- ) {
575
- tableContext.unfocus();
576
- }
577
- mousedDownEmptySpace = false;
578
-
579
- if (tableContext.isMousingDown) {
580
- tableContext.isMousingDown = false;
581
- }
582
-
583
- removeOverrideResizeHandler();
584
- }}
585
- onclick={(e: MouseEvent) => {
586
- handleClickOutside(e);
587
- }}
588
- onmousedown={(e: MouseEvent) => {
589
- mousedDownEmptySpace =
590
- e.target === unfocusRef || (e.target as HTMLElement).closest('.table-wrapper') == null;
591
- }}
592
- />
593
-
594
- <div
595
- class="grid"
596
- class:nested
597
- style="--show-top-shadow: {hasScrolledPastHeader
598
- ? 1
599
- : 0}; --show-bottom-shadow: {hasContentBelowFooter ? 1 : 0};"
600
- >
601
- {#if tableContext.dataRepo.isLoading}
602
- <LoadingOverlay
603
- absolute
604
- borderRadius={cssLength('4px')}
605
- message={tableContext.dataRepo.loadingMessage}
606
- />
607
- {/if}
608
-
609
- <div
610
- class="horizontal-bar"
611
- class:border={tableContext.toolbarPosition === ToolbarPosition.Top}
612
- >
613
- {#if tableContext.toolbarPosition === ToolbarPosition.Top}
614
- <TableHorizontalBar />
615
- {/if}
616
- </div>
617
-
618
- <div
619
- class="sidebar-wrapper sidebar-wrapper-left"
620
- class:nested
621
- style="--nested-height: {nestedHeight}px;"
622
- >
623
- {#if tableContext.toolbarPosition === ToolbarPosition.Left}
624
- <div class="sidebar toolbar-left">
625
- <TableSidebar />
626
- </div>
627
- {/if}
628
- </div>
629
- <div
630
- class="sidebar-wrapper sidebar-wrapper-right"
631
- class:nested
632
- style="--nested-height: {nestedHeight}px;"
633
- >
634
- {#if tableContext.toolbarPosition === ToolbarPosition.Right}
635
- <div class="sidebar toolbar-right">
636
- <TableSidebar />
637
- </div>
638
- {/if}
639
- </div>
640
-
641
- <div class="footer">
642
- {#if tableContext.displayFooter}
643
- <TableFooter />
644
- {/if}
645
- </div>
646
-
647
- <!-- svelte-ignore a11y_no_static_element_interactions -->
648
- <div
649
- bind:this={tableRef}
650
- class="datatable"
651
- class:nested
652
- class:compact={tableContext.isCompact}
653
- class:quick-filtering={tableContext.showQuickFiltering}
654
- onkeydown={tableContext.handleTdKeydown}
655
- style="--pinned-left: {totalPinnedLeftWidth}px; --pinned-right: {totalPinnedRightWidth}px; --sticky-top-height: {stickyTopHeight}px;"
656
- >
657
- <div
658
- class="flex_table"
659
- bind:this={ref}
660
- class:nested
661
- >
662
- <div
663
- bind:this={unfocusRef}
664
- class="table-wrapper"
665
- oncontextmenu={openContextMenu}
666
- >
667
- <div
668
- role="table"
669
- class="datatable-grid"
670
- style="--cols: {colsTemplate};"
671
- >
672
- <TableHead />
673
- <TableBody />
674
- </div>
675
- </div>
676
- </div>
677
-
678
- {#if nested}
679
- <!-- svelte-ignore a11y_no_static_element_interactions -->
680
- <div
681
- class:isResizing
682
- class="resizer"
683
- onmousedown={(e) => {
684
- e.preventDefault();
685
- addOverrideResizeHandler();
686
- }}
687
- ></div>
688
- {/if}
689
- </div>
690
- </div>
691
-
425
+ window.removeEventListener("scroll", onWindow);
426
+ window.removeEventListener("resize", onWindow);
427
+ ro?.disconnect();
428
+ stickyRo?.disconnect();
429
+ };
430
+ });
431
+ </script>
432
+
433
+ <svelte:window
434
+ onmouseup={(e: MouseEvent) => {
435
+ if (
436
+ // if it's not a click on an item in the context menu
437
+ !(e.target as HTMLElement | undefined)?.classList.contains('context-menu-item') &&
438
+ // if it's outside the table or inside the empty space of the table
439
+ (e.target === unfocusRef ||
440
+ ((e.target as HTMLElement | undefined)?.closest('.table-wrapper') == null &&
441
+ mousedDownEmptySpace))
442
+ ) {
443
+ tableContext.unfocus();
444
+ }
445
+ mousedDownEmptySpace = false;
446
+
447
+ if (tableContext.isMousingDown) {
448
+ tableContext.isMousingDown = false;
449
+ }
450
+
451
+ removeOverrideResizeHandler();
452
+ }}
453
+ onclick={(e: MouseEvent) => {
454
+ handleClickOutside(e);
455
+ }}
456
+ onmousedown={(e: MouseEvent) => {
457
+ mousedDownEmptySpace =
458
+ e.target === unfocusRef || (e.target as HTMLElement).closest('.table-wrapper') == null;
459
+ }}
460
+ />
461
+
462
+ <div
463
+ class="grid"
464
+ class:nested
465
+ style="--show-top-shadow: {hasScrolledPastHeader
466
+ ? 1
467
+ : 0}; --show-bottom-shadow: {hasContentBelowFooter ? 1 : 0};"
468
+ >
469
+ {#if tableContext.dataRepo.isLoading}
470
+ <LoadingOverlay
471
+ absolute
472
+ borderRadius={cssLength('4px')}
473
+ message={tableContext.dataRepo.loadingMessage}
474
+ />
475
+ {/if}
476
+
477
+ <div
478
+ class="horizontal-bar"
479
+ class:border={tableContext.toolbarPosition === ToolbarPosition.Top}
480
+ >
481
+ {#if tableContext.toolbarPosition === ToolbarPosition.Top}
482
+ <TableHorizontalBar />
483
+ {/if}
484
+ </div>
485
+
486
+ <div
487
+ class="sidebar-wrapper sidebar-wrapper-left"
488
+ class:nested
489
+ style="--nested-height: {nestedHeight}px;"
490
+ >
491
+ {#if tableContext.toolbarPosition === ToolbarPosition.Left}
492
+ <div class="sidebar toolbar-left">
493
+ <TableSidebar />
494
+ </div>
495
+ {/if}
496
+ </div>
497
+ <div
498
+ class="sidebar-wrapper sidebar-wrapper-right"
499
+ class:nested
500
+ style="--nested-height: {nestedHeight}px;"
501
+ >
502
+ {#if tableContext.toolbarPosition === ToolbarPosition.Right}
503
+ <div class="sidebar toolbar-right">
504
+ <TableSidebar />
505
+ </div>
506
+ {/if}
507
+ </div>
508
+
509
+ <div class="footer">
510
+ {#if tableContext.displayFooter}
511
+ <TableFooter />
512
+ {/if}
513
+ </div>
514
+
515
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
516
+ <div
517
+ bind:this={tableRef}
518
+ class="datatable"
519
+ class:nested
520
+ class:compact={tableContext.isCompact}
521
+ class:quick-filtering={tableContext.showQuickFiltering}
522
+ onkeydown={tableContext.handleTdKeydown}
523
+ style="--pinned-left: {totalPinnedLeftWidth}px; --pinned-right: {totalPinnedRightWidth}px; --sticky-top-height: {stickyTopHeight}px;"
524
+ >
525
+ <div
526
+ class="flex_table"
527
+ bind:this={ref}
528
+ class:nested
529
+ >
530
+ <div
531
+ bind:this={unfocusRef}
532
+ class="table-wrapper"
533
+ oncontextmenu={openContextMenu}
534
+ >
535
+ <div
536
+ role="table"
537
+ class="datatable-grid"
538
+ style="--cols: {colsTemplate};"
539
+ >
540
+ <TableHead />
541
+ <TableBody />
542
+ </div>
543
+ </div>
544
+ </div>
545
+
546
+ {#if nested}
547
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
548
+ <div
549
+ class:isResizing
550
+ class="resizer"
551
+ onmousedown={(e) => {
552
+ e.preventDefault();
553
+ addOverrideResizeHandler();
554
+ }}
555
+ ></div>
556
+ {/if}
557
+ </div>
558
+ </div>
559
+
692
560
  <style>div.grid {
693
561
  border-radius: 4px;
694
562
  display: grid;
@@ -840,4 +708,4 @@ div.sidebar-wrapper .sidebar.toolbar-right {
840
708
  text-align: left;
841
709
  min-width: max-content;
842
710
  isolation: isolate;
843
- }</style>
711
+ }</style>