Plan step 35: <x-pane>, a content region with M3's margins (16px below medium, 24px from it) drawn once however panes and layouts nest, a width cap, and an optional pane app bar - <x-app-bar> as a direct child of the pane with title, subtitle, actions, a back link or action, a leading slot, and the section navigation under it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
173 lines
8.0 KiB
PHP
173 lines
8.0 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\Blade;
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
/**
|
|
* The layout components at M3's breakpoints (plan step 35): each on a page of its own, measured at
|
|
* the pixel either side of the breakpoint it changes at. The window is resized after the page has
|
|
* loaded, which is what a person does; a media query answers at once, and a script listening to
|
|
* one (resources/js/layout.js) hears a change event.
|
|
*/
|
|
function layoutPage(string $body, int $width, int $height = 900, string $dir = 'ltr'): mixed
|
|
{
|
|
// Until the Workbench leaves Tailwind, its build turns `:dir(rtl)` into a list of `:lang()`s, so a
|
|
// right-to-left page also says which language it is in.
|
|
$lang = $dir === 'rtl' ? 'ar' : 'en';
|
|
$path = '/layout-probe/'.md5($body.$dir);
|
|
|
|
Route::middleware('web')->get($path, fn () => Blade::render(<<<BLADE
|
|
<!DOCTYPE html>
|
|
<html dir="{$dir}" lang="{$lang}">
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<x-theme-script />
|
|
@vite(config('livewire-material.showcase.vite'))
|
|
@livewireStyles
|
|
</head>
|
|
<body>
|
|
{$body}
|
|
@livewireScripts
|
|
</body>
|
|
</html>
|
|
BLADE));
|
|
|
|
return layoutReady(visit($path)->resize($width, $height));
|
|
}
|
|
|
|
function layoutReady(mixed $page): mixed
|
|
{
|
|
return $page->waitForEvent('networkidle')
|
|
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
|
}
|
|
|
|
/** A computed style of the element matching the selector. */
|
|
function layoutStyle(string $selector, string $property): string
|
|
{
|
|
return "getComputedStyle(document.querySelector('{$selector}')).{$property}";
|
|
}
|
|
|
|
/** A side of the element's box, rounded to the pixel. */
|
|
function layoutRect(string $selector, string $side): string
|
|
{
|
|
return "Math.round(document.querySelector('{$selector}').getBoundingClientRect().{$side})";
|
|
}
|
|
|
|
/** How many column tracks a grid has. */
|
|
function layoutColumns(string $selector): string
|
|
{
|
|
return layoutStyle($selector, 'gridTemplateColumns').".split(' ').length";
|
|
}
|
|
|
|
it('spaces a stack with its token, never with its parent\'s, and hides it from a breakpoint over its own display', function () {
|
|
$body = '<x-stack id="outer" gap="space300"><x-stack id="inner"><p>One</p><p>Two</p></x-stack><p>Three</p></x-stack>'
|
|
.'<x-stack id="hidden-from" hide-from="medium"><p>Compact only</p></x-stack>';
|
|
|
|
layoutPage($body, 599)
|
|
->assertScript(layoutStyle('#outer', 'flexDirection')." === 'column'")
|
|
->assertScript(layoutStyle('#outer', 'rowGap')." === '24px'")
|
|
->assertScript(layoutStyle('#inner', 'rowGap')." === '0px'")
|
|
->assertScript(layoutStyle('#hidden-from', 'display')." === 'flex'");
|
|
|
|
layoutPage($body, 600)
|
|
->assertScript(layoutStyle('#hidden-from', 'display')." === 'none'");
|
|
});
|
|
|
|
it('stacks a row below its breakpoint and lays it side by side from it', function () {
|
|
$body = '<x-row id="row" gap="space200" stack-below="medium"><span>One</span><span>Two</span></x-row>'
|
|
.'<x-row id="aligned" align="start" stack-below="medium"><span>One</span></x-row>'
|
|
.'<x-row id="hidden" hide-below="medium"><span>Medium and up</span></x-row>';
|
|
|
|
layoutPage($body, 599)
|
|
->assertScript(layoutStyle('#row', 'flexDirection')." === 'column'")
|
|
->assertScript(layoutStyle('#row', 'alignItems')." === 'stretch'")
|
|
->assertScript(layoutStyle('#aligned', 'alignItems')." === 'flex-start'")
|
|
->assertScript(layoutStyle('#row', 'rowGap')." === '16px'")
|
|
->assertScript(layoutStyle('#hidden', 'display')." === 'none'");
|
|
|
|
layoutPage($body, 600)
|
|
->assertScript(layoutStyle('#row', 'flexDirection')." === 'row'")
|
|
->assertScript(layoutStyle('#row', 'alignItems')." === 'center'")
|
|
->assertScript(layoutStyle('#hidden', 'display')." === 'flex'");
|
|
});
|
|
|
|
it('mirrors a row in a right-to-left document', function () {
|
|
layoutPage('<x-row id="row"><span id="first">First</span><span id="second">Second</span></x-row>', 800, 600, 'rtl')
|
|
->assertScript(layoutRect('#first', 'left').' > '.layoutRect('#second', 'left'));
|
|
});
|
|
|
|
it('gives a grid its columns per breakpoint, and a nested grid its own', function () {
|
|
$body = <<<'BLADE'
|
|
<x-grid id="outer" :columns="['medium' => 3, 'large' => 4]" gap="space200">
|
|
<x-grid id="inner"><div>A</div><div>B</div></x-grid>
|
|
<div>2</div><div>3</div><div>4</div>
|
|
</x-grid>
|
|
BLADE;
|
|
|
|
layoutPage($body, 599)
|
|
->assertScript(layoutColumns('#outer').' === 1')
|
|
->assertScript(layoutColumns('#inner').' === 1');
|
|
|
|
layoutPage($body, 600)
|
|
->assertScript(layoutColumns('#outer').' === 3')
|
|
->assertScript(layoutStyle('#outer', 'columnGap')." === '16px'")
|
|
// The parent's three never reach the grid inside it.
|
|
->assertScript(layoutColumns('#inner').' === 1');
|
|
|
|
layoutPage($body, 1199)->assertScript(layoutColumns('#outer').' === 3');
|
|
layoutPage($body, 1200)->assertScript(layoutColumns('#outer').' === 4');
|
|
});
|
|
|
|
it('fills a grid with columns of a minimum width, capped by the count', function () {
|
|
$body = '<div style="width: 580px"><x-grid id="fill" min-item="180px"><div>1</div><div>2</div><div>3</div><div>4</div></x-grid></div>'
|
|
.'<div style="width: 580px"><x-grid id="capped" min-item="100px" :columns="[\'compact\' => 2]" gap="space200"><div>1</div><div>2</div><div>3</div><div>4</div></x-grid></div>';
|
|
|
|
layoutPage($body, 800)
|
|
->assertScript(layoutColumns('#fill').' === 3')
|
|
->assertScript(layoutColumns('#capped').' === 2');
|
|
});
|
|
|
|
it('draws a surface in its role, padding and corner, and hides it below a breakpoint', function () {
|
|
$body = '<x-surface id="surface" level="surface-container-high" padding="space300" corner="lg" outlined hide-below="medium">Surface</x-surface>'
|
|
.'<div id="role" style="background-color: var(--md-sys-color-surface-container-high)"></div>';
|
|
|
|
layoutPage($body, 599)
|
|
->assertScript(layoutStyle('#surface', 'display')." === 'none'");
|
|
|
|
layoutPage($body, 600)
|
|
->assertScript(layoutStyle('#surface', 'display')." === 'block'")
|
|
->assertScript(layoutStyle('#surface', 'backgroundColor').' === '.layoutStyle('#role', 'backgroundColor'))
|
|
->assertScript(layoutStyle('#surface', 'paddingTop')." === '24px'")
|
|
->assertScript(layoutStyle('#surface', 'borderTopLeftRadius')." === '16px'")
|
|
->assertScript(layoutStyle('#surface', 'borderTopWidth')." === '1px'")
|
|
->assertNoJavaScriptErrors();
|
|
});
|
|
|
|
it('keeps M3\'s margin in a pane, once, with its app bar across the whole pane', function () {
|
|
$body = <<<'BLADE'
|
|
<x-pane id="pane" title="Settings">
|
|
<p id="text">Body</p>
|
|
<x-pane id="inner"><p>Nested</p></x-pane>
|
|
<x-surface id="card"><x-pane id="on-surface"><p id="surface-text">On a surface</p></x-pane></x-surface>
|
|
</x-pane>
|
|
<x-pane id="narrow" width="narrow"><p>Narrow</p></x-pane>
|
|
BLADE;
|
|
|
|
layoutPage($body, 599)
|
|
->assertScript(layoutRect('#text', 'left').' === 16')
|
|
->assertScript(layoutRect('#pane [data-md-pane-bar]', 'width').' === '.layoutRect('#pane', 'width'))
|
|
->assertScript(layoutStyle('#inner [data-md-pane-body]', 'paddingLeft')." === '0px'");
|
|
|
|
layoutPage($body, 600)
|
|
->assertScript(layoutRect('#text', 'left').' === 24')
|
|
->assertScript(layoutRect('#pane [data-md-pane-bar]', 'left').' === 0')
|
|
->assertScript(layoutStyle('#inner [data-md-pane-body]', 'paddingLeft')." === '0px'")
|
|
// A surface is a new edge, so a pane on it keeps its margin again.
|
|
->assertScript(layoutRect('#surface-text', 'left').' - '.layoutRect('#card', 'left').' === 24');
|
|
|
|
layoutPage($body, 1200)
|
|
->assertScript(layoutRect('#narrow', 'width').' === 640')
|
|
->assertScript(layoutRect('#narrow', 'left').' === 280')
|
|
->assertNoJavaScriptErrors();
|
|
});
|