Give a closing standard side sheet's room back as it leaves

From `expanded` a standard side sheet sits in the layout beside the
content, and the view took it out of the layout the moment it closed:
`data-md-drawer-collapsed` (`display: none` on the root) followed `! open`
at once, so its fade was never seen and the content beside it took the
sheet's width, and the gap, in one jump — in every engine. M3's side
sheets: opening a standard sheet shrinks the body beside it, and closing
gives the room back.

drawer.css: the standard root is a clipping flex box whose `inline-size`
springs between none and the sheet's width, with a negative margin as
wide as its flex parent's gap (`--md-drawer-gap`), on the sheet's own
exit and entry timings; the sheet keeps its width at the root's far edge,
so it is uncovered from its inner edge while it fades. drawer.blade.php:
`settle()`, from `x-effect`, measures the gap on each change and holds
the collapse (`closing`) for the root's and the sheet's closing
durations, read a frame on; reopening lets a pending end go by. The
binding reads `collapsed`, which only `settle()` and the window's width
write: a binding reading `open` ran before `settle()` in the same flush
and, already queued, did not run again once `closing` changed.

ContainmentTest closes the showcase's sheet at 1000px and samples, in the
page, for the root part-way to none while the column beside it has grown
part of the way, then checks the collapse, the column at the row's full
width, and a reopen part-way through the exit ending fully open; it fails
on the previous code in Chrome, Firefox and Safari. OverlayTest pins the
root's declarations and the view's bindings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-16 21:00:23 +02:00
co-authored by Claude Opus 5
parent db6023bf80
commit 99c18e01d8
4 changed files with 172 additions and 7 deletions
+56
View File
@@ -509,6 +509,62 @@ it('is a standard side sheet from 840px, without a scrim or a focus trap, and th
->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') !== null");
});
it('gives a closing standard side sheet\'s room back to the content beside it as it leaves, from 840px', function () {
$sheet = "document.querySelector('#containment aside[data-md-side]')";
$root = "{$sheet}.parentElement";
$column = "{$root}.previousElementSibling";
$toggle = "Array.from(document.querySelectorAll('#containment button')).find((button) => button.textContent.trim() === 'Show or hide the filters')";
// The example starts open, and grows in once Alpine opens it: its entry has run when the sheet
// stands at its full 400px and fully shown, with nothing on it still animating.
$page = containment()->resize(1000, 800)
->assertScript("{$root}.hasAttribute('data-md-open') && Math.round({$root}.getBoundingClientRect().width) === 400 && getComputedStyle({$sheet}).opacity === '1' && {$root}.getAnimations({ subtree: true }).length === 0");
// Closed and sampled in one round trip: the sheet's root caught part-way between its width
// and none, still in the layout, while the column beside it has grown part of the way — not
// the whole sheet and its gap handed back in one jump at the end.
$midExit = $page->script(<<<JS
(async () => {
const root = {$root}
const column = {$column}
const open = { root: root.getBoundingClientRect().width, column: column.getBoundingClientRect().width, row: root.parentElement.getBoundingClientRect().width }
{$toggle}.click()
for (let i = 0; i < 80; i++) {
const width = root.getBoundingClientRect().width
const columnWidth = column.getBoundingClientRect().width
if (! root.hasAttribute('data-md-drawer-collapsed') && getComputedStyle(root).display !== 'none' && width > 1 && width < open.root - 1 && columnWidth > open.column + 1 && columnWidth < open.row - 1) return true
await new Promise((resolve) => setTimeout(resolve, 5))
}
return false
})()
JS);
expect($midExit)->toBeTrue();
// Settled: out of the layout, and the column is the whole row, gap included.
$page->assertScript("{$root}.hasAttribute('data-md-drawer-collapsed') && getComputedStyle({$root}).display === 'none'")
->assertScript("Math.abs({$column}.getBoundingClientRect().width - {$root}.parentElement.getBoundingClientRect().width) < 1");
// Reopened part-way through its exit, it ends open, not collapsed. Each script stays well under
// the browser plugin's one-second call timeout, past which it runs the script a second time.
$page->script("{$toggle}.click()");
$page->wait(0.6)
->assertScript("{$root}.hasAttribute('data-md-open') && {$root}.getAnimations({ subtree: true }).length === 0");
$page->script(<<<JS
(async () => {
{$toggle}.click()
await new Promise((resolve) => setTimeout(resolve, 60))
{$toggle}.click()
})()
JS);
$page->wait(0.9)
->assertScript("{$root}.hasAttribute('data-md-open') && ! {$root}.hasAttribute('data-md-drawer-collapsed') && Math.abs({$root}.getBoundingClientRect().width - 400) < 1 && getComputedStyle({$sheet}).opacity === '1'");
});
it('opens a row\'s opener from a press anywhere on the row, but not from its own buttons', function () {
$page = containment();