Files
livewire-material/tests/Browser/LayoutTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 da7a05a619 Add the grid layout component
Plan step 35: <x-grid :columns gap min-item>. The column count per
breakpoint is written as five inline custom properties, each filled from
the nearest smaller breakpoint, so a nested grid never inherits its
parent's; min-item fills a row by the room the grid has, and with columns
the counts become a ceiling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:50:47 +02:00

129 lines
5.7 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');
});