renders++; } public function render(): string { return <<<'BLADE'

confirming: {{ var_export($confirming, true) }}

deleting: {{ var_export($deletingId, true) }}

renders: {{ $renders }}

BLADE; } } class CollapseProbe extends Component { public bool $fineTuning = false; public int $renders = 0; public function touch(): void { $this->renders++; } public function expand(): void { $this->fineTuning = true; } public function collapse(): void { $this->fineTuning = false; } public function render(): string { return <<<'BLADE'

fine tuning: {{ var_export($fineTuning, true) }}

renders: {{ $renders }}

Zones and paces.

advanced:

Everything else.
BLADE; } } function collapseProbe() { Livewire::component('collapse-probe', CollapseProbe::class); Route::middleware('web')->get('/collapse-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); return visit('/collapse-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } function overlayProbe() { Livewire::component('overlay-probe', OverlayProbe::class); Route::middleware('web')->get('/overlay-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); return visit('/overlay-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } function containment() { return visit('/material/containment')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } it('writes back what closed means: false for a flag, null for an id', function () { $page = overlayProbe(); $page->click('button:has-text("Confirm")') ->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Are you sure?'))"); $page->click('dialog[open] button:has-text("Re-render")') ->assertSeeIn('#renders', '1') ->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Are you sure?'))"); $page->keys('dialog[open] button:has-text("Re-render")', 'Escape') ->assertScript("! [...document.querySelectorAll('dialog')].some((d) => d.open)") ->assertSeeIn('#confirming', 'false'); $page->click('button:has-text("Delete 7")') ->assertSeeIn('#deleting', '7') ->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Delete share?'))"); $page->script("document.querySelector('dialog[open]').dispatchEvent(new Event('cancel', { cancelable: true }))"); $page->assertSeeIn('#deleting', 'NULL'); }); it('closes a showcase dialog on Escape and gives focus back to its trigger', function () { // Opened from the keyboard: Safari does not focus a button on click, so after a click there // is nothing for the dialog to hand focus back to. $page = containment(); $page->script("[...document.querySelectorAll('#containment button')].find((b) => b.textContent.trim() === 'Basic dialog').focus()"); $page->keys(':focus', 'Enter') ->assertScript("document.querySelector('#containment dialog').open"); $page->keys('#containment dialog[open] button:has-text("Cancel")', 'Escape') ->assertScript("! document.querySelector('#containment dialog').open") ->assertScript("document.activeElement.textContent.trim() === 'Basic dialog'"); }); it('slides a side sheet in, traps the page, and closes on the scrim', function () { $sheet = "document.querySelector('#containment aside[role=\"dialog\"]')"; $page = containment() ->click('#containment button:has-text("Side sheet")') ->assertScript("getComputedStyle({$sheet}).display !== 'none'") ->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') !== null"); // This sheet's own scrim: the standard side sheet above it on the page is a `[data-md-drawer]` too. $page->script("{$sheet}.parentElement.querySelector(':scope > [aria-hidden=\"true\"]').click()"); $page->assertScript("getComputedStyle({$sheet}).display === 'none'") ->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') === null"); }); it('dismisses a bottom sheet dragged down past a quarter of its height', function () { $sheet = "document.querySelector('#containment section[role=\"dialog\"]')"; $page = containment() ->click('#containment button:has(> span:text-is("Bottom sheet"))') ->assertScript("getComputedStyle({$sheet}).display !== 'none'") ->wait(0.5); $page->script(<< { const handle = {$sheet}.querySelector('[data-md-bottom-sheet-handle]'); const box = handle.getBoundingClientRect(); const x = box.x + box.width / 2, y = box.y + box.height / 2; handle.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: x, clientY: y, button: 0 })); window.dispatchEvent(new PointerEvent('pointermove', { clientX: x, clientY: y + 400 })); window.dispatchEvent(new PointerEvent('pointerup', { clientX: x, clientY: y + 400 })); })() JS); $page->assertScript("getComputedStyle({$sheet}).display === 'none'"); }); class NestedDialogProbe extends Component { public bool $outer = false; public bool $inner = false; public function render(): string { return <<<'BLADE'

Outer body, long enough to scroll on its own: line one, line two, line three, line four, line five, line six, line seven, line eight, line nine, line ten, line eleven, line twelve.

Inner body, long enough to scroll on its own, independent of the outer dialog it opened from: line one, line two, line three, line four, line five, line six, line seven, line eight, line nine, line ten, line eleven, line twelve.

BLADE; } } function nestedDialogProbe() { Livewire::component('nested-dialog-probe', NestedDialogProbe::class); Route::middleware('web')->get('/nested-dialog-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); return visit('/nested-dialog-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } it('marks a scrolling dialog\'s divider at the edge with more content, and a nested dialog keeps its own marks', function () { $page = nestedDialogProbe(); $page->click('button:has-text("Open outer")'); $outer = "[...document.querySelectorAll('dialog[open]')].find((d) => d.textContent.includes('Outer'))"; $outerHead = "{$outer}.querySelector('[data-md-modal-head]')"; $outerBody = "{$outer}.querySelector('[data-md-modal-body]')"; // Scrolled to the top: nothing hidden above, so the head's rule stays off. $page->assertScript("getComputedStyle({$outerHead}, '::after').opacity === '0'"); $page->click('button:has-text("Open inner")'); $inner = "[...document.querySelectorAll('dialog[open]')].find((d) => d.textContent.includes('Inner'))"; $innerHead = "{$inner}.querySelector('[data-md-modal-head]')"; $innerBody = "{$inner}.querySelector('[data-md-modal-body]')"; $page->assertScript("getComputedStyle({$innerHead}, '::after').opacity === '0'"); $page->script("{$innerBody}.scrollTop = {$innerBody}.scrollHeight"); // The inner dialog's own body scrolled to its end marks its own head; the outer dialog, whose // body was never touched, keeps its own mark off — each dialog watches only its own scroll. $page->assertScript("getComputedStyle({$innerHead}, '::after').opacity === '1'") ->assertScript("getComputedStyle({$outerHead}, '::after').opacity === '0'"); }); it('shows a full-screen dialog\'s 56px phone bar below 600px, and the basic dialog above it', function () { $page = containment()->resize(400, 800); $page->click('#containment button:has-text("Full screen on a phone")'); $dialog = "document.querySelector('#containment dialog[open]')"; $bar = "{$dialog}.querySelector('[data-md-modal-bar]')"; $page->assertScript("getComputedStyle({$bar}).display !== 'none'") ->assertScript("Math.round({$bar}.getBoundingClientRect().height) === 56"); $page->resize(900, 800) ->assertScript("getComputedStyle({$bar}).display === 'none'"); }); it('draws separator dividers whatever the scroll, on a dialog that fits without scrolling', function () { $page = containment(); $page->click('#containment button:has-text("Always divided (separator)")'); $dialog = "document.querySelector('#containment dialog[open]')"; $head = "{$dialog}.querySelector('[data-md-modal-head]')"; $actions = "{$dialog}.querySelector('[data-md-modal-actions]')"; $body = "{$dialog}.querySelector('[data-md-modal-body]')"; // The body fits without scrolling, so an unmarked dialog would show neither rule. $page->assertScript("{$body}.scrollHeight <= {$body}.clientHeight + 1") ->assertScript("getComputedStyle({$head}, '::after').opacity === '1'") ->assertScript("getComputedStyle({$actions}, '::before').opacity === '1'"); }); it('opens a dialog with no fade under reduced motion', function () { $page = visit('/material/containment', ['reducedMotion' => 'reduce'])->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); $page->click('#containment button:has-text("Basic dialog")'); $dialog = "document.querySelector('#containment dialog[open]')"; // Reduced motion takes the spring's duration to zero, so the dialog is at its final opacity // at once rather than mid-fade. $page->assertScript("getComputedStyle({$dialog}).opacity === '1'"); }); class TextOnlyDialogProbe extends Component { public bool $open = false; public function render(): string { return <<<'BLADE'

Line one of a body with nothing else focusable inside it, so the dialog hands its first focus to the scrolling body itself: line two, line three, line four, line five, line six, line seven, line eight, line nine, line ten, line eleven, line twelve, line thirteen, line fourteen, line fifteen.

BLADE; } } function textOnlyDialogProbe() { Livewire::component('text-only-dialog-probe', TextOnlyDialogProbe::class); Route::middleware('web')->get('/text-only-dialog-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); return visit('/text-only-dialog-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } it('gives the scrolling body an inset focus ring when a text-only dialog opens from the keyboard', function () { $page = textOnlyDialogProbe(); $page->script("document.querySelector('button').focus()"); $page->keys(':focus', 'Enter'); $body = "document.querySelector('dialog[open] [data-md-modal-body]')"; $page->assertScript("document.activeElement.matches('[data-md-modal-body]')") ->assertScript("getComputedStyle({$body}).outlineStyle === 'solid'") ->assertScript("getComputedStyle({$body}).outlineWidth === '3px'") ->assertScript("getComputedStyle({$body}).outlineOffset === '-3px'"); }); it('cycles a bottom sheet\'s preset heights from its handle, announcing each, and closes from the last', function () { $page = containment(); $page->click('#containment button:has-text("Bottom sheet with preset heights")'); $sheet = "document.querySelector('#containment [data-md-bottom-sheet-panel][data-md-preset]')"; $handle = "{$sheet}.querySelector('[data-md-bottom-sheet-grip]')"; $announce = "{$sheet}.querySelector('[data-md-bottom-sheet-announce]')"; $page->assertScript("getComputedStyle({$sheet}).display !== 'none'") ->assertScript("{$sheet}.style.getPropertyValue('--sheet-stop').trim() === '50dvh'"); $page->click($handle); $page->assertScript("{$sheet}.style.getPropertyValue('--sheet-stop').trim() === '90dvh'") ->assertScript("{$announce}.textContent.trim() === 'Height 3 of 3'"); // From the last stop, activating the handle closes the sheet, as a handle with no stops does. $page->click($handle); $page->assertScript("getComputedStyle({$sheet}).display === 'none'"); }); it('settles a dragged bottom sheet on the nearest preset height', function () { $page = containment(); $page->click('#containment button:has-text("Bottom sheet with preset heights")'); $sheet = "document.querySelector('#containment [data-md-bottom-sheet-panel][data-md-preset]')"; $page->assertScript("getComputedStyle({$sheet}).display !== 'none'") ->wait(0.5); // Drag up from the 50dvh stop a little way — not as far as 90dvh — and release: it settles // back on 50dvh, the nearest stop, rather than creep on to the next one. $page->script(<< { const handle = {$sheet}.querySelector('[data-md-bottom-sheet-handle]'); const box = handle.getBoundingClientRect(); const x = box.x + box.width / 2, y = box.y + box.height / 2; handle.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: x, clientY: y, button: 0 })); window.dispatchEvent(new PointerEvent('pointermove', { clientX: x, clientY: y - 40 })); window.dispatchEvent(new PointerEvent('pointerup', { clientX: x, clientY: y - 40 })); })() JS); $page->assertScript("{$sheet}.style.getPropertyValue('--sheet-stop').trim() === '50dvh'"); }); it('is a standard side sheet from 840px, without a scrim or a focus trap, and the modal one below', function () { $page = containment()->resize(1000, 800); $sheet = "document.querySelector('#containment aside[data-md-side]')"; $scrim = "{$sheet}.parentElement.querySelector(':scope > [data-md-drawer-scrim]')"; $toggle = "document.querySelector('#containment button:has-text(\"Show or hide the filters\")')"; // From 840px it opens co-planar (the example starts `open: true`): no scrim shown, and the // rest of the page stays interactive. $page->assertScript("getComputedStyle({$sheet}).position === 'relative'") ->assertScript("getComputedStyle({$scrim}).display === 'none'") ->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') === null") ->assertScript("! {$sheet}.hasAttribute('inert')"); $page->script("{$toggle}.click()"); $page->assertScript("getComputedStyle({$sheet}).opacity === '0'"); // Below 840px the same sheet becomes the modal one: fixed, with a scrim, and the page inert. $page->resize(700, 800) ->script("{$toggle}.click()"); $page->assertScript("getComputedStyle({$sheet}).position === 'fixed'") ->assertScript("getComputedStyle({$scrim}).display !== 'none'") ->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') !== null"); }); it('opens a row\'s opener from a press anywhere on the row, but not from its own buttons', function () { $page = containment(); $row = '#containment [data-md-card][data-md-list-row]:not([data-md-list-actionable])'; $page->script("window.__opened = 0; document.querySelector('{$row} [data-md-list-open]').addEventListener('click', (event) => { event.preventDefault(); window.__opened++ })"); $page->click("{$row} p") ->assertScript('window.__opened === 1'); $page->click("{$row} button:has-text(\"Copy link\")") ->assertScript('window.__opened === 1'); }); it('makes a directly actionable card the one tab stop, answering Enter', function () { $card = '#containment [data-md-card][data-md-list-actionable]'; $page = containment(); $page->script("window.__opened = 0; document.querySelector('{$card} [data-md-list-open]').addEventListener('click', (event) => { event.preventDefault(); window.__opened++ })"); $page->script("document.querySelector('{$card}').focus()"); $page->assertScript("document.activeElement.matches('{$card}')"); $page->keys($card, 'Enter') ->assertScript('window.__opened === 1'); }); it('binds a collapse to a Livewire property both ways', function () { $collapse = "document.querySelector('#fine-tuning-collapse')"; $page = collapseProbe() ->assertScript("{$collapse}.open === false"); $page->click('#fine-tuning-collapse summary') ->assertScript("{$collapse}.open === true") ->click('button:has-text("Re-render")') ->assertSeeIn('#renders', '1') ->assertSeeIn('#fine-tuning', 'true') ->assertScript("{$collapse}.open === true"); $page->click('button:has-text("Close from the server")') ->assertSeeIn('#fine-tuning', 'false') ->assertScript("{$collapse}.open === false"); $page->click('button:has-text("Open from the server")') ->assertSeeIn('#fine-tuning', 'true') ->assertScript("{$collapse}.open === true"); }); it('binds a collapse to an Alpine property both ways', function () { $collapse = "document.querySelector('#advanced-collapse')"; $page = collapseProbe() ->assertScript("{$collapse}.open === false"); $page->click('#advanced-collapse summary') ->assertScript("{$collapse}.open === true") ->assertSeeIn('#advanced', 'true'); $page->click('button:has-text("Close from Alpine")') ->assertSeeIn('#advanced', 'false') ->assertScript("{$collapse}.open === false"); });