Skip to content

Fix dashboard URL hash handling - #14820

Open
cwickham wants to merge 3 commits into
mainfrom
fix/14818-dashboard-non-page-hash
Open

Fix dashboard URL hash handling#14820
cwickham wants to merge 3 commits into
mainfrom
fix/14818-dashboard-non-page-hash

Conversation

@cwickham

Copy link
Copy Markdown
Member

Description

Two dashboard bugs in URL hash handling. Both predate #14685; both surfaced while reviewing it.

Closes #14818. Closes #14819.

A hash that names no page blanked the dashboard (#14818)

showPage() removes active from every nav tab and every .tab-pane, then restores it only on the pane that matches the hash. When no pane matches, nothing is restored and the content area is empty.

Two callers passed the hash through without a test: the processing on load, and the popstate handler, which runs on every hash change. Any in-page anchor therefore blanked a dashboard with more than one page. A footnote link, a cross-reference, or a hand-written anchor all did it, and a reload of that URL showed the same empty state.

Both callers now test with isPage() first.

isPage() needed work before it could be used as a guard. It built a CSS selector by concatenating the hash, which throws a SyntaxError for a hash that is not a valid selector, such as #, #1foo, or #a:b. On load, that throw would happen before the hidden class is removed, and the whole dashboard would stay invisible. It now uses getElementById.

#9411 was a narrower instance of the same root cause. #11264 closed it by keeping external links away from showPage(), and left the load and popstate paths unchanged.

Landing on a page hash put the navbar out of reach (#14819)

A dashboard navbar links to its pages with a URL hash, so dashboard.html#sales is the shareable URL for a page. Loading such a URL makes the browser set the sequential focus navigation starting point to the target element, which for a dashboard is the whole .tab-pane. The first Tab press landed inside the page content, and the navbar comes earlier in DOM order, so tabbing forward never reached it.

A page hash selects a page. It is not a position within one. When the hash names a page, the focus starting point now returns to the top of the document. This runs after load, because the browser applies its own fragment behavior later than DOMContentLoaded.

Tests

New Playwright spec at tests/integration/playwright/tests/dashboard-hash-navigation.spec.ts, with a fixture at tests/docs/playwright/dashboard/hash-navigation.qmd. This is the first dashboard spec in the suite.

Five tests, passing on Chromium, Firefox, and WebKit:

  • a hash that names no page keeps the current page, on load and on a hash change
  • a hash that is not a valid CSS selector does not leave the dashboard hidden
  • page navigation, Back, and Forward still work
  • landing on a page hash starts tabbing at the top, and the navbar tab is reachable

Verified against a build without the fix: four of the five fail. The fifth is the navigation regression guard, which passes either way.

WebKit only tabs to links when Alt is held, which matches Safari's default setting, so the focus test picks its key by browser.

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes
  • updated the appropriate changelog in the PR
  • ensured the present test suite passes
  • added new tests
  • created a separate documentation PR in Quarto's website repo and linked it to this PR
AI-assisted PR
  • AI tool used: Claude Code
  • Codebase grounding: local clone
  • Human review: I have reviewed, tested, and verified the AI-generated content before submitting.

showPage() removes `active` from every nav tab and every .tab-pane, then
restores it only on the pane matching the hash. When the hash names no
page, nothing is restored and the dashboard shows an empty content area.

Both callers passed the hash through unchecked: the processing on load
and the popstate handler, which runs on every hash change. So any
in-page anchor -- a footnote link, a cross-reference, a hand-written
anchor -- blanked a dashboard with more than one page.

Guard both with isPage(). isPage() itself built a CSS selector by
concatenating the hash, which throws a SyntaxError for a hash that is
not a valid selector (`#`, `#1foo`, `#a:b`). On load that throw would
happen before the `hidden` class is removed, leaving the whole dashboard
invisible, so switch it to getElementById.

#9411 was a narrower instance of this, fixed in #11264 by keeping
external links away from showPage(); the load and popstate paths were
left unchanged.

Closes #14818
A dashboard navbar links to its pages with a URL hash, so `#sales` is
the shareable URL for a page. Loading such a URL makes the browser set
the sequential focus navigation starting point to the target element,
which for a dashboard is the whole .tab-pane. The first Tab press then
landed inside the page content, and the navbar -- earlier in DOM order
-- could not be reached by tabbing forward at all.

A page hash selects a page; it is not a position within one. So when
the hash names a page, put the focus starting point back at the top of
the document by focusing the body. This has to run after the browser
has applied its own fragment behaviour, which is later than
DOMContentLoaded, hence the deferral to load.

Closes #14819
@posit-snyk-bot

posit-snyk-bot commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

The focus-order test asserted that the navbar tab is the second stop.
What sits at the top of the body is a format concern and can change --
a skip-to-content link would add a stop before it. Tab forward a bounded
number of times and assert the navbar tab is reached, which tests the
property the issue is about: the navbar is reachable by tabbing forward.
@cwickham

Copy link
Copy Markdown
Member Author

Stacked below #14685, which adds the skip-to-content link. That PR targets this branch and exposed both of these bugs on every dashboard, but neither is caused by it — both reproduce on main and in released Quarto 1.10.18. Merge this one first.

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.

a11y: a dashboard page hash leaves the navbar unreachable by forward Tab A URL hash that is not a page name blanks a multi-page dashboard

2 participants