一括選択バー
stimeo--bulk-select
行チェックボックスとスティッキーな一括操作バーを連動し、件数を読み上げる。
stimeo--bulk-select コントローラは全選択チェックボックスと行チェックボックスを連動させ(indeterminate 中間状態を含む)、1 件以上選択するとスティッキーな操作バーを表示し、選択件数を保持し、「全ページ選択」の 2 段階モードも提供します。行単位の選択は Data Grid の担当で、本パーツはその上に乗る文脈アクションバー層です。選択状態は各チェックボックスの checked のみに保持し(モジュールスコープの集合を持たない)、connect は Turbo 遷移後も DOM から冪等に再計算します。行の変更はイベント委譲で扱うため、動的に追加した行も個別結線なしで動作します。バー表示でフォーカスを奪わず(WCAG 2.2 2.4.3)、件数はバー自身の aria-live 領域で告知します(WCAG 2.2 4.1.3)。stimeo--bulk-select:change を発火し、委譲リスナは disconnect(Turbo 遷移含む)で解除します。ライブラリは挙動のみを提供し、一括操作の実行と見た目はこの Playground が持ちます。
| 名前 | 役割 | |
|---|---|---|
| 山田太郎 | エンジニア | |
| 鈴木花子 | 研究員 | |
| 佐藤次郎 | エンジニア |
<%# Markup for the bulk-select / batch action bar demo.
The controller links the select-all box to the rows (with indeterminate), reveals
the sticky bar once one or more rows are selected, keeps the count, and announces
it through the bar's own aria-live region. Row changes are handled by delegation,
so the "Add row" button (demo.js) works without per-row wiring. %>
<%# Sample rows come from the locale file so the table reads in the page language. %>
<%
rows = t("components.bulk_select.demo.rows")
%>
<div class="bulk-demo" data-controller="stimeo--bulk-select"
data-stimeo--bulk-select-total-count-value="42">
<table class="bulk-demo__table">
<thead>
<tr>
<th scope="col">
<input type="checkbox" data-stimeo--bulk-select-target="all"
aria-label="<%= t("components.bulk_select.demo.select_all") %>">
</th>
<th scope="col"><%= t("components.bulk_select.demo.col_name") %></th>
<th scope="col"><%= t("components.bulk_select.demo.col_role") %></th>
</tr>
</thead>
<tbody data-bulk-demo="rows">
<% rows.each do |row| %>
<% row_label = t("components.bulk_select.demo.select_row", name: row[:name]) %>
<tr>
<td>
<input type="checkbox" data-stimeo--bulk-select-target="item"
aria-label="<%= row_label %>">
</td>
<td><%= row[:name] %></td>
<td><%= row[:role] %></td>
</tr>
<% end %>
</tbody>
</table>
<%# Localized templates for demo.js's appended rows ({n} = row number, {name} = the
generated name), so dynamically-added rows match the page locale too. %>
<button class="demo-trigger" type="button" data-bulk-demo="add"
data-bulk-add-name="<%= t("components.bulk_select.demo.add_name_template") %>"
data-bulk-add-role="<%= t("components.bulk_select.demo.add_role") %>"
data-bulk-add-label="<%= t("components.bulk_select.demo.add_row_label") %>">
<%= t("components.bulk_select.demo.add_row") %>
</button>
<%# Sticky batch action bar: its aria-live region announces the count. %>
<div class="bulk-demo__bar" data-stimeo--bulk-select-target="bar" hidden
role="toolbar" aria-live="polite"
aria-label="<%= t("components.bulk_select.demo.bar_label") %>">
<span class="bulk-demo__count">
<span data-stimeo--bulk-select-target="count">0</span>
<%= t("components.bulk_select.demo.selected_suffix") %>
</span>
<button class="demo-trigger" type="button"
data-stimeo--bulk-select-target="selectAllPages"
data-action="click->stimeo--bulk-select#selectAllPages">
<%= t("components.bulk_select.demo.select_all_pages") %>
</button>
<button class="demo-trigger" type="button"
data-action="click->stimeo--bulk-select#clear">
<%= t("components.bulk_select.demo.clear") %>
</button>
</div>
</div>
/*
* Presentation-only styles for the bulk-select demo.
* The library toggles the bar's hidden attribute, the select-all indeterminate
* state, and data-selected-count; this CSS owns the table and sticky bar styling.
*/
.bulk-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 36rem;
}
.bulk-demo__table {
width: 100%;
border-collapse: collapse;
}
.bulk-demo__table th,
.bulk-demo__table td {
padding: 0.5rem 0.6rem;
text-align: left;
border-bottom: 1px solid var(--border);
font-size: 0.95rem;
}
.bulk-demo__bar {
position: sticky;
bottom: 0;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0.9rem;
border: 1px solid var(--color-primary);
border-radius: 0.5rem;
background: var(--color-primary-soft);
color: var(--vital-800);
}
.bulk-demo__bar[hidden] {
display: none;
}
.bulk-demo__count {
font-weight: 600;
margin-right: auto;
}
// Consumer-side JS for the bulk-select demo (demo-only).
// Demonstrates that rows added after connect are handled by the controller's
// delegated change listener — no per-row wiring needed. The "Add row" button
// appends a new row; checking it updates the count and bar just like the rest.
const tbody = document.querySelector("[data-bulk-demo='rows']");
const addButton = document.querySelector("[data-bulk-demo='add']");
// Idempotent: Turbo can re-run this inline module on navigation, so wire once (keyed on
// the Add-row button). Both listeners attach to elements inside the demo that are
// discarded with the <body> on a Turbo visit, so they are torn down rather than stacked.
if (tbody && addButton && !addButton.dataset.demoWired) {
addButton.dataset.demoWired = "1";
let nextId = tbody.querySelectorAll("tr").length + 1;
// Localized templates passed from the ERB via data attributes ({n} = row number,
// {name} = the generated name), so appended rows match the page locale just like
// the server-rendered ones.
const nameTemplate = addButton.dataset.bulkAddName ?? "New person {n}";
const role = addButton.dataset.bulkAddRole ?? "Member";
const labelTemplate = addButton.dataset.bulkAddLabel ?? "Select {name}";
addButton.addEventListener("click", () => {
const name = nameTemplate.replace("{n}", String(nextId));
const label = labelTemplate.replace("{name}", name);
const tr = document.createElement("tr");
tr.innerHTML =
`<td><input type="checkbox" data-stimeo--bulk-select-target="item"` +
` aria-label="${label}"></td>` +
`<td>${name}</td><td>${role}</td>`;
tbody.appendChild(tr);
nextId += 1;
});
// Example of reacting to the selection-change event (analytics, enabling a button…).
// Listen on the demo root (the controller element the event is dispatched on), not
// document: a document listener would survive Turbo body swaps and stack up on every
// navigate-away→back, whereas this is torn down with the body.
addButton.closest(".bulk-demo")?.addEventListener("stimeo--bulk-select:change", (event) => {
console.log(`[bulk-select] count=${event.detail.count} allPages=${event.detail.allPages}`);
});
}
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--bulk-select"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
all
|
全選択チェックボックス。各行へ反映し、行の状態を映す。 | data-stimeo--bulk-select-target="all" |
item
必須
|
行チェックボックス。選択状態はここに保持(変更は委譲で処理)。 | data-stimeo--bulk-select-target="item" |
bar
必須
|
一括操作バー。選択に応じて hidden を切り替え、件数を告知する。 |
data-stimeo--bulk-select-target="bar" |
count
|
選択(または総)件数のテキストを設定する要素。 | data-stimeo--bulk-select-target="count" |
selectAllPages
|
全ページ選択モードに入る任意のコントロール。 | data-stimeo--bulk-select-target="selectAllPages" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
totalCount
|
全ページの総行数。全ページモードで表示する(既定 0)。 | data-stimeo--bulk-select-total-count-value |
announce
|
件数変化を aria-live で告知するか(既定 true)。 |
data-stimeo--bulk-select-announce-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
clear
|
全行と全選択チェックを解除し、全ページモードを抜ける。 | stimeo--bulk-select#clear |
selectAllPages
|
全ページ選択モードに入る(件数は totalCount を表示)。 |
stimeo--bulk-select#selectAllPages |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
選択が変化したとき発火。detail に count と allPages を伴う。 |
stimeo--bulk-select:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
hidden |
アクションバー | 未選択のあいだ付与される(バー非表示)。 |
data-selected-count |
ルート要素 | 現在選択中の行数。 |
data-all-pages |
ルート要素 | 全ページ選択モードが有効なあいだ "true"。 |
indeterminate |
全選択チェックボックス | 一部だけ選択しているときに付与される。 |