From c33d90c653e2a940299d0b1407a50fe39456c7a4 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold Date: Mon, 14 Sep 2026 06:11:09 +0200 Subject: [PATCH] Stop the alert interrupting, unless it is asked to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An error or warning alert was role="alert", an assertive live region — but the usual alert is part of the page as it renders, which some screen readers then announce over the page title, and a Livewire morph re-announces. It is now role="status" whatever its colour, with an `assertive` prop for a notice put on screen in answer to something the person just did. Its 40px dismiss button takes the shared touch-target. Plan step 18, actions.md ACT-18, ACT-36. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- .../skills/livewire-material-development/SKILL.md | 2 +- resources/views/components/alert.blade.php | 15 +++++++++++---- tests/Feature/Components/AlertTest.php | 8 +++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 1bc54c1f..14e8904b 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -388,7 +388,7 @@ A notice in the page, in the state's container colour with its icon: ``` -`color` (alias `tone`): `info` (default), `success`, `warning`, `error`, `primary`, `secondary`, `tertiary`, `neutral`. `icon` overrides the state icon; `:icon="false"` removes it. Errors and warnings are `role="alert"`, the rest `role="status"`. +`color` (alias `tone`): `info` (default), `success`, `warning`, `error`, `primary`, `secondary`, `tertiary`, `neutral`. `icon` overrides the state icon; `:icon="false"` removes it. Every alert is `role="status"`, whatever its colour: it is usually on the page as it renders, and an assertive region talks over the page title on load. Pass `assertive` for one put on screen in answer to something the person just did. ### `` diff --git a/resources/views/components/alert.blade.php b/resources/views/components/alert.blade.php index a0ae2803..e16740f4 100644 --- a/resources/views/components/alert.blade.php +++ b/resources/views/components/alert.blade.php @@ -5,9 +5,15 @@ text from `description` or the slot, and an `actions` slot for one or two text buttons. `color` (alias `tone`): `info` (the default), `success`, `warning`, `error`, `primary`, `secondary`, `tertiary`, or `neutral` for surface-container-high. `icon` replaces the state's - icon; `:icon="false"` drops it. `dismissible` adds a close button that hides it in the browser. + icon; `:icon="false"` drops it. `dismissible` adds a close button that hides it in the browser, + 40px drawn with the 48px target M3 asks for. - Errors and warnings are `role="alert"` and announced at once; the rest are `role="status"`. --}} + It is a `role="status"`, whatever its colour: an alert is usually part of the page as it + renders, and `role="alert"` is an assertive live region that some screen readers announce over + the page title on load — and that a Livewire morph re-announces. `assertive` opts into + `role="alert"` for the case the prop is named after: a notice put on screen in answer to + something the person just did. M3 publishes no banner; the nearest thing it does publish, the + snackbar, says polite and never assertive. --}} @props([ 'title' => null, @@ -16,6 +22,7 @@ 'tone' => null, 'icon' => null, 'dismissible' => false, + 'assertive' => false, ]) @php @@ -35,7 +42,7 @@ @endphp
class(['flex items-start gap-3 rounded-corner-md p-4', $colours]) }} > @@ -58,7 +65,7 @@
@if ($dismissible) - @endif diff --git a/tests/Feature/Components/AlertTest.php b/tests/Feature/Components/AlertTest.php index 409efecc..4dd6b310 100644 --- a/tests/Feature/Components/AlertTest.php +++ b/tests/Feature/Components/AlertTest.php @@ -4,17 +4,18 @@ it('states a notice in its state\'s container, with the state\'s icon', function $html = (string) $this->blade(''); expect($html) - ->toContain('role="alert"') + ->toContain('role="status"') ->toContain('bg-warning-container text-on-warning-container') ->toContain('Storage almost full') ->toContain('3.8 of 4 GB') ->toContain('blade(''))->toContain('role="status"')->toContain('bg-success-container') ->and((string) $this->blade(''))->toContain('role="status"')->toContain('bg-info-container') - ->and((string) $this->blade(''))->toContain('role="alert"'); + ->and((string) $this->blade(''))->toContain('role="status"') + ->and((string) $this->blade(''))->toContain('role="alert"'); }); it('takes actions, drops its icon on request, and can be dismissed', function () { @@ -30,5 +31,6 @@ it('takes actions, drops its icon on request, and can be dismissed', function () ->toContain('Try again') ->toContain('x-data="{ shown: true }"') ->toContain('aria-label="Dismiss"') + ->toContain('touch-target') ->and(substr_count($html, 'toBe(1); });