@svgrid/grid 3.0.5 → 3.0.7

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 (166) hide show
  1. package/README.md +28 -7
  2. package/dist/GridMenus.svelte +34 -4
  3. package/dist/SvGrid.controller.svelte.d.ts +23 -3
  4. package/dist/SvGrid.controller.svelte.js +235 -54
  5. package/dist/SvGrid.css +143 -7
  6. package/dist/SvGrid.helpers.js +2 -4
  7. package/dist/SvGrid.svelte +223 -38
  8. package/dist/SvGrid.types.d.ts +77 -4
  9. package/dist/SvGridDropdown.svelte +20 -0
  10. package/dist/SvGridDropdown.svelte.d.ts +12 -0
  11. package/dist/SvModal.svelte +22 -0
  12. package/dist/SvModal.svelte.d.ts +7 -0
  13. package/dist/a11y/dismissable.d.ts +10 -0
  14. package/dist/a11y/dismissable.js +24 -2
  15. package/dist/build-api.js +81 -31
  16. package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-BgUS-W3o.js} +151 -142
  17. package/dist/cdn/GridMenus-CcwU8I4t.js +644 -0
  18. package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
  19. package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
  20. package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
  21. package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
  22. package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-9yz-SbdZ.js} +2 -2
  23. package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-D7-ql1Hd.js} +2 -2
  24. package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DMUOFxOe.js} +4 -4
  25. package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-N7ik0oWy.js} +4 -4
  26. package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-CB_Jnm2k.js} +4 -4
  27. package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-Ca7gF8ZK.js} +4 -4
  28. package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-9GOJRHP7.js} +5 -5
  29. package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-Dl-FujN8.js} +5 -5
  30. package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-B1LFgoPP.js} +2 -2
  31. package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-By3n7DGZ.js} +2 -2
  32. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
  33. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
  34. package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
  35. package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
  36. package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-Mnv84zoE.js} +28 -6
  37. package/dist/cdn/{chart-Dz7SqMXH.js → chart-By7h2Guv.js} +1 -1
  38. package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-BC-tOgFY.js} +1 -1
  39. package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DUaXr-sb.js} +1 -1
  40. package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-BDtrNEiA.js} +1 -1
  41. package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-C1aOAPBZ.js} +1 -1
  42. package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
  43. package/dist/cdn/{editor-registry-BhgE3S84.js → editor-registry-BVeHvlZR.js} +49 -50
  44. package/dist/cdn/{export-format-CUDLy2yn.js → export-format-y1wdyCcv.js} +93 -97
  45. package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
  46. package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
  47. package/dist/cdn/{src-jcFMkBnN.js → src-25JmJ_7g.js} +4768 -4149
  48. package/dist/cdn/{src-Bklofsad.js → src-kE9B98r6.js} +4796 -4177
  49. package/dist/cdn/svgrid.js +18 -18
  50. package/dist/cdn/svgrid.svelte-external.js +18 -18
  51. package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
  52. package/dist/cell-formatting.d.ts +9 -0
  53. package/dist/cell-formatting.js +52 -1
  54. package/dist/cell-render.js +25 -6
  55. package/dist/clipboard.d.ts +2 -2
  56. package/dist/clipboard.js +72 -23
  57. package/dist/columns.js +7 -2
  58. package/dist/conditional-formatting.d.ts +2 -0
  59. package/dist/conditional-formatting.js +2 -0
  60. package/dist/core.d.ts +22 -6
  61. package/dist/core.js +478 -40
  62. package/dist/editing.js +24 -0
  63. package/dist/export-format.d.ts +8 -0
  64. package/dist/export-format.js +23 -16
  65. package/dist/grid-messages.d.ts +4 -0
  66. package/dist/grid-messages.js +4 -0
  67. package/dist/headless.d.ts +5 -0
  68. package/dist/headless.js +12 -0
  69. package/dist/history.d.ts +21 -0
  70. package/dist/history.js +34 -4
  71. package/dist/index.d.ts +1 -1
  72. package/dist/keyboard-handlers.js +24 -1
  73. package/dist/keyboard.js +4 -0
  74. package/dist/row-drag-touch.d.ts +1 -1
  75. package/dist/row-drag.js +35 -1
  76. package/dist/row-model.d.ts +6 -0
  77. package/dist/row-resize.js +22 -1
  78. package/dist/selection.js +1 -1
  79. package/dist/server-block-cache.d.ts +5 -0
  80. package/dist/server-block-cache.js +8 -0
  81. package/dist/server-data-source.d.ts +35 -3
  82. package/dist/server-data-source.js +11 -2
  83. package/dist/server.d.ts +1 -1
  84. package/dist/sparkline.js +7 -5
  85. package/dist/svgrid-wrapper.types.d.ts +33 -0
  86. package/dist/validate.js +9 -1
  87. package/dist/virtualization/column-virtualizer.d.ts +1 -0
  88. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +1 -0
  89. package/dist/virtualization/svelte-virtualizer.svelte.js +1 -0
  90. package/dist/virtualization/virtualizer.d.ts +3 -0
  91. package/dist/virtualization/virtualizer.js +14 -1
  92. package/package.json +6 -1
  93. package/src/GridMenus.svelte +34 -4
  94. package/src/SvGrid.controller.svelte.ts +236 -57
  95. package/src/SvGrid.css +143 -7
  96. package/src/SvGrid.helpers.ts +2 -4
  97. package/src/SvGrid.svelte +223 -38
  98. package/src/SvGrid.types.ts +77 -4
  99. package/src/SvGridDropdown.svelte +20 -0
  100. package/src/SvModal.svelte +22 -0
  101. package/src/SvModal.test.ts +30 -1
  102. package/src/a11y/dismissable.test.ts +10 -1
  103. package/src/a11y/dismissable.ts +22 -2
  104. package/src/build-api.ts +85 -27
  105. package/src/cell-formatting.test.ts +17 -0
  106. package/src/cell-formatting.ts +49 -1
  107. package/src/cell-render.test.ts +24 -2
  108. package/src/cell-render.ts +24 -8
  109. package/src/clipboard.test.ts +107 -0
  110. package/src/clipboard.ts +62 -23
  111. package/src/columns.test.ts +4 -4
  112. package/src/columns.ts +7 -2
  113. package/src/conditional-formatting.test.ts +8 -0
  114. package/src/conditional-formatting.ts +3 -0
  115. package/src/core.sort.test.ts +86 -0
  116. package/src/core.tick-repair.test.ts +287 -0
  117. package/src/core.ts +478 -40
  118. package/src/editing.test.ts +52 -0
  119. package/src/editing.ts +25 -0
  120. package/src/export-dates.test.ts +114 -0
  121. package/src/export-format.ts +26 -14
  122. package/src/grid-messages.test.ts +7 -0
  123. package/src/grid-messages.ts +10 -0
  124. package/src/headless.ts +24 -0
  125. package/src/history.test.ts +49 -1
  126. package/src/history.ts +48 -2
  127. package/src/index.ts +2 -0
  128. package/src/keyboard-handlers.coverage.test.ts +65 -0
  129. package/src/keyboard-handlers.ts +26 -1
  130. package/src/keyboard.ts +3 -0
  131. package/src/row-drag-touch.ts +2 -2
  132. package/src/row-drag.test.ts +51 -0
  133. package/src/row-drag.ts +35 -2
  134. package/src/row-model.ts +6 -0
  135. package/src/row-resize.ts +24 -1
  136. package/src/selection.ts +1 -1
  137. package/src/server-block-cache.test.ts +27 -0
  138. package/src/server-block-cache.ts +13 -0
  139. package/src/server-data-source.infinite.test.ts +62 -0
  140. package/src/server-data-source.test.ts +19 -0
  141. package/src/server-data-source.ts +48 -4
  142. package/src/server.ts +2 -0
  143. package/src/sparkline.test.ts +7 -0
  144. package/src/sparkline.ts +6 -4
  145. package/src/svgrid-wrapper.types.ts +32 -0
  146. package/src/svgrid.api.test.ts +21 -0
  147. package/src/svgrid.charting.test.ts +12 -4
  148. package/src/svgrid.context-menu.test.ts +12 -0
  149. package/src/svgrid.data-push.svelte.test.ts +94 -0
  150. package/src/svgrid.detail-rows.svelte.test.ts +187 -0
  151. package/src/svgrid.filter-menu-scroll.test.ts +9 -0
  152. package/src/svgrid.gantt-seam.test.ts +1 -1
  153. package/src/svgrid.live-update-paths.svelte.test.ts +186 -0
  154. package/src/svgrid.menu-scroll-close.test.ts +10 -0
  155. package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
  156. package/src/svgrid.row-pinning.test.ts +15 -1
  157. package/src/svgrid.selection-bar-seam.test.ts +1 -1
  158. package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
  159. package/src/svgrid.upsell-license.test.ts +1 -1
  160. package/src/test-setup.ts +12 -0
  161. package/src/transaction.test.ts +58 -0
  162. package/src/validate.ts +7 -1
  163. package/src/virtualization/svelte-virtualizer.svelte.ts +1 -0
  164. package/src/virtualization/virtualizer.test.ts +44 -0
  165. package/src/virtualization/virtualizer.ts +13 -1
  166. package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
@@ -45,6 +45,18 @@
45
45
  * loading line instead of "No options", which would read as "nothing to
46
46
  * pick" when the truth is "not here yet". */
47
47
  loading?: boolean
48
+ /**
49
+ * The trigger's element id, so a form's `<label for>` reaches it and
50
+ * clicking the label focuses it. Without one a select in a form was a
51
+ * button with no name.
52
+ */
53
+ id?: string
54
+ /** Accessible name for the trigger when no label points at `id`. */
55
+ ariaLabel?: string
56
+ /** Marks the trigger `aria-invalid`, the way an input in the same form is. */
57
+ invalid?: boolean
58
+ /** `aria-describedby` for the trigger: the error or help text of the field. */
59
+ describedBy?: string
48
60
  }
49
61
 
50
62
  let {
@@ -59,6 +71,10 @@
59
71
  onCommit,
60
72
  onCancel,
61
73
  loading = false,
74
+ id,
75
+ ariaLabel,
76
+ invalid = false,
77
+ describedBy,
62
78
  }: Props = $props()
63
79
 
64
80
  /** Stable per-instance id used to build option ids for
@@ -348,10 +364,14 @@
348
364
  <!-- svelte-ignore a11y_role_supports_aria_props_implicit -->
349
365
  <button
350
366
  type="button"
367
+ {id}
351
368
  class="sv-grid-dropdown-trigger"
352
369
  class:sv-grid-dropdown-trigger-chips={renderChipsInTrigger && selectedArr.length > 0}
353
370
  aria-haspopup="listbox"
354
371
  aria-expanded={open}
372
+ aria-label={ariaLabel}
373
+ aria-invalid={invalid || undefined}
374
+ aria-describedby={describedBy}
355
375
  aria-activedescendant={open ? optionId(highlighted) : undefined}
356
376
  bind:this={triggerEl}
357
377
  use:focusTrigger
@@ -42,6 +42,18 @@ type Props = {
42
42
  * loading line instead of "No options", which would read as "nothing to
43
43
  * pick" when the truth is "not here yet". */
44
44
  loading?: boolean;
45
+ /**
46
+ * The trigger's element id, so a form's `<label for>` reaches it and
47
+ * clicking the label focuses it. Without one a select in a form was a
48
+ * button with no name.
49
+ */
50
+ id?: string;
51
+ /** Accessible name for the trigger when no label points at `id`. */
52
+ ariaLabel?: string;
53
+ /** Marks the trigger `aria-invalid`, the way an input in the same form is. */
54
+ invalid?: boolean;
55
+ /** `aria-describedby` for the trigger: the error or help text of the field. */
56
+ describedBy?: string;
45
57
  };
46
58
  declare const SvGridDropdown: import("svelte").Component<Props, {}, "">;
47
59
  type SvGridDropdown = ReturnType<typeof SvGridDropdown>;
@@ -16,6 +16,7 @@
16
16
  import { portalToBody } from './popover'
17
17
  import { nextEditorId } from './editor-contract'
18
18
  import { createOverlay } from './createOverlay.svelte'
19
+ import { getFocusable } from './a11y/focus-trap'
19
20
 
20
21
  type Props = {
21
22
  open?: boolean
@@ -33,6 +34,13 @@
33
34
  closeOnEsc?: boolean
34
35
  /** Hide the header close (x) button. */
35
36
  hideClose?: boolean
37
+ /**
38
+ * The element to focus when the dialog opens. Defaults to the first
39
+ * focusable control in the body or footer - never the header Close (x),
40
+ * so opening a dialog lands on its content and Enter does not dismiss it.
41
+ * Pass a getter to target a specific field (e.g. a search input).
42
+ */
43
+ initialFocus?: () => HTMLElement | null
36
44
  children?: Snippet
37
45
  footer?: Snippet
38
46
  }
@@ -48,6 +56,7 @@
48
56
  closeOnBackdrop = true,
49
57
  closeOnEsc = true,
50
58
  hideClose = false,
59
+ initialFocus,
51
60
  children,
52
61
  footer,
53
62
  }: Props = $props()
@@ -69,6 +78,19 @@
69
78
  closeOnEsc: () => closeOnEsc,
70
79
  closeOnBackdrop: () => closeOnBackdrop,
71
80
  onOpen: () => { drag = { x: 0, y: 0 }; box = null },
81
+ // Land focus on the dialog's content, not the header Close (x). The x is
82
+ // the first focusable in DOM order, so the trap's "first focusable"
83
+ // default sent focus there - where Enter immediately dismissed the
84
+ // dialog and the loud focus ring pointed at "close" over the primary
85
+ // action. Prefer the first focusable outside the header; fall back to the
86
+ // panel itself when the body has no control of its own.
87
+ initialFocus: () => {
88
+ const explicit = initialFocus?.()
89
+ if (explicit) return explicit
90
+ if (!dialogEl) return null
91
+ const first = getFocusable(dialogEl).find((el) => !el.closest('.sv-modal__header'))
92
+ return first ?? dialogEl
93
+ },
72
94
  })
73
95
 
74
96
  // --- Drag (header) ---------------------------------------------------------
@@ -28,6 +28,13 @@ type Props = {
28
28
  closeOnEsc?: boolean;
29
29
  /** Hide the header close (x) button. */
30
30
  hideClose?: boolean;
31
+ /**
32
+ * The element to focus when the dialog opens. Defaults to the first
33
+ * focusable control in the body or footer - never the header Close (x),
34
+ * so opening a dialog lands on its content and Enter does not dismiss it.
35
+ * Pass a getter to target a specific field (e.g. a search input).
36
+ */
37
+ initialFocus?: () => HTMLElement | null;
31
38
  children?: Snippet;
32
39
  footer?: Snippet;
33
40
  };
@@ -37,6 +37,16 @@ export declare function dismissableDepth(): number;
37
37
  * window listener gets wrong: inner scroll containers don't bubble, so the
38
38
  * capture listener sees them and dismisses the panel the user is scrolling.
39
39
  *
40
+ * The listener arms one animation frame after it is asked for, not at once.
41
+ * Scroll events are queued and dispatched in the next rendering step, so a
42
+ * scroll that FINISHED before the click that opened the panel - the browser
43
+ * pulling a half-hidden funnel button into view as it focuses it, a test
44
+ * driver scrolling the header into place - still lands after the panel has
45
+ * mounted and wired this listener, and used to close it on the spot. The
46
+ * panel measured its anchor after that scroll, so the event carries nothing
47
+ * it needs to react to. Rendering runs the scroll steps before the animation
48
+ * frame callbacks, which is why one frame is exactly the right delay.
49
+ *
40
50
  * Returns the unsubscribe function.
41
51
  */
42
52
  export declare function onScrollOutside(element: () => DismissTarget, onOutside: () => void): () => void;
@@ -60,6 +60,16 @@ export function dismissableDepth() {
60
60
  * window listener gets wrong: inner scroll containers don't bubble, so the
61
61
  * capture listener sees them and dismisses the panel the user is scrolling.
62
62
  *
63
+ * The listener arms one animation frame after it is asked for, not at once.
64
+ * Scroll events are queued and dispatched in the next rendering step, so a
65
+ * scroll that FINISHED before the click that opened the panel - the browser
66
+ * pulling a half-hidden funnel button into view as it focuses it, a test
67
+ * driver scrolling the header into place - still lands after the panel has
68
+ * mounted and wired this listener, and used to close it on the spot. The
69
+ * panel measured its anchor after that scroll, so the event carries nothing
70
+ * it needs to react to. Rendering runs the scroll steps before the animation
71
+ * frame callbacks, which is why one frame is exactly the right delay.
72
+ *
63
73
  * Returns the unsubscribe function.
64
74
  */
65
75
  export function onScrollOutside(element, onOutside) {
@@ -70,6 +80,18 @@ export function onScrollOutside(element, onOutside) {
70
80
  return;
71
81
  onOutside();
72
82
  };
73
- window.addEventListener('scroll', handler, true);
74
- return () => window.removeEventListener('scroll', handler, true);
83
+ let armed = false;
84
+ let frame = requestAnimationFrame(() => {
85
+ frame = null;
86
+ armed = true;
87
+ window.addEventListener('scroll', handler, true);
88
+ });
89
+ return () => {
90
+ if (frame !== null)
91
+ cancelAnimationFrame(frame);
92
+ frame = null;
93
+ if (armed)
94
+ window.removeEventListener('scroll', handler, true);
95
+ armed = false;
96
+ };
75
97
  }
package/dist/build-api.js CHANGED
@@ -2,8 +2,30 @@ import "./sv-grid-scrollbar.js";
2
2
  import { untrack } from "svelte";
3
3
  import { isGroupRow, getColumnAlign, columnDefMatchesId, } from "./cell-values.js";
4
4
  import { hasAdvancedFilterEngine } from "./advanced-filter.svelte.js";
5
- import { undoHistory, redoHistory } from "./history.js";
5
+ import { undoHistory, redoHistory, peekUndoHistory, peekRedoHistory } from "./history.js";
6
6
  import { buildCommandContext } from "./command-context.js";
7
+ /**
8
+ * Row id -> index, per data array, for `applyTransaction`.
9
+ *
10
+ * An update batch used to walk every row calling `getRowId` to find the ones
11
+ * it replaced - 100k calls per tick on a 100k-row feed, whatever the batch
12
+ * size. The map is built once per array and, for an update-only transaction,
13
+ * carried to the array it produces: same ids at the same indices. An add or
14
+ * remove changes indices, so those transactions leave the next array to
15
+ * build its own map on first use. Keyed weakly so a replaced array takes its
16
+ * map with it.
17
+ */
18
+ const rowIndexByData = new WeakMap();
19
+ function rowIndexOf(data, getId) {
20
+ let byId = rowIndexByData.get(data);
21
+ if (!byId) {
22
+ byId = new Map();
23
+ for (let i = 0; i < data.length; i++)
24
+ byId.set(getId(data[i], i), i);
25
+ rowIndexByData.set(data, byId);
26
+ }
27
+ return byId;
28
+ }
7
29
  /**
8
30
  * The selection as the user made it: committed ranges first, the active one
9
31
  * last, each keeping its anchor as `from` and its focus as `to`. The active
@@ -69,6 +91,10 @@ export function createGridApi(ctx) {
69
91
  return projectGridRows(exportScopeRows(scope), exportColumns(), { columns, rawValues });
70
92
  };
71
93
  return {
94
+ getElement() {
95
+ const el = ctx.scrollContainer;
96
+ return el?.closest(".sv-grid-root") ?? null;
97
+ },
72
98
  getCellValue(rowIndex, columnId) {
73
99
  const row = ctx.internalData[rowIndex];
74
100
  const column = findColumn(columnId);
@@ -208,39 +234,41 @@ export function createGridApi(ctx) {
208
234
  },
209
235
  applyTransaction(tx) {
210
236
  const getId = ctx.props.getRowId;
211
- let next = ctx.internalData.slice();
237
+ const current = ctx.internalData;
238
+ let next = current.slice();
212
239
  let added = 0;
213
240
  let updated = 0;
214
241
  let removed = 0;
242
+ // Everything resolves against the indices of `current`: the id ->
243
+ // index map answers updates and id removals with one lookup each, a
244
+ // reference removal with one `indexOf`, and no pass over the rows
245
+ // that did not change. A row both updated and removed in the same
246
+ // transaction ends up removed, as it did when removes ran first.
247
+ const removeIdx = new Set();
215
248
  if (tx.remove?.length) {
216
- const removeIds = new Set();
217
- const removeRefs = new Set();
249
+ const byId = getId ? rowIndexOf(current, getId) : null;
218
250
  for (const r of tx.remove) {
219
- if (typeof r === "string")
220
- removeIds.add(r);
221
- else
222
- removeRefs.add(r);
251
+ const idx = typeof r === "string" ? byId?.get(r) : current.indexOf(r);
252
+ if (idx !== undefined && idx >= 0)
253
+ removeIdx.add(idx);
223
254
  }
224
- next = next.filter((row, i) => {
225
- const hit = removeRefs.has(row) ||
226
- (getId ? removeIds.has(getId(row, i)) : false);
227
- if (hit)
228
- removed += 1;
229
- return !hit;
230
- });
231
255
  }
232
256
  if (tx.update?.length && getId) {
233
- const byId = new Map();
234
- for (const u of tx.update)
235
- byId.set(getId(u, 0), u);
236
- next = next.map((row, i) => {
237
- const u = byId.get(getId(row, i));
238
- if (u) {
239
- updated += 1;
240
- return u;
241
- }
242
- return row;
243
- });
257
+ const byId = rowIndexOf(current, getId);
258
+ for (const u of tx.update) {
259
+ const idx = byId.get(getId(u, 0));
260
+ if (idx === undefined)
261
+ continue;
262
+ next[idx] = u;
263
+ updated += 1;
264
+ }
265
+ // Same ids at the same indices: the next array can reuse the map.
266
+ if (!removeIdx.size && !tx.add?.length)
267
+ rowIndexByData.set(next, byId);
268
+ }
269
+ if (removeIdx.size) {
270
+ next = next.filter((_, i) => !removeIdx.has(i));
271
+ removed = removeIdx.size;
244
272
  }
245
273
  if (tx.add?.length) {
246
274
  next.push(...tx.add);
@@ -472,6 +500,26 @@ export function createGridApi(ctx) {
472
500
  getData() {
473
501
  return ctx.internalData;
474
502
  },
503
+ getRowId(row) {
504
+ // The stable id the grid keys a row by: what `getRowId` computed,
505
+ // or the row's position when no `getRowId` prop is set. A number is
506
+ // a DISPLAY index into the rendered rows, the space getActiveCell,
507
+ // scrollToRow and the cell methods work in; a row object is looked
508
+ // up wherever it sits. Null when the index or the object is unknown,
509
+ // so a caller can tell "no such row" from a real id.
510
+ if (typeof row === "number") {
511
+ const found = ctx.allRows[row];
512
+ return found && !isGroupRow(found) ? found.id : null;
513
+ }
514
+ const inRows = ctx.allRows.find((r) => r.original === row);
515
+ if (inRows)
516
+ return inRows.id;
517
+ const getId = ctx.props.getRowId;
518
+ const index = ctx.internalData.indexOf(row);
519
+ if (index < 0)
520
+ return null;
521
+ return getId ? getId(row, index) : String(index);
522
+ },
475
523
  getColumns() {
476
524
  // Snapshot every column with its human-readable label and
477
525
  // visibility flag. Used by external code (exporters, column
@@ -580,11 +628,10 @@ export function createGridApi(ctx) {
580
628
  ctx.autosizeColumn(columnId);
581
629
  },
582
630
  getRowHeight(rowIndex) {
583
- const own = ctx.rowResizeHeightPx(rowIndex);
584
- if (own != null)
585
- return own;
586
- const declared = ctx.props.rowHeight;
587
- return typeof declared === "function" ? declared(rowIndex) : (declared ?? 30);
631
+ // The height the grid draws the row at, whatever decided it: a drag,
632
+ // a measurement under auto height, a detail row's own height, the
633
+ // declared rowHeight or the default.
634
+ return ctx.rowSizePxOf(rowIndex);
588
635
  },
589
636
  setRowHeight(rowIndex, height) {
590
637
  ctx.setRowResizeHeight(rowIndex, height);
@@ -657,6 +704,9 @@ export function createGridApi(ctx) {
657
704
  canUndo() { void ctx.historyVersion; return ctx.historyPtr >= 0; },
658
705
  canRedo() { void ctx.historyVersion; return ctx.historyPtr < ctx.history.length - 1; },
659
706
  clearHistory() { ctx.history = []; ctx.historyPtr = -1; ctx.historyVersion += 1; },
707
+ setHistoryTag(tag) { ctx.historyTag = tag; },
708
+ peekUndo() { void ctx.historyVersion; return peekUndoHistory(ctx); },
709
+ peekRedo() { void ctx.historyVersion; return peekRedoHistory(ctx); },
660
710
  // ---- Find
661
711
  openFind() { ctx.findOpen = true; },
662
712
  closeFind() { ctx.findOpen = false; ctx.findQuery = ''; },