フラッシュ橋渡し
stimeo--flash
Rails の flash をライブリージョン通知+自動消去+スタック表示へ橋渡し。
stimeo--flash コントローラは Rails の flash 要素(data-flash-type="notice|alert")をアクセシブルな通知へ橋渡しします。種別を role="status"(notice)/ role="alert"(alert / error)へマッピングし、 data-flash-state="visible" を付与、duration 後に自動消去し(pauseOnHover 時は hover/focus 中は停止、 WCAG 2.2.1)、max で同時表示の上限を管理します。読み上げは共有 Announcer に委譲しますが、橋渡しは初期(ページ読込時)の flash のみです。読込時に既にある in-place ライブリージョンは自動読み上げされないためで、Turbo Stream で後から挿入された flash は自身の挿入された role が読み上げるため二重には橋渡ししません。動的挿入は MutationObserver で検知し、dismiss アクションを結線したクローズ操作で手動消去できます。挙動のみで装飾は持たず(data-flash-state="leaving" で CSS が退場アニメ)、フォーカスは奪いません(WCAG 2.2 4.1.3)。observer・タイマ・各メッセージのリスナは disconnect(Turbo 遷移含む)で解除します。
<%# 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.
if (root.dataset.demoWired) return;
root.dataset.demoWired = "1";
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", "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 |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-flash-state |
各メッセージ | 表示中は visible、除去前の退場アニメ中は leaving。 |
role |
各メッセージ | notice は status、alert / error は alert(data-flash-type から導出)。 |