リストボックス
stimeo--listbox
選択肢を開いて 1 つ選ぶセレクト。フォーカスはボタンに残ったまま候補を辿れます。
WAI-ARIA の Listbox パターンを、開いて選ぶ形で実装しています。ボタンを押すと一覧が開きます。フォーカスはボタンに残ったまま、いまどの候補を指しているかが読み上げに伝わります。上下キーで巡回し、Home と End で両端へ、文字を打つとその頭文字の候補へ飛びます。開いたときは、選ばれている候補、無ければ先頭が選ばれた状態から始まります。決めるとボタンの表示と、フォームへ送る値の両方に反映されます。Esc、外側のクリック、Tab で閉じます。選ぶか Esc で閉じたときはボタンへフォーカスが戻ります。位置はこのデモの CSS で決めています。
- りんご
- あんず
- バナナ
- ブルーベリー
- さくらんぼ
- ぶどう
- オレンジ
キーボード操作
| キー | 動作 |
|---|---|
| Enter / Space / ↓ / ↑ | 閉じている状態でリストを開く。 |
| ↓ / ↑ | 開いている間、アクティブ候補を移動(ループ)。 |
| Home / End | 先頭/末尾の候補へ。 |
| 印字可能文字 | 入力した文字で始まる次の候補へ型先読み。同じ文字の連打で候補を巡回する。 |
| Enter / Space | アクティブ候補を選択して閉じる。 |
| Esc | 選択せず閉じ、トリガーへフォーカスを戻す。 |
<%# Markup for the listbox demo.
Pressing the role="combobox" trigger opens a role="listbox"; arrows / typeahead
navigate the options to pick one. Focus stays on the trigger and the active option
is shown via aria-activedescendant. The library handles open/close, option
movement, single selection, reflecting into the trigger label and hidden input, and
closing on Escape / outside click. Static placement is in demo.css. %>
<div class="listbox" data-controller="stimeo--listbox">
<span id="listbox-label" class="listbox__label"><%= t("components.listbox.demo.label") %></span>
<button
type="button"
class="demo-trigger listbox__trigger"
role="combobox"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="listbox-options"
aria-labelledby="listbox-label listbox-value"
data-stimeo--listbox-target="trigger"
data-action="click->stimeo--listbox#toggle keydown->stimeo--listbox#onTriggerKeydown">
<span id="listbox-value" data-stimeo--listbox-target="value">
<%= t("components.listbox.demo.placeholder") %>
</span>
<span class="listbox__chevron" aria-hidden="true">▾</span>
</button>
<ul
id="listbox-options"
class="listbox__list"
role="listbox"
aria-label="<%= t("components.listbox.demo.label") %>"
hidden
data-stimeo--listbox-target="list">
<%# Two pairs share a first letter (apple/apricot, banana/blueberry) so pressing
that letter repeatedly cycles between them — the APG type-ahead behavior. %>
<% %w[apple apricot banana blueberry cherry grape orange].each_with_index do |fruit, index| %>
<li
id="listbox-opt-<%= index %>"
class="listbox__option"
role="option"
aria-selected="false"
data-value="<%= fruit %>"
data-stimeo--listbox-target="option"
data-action="click->stimeo--listbox#select">
<%= t("components.listbox.demo.options.#{fruit}") %>
</li>
<% end %>
</ul>
<input type="hidden" name="fruit" data-stimeo--listbox-target="field" />
</div>
/*
* Presentation-only styles for the listbox demo.
* The library toggles the list's hidden, options' aria-selected, and the active
* candidate's data-active highlight. Placement (directly below the trigger) is static
* and the consumer's CSS responsibility; use stimeo-ui/positioning for dynamic flip.
*/
.listbox {
position: relative;
display: inline-flex;
flex-direction: column;
gap: 0.35rem;
min-width: 14rem;
}
.listbox__label {
font-size: 0.8125rem;
font-weight: 600;
color: var(--fg, var(--color-text));
}
/* Only the select-style layout is the demo's own; the look comes from .demo-trigger. */
.listbox__trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.listbox__chevron {
color: var(--color-text-muted);
}
.listbox__list {
position: absolute;
top: calc(100% + 0.25rem);
left: 0;
right: 0;
z-index: 10;
margin: 0;
padding: 0.25rem;
list-style: none;
background: var(--surface, var(--surface-card));
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
box-shadow: 0 8px 24px rgb(15 23 42 / 0.12);
}
.listbox__option {
padding: 0.45rem 0.6rem;
border-radius: 0.25rem;
cursor: pointer;
}
/* A theme-aware soft accent surface, not the raw `--vital-100`: this rule sets a
background but lets the text inherit `--color-text`, which flips with the theme.
A fixed light mint therefore paired light text on light in dark mode — measured
1.02:1, i.e. the row the keyboard is on was effectively invisible. */
.listbox__option[data-active] {
background: var(--color-primary-soft);
}
/* `--accent-800`, one step stronger than the plain accent text token: this text can
land on the tinted active/selected surface as well as the card, and on the tinted
one in dark `--accent-700` measures 4.44:1 — just under the floor. */
.listbox__option[aria-selected="true"] {
font-weight: 600;
color: var(--accent-800);
}
.listbox__option[aria-selected="true"]::after {
content: "✓";
margin-left: 0.4rem;
}
このデモに固有の消費側 JS はありません(挙動はコントローラが担います)。
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--listbox"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
trigger
必須
|
リストを開き、aria-activedescendant でアクティブ選択肢を追跡する折りたたみコンボボックスのボタン。 |
data-stimeo--listbox-target="trigger" |
value
|
選択中の選択肢のラベルを表示するトリガー内の span。 | data-stimeo--listbox-target="value" |
list
必須
|
hidden 属性で開閉する role=listbox のポップアップ。 |
data-stimeo--listbox-target="list" |
option
必須
|
aria-selected/data-active の状態が管理される role=option。 |
data-stimeo--listbox-target="option" |
field
|
フォーム送信用に選択値を反映する隠し input。 | data-stimeo--listbox-target="field" |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
close
|
リストを閉じ、アクティブ選択肢を解除し、タイプアヘッドバッファをリセットする。 | stimeo--listbox#close |
onTriggerKeydown
|
APG セレクト専用モデルに沿ったトリガーのキーボード操作(開くキー、上下矢印の循環、Home/End、タイプアヘッド、Enter/Space、Escape、Tab)を処理する。 | stimeo--listbox#onTriggerKeydown |
open
|
リストを開き、選択中の選択肢(なければ先頭)をアクティブにする。 | stimeo--listbox#open |
select
|
クリックされた選択肢を確定し、閉じてトリガーへフォーカスを戻す。 | stimeo--listbox#select |
toggle
|
実際のマウスクリックでリストの開閉を切り替える(合成キーボードクリックは無視)。 | stimeo--listbox#toggle |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
選択時に発火。detail は { value, option }。 |
stimeo--listbox:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-expanded |
トリガー | リストの開閉状態。 |
aria-activedescendant |
トリガー | アクティブ候補の id(無いときは除去)。 |
aria-selected |
候補 | 選択中の候補に "true"、ほかの候補には "false"。サーバー側で初期選択を書いておける(複数書いた場合は先頭が勝つ)。 |
hidden |
リスト | 閉じているときは付与。 |
data-active |
候補 | アクティブ候補に付与(CSS ハイライト用)。 |