1
/
5

SelectBoxのキーボード操作を再現してみた


この前、JSのある実装に少してこずったのでそちらについて紹介したいと思います。

multiple属性で複数選択可能にしたselectの選択移動をキーボード操作で行うというものです。

「そんなのデフォルトで、方向キーで移動できるよ」と思いますが、

デフォルトでは上方向キー(↑)と下方向キー(↓)でしか、選択移動できなかったので

今回の実装では、左方向キー(←)と右方向キー(→)でも上記と同じ操作ができるようにしました。

まずはざっくり準備

※今回の記事はReactでの説明になります。

キーボード操作のイベントハンドラを記述するために以下のように準備します。

DOMへの参照をrefで行い、onkeyDownに今回行う処理の関数を作成し設定します。

function App() {
 const selectRef = useRef(null);

 const onKeyDownSelect = (e) => {
 // ここに書いていきます
};


return (
  <select multiple onKeyDown={(e) => onKeyDownSelect(e)} ref={selectRef}>
   <option>りんご</option>
   <option>ごりら</option>
   <option>らっぱ</option>
   <option>ぱんだ</option>
   <option>だんご</option>
   <option>ごりら</option>
   <option>らいおん</option>
  </select>
 );
}

イベントハンドラ

refで参照したDOMからselectタグ内のオプション配列を取得し、選択したオプションのindexをcurrentIndexとします。

そしてキーボード操作が行われるごとに、選択値を配列のindexを操作しながら移動させます。

const selectRef = useRef(null);

const onKeyDownSelect = (e) => {
 const selectElement = selectRef.current;
 if (!selectElement) return;

 const options = selectElement.options;
 const currentIndex = Array.from(options).findIndex(
  (option) => option.selected
 );

 if (e.key === "ArrowRight" || e.key === "ArrowDown") {
  e.preventDefault();
  if (currentIndex < options.length - 1) {
   options[currentIndex].selected = false;
   options[currentIndex + 1].selected = true;
  }
 }

 if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
  e.preventDefault();
  if (currentIndex > 0) {
   options[currentIndex].selected = false;
   options[currentIndex - 1].selected = true;
  }
 }
};

問題発生

これで矢印キー左/右でも選択する値を変更できるようになりました!が…

オプションが多数あり、selectボックス内にオプションの値が収まっていない場合、選択移動はされるものの画面内に表示されません。

これでは何が選択されているかわからなくなります。

よって次にscrollIntoViewメソッドを使って、選択されている値が画面に収まるよう修正します。

さらにscrollIntoView処理をrequestAnimationFrameメソッドの引数にいれることで、他の処理の影響を受けることなく、最適なタイミングで処理が行われるようにします。

結果、ブラウザのフレーム更新ごとに処理が実行されるので、選択移動ごとにガタつくことなく、滑らかな動きを実現できました。

const onKeyDownSelect = (e) => {
 const selectElement = selectRef.current;
 if (!selectElement) return;

 const options = selectElement.options;
 const currentIndex = Array.from(options).findIndex(
  (option) => option.selected
 );

 if (e.key === "ArrowRight" || e.key === "ArrowDown") {
  e.preventDefault();
  if (currentIndex < options.length - 1) {
   options[currentIndex].selected = false;
   options[currentIndex + 1].selected = true;
   requestAnimationFrame(() => {
    options[currentIndex + 1].scrollIntoView({block: "nearest"});
   });
  }
 }

 if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
  e.preventDefault();
  if (currentIndex > 0) {
   options[currentIndex].selected = false;
   options[currentIndex - 1].selected = true;
   requestAnimationFrame(() => {
    options[currentIndex - 1].scrollIntoView({block: "nearest"});
   });
  }
 }
};

おわりに

すこし地味な実装ですが、思った通りに処理が動くのはやはり嬉しいですね。

エンジニアの喜びを感じた瞬間でした。

 

記事を読んで興味を持った方はぜひコチラ↓

インターンシップ
2026年卒!急成長の会社でスペシャリストを目指すインターン生募集!!
◆『技術のロジカルスタジオ』 システムに強いクリエイティブプロダクション  当社は、ソフトウェア開発会社として培った技術力を強みにディレクションからデザイン、システム開発、コーディング、メンテナンスまでワンストップで請け負うクリエイティブプロダクションです。 企画提案からディレクションを行い、イラスト・グラフィック・動画制作などのクリエイティブから、データベースが絡むシステム構築まで幅広くカバーできる強みを生かし、さらなる業務拡大を目指しています。 ◆コーポレートサイト https://logical-studio.com/
株式会社ロジカルスタジオ



Invitation from 株式会社ロジカルスタジオ
If this story triggered your interest, have a chat with the team?
株式会社ロジカルスタジオ's job postings
12 Likes
12 Likes

Weekly ranking

Show other rankings
Like 金 光洙's Story
Let 金 光洙's company know you're interested in their content