Popover remains stuck on screen after navigating back from a PDF/non-HTML link on touch devices (bfcache restore)
Describe the bug
On a touch device (iPad, iOS Safari), tapping an internal link that points to a non-HTML asset (e.g. a PDF) navigates to the PDF as expected, but pressing Back restores the previous page with a stuck, blank popover overlaid on it. The popover cannot be dismissed by tapping and stays until a manual page refresh.
This only happens for non-HTML link targets. Internal HTML pages and external links are unaffected.
To reproduce
- Build a site (v5 branch) with
enablePopovers: trueandenableSPA: true. - In a note, add an internal link to a PDF.
- Open the page on an iPad (Safari or any iOS WebKit browser).
- Tap the PDF link. The browser loads and opens the PDF in its viewer.
- Tap Back.
Result: the page is restored with an empty popover box stuck on screen;
it never disappears (no mouseleave ever fires on touch) until a full page reload.
Expected: the page is restored with no popover visible.
Root cause
Three behaviors combine:
Touch tap fires a synthetic
mouseenter. iOS Safari emulates hover on tap, somouseEnterHandlerinquartz/components/scripts/popover.inline.tsruns before the click. Forapplication/pdfit builds a popover containing an<iframe>and adds.active-popover. On a touch devicemouseleavenever fires, so nothing hides it.The SPA router falls back to a real navigation for non-HTML targets. In
quartz/components/scripts/spa.inline.ts,_navigate()fetches the URL, sees the response content-type is nottext/html, and callswindow.location.assign(url), a full page unload. (For internal HTML pages this doesn't happen because micromorph swapsdocument.bodywhich removes the popover node.)Safari's back/forward cache (bfcache) restores the frozen page. The snapshot taken at unload still contains the popover element with
.active-popover. On Back, scripts don't re-run, thenavevent doesn't fire, and nothing listens forpageshowwithevent.persisted === true, so the stale popover is resurrected. iframe content isn't restored across bfcache, which is why the popover comes back blank.
Suggested fix
Clear popovers when a page is restored from bfcache, e.g. in
popover.inline.ts:
window.addEventListener("pageshow", (e) => {
if (e.persisted) {
document.querySelectorAll(".popover").forEach((p) => p.remove())
}
})This covers Back from any full-page navigation (PDFs, images, downloads).
Optionally, popover mouseenter listeners could also be skipped entirely on
touch-only devices (matchMedia("(hover: none)")), since on those devices the
popover only flashes for an instant before navigation and provides no value.
Desktop
Not reproducible on desktop with a mouse (hovering off the link fires
mouseleave, and bfcache restore without a stuck popover is harmless).
Smartphone
- Device: iPad
- OS: iPadOS
- Browser: Safari (any iOS WebKit browser)
Additional context
Reproduced on the current v5 branch — popover.inline.ts has no
pageshow/bfcache handling and no touch guard as of
0d9b356 ("fix: resolve client-side navigation and popover issues").
Source: jackyzha0/quartz