アナウンサー
stimeo--announcer
スクリーンリーダー通知のための polite/assertive ライブリージョン共有基盤。
stimeo--announcer コントローラは polite/assertive のライブリージョンを 1 組用意し、各挙動が個別にライブリージョンを抱える代わりに共有できる土台を提供します。announce アクションパラメータ(属性のみ)か、detail.message(任意で detail.assertive)を伴う stimeo--announcer:announce カスタムイベントの発火で通知でき、Turbo Stream 更新や非同期結果の告知に便利です。フォーカスは一切移動せず(通知でフォーカスを奪わない・WCAG 2.2 4.1.3)、同一文言はクリア→再設定で再読み上げさせ、clearAfter 経過後に自動クリアします。polite/assertive ターゲットが無い場合は視覚的に隠したリージョンを実行時に生成します。リスナ・タイマー・生成したリージョンは disconnect(Turbo 遷移含む)で解放します。ライブラリは挙動のみを提供し、リージョンを隠す見た目はこの Playground が持ちます。
ライブリージョンは視覚的に隠れているため、スクリーンリーダーが各メッセージを読み上げ、下のトランスクリプトが目で見える形に複製します。
<%# Markup for the live-region announcer demo.
The library owns one pair of polite/assertive live regions; triggers announce a
message into the matching region via the announce action param (attribute-only).
The regions are visually hidden (screen-reader only), so demo.js mirrors each
announcement into a visible transcript for sighted viewers. %>
<%
polite_msg = t("components.announcer.demo.polite_message")
alert_msg = t("components.announcer.demo.assertive_message")
%>
<%# The controller wraps the whole demo so the attribute-only trigger buttons sit
within its scope — a Stimulus action binds to a controller on the element or an
ancestor, so triggers placed outside the controller element never fire. %>
<div class="announcer-demo" data-controller="stimeo--announcer">
<div class="announcer-demo__controls">
<button class="demo-trigger" type="button"
data-action="click->stimeo--announcer#announce"
data-stimeo--announcer-message-param="<%= polite_msg %>">
<%= t("components.announcer.demo.announce_polite") %>
</button>
<button class="demo-trigger" type="button"
data-action="click->stimeo--announcer#announce"
data-stimeo--announcer-message-param="<%= alert_msg %>"
data-stimeo--announcer-assertive-param="true">
<%= t("components.announcer.demo.announce_assertive") %>
</button>
</div>
<%# Place once per page. The consumer (this Playground) owns the visually-hidden
styling via .visually-hidden; the library only writes the message text. %>
<div class="visually-hidden"
data-stimeo--announcer-target="polite" aria-live="polite" aria-atomic="true"></div>
<div class="visually-hidden"
data-stimeo--announcer-target="assertive" aria-live="assertive" aria-atomic="true"></div>
<p class="announcer-demo__note"><%= t("components.announcer.demo.note") %></p>
<%# Visible echo of what was announced (the live regions themselves are hidden). %>
<ul class="announcer-log" data-announcer-log aria-hidden="true"></ul>
</div>
/*
* Presentation-only styles for the live-region announcer demo.
* The library only writes message text into the regions; this CSS owns the
* visually-hidden ("sr-only") treatment and the visible transcript styling.
*/
.announcer-demo {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 32rem;
}
.announcer-demo__controls {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* The live regions use the shared `.visually-hidden` base primitive (kept in the
* a11y tree, removed from view) — no per-demo duplication. */
.announcer-demo__note {
margin: 0;
font-size: 0.85rem;
color: var(--color-text-muted);
}
.announcer-log {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.announcer-log li {
padding: 0.4rem 0.6rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
font-size: 0.9rem;
font-family: ui-monospace, monospace;
}
// Consumer-side JS for the live-region announcer demo (demo-only).
// The live regions are visually hidden (screen-reader only), so this mirror echoes
// each announcement into a visible transcript for sighted viewers. It also shows
// the programmatic path: any code can announce by dispatching the custom event,
// e.g. window.dispatchEvent(new CustomEvent("stimeo--announcer:announce",
// { detail: { message: "Saved", assertive: false } })).
const log = document.querySelector("[data-announcer-log]");
const regions = document.querySelectorAll("[data-stimeo--announcer-target]");
regions.forEach((region) => {
const observer = new MutationObserver(() => {
const text = (region.textContent || "").trim();
if (!text || !log) return;
const level = region.getAttribute("aria-live");
const item = document.createElement("li");
item.textContent = `[${level}] ${text}`;
log.prepend(item);
});
observer.observe(region, { childList: true, characterData: true, subtree: true });
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--announcer"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
polite
|
緊急でないメッセージ用の aria-live="polite" リージョン。無ければ生成。 |
data-stimeo--announcer-target="polite" |
assertive
|
緊急メッセージ用の aria-live="assertive" リージョン。無ければ生成。 |
data-stimeo--announcer-target="assertive" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
clearAfter
|
リージョンのテキストを消すまでのミリ秒。0 で無効(既定 1000)。 | data-stimeo--announcer-clear-after-value |
dedupeReannounce
|
同一文言をクリア→再設定で再読み上げするか(既定 true)。 |
data-stimeo--announcer-dedupe-reannounce-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
announce
|
アクションパラメータ(message・任意の assertive)かイベント detail からメッセージを通知する。 |
stimeo--announcer#announce |