mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(ui): don't create a compositing layer for a hidden full-screen overlay
`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 08442dfee1)
This commit is contained in:
committed by
Codeman maintainer
parent
c7cc8e28d5
commit
e5684d0bba
@@ -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] {
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user