Skip to content

fix(favorites): preserve the scroll position across a tab swap - #496

Merged
ciotlosm merged 3 commits into
mainfrom
fix/favorites-tab-scroll
Oct 25, 2026
Merged

ciotlosm merged 3 commits into
mainfrom
fix/favorites-tab-scroll

Conversation

@ciotlosm

@ciotlosm ciotlosm commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Switching between the Stations and Routes tabs on /favorites threw the rider back to the top of the page.

The cause

The browser scrolls the focused tab trigger into view when the tab is activated, and the trigger strip sits at the top of the page - so by the time the app's click handler ran, window.scrollY was already 0, and the handler dutifully restored 0.

The fix

  • The offset is captured on pointerdown, which fires before focus and before the browser's scroll, so a rider deep in a list keeps their place.
  • Each tab remembers its own offset, and the target tab's offset is restored.
  • The restore waits for the document height to settle: the inactive panel is display: none, so the height changes across the swap and a single rAF can be clamped away.

The old test never tested this

tests/favorites-tabswap.spec.ts asserted the document height was stable, which display: none makes impossible - and it skipped whenever the stations catalog failed to populate (no feed bound in CI), so it never validated the behaviour it was written for.

It now asserts the scroll position, and reproduces the browser's event order (pointerdown -> focus -> scroll -> click) so it fails against the old code:

scroll jumped on tab swap (before=600, after=0) - the rider was thrown back to the top

pnpm check -> 0 errors, pnpm test -> 386 passed, pnpm build -> OK (verified on this branch in isolation).

Switching between the Stations and Routes tabs threw the rider back to
the top of the page.

The cause: the browser scrolls the focused tab trigger into view when
the tab is activated, and the trigger strip sits at the TOP of the page
- so by the time the app's click handler ran, window.scrollY was
already 0 and the handler restored 0.

The offset is now captured on pointerdown, which fires before focus and
before the browser's scroll, so a rider deep in a list keeps their
place. Each tab also remembers its own offset, and the restore waits
for the document height to settle (the inactive panel is
`display: none`, so the height changes across the swap and a single
rAF can be clamped away).

tests/favorites-tabswap.spec.ts asserted the document HEIGHT was
stable, which `display: none` makes impossible - it skipped whenever
the stations catalog failed to populate, so it never validated the
behaviour it was written for. It now asserts the scroll position, and
reproduces the browser's event order (pointerdown, focus, scroll,
click) so it fails against the old code.
@ciotlosm
ciotlosm force-pushed the fix/favorites-tab-scroll branch from 6532805 to 57b4e92 Compare October 24, 2026 18:41
@ciotlosm
ciotlosm merged commit 0c9763d into main Oct 25, 2026
3 checks passed
@ciotlosm
ciotlosm deleted the fix/favorites-tab-scroll branch October 25, 2026 06:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant