From 7b883d0df817e46a1da9680b0dfe0161f192282e Mon Sep 17 00:00:00 2001 From: paoloredis Date: Thu, 10 Sep 2026 15:54:27 +0200 Subject: [PATCH 1/2] Let scrollbar clicks pass through the Qualified widget strip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On production the Qualified chat widget (injected via GTM, not part of this repo) restyles to overflow:hidden so becomes the page's scroll container, and parks an invisible ~20px-wide, full-height, max-z-index iframe (#q-messenger-frame) against the right edge of the viewport — exactly where body's scrollbar sits. Chrome hit-tests its scrollbars before page content, but Firefox hit-tests content first, so the invisible strip swallowed every click and hover on the scrollbar: it never reacted and, with overlay scrollbars, never faded in. Reported as 'the scrollbar doesn't show up on Firefox'; never reproduces on hugo server because GTM only runs on production. Disable pointer events on the strip while the chat is closed. The widget sets the q-docked attribute on whenever the panel is open (on mobile too, where the docked width is 0), so scoping the override to html:not([q-docked]) hands pointer events back to the open panel. Verified by hit-testing (elementFromPoint) a repro page built from the widget's captured stylesheet plus a simulated messenger iframe: without the fix the strip captures the point next to the scrollbar; with the fix clicks pass through; with q-docked set the frame captures again. Co-Authored-By: Claude Fable 5 --- assets/css/index.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/assets/css/index.css b/assets/css/index.css index c85a372c49..bbb9149f44 100644 --- a/assets/css/index.css +++ b/assets/css/index.css @@ -1863,3 +1863,19 @@ a[href*="#no-click"], img[src*="#no-click"] { white-space: nowrap; border: 0; } + +/* Qualified chat widget (production only, injected via GTM): while the chat + is closed, its #q-messenger-frame iframe is an invisible ~20px-wide strip + fixed to the right edge of the viewport, with pointer-events enabled. The + widget also restyles to overflow:hidden, so is the page's + scroll container and body's scrollbar sits exactly under that strip. + Firefox hit-tests page content before non-root scrollbars, so the strip + swallows every click and hover on the scrollbar — it never reacts and, with + overlay scrollbars, never fades in (Chrome hit-tests its scrollbars first, + which is why only Firefox users reported it). Let clicks pass through the + strip while the chat is closed. The widget sets the q-docked attribute on + when the chat panel is open, which restores normal pointer events + for the panel. */ +html:not([q-docked]) iframe#q-messenger-frame { + pointer-events: none !important; +} From 7f5cb8ec1a8f622249b26f46c0bd75e114fc6bc5 Mon Sep 17 00:00:00 2001 From: paoloredis Date: Fri, 11 Sep 2026 11:58:44 +0200 Subject: [PATCH 2/2] Keep the page scrollbar visible under the Qualified widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewers on DOC-7044 still saw the scrollbar flash at load and vanish. The pointer-events fix in the previous commit was real but secondary; the disappearance itself has a different mechanism. The Qualified widget replaces root scrolling with a scroller. At init it measures the root scrollbar width N, then applies html { width: calc(100dvw - N); margin-right: N; overflow: hidden } and body { width: calc(100% + N) }, so that body's scrollbar lands exactly where the root scrollbar was. With overlay scrollbars N = 0 and nothing breaks, which is why this never reproduced on trackpad Macs. With classic always-visible scrollbars (Windows, or macOS with a mouse) N is ~15-17px: body overshoots html's box by N, and html's own overflow: hidden clips body's scrollbar out of view. The root scrollbar dies when the widget loads and its replacement is never visible. While the chat is closed (html[q-docked-target]:not([q-docked])), neutralize the compensation: html back to 100dvw with no margin, body back to 100%. Body then fills the window and its scrollbar sits at the window edge, unclipped. While the panel is docked open the widget's own geometry applies untouched — verified html still shrinks by the panel width. Also scope our html { scrollbar-gutter: stable } to auto while the widget is active: a reserved gutter inside the widget's overflow-hidden html box would push body's scrollbar inboard again. Verified against the live staging build in Firefox by injecting these rules at document start: body flush with the window, a real mouse drag on the thumb at page top scrolls (previously dead), wheel scrolling unaffected, and docked mode still lays out correctly. Co-Authored-By: Claude Fable 5 --- assets/css/index.css | 56 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/assets/css/index.css b/assets/css/index.css index bbb9149f44..d677cabeb2 100644 --- a/assets/css/index.css +++ b/assets/css/index.css @@ -1864,18 +1864,48 @@ a[href*="#no-click"], img[src*="#no-click"] { border: 0; } -/* Qualified chat widget (production only, injected via GTM): while the chat - is closed, its #q-messenger-frame iframe is an invisible ~20px-wide strip - fixed to the right edge of the viewport, with pointer-events enabled. The - widget also restyles to overflow:hidden, so is the page's - scroll container and body's scrollbar sits exactly under that strip. - Firefox hit-tests page content before non-root scrollbars, so the strip - swallows every click and hover on the scrollbar — it never reacts and, with - overlay scrollbars, never fades in (Chrome hit-tests its scrollbars first, - which is why only Firefox users reported it). Let clicks pass through the - strip while the chat is closed. The widget sets the q-docked attribute on - when the chat panel is open, which restores normal pointer events - for the panel. */ -html:not([q-docked]) iframe#q-messenger-frame { +/* Qualified chat widget accommodations. The widget (production only, + injected via GTM, owned by web/marketing) replaces root scrolling with a + scroller: it measures the root scrollbar width N, then applies + html { width: calc(100dvw - N); margin-right: N; overflow: hidden } and + body { width: calc(100% + N) }, expecting body's scrollbar to land where + the root scrollbar was. When N > 0 (classic, always-visible scrollbars: + Windows, or macOS with a mouse), body overshoots html's box by N and + html's own overflow: hidden clips body's scrollbar out of view — the page + scrollbar disappears the moment the widget loads. The widget marks + with q-docked-target while it is active and q-docked while the chat panel + is open. */ + +/* While the chat is closed, neutralize the width/margin compensation on + both elements so body fills the window exactly and its scrollbar sits at + the window edge, unclipped. While the panel is docked open (q-docked) the + widget's own geometry must win, so none of this applies then. */ +html[q-docked-target]:not([q-docked]) { + width: 100dvw !important; + min-width: 100dvw !important; + max-width: 100dvw !important; + margin-right: 0 !important; +} + +html[q-docked-target]:not([q-docked]) body { + width: 100% !important; + max-width: 100% !important; +} + +/* The html { scrollbar-gutter: stable } rule set earlier in this file would + re-reserve a gutter inside the widget's overflow-hidden html box and push + body's scrollbar inboard by another scrollbar-width. Scoped to the widget + being active so normal pages keep the stable gutter. */ +html[q-docked-target] { + scrollbar-gutter: auto !important; +} + +/* While the chat is closed, the widget's invisible #q-messenger-frame + iframe overlays page content near the right edge with pointer events + enabled, stealing clicks and hovers from whatever is underneath — + including the page scrollbar, since Firefox hit-tests page content before + non-root scrollbars. Let events pass through; q-docked restores normal + pointer events for the open panel. */ +html:not([q-docked]) #q-messenger-frame { pointer-events: none !important; }