Plan step 35: <x-app-shell> becomes <x-scaffold>, M3's and Compose's name for the structure of bars, rails and panes, with no alias; every package view, the showcase, the tests, the skills, README and UPGRADE follow. Its markup and Tailwind classes stay for the navigation rewrite. The new fab slot places the FAB as Compose's Scaffold does - bottom-end, 16px from the edges below medium and 24px from it, clear of the navigation bar and of a snackbar on screen (resources/css/layout/scaffold.css) - and the content region tells a pane inside that the margin is already drawn. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
466 lines
22 KiB
PHP
466 lines
22 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\Blade;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Livewire\Component;
|
|
use Livewire\Livewire;
|
|
|
|
/**
|
|
* 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();
|
|
});
|
|
|
|
class LayoutListDetailProbe extends Component
|
|
{
|
|
public ?int $messageId = null;
|
|
|
|
public function render(): string
|
|
{
|
|
return <<<'BLADE'
|
|
<div>
|
|
<p>selected: <span id="selected">{{ var_export($messageId, true) }}</span></p>
|
|
|
|
<x-list-detail id="inbox" wire:model.live="messageId">
|
|
<x-slot:list>
|
|
<x-pane title="Inbox" :sticky="false">
|
|
<x-list label="Messages">
|
|
@foreach ([1 => 'Photos', 2 => 'Contract', 3 => 'Trains'] as $id => $subject)
|
|
<x-list-item :title="$subject" link="#message-{{ $id }}" no-wire-navigate wire:click.prevent="$set('messageId', {{ $id }})" :selected="$messageId === $id" wire:key="message-{{ $id }}" />
|
|
@endforeach
|
|
</x-list>
|
|
</x-pane>
|
|
</x-slot:list>
|
|
|
|
<x-slot:detail>
|
|
<x-pane :title="$messageId ? 'Message '.$messageId : 'Nothing selected'" heading="h2" back :sticky="false">
|
|
<p id="detail-text">{{ $messageId ? 'The message' : 'Select a message' }}</p>
|
|
</x-pane>
|
|
</x-slot:detail>
|
|
</x-list-detail>
|
|
</div>
|
|
BLADE;
|
|
}
|
|
}
|
|
|
|
function listDetailPage(int $width, string $dir = 'ltr'): mixed
|
|
{
|
|
Livewire::component('layout-list-detail-probe', LayoutListDetailProbe::class);
|
|
|
|
return layoutPage('<livewire:layout-list-detail-probe />', $width, 900, $dir);
|
|
}
|
|
|
|
const LIST_PANE = '[data-md-list-detail-pane="list"]';
|
|
const DETAIL_PANE = '[data-md-list-detail-pane="detail"]';
|
|
|
|
function listDetailVisible(string $pane): string
|
|
{
|
|
return "document.querySelector('{$pane}').checkVisibility()";
|
|
}
|
|
|
|
it('shows the list alone below expanded, and the detail with a back button once something is selected', function () {
|
|
foreach ([599, 600, 839] as $width) {
|
|
listDetailPage($width)
|
|
->assertScript(listDetailVisible(LIST_PANE))
|
|
->assertScript('! '.listDetailVisible(DETAIL_PANE));
|
|
}
|
|
|
|
$page = listDetailPage(599);
|
|
|
|
$page->script("document.querySelector('a[href=\"#message-2\"]').focus()");
|
|
$page->keys('a[href="#message-2"]', 'Enter')
|
|
->assertSeeIn('#selected', '2')
|
|
->assertSeeIn('#detail-text', 'The message')
|
|
->assertScript('! '.listDetailVisible(LIST_PANE))
|
|
->assertScript(listDetailVisible(DETAIL_PANE))
|
|
// Focus moves with the person to the pane that replaced the list.
|
|
->assertScript("document.activeElement === document.querySelector('".DETAIL_PANE."')")
|
|
->assertScript("document.querySelector('".DETAIL_PANE." [data-md-list-detail-back] button').checkVisibility()");
|
|
|
|
$page->click(DETAIL_PANE.' [data-md-list-detail-back] button')
|
|
->assertSeeIn('#selected', 'NULL')
|
|
->assertScript(listDetailVisible(LIST_PANE))
|
|
->assertScript('! '.listDetailVisible(DETAIL_PANE))
|
|
// And back to the item it came from.
|
|
->assertScript("document.activeElement === document.querySelector('a[href=\"#message-2\"]')")
|
|
->assertNoJavaScriptErrors();
|
|
});
|
|
|
|
it('shows both panes from expanded at M3\'s fixed widths, with no back button and no focus move', function () {
|
|
foreach ([840 => 360, 1199 => 360, 1200 => 412] as $width => $list) {
|
|
listDetailPage($width)
|
|
->assertScript(listDetailVisible(LIST_PANE))
|
|
->assertScript(listDetailVisible(DETAIL_PANE))
|
|
->assertScript(layoutRect(LIST_PANE, 'width')." === {$list}")
|
|
// M3's 24px margin and 24px spacer.
|
|
->assertScript(layoutRect(LIST_PANE, 'left').' === 24')
|
|
->assertScript(layoutRect(DETAIL_PANE, 'left').' === '.(24 + $list + 24))
|
|
->assertScript("! document.querySelector('".DETAIL_PANE." [data-md-list-detail-back]').checkVisibility()");
|
|
}
|
|
|
|
$page = listDetailPage(1200);
|
|
|
|
$page->script("document.querySelector('a[href=\"#message-3\"]').focus()");
|
|
$page->keys('a[href="#message-3"]', 'Enter')
|
|
->assertSeeIn('#detail-text', 'The message')
|
|
->assertScript(listDetailVisible(LIST_PANE))
|
|
->assertScript("document.activeElement === document.querySelector('a[href=\"#message-3\"]')");
|
|
|
|
// Narrowed past 840px with focus in the list that is about to hide, focus goes to the detail.
|
|
$page->resize(839, 900)
|
|
->wait(0.2)
|
|
->assertScript('! '.listDetailVisible(LIST_PANE))
|
|
->assertScript("document.activeElement === document.querySelector('".DETAIL_PANE."')")
|
|
->assertNoJavaScriptErrors();
|
|
});
|
|
|
|
it('mirrors the list-detail in a right-to-left document', function () {
|
|
listDetailPage(1200, 'rtl')
|
|
->assertScript(layoutRect(LIST_PANE, 'left').' > '.layoutRect(DETAIL_PANE, 'left'))
|
|
->assertScript(layoutRect(LIST_PANE, 'right').' === window.innerWidth - 24');
|
|
|
|
$page = listDetailPage(599, 'rtl');
|
|
|
|
$page->click('a[href="#message-1"]')
|
|
->assertSeeIn('#selected', '1')
|
|
->assertScript("getComputedStyle(document.querySelector('".DETAIL_PANE." [data-md-list-detail-back] svg')).transform !== 'none'");
|
|
});
|
|
|
|
it('selects from Alpine through x-model, with the layout\'s own back row', function () {
|
|
$body = <<<'BLADE'
|
|
<div x-data="{ chosen: null }">
|
|
<p>chosen: <span id="chosen" x-text="String(chosen)"></span></p>
|
|
<x-list-detail x-model="chosen">
|
|
<x-slot:list><button type="button" id="pick" x-on:click="selected = 'b'">Pick b</button></x-slot:list>
|
|
<x-slot:detail><p id="shown" x-text="selected"></p></x-slot:detail>
|
|
</x-list-detail>
|
|
</div>
|
|
BLADE;
|
|
|
|
$page = layoutPage($body, 599);
|
|
|
|
$page->click('#pick')
|
|
->assertSeeIn('#chosen', 'b')
|
|
->assertSeeIn('#shown', 'b')
|
|
->assertScript('! '.listDetailVisible(LIST_PANE))
|
|
->assertScript("document.querySelector('[data-md-list-detail-back-row]').checkVisibility()");
|
|
|
|
$page->click('[data-md-list-detail-back-row] button')
|
|
->assertSeeIn('#chosen', 'null')
|
|
->assertScript(listDetailVisible(LIST_PANE))
|
|
->assertScript("document.activeElement === document.querySelector('#pick')");
|
|
});
|
|
|
|
const SUPPORTING_MAIN = '[data-md-supporting-pane-main]';
|
|
const SUPPORTING_SIDE = '[data-md-supporting-pane-supporting]';
|
|
|
|
function supportingPanePage(int $width, string $attributes = '', string $dir = 'ltr'): mixed
|
|
{
|
|
$body = <<<BLADE
|
|
<x-supporting-pane label="Comments" {$attributes}>
|
|
<x-slot:main><article id="main" style="block-size: 1400px">The proposal</article></x-slot:main>
|
|
<x-slot:supporting><p id="comment">Friday works for me.</p><button type="button" id="reply">Reply</button></x-slot:supporting>
|
|
</x-supporting-pane>
|
|
BLADE;
|
|
|
|
return layoutPage($body, $width, 800, $dir);
|
|
}
|
|
|
|
it('puts the supporting pane under the focus pane below expanded', function () {
|
|
foreach ([599 => 16, 600 => 24, 839 => 24] as $width => $margin) {
|
|
supportingPanePage($width)
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'top').' >= '.layoutRect(SUPPORTING_MAIN, 'bottom'))
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'width').' === '.layoutRect(SUPPORTING_MAIN, 'width'))
|
|
->assertScript(layoutRect(SUPPORTING_MAIN, 'left')." === {$margin}")
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'top').' - '.layoutRect(SUPPORTING_MAIN, 'bottom')." === {$margin}");
|
|
}
|
|
});
|
|
|
|
it('puts the supporting pane beside the focus pane from expanded, at M3\'s widths or the two-thirds split', function () {
|
|
foreach ([840 => 360, 1199 => 360, 1200 => 412] as $width => $side) {
|
|
supportingPanePage($width)
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'top').' === '.layoutRect(SUPPORTING_MAIN, 'top'))
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'width')." === {$side}")
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'right')." === {$width} - 24")
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'left').' - '.layoutRect(SUPPORTING_MAIN, 'right').' === 24');
|
|
}
|
|
|
|
supportingPanePage(1200, 'width="split"')
|
|
->assertScript('Math.abs('.layoutRect(SUPPORTING_MAIN, 'width').' - 2 * '.layoutRect(SUPPORTING_SIDE, 'width').') <= 1');
|
|
|
|
supportingPanePage(1200, '', 'rtl')
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'left').' === 24')
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'right').' < '.layoutRect(SUPPORTING_MAIN, 'left'));
|
|
});
|
|
|
|
it('docks the supporting pane as a bottom sheet below expanded, opened and closed by its handle', function () {
|
|
$handle = '[data-md-supporting-pane-handle]';
|
|
|
|
$page = supportingPanePage(599, 'compact="sheet"')
|
|
->assertScript("document.querySelector('{$handle}').checkVisibility()")
|
|
->assertAttribute($handle, 'aria-expanded', 'false')
|
|
->assertScript("! document.querySelector('#comment').checkVisibility()")
|
|
// Docked to the bottom of the window while the focus pane scrolls under it.
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'bottom').' === window.innerHeight')
|
|
->assertScript(layoutStyle(SUPPORTING_SIDE, 'borderTopLeftRadius')." === '28px'");
|
|
|
|
$page->click($handle)
|
|
->assertAttribute($handle, 'aria-expanded', 'true')
|
|
->assertScript("document.querySelector('#comment').checkVisibility()")
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'bottom').' === window.innerHeight');
|
|
|
|
$page->keys('#reply', 'Escape')
|
|
->assertAttribute($handle, 'aria-expanded', 'false')
|
|
->assertScript("! document.querySelector('#comment').checkVisibility()")
|
|
->assertScript("document.activeElement === document.querySelector('{$handle}')");
|
|
|
|
// Scrolled to the end, the sheet has its own room and covers nothing of the focus pane.
|
|
$page->script('window.scrollTo(0, document.documentElement.scrollHeight)');
|
|
$page->assertScript(layoutRect(SUPPORTING_SIDE, 'top').' >= '.layoutRect(SUPPORTING_MAIN, 'bottom'));
|
|
|
|
supportingPanePage(840, 'compact="sheet"')
|
|
->assertScript("! document.querySelector('{$handle}').checkVisibility()")
|
|
->assertScript("document.querySelector('#comment').checkVisibility()")
|
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'width').' === 360')
|
|
->assertNoJavaScriptErrors();
|
|
});
|
|
|
|
it('lays a feed out in one column below medium and in columns of its minimum width from it, in order', function () {
|
|
$body = '<x-feed id="feed" min-item="240px">'
|
|
.collect(range(1, 6))->map(fn (int $item): string => "<article id=\"item-{$item}\">Item {$item}</article>")->implode('')
|
|
.'</x-feed>';
|
|
|
|
layoutPage($body, 599)
|
|
->assertScript(layoutColumns('#feed').' === 1')
|
|
->assertScript(layoutStyle('#feed', 'rowGap')." === '16px'");
|
|
|
|
// 600px, less no margins, fits two 240px columns with M3's 24px spacer between them.
|
|
layoutPage($body, 600)
|
|
->assertScript(layoutColumns('#feed').' === 2')
|
|
->assertScript(layoutStyle('#feed', 'columnGap')." === '24px'")
|
|
->assertScript(layoutRect('#item-2', 'left').' > '.layoutRect('#item-1', 'left'))
|
|
->assertScript(layoutRect('#item-3', 'top').' > '.layoutRect('#item-1', 'top'));
|
|
|
|
layoutPage($body, 1200)
|
|
->assertScript(layoutColumns('#feed').' === 4')
|
|
->assertScript(layoutRect('#item-5', 'left').' === '.layoutRect('#item-1', 'left'));
|
|
});
|
|
|
|
function scaffoldFabPage(int $width): mixed
|
|
{
|
|
$body = <<<'BLADE'
|
|
<x-scaffold :destinations="[
|
|
['title' => 'Inbox', 'icon' => 'inbox', 'url' => '/inbox', 'active' => true],
|
|
['title' => 'Sent', 'icon' => 'send', 'url' => '/sent'],
|
|
['title' => 'Drafts', 'icon' => 'draft', 'url' => '/drafts'],
|
|
]">
|
|
<x-slot:fab><x-fab icon="edit" tooltip="Compose" /></x-slot:fab>
|
|
<p style="block-size: 2000px">The page</p>
|
|
</x-scaffold>
|
|
BLADE;
|
|
|
|
return layoutPage($body, $width, 800);
|
|
}
|
|
|
|
const SCAFFOLD_FAB = '[data-md-scaffold-fab]';
|
|
|
|
/** The navigation bar the scaffold pins to the bottom: the one navigation landmark at the window's foot. */
|
|
const SCAFFOLD_BAR = "[...document.querySelectorAll('[data-md-scaffold] nav')].find((nav) => nav.checkVisibility() && Math.round(nav.getBoundingClientRect().bottom) >= window.innerHeight - 1)";
|
|
|
|
it('places the scaffold\'s FAB at the bottom-end corner, clear of the navigation bar and of a snackbar', function () {
|
|
$page = scaffoldFabPage(599)
|
|
->assertScript(layoutStyle(SCAFFOLD_FAB, 'position')." === 'fixed'")
|
|
->assertScript(SCAFFOLD_BAR.' !== undefined')
|
|
->assertScript('window.innerWidth - '.layoutRect(SCAFFOLD_FAB, 'right').' === 16')
|
|
// 16px above the bar's top edge.
|
|
->assertScript('Math.round('.SCAFFOLD_BAR.'.getBoundingClientRect().top) - '.layoutRect(SCAFFOLD_FAB, 'bottom').' === 16');
|
|
|
|
$page->script("window.eval(\"materialToast('Draft saved', { sticky: true })\")");
|
|
|
|
$page->wait(1)
|
|
->assertScript("document.documentElement.style.getPropertyValue('--material-snackbar-height') !== ''")
|
|
// Lifted above the snackbar, which sits 16px above the bar, by M3's 16px margin.
|
|
->assertScript("Math.round(document.querySelector('[x-data=\"materialSnackbar\"] > div').getBoundingClientRect().top) - ".layoutRect(SCAFFOLD_FAB, 'bottom').' === 16')
|
|
->assertScript(layoutRect(SCAFFOLD_FAB, 'bottom')." <= Math.round(document.querySelector('[x-data=\"materialSnackbar\"] > div').getBoundingClientRect().top)");
|
|
|
|
scaffoldFabPage(600)
|
|
->assertScript('window.innerWidth - '.layoutRect(SCAFFOLD_FAB, 'right').' === 24')
|
|
->assertScript('window.innerHeight - '.layoutRect(SCAFFOLD_FAB, 'bottom').' === 24')
|
|
->assertNoJavaScriptErrors();
|
|
});
|
|
|
|
it('draws no second margin for a pane or a canonical layout inside the scaffold\'s content region', function () {
|
|
$body = <<<'BLADE'
|
|
<x-scaffold :destinations="[['title' => 'Inbox', 'icon' => 'inbox', 'url' => '/inbox', 'active' => true]]">
|
|
<x-pane id="pane"><p id="text">Body</p></x-pane>
|
|
</x-scaffold>
|
|
BLADE;
|
|
|
|
// The content region's own 24px beside the 96px collapsed rail, and nothing more.
|
|
layoutPage($body, 700)
|
|
->assertScript(layoutRect('#text', 'left').' - '.layoutRect('main', 'left').' === 24')
|
|
->assertScript(layoutStyle('#pane [data-md-pane-body]', 'paddingLeft')." === '0px'");
|
|
});
|