improve notification accessibility

- use role="status" for info messages, role="alert" for errors
- add aria-live attribute (polite for info, assertive for errors)
- move phx-click to close button for better keyboard navigation
- add close buttons to shop flash messages
- add aria-hidden to decorative icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-03-08 07:53:17 +00:00
parent 8af5cbf41e
commit 5e03dccb69
3 changed files with 62 additions and 19 deletions

View File

@ -2491,6 +2491,22 @@
color: hsl(0 70% 50%); color: hsl(0 70% 50%);
} }
.shop-flash-close {
margin-left: auto;
padding: 0.25rem;
cursor: pointer;
opacity: 0.5;
transition: opacity 150ms ease;
border-radius: 0.25rem;
&:hover { opacity: 0.8; }
&:focus-visible {
opacity: 1;
outline: 2px solid var(--t-accent);
outline-offset: 2px;
}
}
/* Transition classes for JS.hide flash dismiss */ /* Transition classes for JS.hide flash dismiss */
.fade-out { transition: opacity 200ms ease-out; } .fade-out { transition: opacity 200ms ease-out; }
.fade-out-from { opacity: 1; } .fade-out-from { opacity: 1; }

View File

@ -45,8 +45,8 @@ defmodule BerrypodWeb.CoreComponents do
<div <div
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)} :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
id={@id} id={@id}
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")} role={if(@kind == :error, do: "alert", else: "status")}
role="alert" aria-live={if(@kind == :error, do: "assertive", else: "polite")}
class="admin-banner" class="admin-banner"
{@rest} {@rest}
> >
@ -62,7 +62,12 @@ defmodule BerrypodWeb.CoreComponents do
<p>{msg}</p> <p>{msg}</p>
</div> </div>
<div class="admin-alert-spacer" /> <div class="admin-alert-spacer" />
<button type="button" class="admin-alert-close" aria-label={gettext("close")}> <button
type="button"
class="admin-alert-close"
aria-label={gettext("close")}
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
>
<.icon name="hero-x-mark" class="size-5" /> <.icon name="hero-x-mark" class="size-5" />
</button> </button>
</div> </div>

View File

@ -1132,19 +1132,13 @@ defmodule BerrypodWeb.ShopComponents.Content do
def shop_flash_group(assigns) do def shop_flash_group(assigns) do
~H""" ~H"""
<div id="shop-flash-group" aria-live="polite" class="shop-flash-group"> <div id="shop-flash-group" class="shop-flash-group">
<%= if msg = Phoenix.Flash.get(@flash, :info) do %> <%= if msg = Phoenix.Flash.get(@flash, :info) do %>
<div <div
id="shop-flash-info" id="shop-flash-info"
class="shop-flash shop-flash--info" class="shop-flash shop-flash--info"
role="alert" role="status"
phx-click={ aria-live="polite"
Phoenix.LiveView.JS.push("lv:clear-flash", value: %{key: :info})
|> Phoenix.LiveView.JS.hide(
to: "#shop-flash-info",
transition: {"fade-out", "fade-out-from", "fade-out-to"}
)
}
> >
<svg <svg
class="shop-flash-icon shop-flash-icon--info" class="shop-flash-icon shop-flash-icon--info"
@ -1154,10 +1148,27 @@ defmodule BerrypodWeb.ShopComponents.Content do
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
aria-hidden="true"
> >
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg> </svg>
<p>{msg}</p> <p>{msg}</p>
<button
type="button"
class="shop-flash-close"
aria-label="Close"
phx-click={
Phoenix.LiveView.JS.push("lv:clear-flash", value: %{key: :info})
|> Phoenix.LiveView.JS.hide(
to: "#shop-flash-info",
transition: {"fade-out", "fade-out-from", "fade-out-to"}
)
}
>
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div> </div>
<% end %> <% end %>
<%= if msg = Phoenix.Flash.get(@flash, :error) do %> <%= if msg = Phoenix.Flash.get(@flash, :error) do %>
@ -1165,13 +1176,7 @@ defmodule BerrypodWeb.ShopComponents.Content do
id="shop-flash-error" id="shop-flash-error"
class="shop-flash shop-flash--error" class="shop-flash shop-flash--error"
role="alert" role="alert"
phx-click={ aria-live="assertive"
Phoenix.LiveView.JS.push("lv:clear-flash", value: %{key: :error})
|> Phoenix.LiveView.JS.hide(
to: "#shop-flash-error",
transition: {"fade-out", "fade-out-from", "fade-out-to"}
)
}
> >
<svg <svg
class="shop-flash-icon shop-flash-icon--error" class="shop-flash-icon shop-flash-icon--error"
@ -1181,6 +1186,7 @@ defmodule BerrypodWeb.ShopComponents.Content do
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
aria-hidden="true"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@ -1189,6 +1195,22 @@ defmodule BerrypodWeb.ShopComponents.Content do
/> />
</svg> </svg>
<p>{msg}</p> <p>{msg}</p>
<button
type="button"
class="shop-flash-close"
aria-label="Close"
phx-click={
Phoenix.LiveView.JS.push("lv:clear-flash", value: %{key: :error})
|> Phoenix.LiveView.JS.hide(
to: "#shop-flash-error",
transition: {"fade-out", "fade-out-from", "fade-out-to"}
)
}
>
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div> </div>
<% end %> <% end %>
</div> </div>