リストボックス
stimeo--listbox
候補リストを開いて 1 件選ぶセレクト。activedescendant で候補を辿る。
stimeo--listbox コントローラは、WAI-ARIA の Listbox パターンを折りたたみ形態(Select-Only Combobox 相当)で実装する。combobox トリガーを押すとリストが開き、フォーカスはトリガーに保持したまま、アクティブ候補は DOM フォーカスを移さず aria-activedescendant で示す。↓/↑(ループ)・Home/End・印字可能文字の型先読みでアクティブ候補を移動し、開いたときは選択済み(無ければ先頭)をアクティブにする。選択すると aria-selected を同期し、ラベルをトリガー表示と隠し input の値へ反映して stimeo--listbox:change を発火する。Enter/Space で選択して閉じ、Escape・外側クリック・Tab で閉じる。選択/Escape での閉鎖はトリガーへフォーカスを戻す。静的配置は利用側 CSS、動的配置は opt-in の stimeo-ui/positioning に委譲する。
- りんご
- バナナ
- さくらんぼ
- ぶどう
- オレンジ
キーボード操作
| キー | 動作 |
|---|---|
| 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="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">
<% %w[apple banana 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));
}
.listbox__trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
background: var(--surface, var(--surface-card));
color: var(--fg, var(--color-text));
font: inherit;
cursor: pointer;
}
.listbox__trigger[aria-expanded="true"] {
border-color: var(--accent, var(--color-primary));
}
.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;
}
.listbox__option[data-active] {
background: var(--vital-100);
}
.listbox__option[aria-selected="true"] {
font-weight: 600;
color: var(--accent, var(--color-primary));
}
.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"。 |
hidden |
リスト | 閉じているときは付与。 |
data-active |
候補 | アクティブ候補に付与(CSS ハイライト用)。 |