Let a list-detail pane close on Escape when asked

A drawer that is a pane from xl ignored Escape, whatever close-on-escape
said: the page beside a pane stays in use, so that is the right default. An
application whose pane is a transient detail (ReStride's activity and
workout panes) can now pass pane-close-on-escape and have Escape close it at
every width.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 20:31:33 +02:00
co-authored by Claude Opus 5
parent 869ccdf447
commit ea529bf785
3 changed files with 15 additions and 3 deletions
@@ -462,7 +462,7 @@ Props: `title`, `subtitle`, `icon` (centred hero icon), `separator`, `persistent
### `<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`, `close-on-escape` (default true), `without-backdrop-close`, `actions` slot. `pane` (with `pane-width`) turns it into a list-detail pane from `xl`: render it after the list inside `<div class="xl:flex xl:items-start xl:gap-6">`. Its body is a size container — lay out inside with `@md:` etc., not `sm:`.
An M3 side sheet, bound like `<x-modal>`; `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`) turns it into a list-detail pane from `xl`: render it after the list inside `<div class="xl:flex xl:items-start xl:gap-6">`. Escape leaves a pane open unless `pane-close-on-escape`. Its body is a size container — lay out inside with `@md:` etc., not `sm:`.
### `<x-bottom-sheet>`
+5 -2
View File
@@ -15,7 +15,9 @@
As a pane (`pane`, from `xl`) nothing is covered: the page renders the drawer after its list in
an `xl:flex xl:items-start xl:gap-6` row, the drawer sticks under the top of the viewport, the
list stays usable and another row swaps what it shows no scrim, no trap, no inert page. While
closed it takes no room. `pane-width` sizes the pane (the sheet's width by default). The body is
closed it takes no room. `pane-width` sizes the pane (the sheet's width by default). Escape closes
the sheet but leaves a pane open, since the page beside it is still in use; `pane-close-on-escape`
closes the pane on Escape too, for a pane that is a transient detail. The body is
a size container, so its contents lay out by the room the sheet or pane actually has (`@md:`),
never by the viewport.
@@ -34,6 +36,7 @@
'width' => '25rem',
'pane' => false,
'paneWidth' => null,
'paneCloseOnEscape' => false,
])
@php
@@ -55,7 +58,7 @@
},
@endif
}"
@if ($closeOnEscape) x-on:keydown.window.escape="if (open && ! wide) close()" @endif
@if ($closeOnEscape) x-on:keydown.window.escape="{{ $paneCloseOnEscape ? 'if (open) close()' : 'if (open && ! wide) close()' }}" @endif
data-sheet="{{ $id }}"
@if ($pane)
x-bind:class="! open && 'xl:hidden'"
+9
View File
@@ -55,6 +55,15 @@ it('keeps a full-screen dialog\'s subtitle on a phone, where its bar carries the
->and((string) $this->blade('<x-modal subtitle="Only a subtitle">Text</x-modal>'))->toContain('<p class="type-body-md text-on-surface-variant">Only a subtitle</p>');
});
it('leaves a pane open on Escape unless it is asked to close then too', function () {
expect((string) $this->blade('<x-drawer pane>Body</x-drawer>'))
->toContain('x-on:keydown.window.escape="if (open &amp;&amp; ! wide) close()"')
->and((string) $this->blade('<x-drawer pane pane-close-on-escape>Body</x-drawer>'))
->toContain('x-on:keydown.window.escape="if (open) close()"')
->and((string) $this->blade('<x-drawer pane pane-close-on-escape :close-on-escape="false">Body</x-drawer>'))
->not->toContain('keydown.window.escape');
});
it('slides a side sheet in from either edge, and is a pane from xl when asked', function () {
expect((string) $this->blade('<x-drawer title="Details" with-close-button>Body</x-drawer>'))
->toContain('x-trap.inert.noscroll="open && ! wide"')