Tabキー移動でのフォーカスを、特定の範囲内に制限するjQuery

ご存じの方も多いかもしれませんが、WebサイトでTabキーを押下すると、ボタン・リンク・フォームコントロールなどに順番にフォーカスが当たります。 (Shiftキーを押したままTabキーを押すと、逆に辿る)

ここで問題になるのが、モバイルメニューなどのモーダルメニューを開いている時で、ユーザーとしてはモーダルメニュー内のリンクだけを辿りたいはずなので、裏側のコンテンツにフォーカスを当てる必要がありません。

ですが何も対策をしていないと、モーダルメニュー内の最後のメニューリンクを過ぎたらフォーカスが裏側のコンテンツに移動してしまって、「今どこにフォーカス当たってるんや!」「モーダルメニューにフォーカス戻すの大変やんけ!」とストレスを与えてしまいます。

これを解決するための理想的な挙動の一つとしては、以下のようなものかと思います。

  • モバイルメニューを開いた直後は、閉じるボタンにフォーカスが当たっている事。
  • モバイルメニューを開いている状態では、Tabキー(+Shiftキー)でのフォーカス移動をモバイルメニュー内の要素に限る事。
  • モバイルメニュー内の最後の要素を過ぎたら、次のフォーカスはモーダルメニュー内の最初の要素に戻る事。逆に、最初の要素でTab+Shiftキーを押下した時(フォーカスを逆に辿る時)は、最後の要素にフォーカスを当てる事。

以上の要件を満たすためのjQueryを書いてみました。

$( function() {
    // モーダルメニュー
    var modalMenu = $( '#modal_menu' );
​
    // モーダルメニューが開いているかどうか
    var isOpenModalMenu;
​
    // モーダルメニュー内でフォーカスを当てたい要素リスト
    var modalMenuElements = modalMenu.find( 'a, button' );
​
    // モーダルメニューを開く
    $( '#open_modal' ).click( function() {
        isOpenModalMenu = true;
        $( '#close_modal' ).focus();
        modalMenu.show();
    });
​
    // モーダルメニューを閉じる
    $( '#close_modal' ).click( function() {
        isOpenModalMenu = false;
        modalMenu.hide();
    });
​
    // キーイベント
    $( document ).keydown( function( event ) {
​
        // モーダルメニューが開かれていた場合
        if ( showDrawer ) {
​
            // フォーカスが当たっている要素
            var activeEl = document.activeElement;
​
            // モーダルメニュー内でフォーカスを当てたい最初の要素
            var firstEl = modalMenuElements[0];
​
            // モーダルメニュー内でフォーカスを当てたい最後の要素
            var lastEl = modalMenuElements[ modalMenuElements.length - 1 ];
​
            // タブキーを押されたかどうか
            var tabKey = ( 9 === event.keyCode );
​
            // Shiftキーが押されているかどうか
            var shiftKey = event.shiftKey;
​
            // 最後の要素でタブキーが押された場合は、最初の要素にフォーカスを当てる
            if ( ! shiftKey && tabKey && lastEl === activeEl ) {
                event.preventDefault();
                firstEl.focus();
            }
​
            // 最初の要素でタブキー+Shiftキーが押された場合は、最後の要素にフォーカスを当てる
            if ( shiftKey && tabKey && firstEl === activeEl ) {
                event.preventDefault();
                lastEl.focus();
            }
        }
    });
});

参考

Twenty Twenty(WordPress公式テーマ) https://github.com/WordPress/twentytwenty/blob/db82df5cb0293db15911b045471cee3f5f0ac389/assets/js/index.js#L442

※素のjavascriptをスラスラ書ける人は、こちらの方が参考になるかもしれません。