Hide a standard side sheet's scrim from 840px again
Plan step 36 review of 9dd527f3. The old view hid a standard sheet's
scrim with `expanded:hidden`; the stylesheet lost that rule (and the
render test lost its assertion), so an open standard sheet at 840px and
up laid a full-window scrim over the page it sits beside, one click from
closing. The rule is back, pinned by a test.
The same review: the scrim now fades out as well as in (with no closed
opacity, allow-discrete only held it at 32% for 200ms and then dropped
it); the modal sheet slides without the fade the old enter and leave
classes never had; it slides from its own edge in a right-to-left
document, which translate-x-full never did; the state rules match the
root's own sheet and scrim, so a sheet nested in an open one keeps its
state; the script's breakpoint is 840px, not 52.5rem, which disagreed
with the stylesheet's px query once the reader's text size grew; and
`width` and the standard sheet's cap are M3's 400px.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
d86392fd47
commit
b6259d45f3
@@ -539,7 +539,7 @@ Props: `title`, `subtitle`, `icon` (centred hero icon), `separator` (draw the di
|
||||
|
||||
### `<x-drawer>`
|
||||
|
||||
An M3 side sheet, bound like `<x-modal>`; `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 `<x-list-detail>` instead — `<x-drawer>` 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 `<x-modal>`; `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 `<x-list-detail>` instead — `<x-drawer>` 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` (`<x-supporting-pane>`, or a caller's own row).
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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('<x-drawer standard side="start" width="30rem">Body</x-drawer>'))
|
||||
->toContain('data-md-side="start"')
|
||||
->toContain('--sheet-width: min(30rem, 25rem)')
|
||||
->toContain('--sheet-width: min(30rem, 400px)')
|
||||
->and((string) $this->blade('<x-drawer title="Filters">Body</x-drawer>'))
|
||||
->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 () {
|
||||
|
||||
Reference in New Issue
Block a user