オートコンプリート
入力に応じて候補リストを絞り込み表示。↑↓キーやタップで選択でき、選ぶと入力欄に反映される。都道府県など日本語ダミー候補つき。
プレビュー
操作実際に入力して試せます。
仕様書
- 向く場面
- 都道府県・駅名など数十件の有限候補から選ぶ入力
- 候補に無い自由入力も許したい欄
- ↑↓と Enter で確定させたい入力補助
- 避ける場面
- 必ずリストの値でなければならない入力(select)
- 候補が数百件超やサーバー検索(デバウンス等が要る)
- 構成要素
- ラベル
- 入力欄
- 候補リスト
- 候補行
- ヒント(件数)
- 持っている状態
- 通常
- ホバー
- フォーカス
- 押下中
- 空
- キーボード
入力で候補が開く。↓↑ で候補を移動(端で止まる)、Enter で確定、Esc で閉じる
- 支援技術
APG combobox: role=combobox・aria-autocomplete=list・aria-expanded・aria-controls、候補は role=option、aria-activedescendant でハイライト位置を伝え閉じるときに外す。件数ヒントは aria-live=polite
- 動き
枠色 200ms、候補の背景 100ms。reduced-motion で無効
デザインの要点
- 候補の確定は click でなく mousedown+preventDefault で拾う。click を待つと先に blur でリストが閉じて選べない
- フォーカスは入力欄に残し、ハイライト位置は aria-activedescendant で伝える。候補へフォーカスを移すと打ち直しが途切れる
- 候補に無い自由入力もそのまま残す。必ず候補から選ばせたいならこの型でなく select
- 件数は「N件の候補」と文言で出す。リストの長さを目で数えさせない
使いどころ
都道府県や駅名のように候補が有限で決まっているデータに向く。この実装は20件のダミー候補をJS内に配列で持ち、入力のたびに includes() で絞り込む。候補にない文字列でもinputの値としてはそのまま残る(選択を強制しない)ため、リストにない自由記述も許容する「候補つきテキスト入力」として使える点がポイントで、都道府県コードのように「必ずリストの値でなければならない」用途にはむしろselect要素の方が適切である。この実装のようにローカル配列を毎回全走査するやり方は、候補が数百件を超えたり、サーバー検索に切り替える必要が出てきたりすると、デバウンスや読み込み中表示、リクエストの中断(AbortController)を別途足さないと破綻する。今のままはあくまで数十件規模の静的候補向けである。
仕組み
input には role="combobox" と aria-autocomplete="list"、aria-expanded、aria-controls="ac-list" が付き、候補は ul[role="listbox"] の中に li[role="option"] として都度生成される。選択の確定は click ではなく mousedown で拾っている点が要で、これは input が blur するタイミングが mousedown の直後(click より先)に来るため、click まで待つと blur によって候補リストがすでに閉じてしまい選択が成立しないという典型的な競合を避ける工夫である。同じ理由でタッチ操作も touchstart + preventDefault で処理している。キーボードは ArrowUp/ArrowDown で activeIdx をMath.min/maxでクランプしながら移動、Enterで確定、Escapeで閉じる。blur時は150msのsetTimeoutで閉じる処理を遅延させており、これは直前のmousedown/touchstartによる選択処理が先に完了する猶予を作るための調整である。
実装の注意
このコードを「簡潔にしよう」と mousedown を click に置き換えると、input の blur が先に発火してリストが hidden になり、選択そのものが効かなくなるという壊れ方をする。一見動くように見えて実機のタイミング差で再現しにくい種類のバグなので、mousedown+preventDefaultの意図はコメントなどで残しておく価値がある。blurの150ms遅延はマジックナンバーであり、低速端末や重い再描画が絡むと選択処理より先にタイムアウトが発火してリストが消えてしまう可能性がゼロではない。より頑健にするなら、タイマーではなく「選択処理中フラグ」でblurのクローズ処理を明示的にガードする方式に置き換えるとよい。また includes() による部分一致は「駅」のような1文字でもほぼ全候補がヒットするため、意図が絞り込みというより「含む語で全出し」になりやすく、前方一致にするか、候補数が多い場合はマッチ度でソートする改善が要る。
アクセシビリティ
role="combobox"・aria-autocomplete="list"・aria-expanded・aria-controls の組み合わせは、WAI-ARIAのlistbox付きコンボボックスパターンの土台として正しい。setActive() で aria-activedescendant を該当オプションのidに更新している点も、フォーカスをinputに残したままハイライト位置をスクリーンリーダーに伝える正しいテクニックである。リストを閉じる処理(selectItem・Escape・blur)は closeList() に集約し、aria-expanded を false に戻すのと対にして removeAttribute("aria-activedescendant") も呼んでいる。これを忘れると、非表示になった過去のオプションIDを参照し続ける。件数のヒント(#ac-hint)は aria-live="polite" なので「5件の候補」「候補なし」が入力を妨げずに読み上げられる。なお、ハイライト中の候補にaria-selected="true"を足したくなるが、この実装のような自由入力・手動確定のコンボボックスでは慎重に。矢印キーで眺めているだけの候補はまだ「選択」されておらず、位置はaria-activedescendantが既に伝えている。未確定の候補にselectedを付けると、読み上げが『選択済み』と案内してEnterでの確定と区別できなくなるため、aria-selectedは実際に確定した選択を表すために取っておく(この見本のように付けない判断でよい)。
調整して使う
現状は静的配列のローカルフィルタだが、fetchベースの非同期検索に切り替えるならデバウンスとローディング表示、AbortControllerでの前リクエスト中断が追加要件になる。マッチ方式もincludes()による部分一致のほか、前方一致優先、読み仮名でのマッチなど日本語入力特有の工夫が考えられる。この単一選択のコンボボックスは、選んだ値をチップとして複数保持する「タグ入力」の候補選択部分にもそのまま転用できる構成であり、候補を都道府県・駅名のようにカテゴリでグループ化してrole="group"の見出しをリスト内に挟む拡張も自然である。
コード(コピーして使えます)
<div class="ac-wrap">
<label class="ac-label" for="ac-input">出発地</label>
<div class="ac-field" id="ac-field">
<input
id="ac-input"
class="ac-input"
type="text"
placeholder="都道府県・駅名を入力"
autocomplete="off"
role="combobox"
aria-autocomplete="list"
aria-expanded="false"
aria-controls="ac-list"
/>
<ul class="ac-list" id="ac-list" role="listbox" aria-label="候補" hidden></ul>
</div>
<p class="ac-hint" id="ac-hint" aria-live="polite">入力で候補を絞り込みます</p>
</div>