Skip to content

Fix repeat fragment link clicks doing nothing - #233

Merged
JoviDeCroock merged 1 commit into
mainfrom
claude/pracht-fragment-nav-bug-2hj7tm
Jul 28, 2026
Merged

Fix repeat fragment link clicks doing nothing#233
JoviDeCroock merged 1 commit into
mainfrom
claude/pracht-fragment-nav-bug-2hj7tm

Conversation

@JoviDeCroock

Copy link
Copy Markdown
Owner

Summary

Fragment links were left to the browser, which works once: the browser pushes an entry with no scroll key, and the router recognizes the popstate that follows as a fragment navigation rather than a traversal — but stamps a scroll key onto that entry in the process. Clicking the same link again reuses the entry, so the key is now there, the popstate reads as a back/forward traversal, and the position saved for the entry is faithfully restored. The click was dead.

The client router now commits fragment link clicks itself — pushing the history entry, scrolling to the target, and moving focus there — so a repeat click always scrolls, and popstate is left to mean "traversal", which is what the scroll-key logic assumes. hashchange is dispatched for the intercepted navigation, since pushState fires none. The popstate guard stays as the fallback for fragment entries created another way (location.hash = "…").

Testing

  • pnpm e2e — Added e2e test for repeat fragment link clicks
  • pnpm format
  • pnpm lint
  • pnpm test — Added comprehensive unit tests covering:
    • In-page fragment link clicks are committed by the router
    • Repeat clicks on the same fragment link scroll again
    • hashchange fires for first click but not repeat clicks
    • preserveScroll attribute skips fragment scrolling
    • Cross-document links with fragments still resolve the route

Checklist

  • Docs updated if needed — Updated ROUTING.md to reflect new fragment link behavior
  • Skills updated if needed — N/A
  • Changeset added if published packages changed — Added changeset for @pracht/core

https://claude.ai/code/session_013UFWjaW9edaC5UZeNgbaMH

…hem to the browser

Clicking a fragment link a second time was a dead click: the target stayed
where it was and the page snapped back to wherever it had been scrolled.

Fragment links were left to the browser, which works exactly once. The browser
pushes an entry with no scroll key, the router recognizes the popstate that
follows as a fragment navigation rather than a traversal — and stamps a scroll
key onto that entry on its way through. A repeat click reuses the entry, so the
key is now present, the popstate reads as a back/forward traversal, and
saveScrollPosition() at the top of the handler has already recorded the current
position under that key. restoreOrResetScroll then faithfully restores it.

popstate alone cannot separate "pushed a fragment entry" from "traversed to a
fragment entry" — the Navigation API's navigationType would, but it is not
universally available. So the click handler now owns fragment links: it pushes
the entry, scrolls to the target and moves focus there, which makes a repeat
click scroll every time and leaves popstate to mean "traversal", which is what
the scroll-key logic already assumes. The popstate guard stays as the fallback
for fragment entries created another way (location.hash = "…").

Along the way:

- A bare `#fragment` href now resolves against the full current URL. Resolving
  against the origin alone dropped the path, which the previous early return
  had hidden.
- Fragment links are handled before the prerender-speculation check, since no
  document is fetched and there is nothing to activate.
- `hashchange` is dispatched for the intercepted navigation, because pushState
  fires none and app listeners would otherwise stop hearing about it.
- An unmatched fragment now matches the browser: top of document only for the
  empty fragment and the legacy `#top`, no scroll otherwise.
- `data-pracht-preserve-scroll` on a fragment link updates the URL without
  moving the viewport.

Covered by jsdom tests for the click path (including the repeat click, the
history-entry count and hashchange) and by an e2e test that reproduces the
reported sequence in a real browser — verified failing against the pre-fix build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UFWjaW9edaC5UZeNgbaMH
@JoviDeCroock
JoviDeCroock merged commit 613a0fa into main Jul 28, 2026
3 checks passed
@JoviDeCroock
JoviDeCroock deleted the claude/pracht-fragment-nav-bug-2hj7tm branch July 28, 2026 06:12
@github-actions github-actions Bot mentioned this pull request Jul 28, 2026
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.

2 participants