BLADE; } }; Livewire::test($component) ->assertSeeHtml('assertSeeHtml('data-md-modal') ->assertSeeHtml('wire:ignore.self') ->assertSeeHtml('open: window.Livewire.find(') ->assertSeeHtml("entangle('confirming').live") ->assertSeeHtml('close() { this.open = typeof this.open === \'boolean\' ? false : null }') ->assertSeeHtml('data-md-modal-box') ->assertSeeHtml('data-md-modal-title') ->assertSee('This cannot be undone.') ->assertSeeHtml('data-md-icon') ->assertDontSeeHtml('wire:model="confirming"'); }); it('uses the surrounding Alpine scope without wire:model, and stays open when persistent', function () { $html = (string) $this->blade('Text'); expect($html) ->toContain('x-data="{ close() { this.open = false } }"') ->toContain('x-on:cancel.prevent=""') ->not->toContain('x-on:click.self') ->toContain('data-md-fullscreen') ->toContain('aria-label="Close"'); }); it('keeps a full-screen dialog\'s subtitle on a phone, where its bar carries the title, in the CSS', function () { $html = (string) $this->blade('Text'); expect($html) ->toMatch('/

Enable 2FA<\/h2>/') ->toMatch('/

Scan the code<\/p>/') ->and((string) $this->blade('Text')) ->toMatch('/

Only a subtitle<\/p>/') ->not->toContain('data-md-modal-title'); // The markup is the same at every width; modal.css (ContainmentStylesheetsTest) decides which // of the bar/head/title/subtitle show below 600px, keyed on the icon and the subtitle. expect(File::get(__DIR__.'/../../../resources/css/components/modal.css')) ->toContain('[data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon], > [data-md-modal-subtitle]))') ->toContain('[data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon])) > [data-md-modal-title]') ->toContain('[data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon])) > [data-md-modal-subtitle]'); // Each through its own box, so a basic dialog opened inside a full-screen one's body keeps its // title and padding on a phone. expect(File::get(__DIR__.'/../../../resources/css/components/modal.css'))->not->toMatch('/\[data-md-fullscreen\](?::[^ ]+)? \[/'); }); it('pins a dialog\'s headline and actions and scrolls only the body between them', function () { $html = (string) $this->blade('Body'); expect($html) ->toContain('data-md-modal-box') ->toMatch('//') ->toMatch('/

\s*
Body<\/div>\s*<\/div>/') ->toMatch('//'); }); it('divides a scrolling body from its header and actions only while content is hidden past them', function () { $html = (string) $this->blade('Body'); // The script marks the dialog, which a morph leaves alone, and nothing is marked before it runs. expect($html) ->toContain('x-dialog-dividers') ->not->toContain('data-md-overflow-top') ->not->toContain('data-md-overflow-bottom') ->not->toContain('data-md-separator') ->not->toContain('role="separator"') ->and(substr_count($html, 'data-md-modal-head'))->toBe(1) ->and(substr_count($html, 'data-md-modal-actions'))->toBe(1); expect(File::get(__DIR__.'/../../../resources/css/components/modal.css')) ->toContain('dialog[data-md-overflow-top] > [data-md-modal-box] > [data-md-modal-head][data-md-modal-divider]::after') ->toContain('dialog[data-md-overflow-bottom] > [data-md-modal-box] > [data-md-modal-actions][data-md-modal-divider]::before') ->toContain('background-color: var(--md-sys-color-outline-variant);') ->toContain('height: 1px;') ->toContain('position: absolute;') // A token that reduced motion sets to zero, so there the rule simply appears. ->toContain('transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);') ->and(File::get(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/modal.css';") ->and(File::get(__DIR__.'/../../../resources/css/tailwind.css'))->not->toContain('dialog.css') ->and(File::get(__DIR__.'/../../../resources/js/material.js'))->toContain("import './dialog.js'") ->and(File::get(__DIR__.'/../../../resources/js/dialog.js')) ->toContain("directive('dialog-dividers'") ->toContain('new ResizeObserver(measure)') ->toContain('[data-md-modal-content]'); }); it('draws the dividers whatever the scroll with separator, and only where a body has neighbours', function () { expect((string) $this->blade('Body')) ->toMatch('//') ->toMatch('//') ->and(File::get(__DIR__.'/../../../resources/css/components/modal.css')) ->toContain('[data-md-modal-head][data-md-modal-divider][data-md-separator]::after') ->toContain('[data-md-modal-actions][data-md-modal-divider][data-md-separator]::before'); // Without a body there is nothing to divide, and the header carries no divider hook. expect((string) $this->blade('')) ->toMatch('//') ->not->toContain('data-md-modal-divider') ->not->toContain('data-md-separator') ->not->toContain('x-dialog-dividers'); // No header: only the actions carry the divider hook; no actions: nothing does. expect((string) $this->blade('Body')) ->not->toContain('data-md-modal-head') ->toMatch('//') ->and((string) $this->blade('Body')) ->toMatch('//') ->not->toContain('data-md-modal-actions'); }); it('puts a full-screen dialog\'s top divider under its bar on a phone, unless the header stays there', function () { $titled = (string) $this->blade('Body'); // Both carry the divider hook here (no icon, no subtitle): the bar shows only below medium, // and this header only from it (modal.css). expect($titled) ->toMatch('//') ->toMatch('//') ->and(preg_match_all('/]/', $titled))->toBe(1) ->and(preg_match_all('/]/', $titled))->toBe(1); // A subtitle keeps the header on a phone, so the rule stays under it rather than under the bar. $subtitled = (string) $this->blade('Body'); expect($subtitled) ->toMatch('//') ->not->toContain('data-md-modal-bar data-md-modal-divider') ->toMatch('//') ->and((string) $this->blade('Body')) ->toMatch('//'); }); it('is an alert dialog when it interrupts, and describes itself by its subtitle', function () { $alert = (string) $this->blade('This cannot be undone.'); preg_match('/id="(material-dialog-[a-z0-9]+)"/', $alert, $id); expect($alert) ->toContain('role="alertdialog"') ->toContain("aria-describedby=\"{$id[1]}-subtitle {$id[1]}-body\"") ->and((string) $this->blade('Body')) ->not->toContain('role="alertdialog"') ->toMatch('/aria-describedby="material-dialog-[a-z0-9]+-subtitle"/') ->and((string) $this->blade('Body')) ->not->toContain('aria-describedby'); }); it('gives a full-screen dialog M3\'s 56px header and action bar', function () { expect((string) $this->blade('Body')) ->toContain('data-md-modal-bar') ->toContain('data-md-fullscreen') ->and(File::get(__DIR__.'/../../../resources/css/components/modal.css')) ->toContain('height: var(--md-sys-measurement-space700);') ->toContain('min-height: var(--md-sys-measurement-space700);'); }); it('always offers a way out of a side sheet, as M3 requires', function () { expect((string) $this->blade('Body'))->toContain('aria-label="Close"') ->and((string) $this->blade('Body'))->not->toContain('aria-label="Close"') ->and((string) $this->blade('Body'))->toContain('aria-label="Close"') ->and((string) $this->blade('Body'))->toContain('aria-label="Close"') ->and((string) $this->blade('Body'))->toContain('aria-label="Close"'); }); it('puts a side sheet\'s actions on the left, in M3\'s 72dp row', function () { expect((string) $this->blade('Body')) ->toMatch('//'); }); it('slides a side sheet in from either edge', function () { expect((string) $this->blade('Body')) ->toContain('x-trap.inert.noscroll="open && ! wide"') ->toContain('data-md-side="end"') ->toContain('data-md-drawer-sheet') ->toContain('role="dialog"') ->toContain('aria-label="Close"') ->and((string) $this->blade('Body'))->toContain('data-md-side="start"'); }); it('draws a modal bottom sheet with a drag handle, or a standard one without a scrim', function () { expect((string) $this->blade('Body')) ->toContain('...materialBottomSheet(false, JSON.parse(') ->toContain('data-md-bottom-sheet-scrim') ->toContain('x-trap.inert.noscroll="open"') ->toContain('data-md-bottom-sheet-panel') ->toContain('data-md-bottom-sheet-handle') ->toContain('data-md-bottom-sheet-grip') ->toContain('aria-modal="true"') ->toContain('md-focus-ring md-touch-target') ->toContain('--sheet-max-height: min(50dvh, calc(100dvh - 72px))') ->and((string) $this->blade('Body')) ->toContain('...materialBottomSheet(true, JSON.parse(') ->not->toContain('data-md-bottom-sheet-scrim') ->not->toContain('aria-modal') ->toContain('data-md-standard') ->and((string) $this->blade('Body')) ->toContain('--sheet-max-height: min(90dvh, calc(100dvh - 72px))'); }); it('is M3\'s standard side sheet from expanded, and the modal one below', function () { $html = (string) $this->blade('Body'); expect($html) ->toContain('data-md-standard') ->toContain('--sheet-width: min(400px, 400px)') ->toContain("matchMedia('(width >= 840px)')") ->toContain('x-trap.inert.noscroll="open && ! wide"') ->toContain('aria-label="Close"') ->and((string) $this->blade('Body')) ->toContain('data-md-side="start"') ->toContain('--sheet-width: min(30rem, 400px)') ->and((string) $this->blade('Body')) ->not->toContain('data-md-standard'); $css = ComponentStylesheet::read('drawer'); $expanded = ['@media (width >= 840px)']; expect($css->has('[data-md-drawer][data-md-standard]', $expanded))->toBeTrue() ->and($css->declarations('[data-md-drawer][data-md-standard][data-md-drawer-collapsed]', $expanded))->toBe(['display' => 'none']) // The old view's `expanded:hidden` on a standard sheet's scrim. ->and($css->declarations('[data-md-drawer][data-md-standard] > [data-md-drawer-scrim]', $expanded))->toBe(['display' => 'none']) ->and($css->declarations('[data-md-drawer][data-md-standard] > [data-md-drawer-sheet]', $expanded))->toHaveKey('box-shadow', 'none') ->and($css->has("[data-md-drawer][data-md-standard] > [data-md-drawer-sheet][data-md-side='end']", $expanded))->toBeTrue() ->and($css->has("[data-md-drawer][data-md-standard] > [data-md-drawer-sheet][data-md-side='start']", $expanded))->toBeTrue(); }); it('slides a side sheet out as it slid in, from its own edge in either direction, and fades its scrim both ways', function () { $css = ComponentStylesheet::read('drawer'); expect($css->declarations('[data-md-drawer-sheet]')) ->toHaveKey('translate', 'var(--sheet-offset) 0') ->toHaveKey('transition-behavior', 'allow-discrete') ->not->toHaveKey('opacity') ->and($css->declarations("[data-md-drawer-sheet][data-md-side='end']:dir(rtl)"))->toBe(['--sheet-offset' => '-100%']) ->and($css->declarations("[data-md-drawer-sheet][data-md-side='start']:dir(rtl)"))->toBe(['--sheet-offset' => '100%']) ->and($css->declarations('[data-md-drawer][data-md-open] > [data-md-drawer-sheet]', ['@starting-style']))->toBe(['translate' => 'var(--sheet-offset) 0']) ->and($css->declarations('[data-md-drawer-scrim]'))->toHaveKey('opacity', '0') ->and($css->declarations('[data-md-drawer][data-md-open] > [data-md-drawer-scrim]'))->toBe(['opacity' => '1']); }); it('draws the drawer from a stylesheet imported from the containment block, with the pane prop gone', function () { $view = File::get(__DIR__.'/../../../resources/views/components/drawer.blade.php'); expect($view) ->not->toContain("'pane'") ->not->toContain('paneWidth') ->not->toContain('paneCloseOnEscape') ->not->toContain('pane-width') ->not->toContain('pane-close-on-escape') ->not->toContain('data-pane') ->and(File::get(__DIR__.'/../../../resources/css/components.css')) ->toContain("@import './components/drawer.css';"); }); it('gives a bottom sheet M3\'s preset heights, cycled from the drag handle', function () { $html = (string) $this->blade('Body'); expect($html) ->toContain('--sheet-stop: 50dvh; --sheet-max-height: min(var(--sheet-stop), calc(100dvh - 72px))') ->toContain('data-md-preset') ->toContain('x-ref="probe"') ->toContain('data-md-bottom-sheet-probe') ->toContain('aria-label="Change the sheet height"') ->toContain('x-bind:aria-label="handleLabel"') ->toContain('x-on:click="activate()"') ->toMatch('/<\/span>/') ->toContain('25dvh') ->toContain('90dvh') ->toContain('Height 2 of 3') ->and((string) $this->blade('Body')) ->toContain('--sheet-stop: 60dvh;') ->toContain('30dvh') ->and((string) $this->blade('Body')) ->toContain('--sheet-stop: 50dvh;') ->and((string) $this->blade('Body')) ->toContain('--sheet-max-height: min(50dvh, calc(100dvh - 72px))') ->not->toContain('--sheet-stop') ->not->toContain('data-md-preset') ->toContain('aria-label="Close"'); }); it('draws the bottom sheet from a stylesheet imported from the containment block and by the menu', function () { expect(File::get(__DIR__.'/../../../resources/css/components.css')) ->toContain("@import './components/bottom-sheet.css';") ->and(File::get(__DIR__.'/../../../resources/css/components/menu.css')) ->toContain("@import './bottom-sheet.css';") ->and(File::get(__DIR__.'/../../../resources/js/bottom-sheet.js')) ->toContain('data-md-bottom-sheet-handle') ->not->toContain('data-drag-handle'); });