fix(mobile): a sheet no longer shows the page through it while it opens

The dim faded in on the sheet's own parent, and opacity multiplies down the tree, so for its first frames even the opaque sheet let the list behind read through as ghost text (Android tester, expense editor). The dim now fades on a layer of its own beside the sheet, and the card is opaque halfway through its pop.
This commit is contained in:
Maurice
2026-11-12 08:42:16 +01:00
parent 860ffc1dee
commit 009be05d22
3 changed files with 37 additions and 3 deletions
+9 -1
View File
@@ -154,6 +154,9 @@ const SHORT_FORM_SHAPE = 'm-form-sheet short:flex-1 short:border-b-0 short:[--m-
// behind read through as sharp ghost text: the frosting it counted on is
// dropped whenever an ancestor animates its opacity (the scrim fading in),
// which WebKit and Chromium both do, so the fields floated over the list behind.
// For the same reason the dim fades on a layer of its own, beside the sheet and
// not around it: opacity multiplies down the tree, so a fading parent made even
// the opaque sheet see-through for its first frames.
const SURFACE = 'bg-[color:var(--m-sheetop)] border border-[color:var(--m-shbr)]'
// The docked sheet rises from below the screen edge like a native sheet; the
@@ -283,12 +286,17 @@ export default function MSheet({
// has to fall back to document.body (sheet mounted in the same commit as
// the shell).
<div
className={`m-root fixed inset-0 z-[60] ${dimTransparent ? 'bg-transparent short:bg-[color:var(--m-dim)]' : 'bg-[color:var(--m-dim)]'} ${open ? 'm-fade-in' : 'm-fade-out'}`}
className="m-root fixed inset-0 z-[60]"
role="presentation"
onClick={(e) => {
if (e.target === e.currentTarget) requestClose()
}}
>
<div
aria-hidden="true"
data-m-scrim=""
className={`pointer-events-none absolute inset-0 ${dimTransparent ? 'bg-transparent short:bg-[color:var(--m-dim)]' : 'bg-[color:var(--m-dim)]'} ${open ? 'm-fade-in' : 'm-fade-out'}`}
/>
<div className={regionClass(variant, wideAnchor, form)}>
<div className={boxClass(variant, wideWidth, form)}>
<PopoverArrow variant={variant} anchor={wideAnchor} />
+3 -1
View File
@@ -123,9 +123,11 @@ html[dir='rtl']:has(.m-shell[data-size='wide'], .m-shell[data-size='split']) .m-
/* ── Keyframes ──────────────────────────────────────────────────────────── */
/* The floating card pops in place, the way a native alert does. */
/* The floating card pops in place, the way a native alert does. It is opaque
halfway through the pop, so the page behind never reads through its fields. */
@keyframes m-sheet-in {
from { opacity: 0; transform: translateY(10px) scale(.96); }
45% { opacity: 1; }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes m-sheet-out {
+25 -1
View File
@@ -538,11 +538,35 @@ describe('MSheet', () => {
<span>body</span>
</MSheet>,
);
const scrim = panel().closest('[role="presentation"]') as HTMLElement;
const root = panel().closest('[role="presentation"]') as HTMLElement;
const scrim = root.querySelector('[data-m-scrim]') as HTMLElement;
expect(scrim.className).toContain('bg-transparent');
expect(scrim.className).toContain('short:bg-[color:var(--m-dim)]');
});
it('FE-MOB-SHEET-036: the dim fades beside the sheet, never around it, so the opaque sheet is never see-through', () => {
const { rerender } = render(
<MSheet open onClose={() => {}} ariaLabel="Sheet">
<span>body</span>
</MSheet>,
);
const root = panel().closest('[role="presentation"]') as HTMLElement;
const scrim = root.querySelector('[data-m-scrim]') as HTMLElement;
expect(scrim.className).toContain('m-fade-in');
expect(scrim.className).toContain('pointer-events-none');
expect(scrim.contains(panel())).toBe(false);
// No ancestor of the sheet animates its opacity.
for (let el: HTMLElement | null = panel().parentElement; el; el = el.parentElement) {
expect(el.className || '').not.toMatch(/\bm-fade-(in|out)\b/);
}
rerender(
<MSheet open={false} onClose={() => {}} ariaLabel="Sheet">
<span>body</span>
</MSheet>,
);
expect(scrim.className).toContain('m-fade-out');
});
it('FE-MOB-SHEET-035: the region lets taps through to the scrim, the sheet itself takes them', () => {
render(
<MSheet open onClose={() => {}} ariaLabel="Sheet">