Repository navigation
Fix #4: keep quibble usable when a modal dialog is open - #6
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a top-layer host container (qbRoot) to dynamically rehome quibble's UI elements inside modal <dialog> elements or fullscreen elements, ensuring they render above browser top-layer components. It also increases the z-index values of quibble's elements to sit just below the maximum integer. The review feedback highlights several important edge cases to improve robustness, including handling early script execution when document.body is null, listening to the fullscreenchange event, preventing click propagation to avoid closing parent modals, and adding a safety guard for null hosts in rehome().
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| rehome(); | ||
| if (document.readyState === "complete") setTimeout(reapply, 80); | ||
| else window.addEventListener("load", function () { setTimeout(reapply, 80); }); |
There was a problem hiding this comment.
If the script is loaded in the <head> of the document, document.body will be null during the initial synchronous execution. In this case, the initial rehome() call does nothing, and qbRoot remains detached. Since reapply() does not call rehome(), the quibble UI (qbRoot and the floating feedback bar) may never be appended to the DOM if no subsequent mutations trigger the MutationObserver.
To ensure the UI is always attached once the DOM is ready, we should call rehome() inside the deferred boot/reapply sequence.
rehome();
function boot() { rehome(); reapply(); }
if (document.readyState === "complete") setTimeout(boot, 80);
else window.addEventListener("load", function () { setTimeout(boot, 80); });There was a problem hiding this comment.
Fixed in 88759a2. boot() now re-runs rehome() once the DOM is ready, so a script loaded in <head> (where document.body is null on first run) still attaches qbRoot — quibble.js:544-549. Verified with the new demo/head-load.html: the floating bar appears even though the script tag is in <head>.
| /* React to dialogs opening/closing (the `open` attribute) and to dialogs being | ||
| added/removed (e.g. a framework unmounting one with qbRoot still inside it). */ | ||
| try { | ||
| new MutationObserver(rehomeSoon).observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["open"] }); | ||
| } catch (e) {} |
There was a problem hiding this comment.
While the :modal pseudo-class matches fullscreen elements, entering or exiting fullscreen mode does not trigger the MutationObserver (as it doesn't change the open attribute or mutate the DOM tree structure). To support fullscreen elements seamlessly as described, we should also listen to the fullscreenchange event.
| /* React to dialogs opening/closing (the `open` attribute) and to dialogs being | |
| added/removed (e.g. a framework unmounting one with qbRoot still inside it). */ | |
| try { | |
| new MutationObserver(rehomeSoon).observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["open"] }); | |
| } catch (e) {} | |
| /* React to dialogs opening/closing (the `open` attribute), dialogs being | |
| added/removed (e.g. a framework unmounting one with qbRoot still inside it), | |
| and fullscreen state changes. */ | |
| try { | |
| new MutationObserver(rehomeSoon).observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["open"] }); | |
| } catch (e) {} | |
| document.addEventListener("fullscreenchange", rehomeSoon); |
There was a problem hiding this comment.
Fixed in 88759a2 — added document.addEventListener("fullscreenchange", rehomeSoon) at quibble.js:205, since entering/exiting fullscreen mutates neither the DOM nor the open attribute. Verified: clicking the fullscreen button in demo/index.html rehomes qbRoot into the fullscreen element (fullscreenElement and qbRoot's parent both become the hero).
There was a problem hiding this comment.
Note: post-rebase the resolving commit is 1d9a5ad and the line is now quibble.js:262 (fullscreenchange listener). Fix unchanged.
| var qbRoot = document.createElement("div"); | ||
| qbRoot.className = "qb-root"; |
There was a problem hiding this comment.
When qbRoot is hosted inside a modal dialog, clicking on quibble's UI elements (like the panel or buttons) might trigger the modal's "click outside to close" handlers, because quibble's elements are not part of the modal's inner content container. Stopping propagation of click events on qbRoot prevents this issue.
var qbRoot = document.createElement("div");
qbRoot.className = "qb-root";
qbRoot.addEventListener("click", function (e) { e.stopPropagation(); });There was a problem hiding this comment.
Fixed in 88759a2 — qbRoot now stops click propagation (quibble.js:180), so a click on quibble's own chrome can't reach a parent modal's click-outside-to-close handler. Verified with the light-dismiss dialog in demo/index.html: clicking the Feedback bar/panel no longer dismisses the modal.
There was a problem hiding this comment.
Note: post-rebase the resolving commit is 1d9a5ad and the line is now quibble.js:237 (qbRoot click stopPropagation). Fix unchanged.
| function rehome() { | ||
| var host = topLayerHost() || document.body; | ||
| if (qbRoot.parentNode !== host) { host.appendChild(qbRoot); layoutRings(); } | ||
| } |
There was a problem hiding this comment.
Note that when qbRoot is moved inside a modal <dialog> (or any other :modal element), if that element or any of its ancestors has a CSS transform, perspective, or filter property, it will establish a new containing block for all descendants with position: fixed.
This will cause quibble's fixed-position elements (like rings, hover overlays, bubble, and composer) to be positioned relative to that transformed element instead of the viewport, leading to misalignment.
While a full workaround (traversing ancestors to compute containing block offsets) might be too complex for this lightweight tool, it is a known limitation worth documenting or keeping in mind if users report alignment issues with animated or transformed modals.
Additionally, we can simplify the host resolution and add a safety guard to prevent errors if host is null during early boot.
| function rehome() { | |
| var host = topLayerHost() || document.body; | |
| if (qbRoot.parentNode !== host) { host.appendChild(qbRoot); layoutRings(); } | |
| } | |
| function rehome() { | |
| var host = topLayerHost(); | |
| if (host && qbRoot.parentNode !== host) { host.appendChild(qbRoot); layoutRings(); } | |
| } |
There was a problem hiding this comment.
Fixed in 88759a2 — rehome() now uses var host = topLayerHost(); with a host && null guard so it safely no-ops during early boot (quibble.js:186-191). The transform/perspective/filter containing-block caveat is documented as an accepted limitation in the comment directly above it.
There was a problem hiding this comment.
Note: post-rebase the resolving commit is 1d9a5ad and the lines are now quibble.js:243-248 (topLayerHost() null-guard). Fix unchanged.
- rehome() in deferred boot() so UI attaches when script loads in <head> (document.body was null during the synchronous boot) - listen for fullscreenchange so the UI rehomes on fullscreen enter/exit (it mutates neither the DOM tree nor the dialog `open` attribute) - stopPropagation on qbRoot clicks so quibble's chrome can't trigger a parent modal's click-outside-to-close handler - null-guard topLayerHost() in rehome(); document the transformed-ancestor fixed-positioning limitation Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed all four review comments in 88759a2:
|
Extends the demo into a manual test harness for the issue #4 fixes reviewed on PR #6: - click-outside-to-close dialog verifies qbRoot's stopPropagation keeps quibble's own chrome from dismissing the modal - a fullscreen button verifies quibble rehomes on fullscreenchange - head-load.html loads quibble in <head> (document.body null at boot) to verify the deferred boot() still attaches the UI Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A modal <dialog> (showModal) and fullscreen elements render in the browser "top layer", which paints above all normal-DOM content regardless of z-index. quibble's bar and overlays lived on <body>, so once a modal opened the "Element" button was unreachable behind the dialog backdrop and the hover/ring overlays were hidden — the item could not be clicked or selected (issue #4). A popover does not beat a modal dialog (verified), so the only reliable fix is to live inside the top-layer element. All quibble surfaces now sit in a single `qb-root` container that is moved into the topmost open modal (`:modal`) while one is open, and back to <body> otherwise. A MutationObserver tracks dialogs opening/closing and being added/removed, so the UI is also rescued if a framework unmounts a dialog with the container still inside it. Overlay-based modals (plain high z-index divs, no top layer) are handled separately by raising quibble's own z-indexes to just below the max int. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- rehome() in deferred boot() so UI attaches when script loads in <head> (document.body was null during the synchronous boot) - listen for fullscreenchange so the UI rehomes on fullscreen enter/exit (it mutates neither the DOM tree nor the dialog `open` attribute) - stopPropagation on qbRoot clicks so quibble's chrome can't trigger a parent modal's click-outside-to-close handler - null-guard topLayerHost() in rehome(); document the transformed-ancestor fixed-positioning limitation Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a dropdown on the GitHub Pages page that loads quibble from any published version via ?ver=, defaulting to the moving @v1 tag. The list is fetched live from the jsDelivr API so RC pre-releases appear automatically. Switching reloads the page (quibble loads once per page). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a Tailwind-styled <dialog> opened via a button, so the demo can exercise quibble's behavior inside a top-layer modal (issue #4) without disturbing the existing sample-page content. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the demo into a manual test harness for the issue #4 fixes reviewed on PR #6: - click-outside-to-close dialog verifies qbRoot's stopPropagation keeps quibble's own chrome from dismissing the modal - a fullscreen button verifies quibble rehomes on fullscreenchange - head-load.html loads quibble in <head> (document.body null at boot) to verify the deferred boot() still attaches the UI Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c17270c to
b4156fc
Compare
Problem
Closes #4. When a modal popup dialog is open, quibble's "Element" button and its hover/ring overlays were unreachable — users could not click or select items.
Root cause
A modal
<dialog>(showModal()) and fullscreen elements render in the browser top layer, which paints above all normal-DOM content regardless of z-index, and its backdrop covers the whole viewport. quibble's bar and overlays lived on<body>, so once a modal opened:elementFromPointover the button returned theDIALOG), andI verified that the Popover API does not beat a modal dialog, so the only reliable way to sit above one is to live inside the top-layer element.
Fix
qb-rootcontainer (display:contents, so fixed positioning is unaffected).:modalelement; otherwise it lives on<body>.MutationObservertracks dialogs opening/closing (openattribute) and being added/removed, so the UI is also rescued if a framework unmounts a dialog with the container still inside it.Verification (real browser, Chrome)
<dialog>— click "Element"<body>Zero-dependency, no API changes. Graceful degradation on browsers without
:modal(falls back to<body>+ the z-index bump).🤖 Generated with Claude Code