JavaScript:エンターキーでフォーカス移動
ブラウザでのエンターキーでのフォーカス移動はやはり一般的ではないのでしょうか?
筆者がそのようなコードはないかネットを探してみたところJavaScriptでコードでnextElementSiblingを利用したものが見つかりました。
ただ、これだと連続してinput要素がなければ使えません。また、textAreaには対応できていなさそうです。他にはjQueryで実装したものも見つかりましたが、JavaScriptだけでできないかと考えて、クラスとES2015のMapオブジェクトを使ってコードを作成してみました。
コードの稼働はブログ修正日時時点のバージョンのGoogle Chromeでのみ確認しております。他のブラウザで挙動がおかしかったらすみません。
サンプル
サンプルは次のようになります。「お名前」の部分にフォーカスを当てた後は、エンターキーで各要素への移動ができます。「同伴者」の部分だけ「申し込み」ボタンの後にフォーカスがあたるように、順序を変えています。また「18:00~」のラジオボタンにはフォーカスが当たらないようにしています。
サンプル
時間
仕組み
仕組みとしては次のようになっています。
- querySelectorAllで対象となる要素を取得します
ここではclass="intputs"のものを取得しています。
- 取得した要素を順序付けします
順序付けにはtabIndex属性を使います。これに入っている数値の大小で並べ替えを行います。また0以下の値が入っていた場合はフォーカスの対象にしません。
- 順序をMapクラスにセットします。
Mapのキーに「移動前」のエレメント、値に「移動先」のエレメントを設定します
- 各要素にエンターキーイベントをセットします
エンターキーが押されたとき、イベント発生元のオブジェクトをキーにして、Mapから値を取得すると次にフォーカスする要素が返ります。その要素からfocus()メソッドを呼びます。
先の例では「同伴者」のtabindexを99にすることで、「申し込み」ボタンの後にフォーカスがあたるようにしています。またtabindexに-1をつけた「18:00~」にはフォーカスは自動で当たりません。
GoogleChromeの環境ではtabindexを指定していない場合値は0に設定されるようです。同じtabindexだった場合は要素を取得するメソッドの順序に依存します。
そして、おそらくこれがエンターキーでのフォーカス移動が一般的にならない理由のひとつだと思いますが、テキストエリアに設定してしまうと、エンターキーでフォーカスが移ってしまい改行がうまくできません。
なのでイベント処理の途中テキストエリアだけは別処理をします。ENTERならフォーカス移動、ALT+ENTERなら改行をします。さらに、テキストエリアにフォーカスが当たる際に preventDefault()を使って、デフォルトのイベントが伝播しないようにしないと、残っているエンターキーイベントによって、テキストエリアに意図しない改行が入ってしまいます。
コーディング
先のサンプルのソースは次のようになっています。
sample.html
<div>
<p><label>お名前<input class="inputs" type="text" name="mainname"/></label></p>
<p><label>同伴者<input class="inputs" type="text" name="subname" tabindex="99"/></label></p>
<p><label>プラン<select class="inputs" name="plan">
<option value="A">Aプラン</option>
<option value="B">Bプラン</option>
</select></label></p>
<p>時間<br>
<label><input class="inputs" type="radio" name="hour" value="15">15:00~</label><br>
<label><input class="inputs" type="radio" name="hour" value="18" tabindex="-1">18:00~</label>
</p>
<p><label>利用規約に同意します。<input class="inputs" type="checkbox"></label></p>
<button class="inputs">申し込み</button>
<p>
その他要望<br>
<textarea class="inputs" name="memo" rows="5" cols="30" tabindex="100">このテキストエリアはエンターキーでフォーカス移動、ALT+エンターキーで改行します。</textarea>
</p>
</div>
次にJavaScript部です。もし既にhtmlを作成済みなら、次のコードをコピーしてbodyタグが閉じる前あたりに貼り付けて、querySelectorAllの部分を修正すれば、そのまま使えます。
sample.html
<!-- sample.htmlの続き -->
<script>
class FocusOrder {
constructor() {
this.nextElements = new Map();//tabOrderを記憶するマップ
this.init=false;
}
initOrder() {
//初期化
//DOMを読み込んだ後に実行してください。
if (this.init) {
//このコードをは複数回呼ばれるのを想定していません
console.log('すでに初期化済みです');
return;
}
this.init = true;
//ターゲットとなる要素をここに指定してください。
let target = document.querySelectorAll(".inputs");
//getElementsByClassName等で取得することも可能です
//let target = document.getElementsByClassName("inputs");
//tabIndexにマイナスを指定してあったら除外
let wk1 = [];
for (let i = 0; i < target.length; i++) {
if ( 0 <= target[i].tabIndex) {
wk1.push(target[i]);
}
}
//tabIndex順にソート
let wk2=wk1.sort((a,b) => {
if (a.tabIndex == b.tabIndex) {
return 0;
} else if(a.tabIndex > b.tabIndex) {
return 1;
} else {
return -1;
}
});
//次にフォーカスするエレメントをMapにセット
for (let i =0; i < wk2.length-1; i++) {
this.nextElements.set(wk2[i],wk2[i+1]);
}
this.nextElements.set(wk2[wk2.length-1],wk2[0]);
//各エレメントにイベントをセット
const keyevent = event => {
if (event.key === 'Enter') {
event.preventDefault();//eneterキーイベントの他の挙動を止めます。
if (event.srcElement) {
if (event.srcElement.tagName.toLowerCase().startsWith('textarea')) {
if (event.altKey==true) {
//要素がテキストエリアでALTがついていたら現在の位置に改行(\n)を入れて抜ける
const intPosition = event.srcElement.selectionStart;
const wk = event.srcElement.value;
event.srcElement.value=wk.substring(0,intPosition)+'\n'+wk.substring(intPosition);
//キャレットの位置を修正
event.srcElement.selectionStart=intPosition+1;
event.srcElement.selectionEnd=intPosition+1;
return;
}
}
}
let elTarget = event.target;
for(let i = 0, max=this.nextElements.size; i < max; i++) {
let elWk = this.nextElements.get(elTarget)
if (elWk.disabled || elWk.getBoundingClientRect().width == 0) {
//要素が使用不能かdisplay noneの際は次の要素を検索
elTarget=elWk;
} else {
elWk.focus();
break;
}
}
}
};
for (let i =0; i < wk2.length; i++) {
wk2[i].onkeydown=function(event){keyevent(event)};
}
}
};
//クラスのインスタンスを作成
//クラスの定義の後に記述する必要があります。
let f = new FocusOrder();
//初期化
f.initOrder();
<script>
...
tabindexについて
MDN:TabIndexによれば、tabindexの最大値は32767ということでした。
エンターキー用にtabindexを設定しましたが、tabindexなので当然タブキーでのフォーカス移動の順序も変更することになりますので注意が必要です。関連して、 MDNのページでは「支援技術に頼っている人がページコンテンツを移動したり操作したりすることが難しくなります」という理由で、tabindexに0より大きい値を設定しないで(-1以外使わないで)下さいとも書いてありました。技術支援が必要な方の利用を想定しているページではtabindexを、data-属性などで置き換えてください。
ちなみにtabindexのかわりにdata-属性を使ったコードはgithubで紹介していますので、よろしければご覧になってください。またコードではコンストラクタでセレクタ文字列を受け取り、そのまま初期化するようにしています。