⌨️
Keyboard の Up / Down Arrow でフォーカスを移動する HTML Attribute を定義する
<!doctype html>
<html>
<head>
<script>
(function () {
const moveFocusFunc = (movement) => {
return (event) => {
const current = event.target;
const group = event.target.closest('[keyboard-focus-group]');
if (!group) return;
const collection = group.querySelectorAll('[keyboard-focusable]');
const length = collection.length;
if (length == 0) return;
for (let index = 0; index < length; index++) {
if (collection.item(index) == current)
collection.item((index + movement + length) % length)?.focus();
}
};
};
const keyMaps = {
ArrowDown: moveFocusFunc(+1),
ArrowUp: moveFocusFunc(-1),
};
window.addEventListener("keydown", (event) => {
const action = keyMaps[event.key];
if (action) {
event.preventDefault();
event.stopPropagation();
action(event);
}
});
})();
</script>
</head>
<body>
<div role="menu" keyboard-focus-group>
<div role="menuitem" keyboard-focusable tabindex="0">Item 1</div>
<div role="menuitem" keyboard-focusable tabindex="0">Item 1</div>
<div role="menuitem" keyboard-focusable tabindex="0">Item 1</div>
<div role="menuitem" keyboard-focusable tabindex="0">Item 1</div>
</div>
</body>
</html>
こんな感じで動作する。
keyboard-focus-group
と keyboard-focusable
を使っているが、ここは別になんでもいい。React などで react/no-unknown-property が出ないようにしたいなら、data-
prefix を付けるか、あるいは ESLint の Config を設定するとか。
要素の追加が行われたり、フォーカスしている要素が onKeyDown をハンドリングしていても、基本的には正常に動作する。window まで伝播してきたイベントによって動作するからだ。逆に、onKeyDown でとくに理由もなく event.stopPropagation()
などすると動作しないと思う。
また、keyboard-focus-group
と keyboard-focusable
の代わりに [role=menu]
と [role=menuitem]
を使う手もある。role によって Keyboard Interaction が決定するという実装は、HTML 要素だけを見たときにそこに Keyboard Interaction に関する動作が定義されていると分かりづらくなる。keyboard-focus-group
と keyboard-focusable
が書いてあれば、実装を読まなくとも意図はわかるかもしれない。
React で ref や id を駆使していい感じに Keyboard Interaction を実装するの、個人的には結構不毛に感じる。無理に React を使わなくても簡単に実装する方法があるなら、そちらを使ったら良い。
Discussion