Add the snackbar, badges, rich tooltips, alerts, stats and empty states
tests / lint (push) Successful in 1m3s
tests / feature (8.4) (push) Failing after 1m5s
tests / feature (8.5) (push) Failing after 1m4s
tests / browser (chrome, chromium) (push) Failing after 1m5s
tests / browser (firefox, firefox) (push) Failing after 1m1s
tests / browser (safari, webkit) (push) Failing after 1m1s
tests / lint (push) Successful in 1m3s
tests / feature (8.4) (push) Failing after 1m5s
tests / feature (8.5) (push) Failing after 1m4s
tests / browser (chrome, chromium) (push) Failing after 1m5s
tests / browser (firefox, firefox) (push) Failing after 1m1s
tests / browser (safari, webkit) (push) Failing after 1m1s
<x-toast> hosts the snackbar queue for the Toasts concern and window.materialToast(), one at a time, paused on hover and focus, with an optional action; it listens from the moment its script loads, so a toast dispatched before Alpine starts is shown rather than lost. <x-badge> is M3's dot and count, plus a tonal or outlined status label; <x-rich-tooltip> is transient or persistent; <x-alert>, <x-stat> and <x-empty-state> are built from M3's parts. 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
cd64f4f371
commit
648ad8efaf
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
const SNACKBAR = "document.querySelector('[x-data=\"materialSnackbar\"] [aria-live]')";
|
||||
|
||||
it('shows a toast as a snackbar, then the next in turn', function () {
|
||||
$page = visit('/material')->waitForEvent('networkidle');
|
||||
|
||||
$page->script("materialToast('First', { type: 'success', timeout: 600 }); materialToast('Second', { type: 'error' })");
|
||||
|
||||
$page->assertScript(SNACKBAR."?.textContent.includes('First')")
|
||||
->assertScript(SNACKBAR."?.getAttribute('role') === 'status'");
|
||||
|
||||
$page->wait(1.2);
|
||||
|
||||
$page->assertScript(SNACKBAR."?.textContent.includes('Second')")
|
||||
->assertScript(SNACKBAR."?.getAttribute('role') === 'alert'");
|
||||
});
|
||||
|
||||
it('shows a toast dispatched as a browser event, as the Toasts concern does', function () {
|
||||
$page = visit('/material')->waitForEvent('networkidle')
|
||||
->assertScript("typeof window.Livewire !== 'undefined' && typeof window.Alpine !== 'undefined'");
|
||||
|
||||
// Through Livewire, in the page's own realm: an event built inside Playwright's evaluate
|
||||
// carries a detail object Firefox's sandbox will not let the page read.
|
||||
$page->script("window.eval(\"Livewire.dispatch('toast', { type: 'info', title: 'Link copied', description: null, timeout: 4000 })\")");
|
||||
|
||||
$page->assertScript(SNACKBAR."?.textContent.includes('Link copied')");
|
||||
});
|
||||
|
||||
it('runs a toast\'s action and dismisses it', function () {
|
||||
$page = visit('/material')->waitForEvent('networkidle');
|
||||
|
||||
$page->script("window.__undone = false; materialToast('Share deleted', { action: { label: 'Undo', handler: () => window.__undone = true } })");
|
||||
|
||||
$page->click('[x-data="materialSnackbar"] button:has-text("Undo")')
|
||||
->assertScript('window.__undone === true')
|
||||
->assertScript(SNACKBAR.' === null');
|
||||
});
|
||||
|
||||
it('opens a persistent rich tooltip on press', function () {
|
||||
$bubble = "document.querySelector('#communication [role=\"dialog\"][popover]')";
|
||||
|
||||
visit('/material')->waitForEvent('networkidle')
|
||||
->click('#communication button:has-text("Press for details")')
|
||||
->assertScript("{$bubble}.matches(':popover-open')");
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
it('states a notice in its state\'s container, with the state\'s icon', function () {
|
||||
$html = (string) $this->blade('<x-alert title="Storage almost full" description="3.8 of 4 GB" color="warning" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('role="alert"')
|
||||
->toContain('bg-warning-container text-on-warning-container')
|
||||
->toContain('Storage almost full')
|
||||
->toContain('3.8 of 4 GB')
|
||||
->toContain('<svg');
|
||||
});
|
||||
|
||||
it('is a status unless it is an error or a warning', function () {
|
||||
expect((string) $this->blade('<x-alert description="Saved." tone="success" />'))->toContain('role="status"')->toContain('bg-success-container')
|
||||
->and((string) $this->blade('<x-alert description="Heads up." />'))->toContain('role="status"')->toContain('bg-info-container')
|
||||
->and((string) $this->blade('<x-alert description="Broken." color="error" />'))->toContain('role="alert"');
|
||||
});
|
||||
|
||||
it('takes actions, drops its icon on request, and can be dismissed', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-alert :icon="false" dismissible>
|
||||
The connection dropped.
|
||||
<x-slot:actions><x-button label="Try again" /></x-slot:actions>
|
||||
</x-alert>
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toContain('The connection dropped.')
|
||||
->toContain('Try again')
|
||||
->toContain('x-data="{ shown: true }"')
|
||||
->toContain('aria-label="Dismiss"')
|
||||
->and(substr_count($html, '<svg'))->toBe(1);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
it('is M3\'s small badge without a value', function () {
|
||||
expect((string) $this->blade('<x-badge />'))
|
||||
->toContain('size-1.5 rounded-corner-full')
|
||||
->toContain('bg-error text-on-error')
|
||||
->toContain('aria-hidden="true"');
|
||||
});
|
||||
|
||||
it('is M3\'s large badge with a count, capped by max', function () {
|
||||
expect((string) $this->blade('<x-badge value="4" />'))->toContain('h-4 min-w-4')->toContain('>4</span>')
|
||||
->and((string) $this->blade('<x-badge value="1204" max="999" />'))->toContain('>999+</span>')
|
||||
->and((string) $this->blade('<x-badge value="12" max="999" color="primary" />'))->toContain('>12</span>')->toContain('bg-primary text-on-primary');
|
||||
});
|
||||
|
||||
it('pins itself to the corner of an icon when floating', function () {
|
||||
expect((string) $this->blade('<x-badge floating />'))->toContain('absolute top-0.5 end-0.5')
|
||||
->and((string) $this->blade('<x-badge value="3" floating label="3 new messages" />'))
|
||||
->toContain('absolute -top-1')
|
||||
->toContain('aria-label="3 new messages"')
|
||||
->not->toContain('aria-hidden');
|
||||
});
|
||||
|
||||
it('draws a status label in a container or an outline', function () {
|
||||
expect((string) $this->blade('<x-badge value="Active" tone="success" tonal />'))
|
||||
->toContain('h-6 gap-1 rounded-corner-sm px-2 type-label-md')
|
||||
->toContain('bg-success-container text-on-success-container')
|
||||
->not->toContain('aria-hidden')
|
||||
->and((string) $this->blade('<x-badge value="Pro" outline />'))
|
||||
->toContain('border border-outline-variant text-on-surface-variant')
|
||||
->not->toContain('bg-error');
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
it('sets an icon on an Expressive shape above its words and action', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-empty-state icon="upload_file" shape="flower" title="No shares yet" description="Files you share appear here.">
|
||||
<x-slot:actions><x-button label="Upload files" /></x-slot:actions>
|
||||
</x-empty-state>
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toContain('text-secondary-container')
|
||||
->toContain('text-on-secondary-container')
|
||||
->toContain('<h3 class="type-title-lg">No shares yet</h3>')
|
||||
->toContain('Files you share appear here.')
|
||||
->toContain('Upload files')
|
||||
->and(substr_count($html, '<svg'))->toBe(2);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
it('wraps its trigger with a surface bubble anchored to it', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-rich-tooltip title="Expiry" text="Recipients lose access after this time.">
|
||||
<button>?</button>
|
||||
</x-rich-tooltip>
|
||||
BLADE);
|
||||
|
||||
preg_match('/anchor-name: (--material-rich-tooltip-[a-z0-9]+)/', $html, $anchor);
|
||||
|
||||
expect($anchor)->not->toBeEmpty()
|
||||
->and($html)
|
||||
->toContain('<button>?</button>')
|
||||
->toContain('x-data="materialRichTooltip(false)"')
|
||||
->toContain('popover="manual"')
|
||||
->toContain('role="tooltip"')
|
||||
->toContain("position-anchor: {$anchor[1]}")
|
||||
->toContain('rounded-corner-md bg-surface-container')
|
||||
->toContain('type-title-sm text-on-surface-variant">Expiry')
|
||||
->toContain('Recipients lose access after this time.');
|
||||
});
|
||||
|
||||
it('opens on press and stays when persistent', function () {
|
||||
expect((string) $this->blade('<x-rich-tooltip text="Details" persistent><button>i</button><x-slot:actions><button>More</button></x-slot:actions></x-rich-tooltip>'))
|
||||
->toContain('x-data="materialRichTooltip(true)"')
|
||||
->toContain('popover="auto"')
|
||||
->toContain('role="dialog"')
|
||||
->toContain('<button>More</button>');
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
it('shows a figure that counts, with its title and description', function () {
|
||||
$html = (string) $this->blade('<x-stat title="Shares" value="1,204" icon="link" description="12 this week"><span>quota</span></x-stat>');
|
||||
|
||||
expect($html)
|
||||
->toContain('bg-surface-container')
|
||||
->toContain('Shares')
|
||||
->toContain('x-figure>1,204</p>')
|
||||
->toContain('type-emphasized-headline-md tabular-nums')
|
||||
->toContain('12 this week')
|
||||
->toContain('<span>quota</span>')
|
||||
->toContain('<svg');
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
it('hosts the snackbar queue, kept across wire:navigate', function () {
|
||||
$html = (string) $this->blade('<x-toast />');
|
||||
|
||||
expect($html)
|
||||
->toContain('x-persist="material-toast"')
|
||||
->toContain('x-data="materialSnackbar"')
|
||||
->toContain('bg-inverse-surface')
|
||||
->toContain('justify-center');
|
||||
});
|
||||
|
||||
it('can sit at the start', function () {
|
||||
expect((string) $this->blade('<x-toast position="bottom-start" />'))->toContain('justify-start');
|
||||
});
|
||||
Reference in New Issue
Block a user