複数選択
stimeo--multi-select
候補を絞り込みつつ複数選択し、選択済みを削除可能なチップで表示するコンボボックス。
stimeo--multi-select コントローラは、WAI-ARIA の Combobox パターンをリストオートコンプリート+複数選択の形態で実装する。入力すると候補を部分一致で絞り込んでリストを開き、フォーカスは入力に保持したまま、アクティブ候補を aria-activedescendant で示す。↓/↑(ループ)と Home/End でアクティブ候補を移動し、Enter で選択をトグル(リストは開いたまま)、Escape/Tab/外側クリックで閉じる。トグルすると aria-selected を同期し、Remove {label} のチップを追加 / 削除し、ライブリージョンを更新して stimeo--multi-select:change を values 付きで発火する。入力変化時は stimeo--multi-select:filter を発火し、非同期候補差し替えに使える。チップは 1 つのロービング Tab ストップ(←/→・Delete/Backspace、空入力の Backspace で末尾を削除)。max で選択上限を制御する。単一選択は Listbox / Combobox、自由入力タグは Tags Input を用いる。
- りんご
- バナナ
- さくらんぼ
- ぶどう
- オレンジ
- メロン
キーボード操作
| キー | 動作 |
|---|---|
| 入力 | 候補を部分一致で絞り込み、リストを開く。 |
| ↓ / ↑ | アクティブ候補を移動(ループ)。 |
| Home / End | 先頭 / 末尾の可視候補へ。 |
| Enter | アクティブ候補をトグル(リストは開いたまま)。 |
| Backspace(空入力) | 直前の選択チップを削除。 |
| Esc | リストを閉じる。 |
<%# Markup for the multi-select (multiple-selection combobox) demo.
Filter candidates as you type, select several, and show the selected ones as chips.
Focus stays on the input; the active candidate is shown via aria-activedescendant
and selection via aria-selected. The library handles filtering, multi-select
toggling, chip create/remove, roving, live announcements, and dismiss. Placement
lives in demo.css. %>
<div class="multi-select" data-controller="stimeo--multi-select">
<span id="multi-select-label" class="multi-select__label">
<%= t("components.multi_select.demo.label") %>
</span>
<ul
class="multi-select__tags"
aria-label="<%= t("components.multi_select.demo.selected_label") %>"
data-stimeo--multi-select-target="tags"></ul>
<input
type="text"
class="multi-select__input"
role="combobox"
aria-expanded="false"
aria-autocomplete="list"
aria-controls="multi-select-list"
aria-labelledby="multi-select-label"
placeholder="<%= t("components.multi_select.demo.placeholder") %>"
data-stimeo--multi-select-target="input"
data-action="input->stimeo--multi-select#filter
keydown->stimeo--multi-select#onKeydown
focus->stimeo--multi-select#open" />
<ul
id="multi-select-list"
class="multi-select__list"
role="listbox"
aria-multiselectable="true"
aria-label="<%= t("components.multi_select.demo.options_label") %>"
hidden
data-stimeo--multi-select-target="list">
<% %w[apple banana cherry grape orange melon].each_with_index do |fruit, index| %>
<li
id="multi-select-opt-<%= index %>"
class="multi-select__option"
role="option"
aria-selected="false"
data-value="<%= fruit %>"
data-stimeo--multi-select-target="option"
data-action="click->stimeo--multi-select#toggleOption">
<%= t("components.multi_select.demo.options.#{fruit}") %>
</li>
<% end %>
</ul>
<span
role="status"
aria-live="polite"
class="multi-select__status visually-hidden"
data-stimeo--multi-select-target="status"></span>
<template data-stimeo--multi-select-target="tagTemplate">
<li class="multi-select__tag" data-stimeo--multi-select-target="tag">
<span data-multi-select-slot="label"></span>
<%# Removal is handled by a delegated listener on the tags container, so it
works instantly without waiting on Stimulus wiring a data-action onto the
dynamically-added button. %>
<button
type="button"
class="multi-select__remove"
tabindex="-1">×</button>
</li>
</template>
</div>
/*
* Presentation-only styles for the multi-select demo.
* The library toggles the list's hidden, options' aria-selected, and the active
* candidate's data-active. Creating/removing chips and roving are the library's
* job too. The list's static placement (directly below the input) is the
* consumer's CSS; dynamic flip is via stimeo-ui/positioning.
*/
.multi-select {
position: relative;
display: inline-flex;
flex-direction: column;
gap: 0.35rem;
min-width: 18rem;
}
.multi-select__label {
font-size: 0.8125rem;
font-weight: 600;
color: var(--fg, var(--color-text));
}
.multi-select__tags {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
margin: 0;
padding: 0;
list-style: none;
}
.multi-select__tag {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.1rem 0.45rem;
border-radius: 999px;
background: var(--vital-100);
color: var(--vital-800);
font-size: 0.8125rem;
}
.multi-select__remove {
border: 0;
background: transparent;
color: inherit;
font: inherit;
line-height: 1;
cursor: pointer;
}
.multi-select__remove:focus-visible {
outline: 2px solid var(--accent, var(--color-primary));
outline-offset: 2px;
border-radius: 50%;
}
.multi-select__input {
padding: 0.5rem 0.65rem;
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
background: var(--surface, var(--surface-card));
color: var(--fg, var(--color-text));
font: inherit;
}
.multi-select__input[aria-expanded="true"] {
border-color: var(--accent, var(--color-primary));
}
.multi-select__list {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 10;
max-height: 14rem;
overflow-y: auto;
margin: 0.25rem 0 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);
}
.multi-select__option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.45rem 0.6rem;
border-radius: 0.25rem;
cursor: pointer;
}
.multi-select__option[hidden] {
display: none;
}
.multi-select__option[data-active] {
background: var(--vital-100);
}
.multi-select__option[aria-selected="true"]::after {
content: "✓";
color: var(--accent, var(--color-primary));
}
.multi-select[data-stimeo--multi-select-empty] .multi-select__list::after {
content: "—";
display: block;
padding: 0.45rem 0.6rem;
color: var(--color-text-subtle);
}
このデモに固有の消費側 JS はありません(挙動はコントローラが担います)。
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--multi-select"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
input
必須
|
フィルタを駆動し aria-activedescendant でアクティブ選択肢を追跡するコンボボックスのテキスト input。 |
data-stimeo--multi-select-target="input" |
list
必須
|
hidden で開閉する role=listbox(aria-multiselectable)のポップアップ。 |
data-stimeo--multi-select-target="list" |
option
|
role=option。hidden でフィルタされ aria-selected で選択を切り替える。 |
data-stimeo--multi-select-target="option" |
tags
必須
|
選択済みチップを保持するコンテナ。1 つの roving Tab 停止位置として操作する。 | data-stimeo--multi-select-target="tags" |
tag
|
選択中の選択肢 1 件を表す削除可能なチップ。 | data-stimeo--multi-select-target="tag" |
tagTemplate
|
各チップ(ラベルと削除ボタン)を生成する複製元の <template>。 |
data-stimeo--multi-select-target="tagTemplate" |
status
|
選択肢の選択・削除を読み上げる live 領域。 | data-stimeo--multi-select-target="status" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
max
|
選択可能な最大件数(0 = 無制限、既定 0)。 | data-stimeo--multi-select-max-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
close
|
リストを閉じ、アクティブ選択肢を解除する。 | stimeo--multi-select#close |
filter
|
input の部分一致で選択肢を絞り込み、リストを開き、アクティブ選択肢を再設定して filter を発火する。 |
stimeo--multi-select#filter |
onKeydown
|
input のキーボード操作(上下矢印の循環、Home/End、Enter で切替、Escape、Backspace/ArrowLeft でチップへ、Tab)を処理する。 | stimeo--multi-select#onKeydown |
open
|
リストを開き、アクティブが無ければ先頭の可視選択肢をアクティブにする。 | stimeo--multi-select#open |
toggleOption
|
クリックされた選択肢の選択を切り替え、チップを追加/削除する。 | stimeo--multi-select#toggleOption |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
選択が変化したときに発火。detail は { values }(選択肢順の選択値)。 |
stimeo--multi-select:change |
filter
|
フィルタ時に発火。detail は { query }(入力された部分文字列。非同期候補取得向け)。 |
stimeo--multi-select:filter |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-expanded |
入力 | リストの開閉状態。 |
aria-activedescendant |
入力 | アクティブ候補の id(無いときは除去)。 |
aria-selected |
候補 | 選択中の候補に "true"。 |
data-active |
候補 | アクティブ候補に付与(CSS ハイライト用)。 |
data-stimeo--multi-select-empty |
コントローラ要素 | フィルタが 0 件のとき付与。 |