From 335ea4f4f1d0b5706ef22f16199f13be1d9a1b86 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 05:57:55 +0200 Subject: [PATCH] Give every side sheet the close affordance M3 requires Plan step 12, containment.md C-04. `with-close-button` defaulted to false, so a drawer could render with no exit at all once `close-on-escape` and the scrim were off. It now defaults to true, and `:with-close-button="false"` is ignored where nothing else closes the sheet: Escape off, the scrim off, or a pane, which has neither. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- .../livewire-material-development/SKILL.md | 2 +- resources/views/components/drawer.blade.php | 17 ++++++++++++++--- tests/Feature/Components/OverlayTest.php | 9 +++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index ba47d3bb..7cd9c5d9 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -479,7 +479,7 @@ Props: `title`, `subtitle`, `icon` (centred hero icon), `separator`, `persistent ### `` -An M3 side sheet, bound like ``; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`), `width` (`25rem`), `with-close-button`, `close-on-escape` (default true), `without-backdrop-close`, `actions` slot. `pane` (with `pane-width`, `22.5rem` — M3's 360dp fixed pane) turns it into a second pane from `expanded`, where M3 shows two panes: render it after the list inside `
`. Escape leaves a pane open unless `pane-close-on-escape`. 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`), `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, and on a pane), `close-on-escape` (default true), `without-backdrop-close`, `actions` slot. `pane` (with `pane-width`, `22.5rem` — M3's 360dp fixed pane) turns it into a second pane from `expanded`, where M3 shows two panes: render it after the list inside `
`. Escape leaves a pane open unless `pane-close-on-escape`. Its body is a size container — lay out inside with `@md:` (a *container* query), never a window class. ### `` diff --git a/resources/views/components/drawer.blade.php b/resources/views/components/drawer.blade.php index e7847ba4..33d67b2b 100644 --- a/resources/views/components/drawer.blade.php +++ b/resources/views/components/drawer.blade.php @@ -25,6 +25,13 @@ a size container, so its contents lay out by the room the sheet or pane actually has (`@md:`), never by the viewport. + M3 **requires** a close affordance on a side sheet — without one nobody can predict the + sheet's open/close flow or tell whether it is transient or permanent + (docs/reference/m3/components-actions-communication-containment.md § Side sheets → + Accessibility) — so `with-close-button` is on by default, and `:with-close-button="false"` is + ignored where nothing else closes the sheet: Escape off, the scrim off, or a pane, which has + neither. + maryUI's API, kept: `title`, `subtitle`, `separator`, `with-close-button`, `close-on-escape`, `without-backdrop-close`, `right` (ignored; use `side`), and an `actions` slot. --}} @@ -34,7 +41,7 @@ 'separator' => false, 'side' => 'end', 'right' => true, - 'withCloseButton' => false, + 'withCloseButton' => true, 'closeOnEscape' => true, 'withoutBackdropClose' => false, 'width' => '25rem', @@ -47,6 +54,10 @@ $model = $attributes->wire('model')->value() ?: null; $id = $attributes->get('id') ?? 'material-sheet-'.substr(md5($model.'|'.$title), 0, 10); $start = $side === 'start'; + + // M3 requires a close affordance; the prop can only ever add one, never take away the last + // way out of the sheet. + $closeButton = $withCloseButton || ! $closeOnEscape || $withoutBackdropClose || ($pane && ! $paneCloseOnEscape); @endphp
get('class'), ]) }} > - @if (filled($title) || $withCloseButton) + @if (filled($title) || $closeButton)
@@ -118,7 +129,7 @@ @endif
- @if ($withCloseButton) + @if ($closeButton) diff --git a/tests/Feature/Components/OverlayTest.php b/tests/Feature/Components/OverlayTest.php index 325f477d..932bbe1c 100644 --- a/tests/Feature/Components/OverlayTest.php +++ b/tests/Feature/Components/OverlayTest.php @@ -64,6 +64,15 @@ it('leaves a pane open on Escape unless it is asked to close then too', function ->not->toContain('keydown.window.escape'); }); +it('always offers a way out of a side sheet, as M3 requires', function () { + expect((string) $this->blade('Body'))->toContain('aria-label="Close"') + ->and((string) $this->blade('Body'))->not->toContain('aria-label="Close"') + ->and((string) $this->blade('Body'))->toContain('aria-label="Close"') + ->and((string) $this->blade('Body'))->toContain('aria-label="Close"') + ->and((string) $this->blade('Body'))->toContain('aria-label="Close"') + ->and((string) $this->blade('Body'))->not->toContain('aria-label="Close"'); +}); + it('slides a side sheet in from either edge, and is a pane from expanded when asked', function () { expect((string) $this->blade('Body')) ->toContain('x-trap.inert.noscroll="open && ! wide"')