Skip to content

fix(projects): one element per anchor — tab panels own the hash - #471

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/tab-anchor-conflict
Sep 3, 2026
Merged

fix(projects): one element per anchor — tab panels own the hash#471
github-actions[bot] merged 1 commit into
mainfrom
fix/tab-anchor-conflict

Conversation

@catomean

@catomean catomean commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Leftover from the tab conversion in #468, and the kind of thing a diff review does not catch.

The conflict

The sections carried ids from when they were scroll anchors (id="feedback", id="settings", …), and the new panels took ids of their own. So both existed for the same concept: a panel panel-feedback wrapping a section still called feedback.

That is not cosmetic. Every panel stays mounted — deliberately, because they hold unsaved drafts and poll while work is in flight. So loading /projects/<id>#feedback handed the browser a real element to scroll to that lived inside a hidden panel: native anchor behaviour racing the tab logic over the same name.

Not an edge case either — ControlInbox and FeedbackItemRow both deep-link exactly that way.

The fix

The panel is the anchor: id={tab.id}, aria-controls={tab.id}, scroll-mt-28 to clear the sticky tab bar. The six nested ids are gone. What the browser scrolls to is now what becomes visible.

Closing the class, not the instance

scripts/test/project-tab-anchors.ts (12 assertions):

  • reads the tab ids out of ProjectWorkspaceView rather than hardcoding them
  • asserts the panel uses id={tab.id} and not a panel- prefix
  • asserts no component under components/projects re-declares a tab id as an element id
  • asserts ControlInbox still deep-links to #feedback — the link that makes the invariant matter

Proven by mutation: reintroducing id="feedback" on the section turns exactly that one assertion red, and restoring it turns it green.

This bug was created by a refactor that looked complete. The test is the part that stops it recurring.

npm run verify exits 0.

Left over from the tab conversion. The sections had ids from when they were
scroll anchors (`id="feedback"`, `id="settings"`, ...), and the new panels
took ids of their own — so both existed for the same concept: a panel
`panel-feedback` wrapping a section still called `feedback`.

That is not cosmetic. Every panel stays mounted, because they hold unsaved
drafts and poll while work is in flight. So loading /projects/<id>#feedback
handed the browser a real element to scroll to that was inside a HIDDEN
panel — native anchor behaviour racing the tab logic over the same name.
ControlInbox and FeedbackItemRow both link exactly that way, so it is the
common path, not an edge case.

The panel is the anchor now: `id={tab.id}`, `aria-controls={tab.id}`,
`scroll-mt-28` to clear the sticky tab bar. The six nested ids are gone.
What the browser scrolls to is what becomes visible.

Pinned by scripts/test/project-tab-anchors.ts rather than left to review:
it reads the tab ids out of ProjectWorkspaceView, asserts the panel uses
them, asserts no component under components/projects re-declares one as an
element id, and asserts ControlInbox still deep-links to #feedback — the
link that makes the invariant matter in the first place.

Proven by mutation: reintroducing id="feedback" on the section turns
exactly that one assertion red.
@github-actions
github-actions Bot merged commit e654e8b into main Sep 3, 2026
3 checks passed
@github-actions
github-actions Bot deleted the fix/tab-anchor-conflict branch September 3, 2026 12:44
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