diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index ddb4bff7..e5f32c7c 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -539,7 +539,7 @@ Props: `title`, `subtitle`, `icon` (centred hero icon), `separator` (draw the di ### `` -An M3 side sheet, bound like ``; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`), `width` (`25rem`), `with-close-button` (**default true** — M3 requires a close affordance; `:with-close-button="false"` is ignored when Escape or the scrim is off, or on a `standard` sheet), `close-on-escape` (default true), `without-backdrop-close`, `actions` slot (**left**-aligned in a 72dp row, which is what the side-sheet spec says; a dialog's are trailing-aligned). For the second pane of a list-detail layout use `` instead — `` no longer has a `pane` prop. Its body is a size container — lay out inside with `@md:` (a *container* query), never a window class. +An M3 side sheet, bound like ``; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`; mirrored in RTL), `width` (`400px`), `with-close-button` (**default true** — M3 requires a close affordance; `:with-close-button="false"` is ignored when Escape or the scrim is off, or on a `standard` sheet), `close-on-escape` (default true), `without-backdrop-close`, `actions` slot (**left**-aligned in a 72dp row, which is what the side-sheet spec says; a dialog's are trailing-aligned). For the second pane of a list-detail layout use `` instead — `` no longer has a `pane` prop. Its body is a size container — lay out inside with `@md:` (a *container* query), never a window class. `standard` is M3's other side-sheet variant: supplementary content beside the primary content — filters, details, a list of actions — co-planar from `expanded`, flat on `surface` with 0dp elevation and no corner, the window's full height, an outline-variant rule down its inner edge instead of a scrim, nothing inert and no focus trap. Below `expanded` it is the modal sheet. Capped at M3's 400dp whatever `width` says, and it always draws the close button. Render it beside its content in a row that only lays out side by side from `expanded` (``, or a caller's own row). diff --git a/resources/css/components/drawer.css b/resources/css/components/drawer.css index 7d1b6f11..9ed13e71 100644 --- a/resources/css/components/drawer.css +++ b/resources/css/components/drawer.css @@ -6,7 +6,7 @@ * material-components-android's `SideSheet.md`). * * [data-md-drawer] the wrapper; data-md-open, data-md-standard - * [data-md-drawer-scrim] modal only (hidden by JS once the sheet is wide/standard) + * [data-md-drawer-scrim] modal only (a standard sheet's is hidden from 840px) * [data-md-drawer-sheet] surface-container-low, elevation 1; data-md-side * [data-md-drawer-head] title/subtitle/close, and the separator's divider * [data-md-drawer-head-row], [data-md-drawer-heading], [data-md-drawer-title], @@ -15,11 +15,16 @@ * width (`@md:`), never the viewport's * [data-md-drawer-actions] left-aligned, 72dp (C-10 — a dialog's are trailing-aligned) * - * 400px cap, 24dp start/end padding, a large corner on the inner edge only, 56dp side-sheet - * width start — all from the specs table. It enters on the emphasized-decelerate easing rather - * than a spring, because a sheet anchored to an edge that overshot would open a gap; below - * `expanded` it slides, and once `standard` makes it co-planar it only fades (an in-flow box does - * not need to travel). + * 400px cap, 24dp start/end padding, a large corner on the inner edge only — from the specs + * table. It enters on the emphasized-decelerate easing rather than a spring, because a sheet + * anchored to an edge that overshot would open a gap, and leaves on emphasized accelerate: the + * view keeps `x-show`, so `@starting-style` gives the entry its start and `allow-discrete` holds + * `display` through the exit, whose timing comes from the closed state's own rule (a transition + * takes the after-change style's). Reduced motion zeroes both duration tokens (tokens/motion.css). + * Below `expanded` it slides, from whichever edge `data-md-side` names in the document's direction; + * once `standard` makes it co-planar it only fades (an in-flow box does not need to travel). Every + * state rule matches the sheet and scrim as the root's children, so a sheet nested in an open one + * keeps its own. * * A `standard` sheet drops the scrim, the trap and the elevation, and swaps surface-container-low * for `surface` with an outline-variant rule down its inner edge, in place of the scrim @@ -36,24 +41,34 @@ @import './divider.css'; @layer material.components { + /* The scrim fades in and, while `display` is held by `allow-discrete`, back out. */ [data-md-drawer-scrim] { position: fixed; 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-drawer][data-md-open] > [data-md-drawer-scrim] { + opacity: 1; + } + @starting-style { - [data-md-drawer-scrim] { + [data-md-drawer][data-md-open] > [data-md-drawer-scrim] { opacity: 0; } } + /* Closed, the sheet waits one width past its own edge (`--sheet-offset`, mirrored in RTL) and + slides there on emphasized accelerate; open, it comes back on emphasized decelerate. */ [data-md-drawer-sheet] { + --sheet-offset: 100%; + position: fixed; top: var(--material-safe-top, env(safe-area-inset-top)); bottom: 0; @@ -66,9 +81,8 @@ background-color: var(--md-sys-color-surface-container-low); color: var(--md-sys-color-on-surface); box-shadow: var(--md-sys-elevation-1); - translate: 100% 0; - opacity: 0; - transition-property: translate, opacity, display; + translate: var(--sheet-offset) 0; + transition-property: translate, display; transition-duration: var(--md-sys-motion-effects-default-duration); transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate); transition-behavior: allow-discrete; @@ -79,25 +93,28 @@ } [data-md-drawer-sheet][data-md-side='start'] { + --sheet-offset: -100%; + inset-inline-start: 0; - translate: -100% 0; } - [data-md-drawer][data-md-open] [data-md-drawer-sheet] { + [data-md-drawer-sheet][data-md-side='end']:dir(rtl) { + --sheet-offset: -100%; + } + + [data-md-drawer-sheet][data-md-side='start']:dir(rtl) { + --sheet-offset: 100%; + } + + [data-md-drawer][data-md-open] > [data-md-drawer-sheet] { translate: 0 0; - opacity: 1; transition-duration: var(--md-sys-motion-spatial-default-duration); transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate); } @starting-style { - [data-md-drawer][data-md-open] [data-md-drawer-sheet] { - translate: 100% 0; - opacity: 0; - } - - [data-md-drawer][data-md-open] [data-md-drawer-sheet][data-md-side='start'] { - translate: -100% 0; + [data-md-drawer][data-md-open] > [data-md-drawer-sheet] { + translate: var(--sheet-offset) 0; } } @@ -134,7 +151,11 @@ display: none; } - [data-md-drawer][data-md-standard] [data-md-drawer-sheet] { + [data-md-drawer][data-md-standard] > [data-md-drawer-scrim] { + display: none; + } + + [data-md-drawer][data-md-standard] > [data-md-drawer-sheet] { position: relative; top: 0; z-index: auto; @@ -149,23 +170,24 @@ transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate); } - [data-md-drawer][data-md-standard][data-md-open] [data-md-drawer-sheet] { + [data-md-drawer][data-md-standard][data-md-open] > [data-md-drawer-sheet] { opacity: 1; transition-duration: var(--md-sys-motion-spatial-default-duration); transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate); } @starting-style { - [data-md-drawer][data-md-standard][data-md-open] [data-md-drawer-sheet] { + [data-md-drawer][data-md-standard][data-md-open] > [data-md-drawer-sheet] { + translate: 0 0; opacity: 0; } } - [data-md-drawer][data-md-standard] [data-md-drawer-sheet][data-md-side='end'] { + [data-md-drawer][data-md-standard] > [data-md-drawer-sheet][data-md-side='end'] { border-inline-start: 1px solid var(--md-sys-color-outline-variant); } - [data-md-drawer][data-md-standard] [data-md-drawer-sheet][data-md-side='start'] { + [data-md-drawer][data-md-standard] > [data-md-drawer-sheet][data-md-side='start'] { border-inline-end: 1px solid var(--md-sys-color-outline-variant); } } diff --git a/resources/views/components/drawer.blade.php b/resources/views/components/drawer.blade.php index a35ae6d3..93bb9fe1 100644 --- a/resources/views/components/drawer.blade.php +++ b/resources/views/components/drawer.blade.php @@ -8,11 +8,11 @@ As a sheet it is modal: the page inert and still (`x-trap.inert.noscroll`), and it enters on emphasized decelerate rather than a spring — a sheet anchored to the edge that overshot would - open a gap. M3's modal side sheet (NavigationDrawerTokens.kt-adjacent side-sheet sources; - docs/reference/m3/components-actions-communication-containment.md § Side sheets): surface- - container-low, a large corner on its inner edge, elevation 1; `side` `end` (the default) or - `start`; `width` from `medium` (a caller's `class` would race the sheet's own — a compact - window gets the full-bleed sheet). + open a gap. M3's modal side sheet (docs/reference/m3/components-actions-communication- + containment.md § Side sheets; Compose has no side-sheet token file): surface-container-low, a + large corner on its inner edge, elevation 1; `side` `end` (the default) or `start`, mirrored + in a right-to-left document; `width` from `medium`, 400px (M3's 400dp cap) unless it says + otherwise — a compact window gets the full-bleed sheet. As a **standard** side sheet (`standard`, from `expanded`) it is M3's other variant: co-planar with the content rather than over it — no scrim, no focus trap, nothing inert, 0dp elevation @@ -54,7 +54,7 @@ 'withCloseButton' => true, 'closeOnEscape' => true, 'withoutBackdropClose' => false, - 'width' => '25rem', + 'width' => '400px', 'standard' => false, ]) @@ -66,7 +66,7 @@ // The side-sheet specs table caps the sheet at 400dp, which is where `width` already starts; // a wider one is the modal sheet's to take, not the co-planar standard sheet's. - $sheetWidth = $standard ? "min({$width}, 25rem)" : $width; + $sheetWidth = $standard ? "min({$width}, 400px)" : $width; // M3 requires a close affordance; the prop can only ever add one, never take away the last // way out of the sheet. A standard sheet keeps no scrim and no trap from `expanded`, and @@ -81,7 +81,7 @@ close() { this.open = typeof this.open === 'boolean' ? false : null; }, @if ($standard) init() { - const query = window.matchMedia('(width >= 52.5rem)'); + const query = window.matchMedia('(width >= 840px)'); this.wide = query.matches; query.addEventListener('change', (event) => this.wide = event.matches); }, diff --git a/tests/Feature/Components/OverlayTest.php b/tests/Feature/Components/OverlayTest.php index debf4307..a53a1a78 100644 --- a/tests/Feature/Components/OverlayTest.php +++ b/tests/Feature/Components/OverlayTest.php @@ -3,6 +3,7 @@ use Illuminate\Support\Facades\File; use Livewire\Component; use Livewire\Livewire; +use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet; it('opens a native dialog entangled with a Livewire property, out of the morph\'s reach', function () { $component = new class extends Component @@ -226,22 +227,40 @@ it('is M3\'s standard side sheet from expanded, and the modal one below', functi expect($html) ->toContain('data-md-standard') - ->toContain('--sheet-width: min(25rem, 25rem)') - ->toContain("matchMedia('(width >= 52.5rem)')") + ->toContain('--sheet-width: min(400px, 400px)') + ->toContain("matchMedia('(width >= 840px)')") ->toContain('x-trap.inert.noscroll="open && ! wide"') ->toContain('aria-label="Close"') ->and((string) $this->blade('Body')) ->toContain('data-md-side="start"') - ->toContain('--sheet-width: min(30rem, 25rem)') + ->toContain('--sheet-width: min(30rem, 400px)') ->and((string) $this->blade('Body')) ->not->toContain('data-md-standard'); - expect(File::get(__DIR__.'/../../../resources/css/components/drawer.css')) - ->toContain('[data-md-drawer][data-md-standard]') - ->toContain('[data-md-drawer][data-md-standard][data-md-drawer-collapsed]') - ->toContain('[data-md-drawer][data-md-standard] [data-md-drawer-sheet][data-md-side=\'end\']') - ->toContain('[data-md-drawer][data-md-standard] [data-md-drawer-sheet][data-md-side=\'start\']') - ->toContain('box-shadow: none;'); + $css = ComponentStylesheet::read('drawer'); + $expanded = ['@media (width >= 840px)']; + + expect($css->has('[data-md-drawer][data-md-standard]', $expanded))->toBeTrue() + ->and($css->declarations('[data-md-drawer][data-md-standard][data-md-drawer-collapsed]', $expanded))->toBe(['display' => 'none']) + // The old view's `expanded:hidden` on a standard sheet's scrim. + ->and($css->declarations('[data-md-drawer][data-md-standard] > [data-md-drawer-scrim]', $expanded))->toBe(['display' => 'none']) + ->and($css->declarations('[data-md-drawer][data-md-standard] > [data-md-drawer-sheet]', $expanded))->toHaveKey('box-shadow', 'none') + ->and($css->has("[data-md-drawer][data-md-standard] > [data-md-drawer-sheet][data-md-side='end']", $expanded))->toBeTrue() + ->and($css->has("[data-md-drawer][data-md-standard] > [data-md-drawer-sheet][data-md-side='start']", $expanded))->toBeTrue(); +}); + +it('slides a side sheet out as it slid in, from its own edge in either direction, and fades its scrim both ways', function () { + $css = ComponentStylesheet::read('drawer'); + + expect($css->declarations('[data-md-drawer-sheet]')) + ->toHaveKey('translate', 'var(--sheet-offset) 0') + ->toHaveKey('transition-behavior', 'allow-discrete') + ->not->toHaveKey('opacity') + ->and($css->declarations("[data-md-drawer-sheet][data-md-side='end']:dir(rtl)"))->toBe(['--sheet-offset' => '-100%']) + ->and($css->declarations("[data-md-drawer-sheet][data-md-side='start']:dir(rtl)"))->toBe(['--sheet-offset' => '100%']) + ->and($css->declarations('[data-md-drawer][data-md-open] > [data-md-drawer-sheet]', ['@starting-style']))->toBe(['translate' => 'var(--sheet-offset) 0']) + ->and($css->declarations('[data-md-drawer-scrim]'))->toHaveKey('opacity', '0') + ->and($css->declarations('[data-md-drawer][data-md-open] > [data-md-drawer-scrim]'))->toBe(['opacity' => '1']); }); it('draws the drawer from a stylesheet imported from the containment block, with the pane prop gone', function () {