Plan step 36 (containment group): <x-bottom-sheet>'s class lists move into resources/css/components/bottom-sheet.css, keyed on data-md-bottom-sheet (data-md-open, data-md-standard) and data-md-bottom-sheet-scrim/-probe/-panel (data-md-preset while heights/snap gives it stops)/-handle/-grip/-announce/-body/-title/ -actions. x-transition's enter/leave classes are replaced the same way as drawer.css: @starting-style + transition-behavior: allow-discrete on the x-show-driven panel, the asymmetric open (spatial-default, emphasized-decelerate) vs close (effects-default, emphasized- accelerate) timing coming from the more specific open-state selector's own transition-duration/-timing-function, which a CSS transition always takes from the after-change style. data-md-preset adds the height-between-stops transition (the spatial spring) alongside it, independent of the open/close slide. The drag handle (C-01, already fixed in the Tailwind-era markup this replaces) renders the shared md-focus-ring and md-touch-target classes instead of a hand-rolled ring: unlike the datepicker's day, its own visible bar is not a smaller indicator drawn inside a bigger box those classes cannot reach, so nothing here refines them further. Its 22px vertical padding (SheetDefaults.kt's DragHandleVerticalPadding) is a literal, matching alert.css's 12px for the same reason: 22 is not on the measurement scale. Hooks renamed data-drag-handle -> data-md-bottom-sheet-handle, updated in resources/js/bottom-sheet.js and tests/Browser/ContainmentTest.php; data-sheet -> data-md-drawer in that test's comment. <x-menu sheet-at-compact> renders this component unchanged; menu.css now imports bottom-sheet.css, and tests/Feature/Components/MenuTest.php's stale Tailwind-class assertion for the embedded sheet is replaced with a hook check. The two sheet-at-compact browser tests in ActionsTest.php stay on assertVisible(): the sheet is still x-show-driven (a deliberate choice for this batch, keeping the existing modal/side-sheet/bottom-sheet architecture and converting only their styling), so open still shows as visibility rather than :popover-open; their comments no longer say "not rewritten in this batch". Imported from the Containment block of components.css. Browser tests owed by docs/plans/material-3-browser-tests.md, added to ContainmentTest.php but not run: dialog divider marks on scroll (including that a nested dialog keeps its own marks), the full-screen phone bar, separator always, no fade under reduced motion, the scrolling body's inset focus ring; bottom sheet preset-height cycling with its announcement and a drag settling on the nearest stop; the standard side sheet from 840px without a scrim or a focus trap, modal below it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
533 lines
22 KiB
PHP
533 lines
22 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\Blade;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Livewire\Component;
|
|
use Livewire\Livewire;
|
|
|
|
class OverlayProbe extends Component
|
|
{
|
|
public bool $confirming = false;
|
|
|
|
public ?int $deletingId = null;
|
|
|
|
public int $renders = 0;
|
|
|
|
public function touch(): void
|
|
{
|
|
$this->renders++;
|
|
}
|
|
|
|
public function render(): string
|
|
{
|
|
return <<<'BLADE'
|
|
<div class="space-y-4 p-4">
|
|
<p>confirming: <span id="confirming">{{ var_export($confirming, true) }}</span></p>
|
|
<p>deleting: <span id="deleting">{{ var_export($deletingId, true) }}</span></p>
|
|
<p>renders: <span id="renders">{{ $renders }}</span></p>
|
|
|
|
<x-button label="Confirm" wire:click="$set('confirming', true)" />
|
|
<x-button label="Delete 7" wire:click="$set('deletingId', 7)" />
|
|
|
|
<x-modal wire:model="confirming" title="Are you sure?">
|
|
<x-slot:actions>
|
|
<x-button label="Re-render" wire:click="touch" />
|
|
</x-slot:actions>
|
|
</x-modal>
|
|
|
|
<x-modal wire:model="deletingId" title="Delete share?" />
|
|
</div>
|
|
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'
|
|
<div class="space-y-4 p-4">
|
|
<p>fine tuning: <span id="fine-tuning">{{ var_export($fineTuning, true) }}</span></p>
|
|
<p>renders: <span id="renders">{{ $renders }}</span></p>
|
|
|
|
<x-button label="Re-render" wire:click="touch" />
|
|
<x-button label="Open from the server" wire:click="expand" />
|
|
<x-button label="Close from the server" wire:click="collapse" />
|
|
|
|
<x-collapse id="fine-tuning-collapse" title="Fine-tuning" wire:model="fineTuning">Zones and paces.</x-collapse>
|
|
|
|
<div x-data="{ advanced: false }">
|
|
<p>advanced: <span id="advanced" x-text="advanced"></span></p>
|
|
<x-button label="Close from Alpine" x-on:click="advanced = false" />
|
|
<x-collapse id="advanced-collapse" title="Advanced" x-model="advanced">Everything else.</x-collapse>
|
|
</div>
|
|
</div>
|
|
BLADE;
|
|
}
|
|
}
|
|
|
|
function collapseProbe()
|
|
{
|
|
Livewire::component('collapse-probe', CollapseProbe::class);
|
|
|
|
Route::middleware('web')->get('/collapse-probe', fn () => Blade::render(<<<'BLADE'
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<x-theme-script />
|
|
@vite(config('livewire-material.showcase.vite'))
|
|
@livewireStyles
|
|
</head>
|
|
<body class="bg-surface">
|
|
<livewire:collapse-probe />
|
|
@livewireScripts
|
|
</body>
|
|
</html>
|
|
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'
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<x-theme-script />
|
|
@vite(config('livewire-material.showcase.vite'))
|
|
@livewireStyles
|
|
</head>
|
|
<body class="bg-surface">
|
|
<livewire:overlay-probe />
|
|
@livewireScripts
|
|
</body>
|
|
</html>
|
|
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(<<<JS
|
|
(() => {
|
|
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'
|
|
<div>
|
|
<x-button label="Open outer" wire:click="$set('outer', true)" />
|
|
|
|
<x-modal wire:model="outer" title="Outer">
|
|
<p>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.</p>
|
|
|
|
<x-button label="Open inner" wire:click="$set('inner', true)" />
|
|
|
|
<x-modal wire:model="inner" title="Inner">
|
|
<p>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.</p>
|
|
</x-modal>
|
|
</x-modal>
|
|
</div>
|
|
BLADE;
|
|
}
|
|
}
|
|
|
|
function nestedDialogProbe()
|
|
{
|
|
Livewire::component('nested-dialog-probe', NestedDialogProbe::class);
|
|
|
|
Route::middleware('web')->get('/nested-dialog-probe', fn () => Blade::render(<<<'BLADE'
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<x-theme-script />
|
|
@vite(config('livewire-material.showcase.vite'))
|
|
@livewireStyles
|
|
</head>
|
|
<body class="bg-surface">
|
|
<livewire:nested-dialog-probe />
|
|
@livewireScripts
|
|
</body>
|
|
</html>
|
|
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'
|
|
<div>
|
|
<x-button label="Open" wire:click="$set('open', true)" />
|
|
|
|
<x-modal wire:model="open" title="Read this">
|
|
<p>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.</p>
|
|
</x-modal>
|
|
</div>
|
|
BLADE;
|
|
}
|
|
}
|
|
|
|
function textOnlyDialogProbe()
|
|
{
|
|
Livewire::component('text-only-dialog-probe', TextOnlyDialogProbe::class);
|
|
|
|
Route::middleware('web')->get('/text-only-dialog-probe', fn () => Blade::render(<<<'BLADE'
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<x-theme-script />
|
|
@vite(config('livewire-material.showcase.vite'))
|
|
@livewireStyles
|
|
</head>
|
|
<body class="bg-surface">
|
|
<livewire:text-only-dialog-probe />
|
|
@livewireScripts
|
|
</body>
|
|
</html>
|
|
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(<<<JS
|
|
(() => {
|
|
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");
|
|
});
|