Keep a closed standard side sheet out of its row from the first paint
A closed standard sheet is `inline-size: 0` with a negative margin as wide as its row's gap, but the view writes that gap (`--md-drawer-gap`) and `data-md-drawer-collapsed` only once Alpine runs. Until then each closed sheet was a zero-wide flex item that still cost the row one gap, and a sheet bound to an open Livewire property had no width at all, so the content beside them changed width when the script started - two closed sheets beside a column made it 48px narrower, then it jumped. Until the view has settled, drawer.css now takes a standard sheet that is not `data-md-open` out of the layout, and the view renders `data-md-open` on a standard sheet whose `wire:model` property is open (truthy as Alpine reads it), so it stands at its width from the first paint. Transitions still wait for `data-md-drawer-settled`. A browser test measures a column beside two closed sheets (one bound to Livewire, one to Alpine) and an open one before Alpine starts and once settled; it fails without the change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
207270a23d
commit
a6dc213e67
@@ -54,6 +54,12 @@
|
|||||||
under an installed app's status bar as the page scrolls; inside `<x-scaffold>` it also sticks
|
under an installed app's status bar as the page scrolls; inside `<x-scaffold>` it also sticks
|
||||||
under a sticky `<x-app-bar>` in the `top` slot, which covered it. It is as tall as the window
|
under a sticky `<x-app-bar>` in the `top` slot, which covered it. It is as tall as the window
|
||||||
below that, and every side sheet keeps the bottom safe area inside its bottom padding.
|
below that, and every side sheet keeps the bottom safe area inside its bottom padding.
|
||||||
|
- **A standard `<x-drawer>` takes its place from the first paint.** Until Alpine started, a closed
|
||||||
|
standard sheet was a zero-wide item that still cost its row one gap, and a sheet bound to an open
|
||||||
|
Livewire property had no width yet, so the content beside them changed width when the script
|
||||||
|
ran. A standard sheet is now out of the row until then, and one whose `wire:model` property is
|
||||||
|
open is rendered open and stands at its width. One opened from an Alpine scope alone still
|
||||||
|
appears when Alpine starts, which only the script can know.
|
||||||
|
|
||||||
## From 2.0.0 to 2.1.0
|
## From 2.0.0 to 2.1.0
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,15 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Before the view has settled, a sheet not rendered open is out of the layout already. The
|
||||||
|
view writes `data-md-drawer-collapsed` and measures the row's gap only once Alpine runs,
|
||||||
|
so until then a closed sheet would be a zero-wide flex item that still costs its row one
|
||||||
|
gap, and the content beside it would widen when the script started. A sheet whose
|
||||||
|
`wire:model` property is open is rendered `data-md-open` and stands at its width. */
|
||||||
|
[data-md-drawer][data-md-standard]:not([data-md-open], [data-md-drawer-settled]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Until the view settles (`data-md-drawer-settled`, two frames after Alpine starts), the
|
/* Until the view settles (`data-md-drawer-settled`, two frames after Alpine starts), the
|
||||||
state the page loads with is drawn at once: a sheet that starts open does not grow in. */
|
state the page loads with is drawn at once: a sheet that starts open does not grow in. */
|
||||||
[data-md-drawer][data-md-standard]:not([data-md-drawer-settled]),
|
[data-md-drawer][data-md-standard]:not([data-md-drawer-settled]),
|
||||||
|
|||||||
@@ -37,7 +37,10 @@
|
|||||||
`open` itself would run before `settle()` in the same flush and, already queued, not run again
|
`open` itself would run before `settle()` in the same flush and, already queued, not run again
|
||||||
once `closing` changed. Nothing moves while the page loads: `data-md-drawer-settled` comes two
|
once `closing` changed. Nothing moves while the page loads: `data-md-drawer-settled` comes two
|
||||||
frames after the state Alpine starts with, and the standard sheet has no transitions until
|
frames after the state Alpine starts with, and the standard sheet has no transitions until
|
||||||
then, so a sheet that starts open stands open rather than growing in.
|
then, so a sheet that starts open stands open rather than growing in. Before the script starts
|
||||||
|
at all, a standard sheet whose `wire:model` property is open is rendered `data-md-open` and
|
||||||
|
stands at its width, and any other is out of the row, so the content beside it keeps its width
|
||||||
|
when Alpine runs.
|
||||||
|
|
||||||
For the second pane of a list-detail layout use `<x-list-detail>` instead (step 35's canonical
|
For the second pane of a list-detail layout use `<x-list-detail>` instead (step 35's canonical
|
||||||
layout); the two are not the same thing — a pane shows what the list beside it selected, a
|
layout); the two are not the same thing — a pane shows what the list beside it selected, a
|
||||||
@@ -82,6 +85,15 @@
|
|||||||
// a wider one is the modal sheet's to take, not the co-planar standard sheet's.
|
// a wider one is the modal sheet's to take, not the co-planar standard sheet's.
|
||||||
$sheetWidth = $standard ? "min({$width}, 400px)" : $width;
|
$sheetWidth = $standard ? "min({$width}, 400px)" : $width;
|
||||||
|
|
||||||
|
// A standard sheet bound to Livewire is drawn open from the first paint when its property is
|
||||||
|
// (truthy as Alpine reads it), so it stands in the row before the script starts rather than
|
||||||
|
// growing into it; drawer.css keeps any other standard sheet out of the row until then.
|
||||||
|
$startsOpen = false;
|
||||||
|
|
||||||
|
if ($standard && $model !== null && ($component = \Livewire\Livewire::current()) !== null) {
|
||||||
|
$startsOpen = ! in_array(data_get($component, $model), [null, false, 0, 0.0, ''], true);
|
||||||
|
}
|
||||||
|
|
||||||
// M3 requires a close affordance; the prop can only ever add one, never take away the last
|
// 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
|
// way out of the sheet. A standard sheet keeps no scrim and no trap from `expanded`, and
|
||||||
// Escape leaves it open there, so it always draws one.
|
// Escape leaves it open there, so it always draws one.
|
||||||
@@ -160,6 +172,7 @@
|
|||||||
x-effect="settle(open)"
|
x-effect="settle(open)"
|
||||||
data-md-drawer
|
data-md-drawer
|
||||||
@if ($standard) data-md-standard @endif
|
@if ($standard) data-md-standard @endif
|
||||||
|
@if ($startsOpen) data-md-open @endif
|
||||||
style="--sheet-width: {{ $sheetWidth }}"
|
style="--sheet-width: {{ $sheetWidth }}"
|
||||||
>
|
>
|
||||||
{{-- `md-transition` is a class name only to turn on Alpine's CSS transition: `x-show` then keeps
|
{{-- `md-transition` is a class name only to turn on Alpine's CSS transition: `x-show` then keeps
|
||||||
|
|||||||
@@ -832,6 +832,64 @@ it('sticks a standard side sheet under the top safe area and a scaffold\'s stick
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class StandardSheetsProbe extends Component
|
||||||
|
{
|
||||||
|
public bool $filters = false;
|
||||||
|
|
||||||
|
public ?int $session = 7;
|
||||||
|
|
||||||
|
public function render(): string
|
||||||
|
{
|
||||||
|
return <<<'BLADE'
|
||||||
|
<div>
|
||||||
|
<x-row id="sheets-row" align="stretch" gap="space300" x-data="{ open: false }">
|
||||||
|
<x-stack id="sheets-column" style="flex: 1 1 0%; min-width: 0;"><p>The session list</p></x-stack>
|
||||||
|
<x-drawer standard wire:model="filters" title="Filters">Narrow the list down</x-drawer>
|
||||||
|
<x-drawer standard wire:model="session" title="Session">The open session</x-drawer>
|
||||||
|
<x-drawer standard title="Notes">Nobody opened these</x-drawer>
|
||||||
|
</x-row>
|
||||||
|
<script>
|
||||||
|
window.firstPaint = {
|
||||||
|
column: document.getElementById('sheets-column').getBoundingClientRect().width,
|
||||||
|
alpine: typeof window.Alpine,
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
BLADE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('draws standard side sheets as the server renders them from the first paint, closed ones out of the row and an open one standing', function () {
|
||||||
|
Livewire::component('standard-sheets-probe', StandardSheetsProbe::class);
|
||||||
|
|
||||||
|
Route::middleware('web')->get('/standard-sheets-probe', fn () => Blade::render(<<<'BLADE'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<x-theme-script />
|
||||||
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
|
@livewireStyles
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<livewire:standard-sheets-probe />
|
||||||
|
@livewireScripts
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
BLADE));
|
||||||
|
|
||||||
|
// Playwright's window is 1280px wide from the first paint, in the band where the sheets stand
|
||||||
|
// in the row. The column measured before Alpine starts is the column once it has settled: two
|
||||||
|
// closed sheets (one bound to Livewire, one to Alpine) take no gap, and the open one its width.
|
||||||
|
$page = visit('/standard-sheets-probe')->waitForEvent('networkidle')
|
||||||
|
->assertScript("window.eval('window.firstPaint.alpine') === 'undefined'")
|
||||||
|
->assertScript("[...document.querySelectorAll('[data-md-drawer]')].every((root) => root.hasAttribute('data-md-drawer-settled'))");
|
||||||
|
|
||||||
|
$settled = $page->script("document.getElementById('sheets-column').getBoundingClientRect().width");
|
||||||
|
|
||||||
|
expect(abs($page->script('window.firstPaint.column') - $settled))->toBeLessThan(1)
|
||||||
|
->and($page->script("Math.round([...document.querySelectorAll('[data-md-drawer]')][1].getBoundingClientRect().width)"))->toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
it('opens a row\'s opener from a press anywhere on the row, but not from its own buttons', function () {
|
it('opens a row\'s opener from a press anywhere on the row, but not from its own buttons', function () {
|
||||||
$page = containment();
|
$page = containment();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user