Add cards, lists, dialogs, sheets and the rest of M3's containment
tests / lint (push) Failing after 2m6s
tests / feature (8.4) (push) Successful in 1m3s
tests / feature (8.5) (push) Successful in 1m7s
tests / browser (safari, webkit) (push) Successful in 3m14s
tests / browser (chrome, chromium) (push) Successful in 2m9s
tests / browser (firefox, firefox) (push) Successful in 2m29s
tests / lint (push) Failing after 2m6s
tests / feature (8.4) (push) Successful in 1m3s
tests / feature (8.5) (push) Successful in 1m7s
tests / browser (safari, webkit) (push) Successful in 3m14s
tests / browser (chrome, chromium) (push) Successful in 2m9s
tests / browser (firefox, firefox) (push) Successful in 2m29s
<x-card> (filled, elevated, outlined), <x-list> and <x-list-item> (plain or M3 Expressive's segmented list), <x-divider>, <x-collapse> on <details>, <x-modal> on the native <dialog>, <x-drawer> as a side sheet or list-detail pane, and <x-bottom-sheet> with drag to dismiss. Dialogs and sheets bind to a Livewire flag or id and write back false or null on close, or use the surrounding Alpine scope. Rows open from anywhere on them through data-list-row and data-list-open. DesignGuard now also reports Blade directives written inside a component tag, where they do not compile. Browser test helpers wait for a complete document with Alpine and Livewire running: in Firefox, networkidle alone could return before a repeated visit had loaded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
co-authored by
Claude Opus 5
parent
e57063b0f4
commit
fef20a9178
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
it('draws M3\'s three cards', function (string $variant, string $classes) {
|
||||
expect((string) $this->blade("<x-card variant=\"{$variant}\">Body</x-card>"))->toContain($classes)->toContain('rounded-corner-md')->toContain('data-card');
|
||||
})->with([
|
||||
'filled' => ['filled', 'bg-surface-container-highest'],
|
||||
'elevated' => ['elevated', 'bg-surface-container-low shadow-elevation-1'],
|
||||
'outlined' => ['outlined', 'border border-outline-variant bg-surface'],
|
||||
]);
|
||||
|
||||
it('is filled unless it knows the variant', function () {
|
||||
expect((string) $this->blade('<x-card variant="glass">Body</x-card>'))->toContain('bg-surface-container-highest');
|
||||
});
|
||||
|
||||
it('lays out a header, menu, media, body and actions', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-card title="holiday-photos.zip" subtitle="248 MB" separator>
|
||||
<x-slot:figure><img src="/x.png" alt=""></x-slot:figure>
|
||||
<x-slot:menu><button>⋮</button></x-slot:menu>
|
||||
Expires in 3 days.
|
||||
<x-slot:actions><button>Copy link</button></x-slot:actions>
|
||||
</x-card>
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toContain('<img src="/x.png" alt="">')
|
||||
->toContain('<h3 class="type-title-md">holiday-photos.zip</h3>')
|
||||
->toContain('248 MB')
|
||||
->toContain('<button>⋮</button>')
|
||||
->toContain('role="separator"')
|
||||
->toContain('Expires in 3 days.')
|
||||
->toContain('<button>Copy link</button>')
|
||||
->and(strpos($html, 'Expires'))->toBeLessThan(strpos($html, 'Copy link'));
|
||||
});
|
||||
|
||||
it('passes row attributes through', function () {
|
||||
expect((string) $this->blade('<x-card data-list-row><a href="/s/1" data-list-open>Open</a></x-card>'))
|
||||
->toContain('data-list-row')
|
||||
->toContain('data-list-open');
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
it('discloses on the native details element, kept open through a morph', function () {
|
||||
$html = (string) $this->blade('<x-collapse title="How long do links last?" icon="schedule" open variant="filled">Until the expiry.</x-collapse>');
|
||||
|
||||
expect($html)
|
||||
->toContain('<details')
|
||||
->toContain('wire:ignore.self')
|
||||
->toContain(' open ')
|
||||
->toContain('rounded-corner-lg bg-surface-container')
|
||||
->toContain('How long do links last?')
|
||||
->toContain('Until the expiry.')
|
||||
->toContain('group-open/collapse:rotate-180');
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
it('separates horizontally or vertically, inset on request', function () {
|
||||
expect((string) $this->blade('<x-divider />'))->toContain('role="separator"')->toContain('aria-orientation="horizontal"')->toContain('h-px')->toContain('bg-outline-variant')
|
||||
->and((string) $this->blade('<x-divider vertical />'))->toContain('aria-orientation="vertical"')->toContain('w-px')
|
||||
->and((string) $this->blade('<x-divider inset />'))->toContain('ms-4')
|
||||
->and((string) $this->blade('<x-divider decorative />'))->toContain('aria-hidden="true"')->not->toContain('role="separator"');
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
it('draws a plain list, with dividers on request', function () {
|
||||
expect((string) $this->blade('<x-list label="Files" dividers><x-list-item title="a.zip" /></x-list>'))
|
||||
->toContain('role="list"')
|
||||
->toContain('aria-label="Files"')
|
||||
->toContain('data-list="plain"')
|
||||
->toContain('divide-y divide-outline-variant')
|
||||
->toContain('role="listitem"');
|
||||
});
|
||||
|
||||
it('draws M3 Expressive\'s segmented list', function () {
|
||||
expect((string) $this->blade('<x-list segmented><x-list-item title="a.zip" /></x-list>'))
|
||||
->toContain('data-list="segmented"')
|
||||
->toContain('gap-0.5');
|
||||
});
|
||||
|
||||
it('grows an item from one to three lines', function () {
|
||||
expect((string) $this->blade('<x-list-item title="a.zip" />'))->toContain('min-h-14')
|
||||
->and((string) $this->blade('<x-list-item title="a.zip" description="248 MB" />'))->toContain('min-h-18')->toContain('248 MB')
|
||||
->and((string) $this->blade('<x-list-item title="a.zip" overline="Protected" description="248 MB" />'))->toContain('min-h-22')->toContain('Protected');
|
||||
});
|
||||
|
||||
it('leads with an icon, an avatar or an image, and trails with text or an icon', function () {
|
||||
expect((string) $this->blade('<x-list-item title="a" icon="folder_zip" trailing="3 days" icon-right="chevron_right" />'))
|
||||
->toContain('3 days')
|
||||
->and(substr_count((string) $this->blade('<x-list-item title="a" icon="folder_zip" icon-right="chevron_right" />'), '<svg'))->toBe(2)
|
||||
->and((string) $this->blade('<x-list-item title="Anna" avatar="AM" />'))->toContain('bg-primary-container')->toContain('>AM</span>')
|
||||
->and((string) $this->blade('<x-list-item title="Anna" avatar="/anna.jpg" />'))->toContain('<img src="/anna.jpg"')
|
||||
->and((string) $this->blade('<x-list-item title="Photo" image="/p.jpg" />'))->toContain('size-14');
|
||||
});
|
||||
|
||||
it('makes a linked item a row that opens from anywhere', function () {
|
||||
expect((string) $this->blade('<x-list-item title="Settings" link="/settings" />'))
|
||||
->toContain('data-list-row')
|
||||
->toContain('href="/settings"')
|
||||
->toContain('data-list-open')
|
||||
->toContain('wire:navigate');
|
||||
});
|
||||
|
||||
it('marks a selected item and takes controls in its slots', function () {
|
||||
expect((string) $this->blade('<x-list-item title="Anna" :selected="true"><x-slot:leading><input type="checkbox"></x-slot:leading><x-slot:end><button>⋮</button></x-slot:end></x-list-item>'))
|
||||
->toContain('data-selected')
|
||||
->toContain('<input type="checkbox">')
|
||||
->toContain('<button>⋮</button>');
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\Livewire;
|
||||
|
||||
it('opens a native dialog entangled with a Livewire property, out of the morph\'s reach', function () {
|
||||
$component = new class extends Component
|
||||
{
|
||||
public bool $confirming = false;
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return <<<'BLADE'
|
||||
<div>
|
||||
<x-modal wire:model="confirming" title="Delete this share?" subtitle="This cannot be undone." icon="delete">
|
||||
<x-slot:actions><button>Delete</button></x-slot:actions>
|
||||
</x-modal>
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
};
|
||||
|
||||
Livewire::test($component)
|
||||
->assertSeeHtml('<dialog')
|
||||
->assertSeeHtml('wire:ignore.self')
|
||||
->assertSeeHtml('open: window.Livewire.find(')
|
||||
->assertSeeHtml("entangle('confirming').live")
|
||||
->assertSeeHtml('close() { this.open = typeof this.open === \'boolean\' ? false : null }')
|
||||
->assertSeeHtml('rounded-corner-xl bg-surface-container-high')
|
||||
->assertSeeHtml('type-headline-sm')
|
||||
->assertSee('This cannot be undone.')
|
||||
->assertSeeHtml('text-center')
|
||||
->assertDontSeeHtml('wire:model="confirming"');
|
||||
});
|
||||
|
||||
it('uses the surrounding Alpine scope without wire:model, and stays open when persistent', function () {
|
||||
$html = (string) $this->blade('<x-modal title="Help" persistent fullscreen>Text</x-modal>');
|
||||
|
||||
expect($html)
|
||||
->toContain('x-data="{ close() { this.open = false } }"')
|
||||
->toContain('x-on:cancel.prevent=""')
|
||||
->not->toContain('x-on:click.self')
|
||||
->toContain('max-sm:h-dvh')
|
||||
->toContain('aria-label="Close"');
|
||||
});
|
||||
|
||||
it('slides a side sheet in from either edge, and is a pane from xl when asked', function () {
|
||||
expect((string) $this->blade('<x-drawer title="Details" with-close-button>Body</x-drawer>'))
|
||||
->toContain('x-trap.inert.noscroll="open && ! wide"')
|
||||
->toContain('end-0 sm:rounded-s-corner-lg')
|
||||
->toContain('bg-surface-container-low')
|
||||
->toContain('role="dialog"')
|
||||
->toContain('aria-label="Close"')
|
||||
->and((string) $this->blade('<x-drawer side="start">Body</x-drawer>'))->toContain('start-0 sm:rounded-e-corner-lg')
|
||||
->and((string) $this->blade('<x-drawer pane pane-width="28rem">Body</x-drawer>'))
|
||||
->toContain('data-pane')
|
||||
->toContain('--pane-width: 28rem')
|
||||
->toContain("matchMedia('(min-width: 80rem)')");
|
||||
});
|
||||
|
||||
it('draws a modal bottom sheet with a drag handle, or a standard one without a scrim', function () {
|
||||
expect((string) $this->blade('<x-bottom-sheet title="Share via">Body</x-bottom-sheet>'))
|
||||
->toContain('...materialBottomSheet(false)')
|
||||
->toContain('bg-scrim/32')
|
||||
->toContain('x-trap.inert.noscroll="open"')
|
||||
->toContain('rounded-t-corner-xl bg-surface-container-low')
|
||||
->toContain('data-drag-handle')
|
||||
->toContain('aria-modal="true"')
|
||||
->and((string) $this->blade('<x-bottom-sheet standard>Body</x-bottom-sheet>'))
|
||||
->toContain('...materialBottomSheet(true)')
|
||||
->not->toContain('bg-scrim/32')
|
||||
->not->toContain('aria-modal');
|
||||
});
|
||||
Reference in New Issue
Block a user