フラッシュ橋渡し
stimeo--flash
Rails の flash を、読み上げに届いて自動で消える通知にします。
Rails の flash メッセージを、読み上げにも届く通知に変えます。お知らせと警告では読み上げの強さを変えるので、大事な警告が埋もれません。しばらくすると自動で消えます。マウスを乗せているときとフォーカスが当たっているときは、読み終わるまで消えません。同時に出す数の上限も決められます。あとから Turbo で挿入された分も自分で見つけて同じ扱いにします。通知が出てもフォーカスは奪いません。閉じるボタンを付ければ手で消せます。消えていく途中かどうかは CSS から読めるので、退場のアニメーションも書けます。
実行中
キーボード操作
このコンポーネント自体はキーボード操作を持ちません。
<%# Flash-bridge demo: the buttons append flash messages into the region (standing in
for a Turbo Stream that renders server flash). The controller maps data-flash-type
to role=status/alert, auto-dismisses after the duration (paused on hover/focus), and
a close button wired to the dismiss action removes one. The library only sets roles
and data-flash-state; demo.css owns the look and the visible/leaving transition. The
message text and the dismiss label are owned here for i18n. %>
<div class="flash-demo" data-dismiss-label="<%= t("components.flash.demo.dismiss") %>">
<div class="flash-demo__bar">
<button
type="button"
class="demo-trigger"
data-flash-demo="notice"
data-flash-text="<%= t("components.flash.demo.notice_text") %>">
<%= t("components.flash.demo.notice") %>
</button>
<button
type="button"
class="demo-trigger"
data-flash-demo="alert"
data-flash-text="<%= t("components.flash.demo.alert_text") %>">
<%= t("components.flash.demo.alert") %>
</button>
</div>
<div data-controller="stimeo--flash" data-stimeo--flash-duration-value="4000">
<div class="flash-demo__region" data-stimeo--flash-target="region"></div>
</div>
</div>
/*
* Presentation-only styles for the flash demo. The library maps each message to
* role=status/alert and reflects data-flash-state (visible / leaving); this CSS owns
* the banner look (colored by data-flash-type) and the fade on the leaving state.
*/
.flash-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 28rem;
}
.flash-demo__bar {
display: flex;
gap: 0.5rem;
}
.flash-demo__region {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.flash-demo__item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
transition: opacity 0.2s ease-out;
}
.flash-demo__item[data-flash-type="notice"] {
border-color: var(--leaf-500);
background: var(--leaf-50);
color: var(--leaf-500);
}
.flash-demo__item[data-flash-type="alert"] {
border-color: var(--danger-500);
background: var(--danger-50);
color: var(--color-accent);
}
.flash-demo__item[data-flash-state="leaving"] {
opacity: 0;
}
.flash-demo__close {
flex: none;
border: 0;
background: transparent;
font-size: 1.125rem;
line-height: 1;
color: inherit;
cursor: pointer;
}
// Flash-bridge demo (consumer-side JS).
//
// This catalog has no Turbo Stream backend, so the buttons append a flash message into
// the region — exactly the mutation a Turbo Stream flash append would make — and the
// controller picks it up via its MutationObserver, maps the role, and auto-dismisses it.
// The message text and the dismiss label come from data-* so they stay i18n'd.
document.querySelectorAll(".flash-demo").forEach((root) => {
const region = root.querySelector('[data-stimeo--flash-target="region"]');
if (!region) return;
// Idempotent: Turbo can re-run this inline module on navigation; wire each root once
// so a click never appends two flashes. The marker is a property, not an attribute:
// Turbo copies attributes into its page snapshot, so an attribute one comes back set
// on a restored page whose elements carry no listeners.
if (root.demoWired) return;
root.demoWired = true;
const dismissLabel = root.dataset.dismissLabel ?? "Dismiss";
root.querySelectorAll("[data-flash-demo]").forEach((button) => {
button.addEventListener("click", () => {
const flash = document.createElement("div");
flash.className = "flash-demo__item";
flash.setAttribute("data-stimeo--flash-target", "message");
flash.setAttribute("data-flash-type", button.dataset.flashDemo);
const text = document.createElement("span");
text.textContent = button.dataset.flashText ?? "";
flash.appendChild(text);
const close = document.createElement("button");
close.type = "button";
close.className = "flash-demo__close";
close.setAttribute("data-action", "click->stimeo--flash#dismiss");
close.setAttribute("aria-label", dismissLabel);
close.textContent = "×"; // ×
flash.appendChild(close);
region.appendChild(flash);
});
});
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--flash"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
region
必須
|
flash メッセージを収めるコンテナ(監視対象のスタック)。 | data-stimeo--flash-target="region" |
message
|
個々の flash。data-flash-type を持ち、自動管理される。 |
data-stimeo--flash-target="message" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
duration
|
自動消去までのミリ秒(0=自動消去しない。既定 5000)。 | data-stimeo--flash-duration-value |
pauseOnHover
|
hover / focus 中は自動消去タイマを停止(既定 true)。 |
data-stimeo--flash-pause-on-hover-value |
max
|
同時表示の最大件数。超過分は古いものから消す(0=無制限)。 | data-stimeo--flash-max-value |
アクション
| 名前 | アクション |
|---|---|
dismiss
|
stimeo--flash#dismiss |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
show
|
flash を表示したとき発火。detail.type / detail.message を伴う。 |
stimeo--flash:show |
dismiss
|
flash を消去したとき発火。detail.element / detail.reason を伴う。 |
stimeo--flash:dismiss |
reconcile
|
Turbo キャッシュの巻き戻しがメッセージを取り除いたとき発火。detail に取り除いた件数 removed。 |
stimeo--flash:reconcile |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-flash-state |
各メッセージ | 表示中は visible、除去前の退場アニメ中は leaving。 |
role |
各メッセージ | notice は status、alert / error は alert(data-flash-type から導出)。 |