From 1a17ada2ab60ec526bd4a7076c8a1d1f94c16ca8 Mon Sep 17 00:00:00 2001 From: Luke Cotter <4013877+lukecotter@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:14:28 +0100 Subject: [PATCH] fix(render): align scrollToRowPosition from the row's actual offsetTop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scrollToRowPosition's center/bottom math derived the offset from the current scrollTop, assuming scrollToRow had placed the target at the top of the window. For rows that cannot sit at the top after the fill (e.g. near the end of the list) this misaligned them — measured ~12px off for center. Compute the alignment from the row's actual offsetTop instead, and sync vDomScrollPosTop/Bottom afterwards so the next user scroll does not diff against a stale tracker and jump (guarded for the basic renderer). Adds a Playwright regression test asserting a near-bottom row lands within 6px of top/center/bottom. --- src/js/core/rendering/Renderer.js | 33 +++++------ test/e2e/scroll-to-row-position.html | 46 +++++++++++++++ test/e2e/scroll-to-row-position.spec.ts | 74 +++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 test/e2e/scroll-to-row-position.html create mode 100644 test/e2e/scroll-to-row-position.spec.ts diff --git a/src/js/core/rendering/Renderer.js b/src/js/core/rendering/Renderer.js index 5f5824940..e95904c24 100644 --- a/src/js/core/rendering/Renderer.js +++ b/src/js/core/rendering/Renderer.js @@ -165,34 +165,35 @@ export default class Renderer extends CoreFeature{ //scroll to row this.scrollToRow(row); - //align to correct position + //align to correct position. After scrollToRow the target row is + //in the rendered window but NOT necessarily at the top, so derive + //the alignment from its actual offsetTop rather than assuming the + //current scrollTop places it at the top. switch(position){ case "middle": case "center": - - if(this.elementVertical.scrollHeight - this.elementVertical.scrollTop == this.elementVertical.clientHeight){ - this.elementVertical.scrollTop = this.elementVertical.scrollTop + (rowEl.offsetTop - this.elementVertical.scrollTop) - ((this.elementVertical.scrollHeight - rowEl.offsetTop) / 2); - }else{ - this.elementVertical.scrollTop = this.elementVertical.scrollTop - (this.elementVertical.clientHeight / 2); - } - + this.elementVertical.scrollTop = rowEl.offsetTop - (this.elementVertical.clientHeight / 2) + (rowEl.offsetHeight / 2); break; case "bottom": - - if(this.elementVertical.scrollHeight - this.elementVertical.scrollTop == this.elementVertical.clientHeight){ - this.elementVertical.scrollTop = this.elementVertical.scrollTop - (this.elementVertical.scrollHeight - rowEl.offsetTop) + rowEl.offsetHeight; - }else{ - this.elementVertical.scrollTop = this.elementVertical.scrollTop - this.elementVertical.clientHeight + rowEl.offsetHeight; - } - + this.elementVertical.scrollTop = rowEl.offsetTop - this.elementVertical.clientHeight + rowEl.offsetHeight; break; case "top": - this.elementVertical.scrollTop = rowEl.offsetTop; + this.elementVertical.scrollTop = rowEl.offsetTop; break; } + //keep the virtual renderer's scroll trackers in sync with the + //manual scrollTop write, so the next user scroll doesn't compute a + //large diff against a stale tracker and trigger a spurious full + //re-render/jump. Guarded for the basic renderer, which has no + //trackers. + if(this.vDomScrollPosTop !== undefined){ + this.vDomScrollPosTop = this.elementVertical.scrollTop; + this.vDomScrollPosBottom = this.elementVertical.scrollTop; + } + resolve(); }else{ diff --git a/test/e2e/scroll-to-row-position.html b/test/e2e/scroll-to-row-position.html new file mode 100644 index 000000000..fbbe8e113 --- /dev/null +++ b/test/e2e/scroll-to-row-position.html @@ -0,0 +1,46 @@ + + +
+ +