⌨️

Keyboard の Up / Down Arrow でフォーカスを移動する HTML Attribute を定義する

2024/03/20に公開
<!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-groupkeyboard-focusable を使っているが、ここは別になんでもいい。React などで react/no-unknown-property が出ないようにしたいなら、data- prefix を付けるか、あるいは ESLint の Config を設定するとか。

要素の追加が行われたり、フォーカスしている要素が onKeyDown をハンドリングしていても、基本的には正常に動作する。window まで伝播してきたイベントによって動作するからだ。逆に、onKeyDown でとくに理由もなく event.stopPropagation() などすると動作しないと思う。

また、keyboard-focus-groupkeyboard-focusable の代わりに [role=menu][role=menuitem] を使う手もある。role によって Keyboard Interaction が決定するという実装は、HTML 要素だけを見たときにそこに Keyboard Interaction に関する動作が定義されていると分かりづらくなる。keyboard-focus-groupkeyboard-focusable が書いてあれば、実装を読まなくとも意図はわかるかもしれない。


React で ref や id を駆使していい感じに Keyboard Interaction を実装するの、個人的には結構不毛に感じる。無理に React を使わなくても簡単に実装する方法があるなら、そちらを使ったら良い。

Discussion