空状態の監視
stimeo--empty-state
リストが空のときだけ「まだありません」を出し、1 件でも入れば引っ込めます。
リストの中身を見張って、空になったら用意しておいた表示に切り替え、1 件でも入れば元に戻します。Turbo Stream で行が飛んでくるページで、件数を自分で数える手間がなくなります。いま空かどうかと何件あるかは CSS から読めるので、見せ方は自由です。空になった瞬間と埋まった瞬間には、それぞれ読み上げの文言を出せます。状態はそのつど中身から数え直すので、ページが差し替わっても、リストごと入れ替わっても追随します。空のときの絵柄と文言は利用側のものです。
実行中
- 項目 1
- 項目 2
まだ何もありません — 項目を追加してください。
キーボード操作
このコンポーネント自体はキーボード操作を持ちません。
<%# Empty-state demo: add/remove items and the empty placeholder toggles itself. The
controller observes the list with a MutationObserver, toggles hidden on the
list / empty targets at the 0 <-> 1+ boundary, and reflects data-empty / data-count.
demo.js adds/removes <li> (standing in for Turbo Stream rows). This demo only styles
it; the empty placeholder's copy is owned here, and the crossing is read out — without
moving focus — through the shared stimeo--announcer your app seats once, in its layout
(a placeholder that only becomes visible at the moment of the change is not reliably
read, so the wording goes to a region that stands in the page independently of it). %>
<div
class="empty-state-demo"
data-controller="stimeo--empty-state"
data-stimeo--empty-state-announce-text-value="<%=
t("components.empty_state.demo.announce_empty") %>"
data-stimeo--empty-state-announce-filled-text-value="<%=
t("components.empty_state.demo.announce_filled") %>">
<div class="empty-state-demo__bar">
<button
type="button"
class="demo-trigger"
data-empty-state-add
data-item-label="<%= t("components.empty_state.demo.item") %>">
<%= t("components.empty_state.demo.add") %>
</button>
<button type="button" class="demo-trigger" data-empty-state-clear>
<%= t("components.empty_state.demo.clear") %>
</button>
</div>
<ul class="empty-state-demo__list" data-stimeo--empty-state-target="list">
<li><%= t("components.empty_state.demo.item") %> 1</li>
<li><%= t("components.empty_state.demo.item") %> 2</li>
</ul>
<p class="empty-state-demo__empty" data-stimeo--empty-state-target="empty" hidden>
<%= t("components.empty_state.demo.empty") %>
</p>
</div>
/*
* Presentation-only styles for the empty-state demo.
* The library toggles `hidden` on the list / empty targets and reflects
* data-empty / data-count; this CSS only lays out the list, items, and placeholder.
*/
.empty-state-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 28rem;
}
.empty-state-demo__bar {
display: flex;
gap: 0.5rem;
}
.empty-state-demo__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.empty-state-demo__list li {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
background: var(--surface-subtle);
}
.empty-state-demo__empty {
margin: 0;
padding: 1.5rem;
border: 1px dashed var(--border);
border-radius: 0.5rem;
text-align: center;
color: var(--color-text-muted);
}
// Empty-state demo (consumer-side JS).
//
// The controller watches the list with a MutationObserver and toggles the empty
// placeholder on its own. This catalog has no Turbo Stream backend, so here the
// buttons add/remove <li> rows — exactly the kind of mutation Turbo Stream would
// make — and the controller reacts to them.
document.querySelectorAll(".empty-state-demo").forEach((root) => {
const list = root.querySelector('[data-stimeo--empty-state-target="list"]');
const add = root.querySelector("[data-empty-state-add]");
const label = add?.dataset.itemLabel;
if (!list) return;
let count = list.children.length;
add?.addEventListener("click", () => {
const li = document.createElement("li");
li.textContent = `${label ?? "Item"} ${(count += 1)}`;
list.appendChild(li);
});
root.querySelector("[data-empty-state-clear]")?.addEventListener("click", () => {
list.replaceChildren();
});
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--empty-state"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
list
必須
|
子要素を数える監視対象コンテナ。 | data-stimeo--empty-state-target="list" |
empty
|
リストが空のときに表示するプレースホルダ。 | data-stimeo--empty-state-target="empty" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
itemSelector
|
一致する子のみ数えるセレクタ。空なら全要素子を数える。 | data-stimeo--empty-state-item-selector-value |
announceText
|
空になったときに読み上げる文言。空なら告知しない。{count} を展開する。 |
data-stimeo--empty-state-announce-text-value |
announceFilledText
|
空でなくなったときに読み上げる文言。空なら告知しない。{count} を展開する。 |
data-stimeo--empty-state-announce-filled-text-value |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
0 ↔ 1+ の境界を越えたときに発火。detail.count / detail.empty を伴う。 |
stimeo--empty-state:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
hidden |
list / empty ターゲット | リスト(1+)か空プレースホルダ(0)を出すために切り替え。 |
data-empty |
コントローラ要素 | 項目数が 0 のときに付与。 |
data-count |
コントローラ要素 | 現在の項目数。 |