From 143cffd7ca92eab4f894f41ac4888636d20ee98c Mon Sep 17 00:00:00 2001 From: Bilawal Sidhu <106619546+bilawalsidhu@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:55:41 -0500 Subject: [PATCH] fix(ui): keep panel scroll position through rail layout passes (#731) The right-rail layout pass lifts the Context, CCTV, Weather and Recent Imagery panel scrollers to measure their natural heights, which clamps their scroll offset to zero, but it only restored scrollers tagged data-rail-scroller. A layer that refreshes its Layers-panel row every second snapped the Context panel back to the top while scrolling. Tag every lifted scroller and pin the pairing with a test that reads the measuring CSS. Co-authored-by: Bilawal Sidhu --- scripts/format-scope.json | 3 ++- src/ui/railScrollers.test.mjs | 30 ++++++++++++++++++++++++++++++ src/ui/templates/context.html | 6 +++--- src/ui/templates/layer-panels.html | 2 +- 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 src/ui/railScrollers.test.mjs diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 97bb0b8f2..c4b1ce72d 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -808,5 +808,6 @@ "src/ui/imageryBoxTool.test.mjs", "src/ui/recentImagery.test.mjs", "src/layers/recentImagery/testDoubles.mjs", - "scripts/qa-recent-imagery.mjs" + "scripts/qa-recent-imagery.mjs", + "src/ui/railScrollers.test.mjs" ] diff --git a/src/ui/railScrollers.test.mjs b/src/ui/railScrollers.test.mjs new file mode 100644 index 000000000..cf549be35 --- /dev/null +++ b/src/ui/railScrollers.test.mjs @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8'); + +// The rail's measuring pass lifts these panel scrollers to their natural +// height, which clamps their scroll offset to zero. Only scrollers tagged +// `data-rail-scroller` get their offset restored afterwards, so every lifted +// scroller must carry the tag or a layout pass snaps it back to the top. +test('every scroller the rail measuring pass lifts restores its scroll offset', () => { + const css = read('./styles/layers.css'); + const templates = [ + read('./templates/context.html'), + read('./templates/layer-panels.html'), + ].join('\n'); + const lifted = [ + ...css.matchAll( + /#right-context-rail\[data-rail-measuring\]\s*>\s*\[data-panel-id\]:not\(\.collapsed\)\s*>\s*\.([a-z-]+)/g, + ), + ].map((match) => match[1]); + assert.ok(lifted.length > 0, 'measuring CSS lists lifted scrollers'); + for (const className of lifted) { + const tag = templates.match( + new RegExp(`
]*>`), + )?.[0]; + if (!tag) continue; + assert.match(tag, /data-rail-scroller/, `${className} restores its scroll`); + } +}); diff --git a/src/ui/templates/context.html b/src/ui/templates/context.html index 6ce5562a6..fbed0cf7c 100644 --- a/src/ui/templates/context.html +++ b/src/ui/templates/context.html @@ -2,7 +2,7 @@