#2537·quartz

Popover remains stuck on screen after navigating back from a PDF/non-HTML link on touch devices (bfcache restore)

Author: mimcmillanCreated Aug 26, 2026Updated Aug 26, 2026
Labelsbug

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

  1. Build a site (v5 branch) with enablePopovers: true and enableSPA: true.
  2. In a note, add an internal link to a PDF.
  3. Open the page on an iPad (Safari or any iOS WebKit browser).
  4. Tap the PDF link. The browser loads and opens the PDF in its viewer.
  5. 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:

  1. Touch tap fires a synthetic mouseenter. iOS Safari emulates hover on tap, so mouseEnterHandler in quartz/components/scripts/popover.inline.ts runs before the click. For application/pdf it builds a popover containing an <iframe> and adds .active-popover. On a touch device mouseleave never fires, so nothing hides it.

  2. 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 not text/html, and calls window.location.assign(url), a full page unload. (For internal HTML pages this doesn't happen because micromorph swaps document.body which removes the popover node.)

  3. 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, the nav event doesn't fire, and nothing listens for pageshow with event.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:

typescript
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").