この前、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"});
});
}
}
};
おわりに
すこし地味な実装ですが、思った通りに処理が動くのはやはり嬉しいですね。
エンジニアの喜びを感じた瞬間でした。
記事を読んで興味を持った方はぜひコチラ↓
株式会社ロジカルスタジオ's job postings