From e5684d0bba45c359c71deff7df5fe4ba04c40dfd Mon Sep 17 00:00:00 2001 From: d fei Date: Tue, 1 Sep 2026 07:05:43 -0700 Subject: [PATCH] fix(ui): don't create a compositing layer for a hidden full-screen overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `backdrop-filter` promotes an element to its own compositing layer. A position:fixed full-screen layer that is created and then hidden was measured to leave a stale hit-test region behind in Chrome: the page renders perfectly, but pointer events across the viewport go nowhere. The report came from a long-lived tab connected to a remote server, where a connection blip shows and then hides #offlineOverlay. The symptoms were a terminal that would not scroll and, at the same time, an unrelated click-to-expand that also stopped responding, while a freshly opened tab was fine; a read-only console command (getComputedStyle + elementFromPoint, both of which force a hit-test recomputation) then cured it. Two unrelated features dying together and one read-only command fixing both points at hit-testing itself rather than at either feature. So the `backdrop-filter` moves onto the actually-visible selector and the layer is never created while hidden. Only the two persistent overlays change: offline-overlay (toggled with [hidden]) and file-preview-overlay (toggled with .visible). path-picker and path-preview are created and removed by JS, leave nothing behind, and are untouched. ⚠️ This is an evidence-based inference, not a fix verified by reproduction: reproducing it needs a long-lived page that has been through a connection blip, which I could not manufacture in a controlled environment. The guard test pins both halves — no such property while hidden, and a real blur while shown — so a later cleanup cannot quietly delete the effect. (cherry picked from commit 08442dfee180a3571b576fa4cf0a9bde35a5f4f5) --- src/web/public/styles.css | 28 +++++++++++-- test/overlay-compositing-hittest.test.ts | 53 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 test/overlay-compositing-hittest.test.ts diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 02902b50..398eb6ce 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -10495,14 +10495,19 @@ kbd { position: fixed; inset: 0; background: var(--modal-backdrop); - backdrop-filter: blur(6px); - -webkit-backdrop-filter: blur(6px); z-index: 5100; display: none; align-items: center; justify-content: center; } +/* Same stale-hit-test reasoning as .offline-overlay above: this one is also a + persistent full-screen fixed element, shown by adding `.visible`. */ +.file-preview-overlay.visible { + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} + .file-preview-overlay.visible { display: flex; } @@ -15316,9 +15321,26 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover { padding-top: calc(20px + var(--safe-area-top)); padding-bottom: calc(20px + var(--safe-area-bottom)); background: rgba(6, 8, 12, 0.93); + overflow-y: auto; +} + +/* ⚠️ `backdrop-filter` is applied ONLY while the overlay is actually shown. + It promotes the element to its own compositing layer, and a full-screen + `position: fixed` layer that is created and then hidden has been observed to + leave a STALE HIT-TEST REGION behind in Chrome: the page keeps rendering + correctly while every pointer event over the viewport lands on nothing. + Symptom (reported on a long-lived tab against a remote server, where a + connection blip shows and then hides #offlineOverlay): the terminal stops + scrolling AND unrelated click-to-expand controls stop responding at the same + time, while a freshly opened tab is fine — and a console one-liner that only + READS layout (getComputedStyle + elementFromPoint, both of which force a + hit-test recompute) restores it. Two unrelated features dying together, and a + read-only command curing them, is what points at hit-testing rather than at + either feature. Keeping the property off the hidden state means the layer is + never created while invisible. */ +.offline-overlay:not([hidden]) { backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); - overflow-y: auto; } .offline-overlay[hidden] { diff --git a/test/overlay-compositing-hittest.test.ts b/test/overlay-compositing-hittest.test.ts new file mode 100644 index 00000000..06ac4dcf --- /dev/null +++ b/test/overlay-compositing-hittest.test.ts @@ -0,0 +1,53 @@ +/** + * A persistent full-screen overlay must not carry `backdrop-filter` while it is + * hidden. + * + * The property promotes the element to its own compositing layer, and a + * full-screen `position: fixed` layer that is created and then hidden has been + * observed to leave a stale HIT-TEST region behind in Chrome: the page renders + * correctly while pointer events over the viewport land on nothing. Reported on + * a long-lived tab against a remote server (where a connection blip shows and + * then hides #offlineOverlay): terminal scrolling AND unrelated click-to-expand + * controls died together, a freshly opened tab was fine, and a console + * one-liner doing nothing but READING layout restored it. + */ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const css = readFileSync(resolve(import.meta.dirname, '../src/web/public/styles.css'), 'utf8'); + +function ruleBody(selector: string): string { + const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const m = new RegExp(`(?:^|\\})\\s*${escaped}\\s*\\{([^}]*)\\}`, 'm').exec(css); + if (!m) throw new Error(`no rule for ${selector}`); + return m[1]; +} + +/** Persistent, full-screen, fixed overlays and the selector that shows each. */ +const PERSISTENT_OVERLAYS: Array<{ base: string; shown: string }> = [ + { base: '.offline-overlay', shown: '.offline-overlay:not([hidden])' }, + { base: '.file-preview-overlay', shown: '.file-preview-overlay.visible' }, +]; + +describe('persistent full-screen overlays do not composite while hidden', () => { + for (const { base, shown } of PERSISTENT_OVERLAYS) { + it(`${base} keeps backdrop-filter off its base rule`, () => { + expect(ruleBody(base)).not.toMatch(/backdrop-filter/); + }); + + it(`${base} still blurs once shown, via ${shown}`, () => { + // Moving the property must not silently DELETE the effect: the overlay is + // meant to blur what is behind it while it is up. + const body = ruleBody(shown); + expect(body).toMatch(/(^|\s)backdrop-filter:\s*blur\(/m); + expect(body).toMatch(/-webkit-backdrop-filter:\s*blur\(/); + }); + } + + it('the offline overlay still forces display:none when hidden', () => { + // The base rule is `display: flex`, so [hidden] alone would not hide it — + // this is the guard that rule stays put while the block is edited. + expect(ruleBody('.offline-overlay[hidden]')).toMatch(/display:\s*none\s*!important/); + }); +});