Files
livewire-material/resources/views/components/toast.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 480387a64e Draw the snackbar's action and close buttons from the classes
Plan step 36 ("Interaction is the shared classes", the user, 2026-09-14).
The action and the close button hand-rolled md-state-layer, md-focus-ring
and md-touch-target, and toast.css's own header said so; both now render
the classes and keep only their own size and colour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 18:10:16 +02:00

101 lines
5.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{-- The snackbar host: shows every toast, one at a time. Put it once in each layout, near the end
of <body>:
<x-toast />
It shows every `toast` browser event what `NoNameWeb\LivewireMaterial\Concerns\Toasts`
dispatches from a Livewire component and for `window.materialToast(title, options)` from
JavaScript (`{ type, description, timeout, sticky, action: { label, handler, event } }`).
Toasts queue and show in turn, each for its `timeout` (4s by default; M3 asks for 410s),
paused while the pointer or focus is on it. A toast with an `action` has no timeout at all,
as M3 requires it waits to be read and acted on unless the caller writes a `timeout` out.
A toast with an action or no timeout gets a close button. Pressing the action closes the
snackbar, calls `handler` and dispatches `event` (a name) on `window`; both may be given.
`sticky: true` keeps a toast until it is dismissed or its action pressed, without holding up
the queue: a toast that arrives meanwhile shows in its place, and the sticky one comes back
once the queue is empty. One is kept at a time; a newer sticky toast replaces it.
The live region is the host itself, not the snackbar: a region must be in the page before its
contents change for a screen reader to announce them reliably, and the snackbar comes and
goes (ACT-02). It is `aria-live="polite" aria-atomic="true"`, as M3 asks for a snackbar
(never assertive); a `type` picks the region's role — `alert` for an error or a warning,
`status` otherwise — and the explicit `aria-live` keeps even those polite.
Hooks for tests and styling: `data-md-toast` on the host, `data-md-toast-snackbar` on the
snackbar on screen, `data-md-toast-action` on its action button and `data-md-toast-dismiss`
on its close button. Drawn by resources/css/components/toast.css.
`@persist` keeps the host across wire:navigate, so a toast dispatched with `redirectTo` is
still on screen when the next page arrives.
There is no state icon: M3 says to avoid one in a snackbar ("use a dialog instead if an icon
feels necessary", ACT-20), and both lines of the message are plain inverse-on-surface — M3
gives the supporting text no fourth colour and tells the two lines apart by position. The
40px action and close buttons reach M3's 48px target without growing the container (ACT-18).
Escape dismisses a snackbar that holds the focus (ACT-34).
Alt+G moves the focus to a snackbar that carries an action, from wherever the page had it
M3 asks for a documented shortcut on the web, since a snackbar never takes the focus itself
and a keyboard has no other way to reach one (ACT-34). It does nothing when the snackbar on
screen has no action.
M3's snackbar (SnackbarTokens, androidx Compose Material 3, Apache-2.0): inverse surface,
body-medium text, a label-large action in inverse-primary, extra-small corners, elevation 3,
48px for one line and 68px for two (SnackbarTokens.TwoLinesContainerHeight; the site's prose
says 64dp, and the token is the more precise of the two). A description is that second line,
so the container is pinned to 68px whenever one is there rather than left to grow into it. On
a compact window a two-line snackbar with an action wraps the action below the text, which is
the third of M3's five snackbar configurations ("two lines with longer action"). `position`:
`bottom` (centred, the default) or `bottom-start`. It lifts above a bottom bar through
`--material-bottom-bar`, and publishes its own height as `--material-snackbar-height` so a
FAB can lift clear of it — M3: a snackbar appears above a FAB, never in front of or behind
one (ACT-17). A compact window (below `medium`, 600px) gets the full-width snackbar; from
`medium` it hugs its line length instead, as M3 asks. --}}
@props(['position' => 'bottom'])
@php
$position = in_array($position, ['bottom', 'bottom-start'], true) ? $position : 'bottom';
@endphp
@persist('material-toast')
<div
x-data="materialSnackbar"
x-bind:role="current && (current.type === 'error' || current.type === 'warning') ? 'alert' : 'status'"
role="status"
aria-live="polite"
aria-atomic="true"
data-md-toast
data-md-position="{{ $position }}"
{{ $attributes }}
>
<template x-if="current">
<div
x-bind:key="current.id"
data-md-toast-snackbar
x-bind:data-md-two-line="current.description ? '' : null"
x-on:mouseenter="pause()"
x-on:mouseleave="resume()"
x-on:focusin="pause()"
x-on:focusout="resume()"
>
<div data-md-toast-content x-bind:data-md-toast-wrap="current.description && current.action ? '' : null">
<p data-md-toast-title x-text="current.title"></p>
<p data-md-toast-description x-show="current.description" x-text="current.description"></p>
</div>
<template x-if="current.action">
<button type="button" class="md-state-layer md-focus-ring md-touch-target" data-md-toast-action x-text="current.action.label" x-on:click="act()"></button>
</template>
<template x-if="current.action || ! current.timeout">
<button type="button" class="md-state-layer md-focus-ring md-touch-target" data-md-toast-dismiss aria-label="{{ __('Dismiss') }}" x-on:click="dismiss()">
<x-livewire-material::icon name="close" size="20" />
</button>
</template>
</div>
</template>
</div>
@endpersist