From 0093671bf33606854e0747ac2e8c47eb55448346 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 22:04:50 +0200 Subject: [PATCH] Fade a bottom sheet's scrim out, and keep a nested sheet's state Plan step 36 review of 287340b0. The scrim had no closed opacity, so allow-discrete held it at full strength for the length of the fade and then dropped it, where x-transition.opacity had faded it out. The open state now matches the root's own scrim and panel: a sheet nested in an open one (a menu's sheet at compact inside a sheet) matched the outer open rule, and closed in place after half a second instead of sliding. The handle's ring stands 4px off its bar again, as the old handle's outline-offset-4 drew it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/components/bottom-sheet.css | 23 ++++++++++++++++++----- tests/Feature/Components/OverlayTest.php | 10 ++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/resources/css/components/bottom-sheet.css b/resources/css/components/bottom-sheet.css index e5abe397..2b1218a5 100644 --- a/resources/css/components/bottom-sheet.css +++ b/resources/css/components/bottom-sheet.css @@ -26,7 +26,11 @@ * The grip button draws the shared `md-focus-ring` and `md-touch-target` classes rather than a * hand-rolled ring: its own visible bar is not a smaller indicator drawn inside a bigger box those * classes cannot reach (unlike the datepicker's day) — the box they draw *is* the button's whole - * hit area, so nothing here refines them further. + * hit area. The one refinement is the ring's offset, 4px rather than 2px, as the old handle drew + * it: 2px from a 4px-tall bar leaves the ring hard to tell from the bar. + * + * The scrim fades out as it fades in, and the state rules match the root's own scrim and panel, + * so a sheet nested in an open one (a menu's sheet at compact inside a sheet) keeps its own state. */ @layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; @@ -37,14 +41,19 @@ inset: 0; z-index: 40; background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent); + opacity: 0; transition-property: opacity, display; transition-duration: var(--md-sys-motion-effects-default-duration); transition-timing-function: var(--md-sys-motion-effects-default); transition-behavior: allow-discrete; } + [data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-scrim] { + opacity: 1; + } + @starting-style { - [data-md-bottom-sheet-scrim] { + [data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-scrim] { opacity: 0; } } @@ -82,14 +91,14 @@ transition-behavior: allow-discrete; } - [data-md-bottom-sheet][data-md-open] [data-md-bottom-sheet-panel] { + [data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-panel] { translate: 0 0; transition-duration: var(--md-sys-motion-spatial-default-duration); transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate); } @starting-style { - [data-md-bottom-sheet][data-md-open] [data-md-bottom-sheet-panel] { + [data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-panel] { translate: 0 100%; } } @@ -103,7 +112,7 @@ transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate), var(--md-sys-motion-spatial-default), var(--md-sys-motion-easing-emphasized-accelerate); } - [data-md-bottom-sheet][data-md-open] [data-md-bottom-sheet-panel][data-md-preset] { + [data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-panel][data-md-preset] { transition-duration: var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-spatial-default-duration); transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate), var(--md-sys-motion-spatial-default), var(--md-sys-motion-easing-emphasized-decelerate); } @@ -127,6 +136,10 @@ background-color: var(--md-sys-color-on-surface-variant); } + [data-md-bottom-sheet-grip]:focus-visible { + outline-offset: 4px; + } + [data-md-bottom-sheet-body] { min-height: 0; flex: 1 1 0%; diff --git a/tests/Feature/Components/OverlayTest.php b/tests/Feature/Components/OverlayTest.php index a53a1a78..2e0242a0 100644 --- a/tests/Feature/Components/OverlayTest.php +++ b/tests/Feature/Components/OverlayTest.php @@ -312,4 +312,14 @@ it('draws the bottom sheet from a stylesheet imported from the containment block ->and(File::get(__DIR__.'/../../../resources/js/bottom-sheet.js')) ->toContain('data-md-bottom-sheet-handle') ->not->toContain('data-drag-handle'); + + $css = ComponentStylesheet::read('bottom-sheet'); + + // The scrim fades out as well as in, a nested sheet keeps its own state, and the handle's ring + // stands 4px off its bar. + expect($css->declarations('[data-md-bottom-sheet-scrim]'))->toHaveKey('opacity', '0') + ->and($css->declarations('[data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-scrim]'))->toBe(['opacity' => '1']) + ->and($css->declarations('[data-md-bottom-sheet][data-md-open] > [data-md-bottom-sheet-panel]', ['@starting-style']))->toBe(['translate' => '0 100%']) + ->and($css->declarations('[data-md-bottom-sheet-grip]:focus-visible'))->toBe(['outline-offset' => '4px']) + ->and($css->css)->not->toContain('[data-md-open] [data-md-'); });