開閉メニューをフルキーボードアクセスにする難しさについて

開発中のWordPressテーマ「mosir」の、すべてのナビゲーションをキーボードで操作できるよう改修していたら日が暮れていました。
WordPress公式テーマの必須条件には、以下のように書かれています。
- すべてのコントロールとリンクはキーボードを使用してアクセスできる必要があります。
- マウスで操作できるすべてのコントロールは、デバイスや画面サイズに関係なく、キーボードでも操作できる必要があります。これには、小型画面、モバイル、その他のタッチスクリーンデバイス向けのレスポンシブバージョンが含まれますが、これに限定されません。
常に見えているナビゲーションであれば、フォーカスしたときの外観に配慮するだけでよいのですが、非表示になっていてホバーやクリックで開閉するメニューがあると、急激に難しくなります。
メガメニュー(ドロップダウンメニュー)


mosirは、日本のグローバル企業のウェブサイトのヘッダを調査し、複雑なレイアウトでも再現できるように多彩なナビゲーションを実装しています。
WordPressのメニュー機能は可能な限り使った方がよいです。 aria-label や aria-current="page" など、手入力すると手間がかかるARIA属性を自動でサポートしてくれます。
メガメニュー(ドロップダウンメニュー)についても、実装しているのであればキーボード操作に対応しなければなりません。WordPressのメニューの各要素は .menu-item なので、ナビゲーションの一階層目の .menu-item にホバーしているか、それよりも下層のa要素にフォーカスしているかをトリガーにしました。
:has() 疑似セレクタのおかげでJavaScriptが要らなくなりましたね。
.p-megaMenu__nav > .menu-item:hover > .sub-menu,
.p-megaMenu__nav > .menu-item:has(a:focus) > .sub-menu {
padding-block: var(--spacing-md);
transition: var(--transition-cubic);
}
.p-megaMenu__nav > .menu-item:hover > .sub-menu::after,
.p-megaMenu__nav > .menu-item:has(a:focus) > .sub-menu::after {
opacity: 1;
transform: scaleY(1);
transition: var(--transition-cubic);
}
.p-megaMenu__nav > .menu-item:hover > .sub-menu > *,
.p-megaMenu__nav > .menu-item:has(a:focus) > .sub-menu > * {
height: auto;
opacity: 1;
transform: translateY(0);
transition: var(--transition-cubic);
}
ドロワー
トグルボタンの位置

ドロワーを開閉するボタンは、私が知っている限りの日本国の慣習では、ヘッダの右上にあります。ということは、Tabキーでフォーカスしてこのボタンに到達しようとすると、画面上で見たまま、つまり、ヘッダの上段メニューを経由した後にボタンにフォーカスすることが期待されます。
ヘッダに何が入るかわからないテーマでは、動的にフォーカスをコントロールできません(いずれにしても魔改造は避けるべきです)。なのでボタンはHTMLのソースコード内でも以下の通り、見た目と同じ場所に設置しなければ意図通りの結果になりません。
ロゴ
矢印がついてるヘッダメニュー
パイプ区切りのヘッダメニュー
ドロワーを開閉するボタン
ドロワー本体の位置
コーディングの経験が長い人はわかると思いますが、最大幅を指定して中央寄せにしたヘッダの中に、全画面を埋めるドロワーを含めるのは、とても難しいです。というかやらない方がいいです。
なので、結果としてドロワーはヘッダから出すことになり、トグルボタンとドロワーは離れた位置にコードを書き、遠隔操作することになります。
ヘッダ
└ ドロワーを開閉するボタン
...
ドロワー
こうなると、チェックボックスを使う今どきのCSSテクニックで実装するのは無理です。いずれにしても mosir のドロワーはアニメーションの遅延(アニメーションがちゃんと終わってからARIA属性を切り替える)に対応しているのでJavaScript一択なのですが。
開いたときのフォーカス位置

これまで実務ではフルキーボードアクセスを意識したことがなかったのですが、ドロワーを開いたあと、ブラウザ内のカーソル位置は、ドロワーの先頭のクリック可能な要素へ移動していなければなりません。 mosir の場合、ヘッダと同じ位置にある閉じるボタンです。
これについてはドロワーを初期化するときにドロワー内の最初の a, button, input 要素を取得しておいて、開いたあとにフォーカスさせています。 querySelector は最初の要素だけを単体で取得するのでこういう処理に向いています。
const open_focus = nav.querySelector('a, button, input');
ドロワーからフォーカスが出るのを防ぐ

キーボードでドロワーを開き、そのままTabキーを連打していくと閉じるボタンに到達します。恥ずかしながらこれまで、ドロワーの下部にある閉じるボタンは意味がないと思っていました。
しかし、このままTabキーを連打すると、黒いカバーがかかっている本文エリアをフォーカスしてしまいます。対策がわからなくて調べたのですが、今は対象の要素以下を反応しない状態にする、inert(イナート)属性というものがあることがわかりました。ドロワーを開いているときだけ、ドロワー以外にこの属性をつければフォーカスされません。
HTML inert グローバル属性 – HTML | MDN
ということは、ドロワー本体の位置はさらに制限されてきます。ドロワー以外のすべて(ヘッダとフッタも)を何らかのdiv要素で囲み、 inert 属性を付与できるようにしなければならないです。
ドロワーを閉じているとき
何らかのdiv要素
ヘッダ
本文
フッタ
ドロワー + inert="inert"
ドロワーが開いたとき
何らかのdiv要素 + inert="inert"
ドロワー以外全部
ドロワー
Escキーで閉じられるようにするのか
エスケープキーを押したときにドロワーを閉じるという操作も検討しました。が、 aria-hidden で隠される前提の要素内でエスケープキーのバインディングをしてはいけないらしいのでやめました。
正直しんどい(二回目)
mosir でフルキーボードアクセスを実装した記録は以上です。もちろんですが、他のARIA属性も対応済みです。VoiceOverでの確認もしました。
勉強にはなりますが…つらすぎます。それほどアクセシビリティに関心がない案件でこれをやったら工数オーバーで怒られてしまいそうです。
ただ、世の中には特殊なブラウジングをする層が少なからず存在します。SNSでも目にしますし、職場の会話でも耳にします。Steam対応のゲーム機に入っているWindowsでアクセスするとか、タブレットにBluetoothキーボードをつないでフルキーボードアクセスとか、老眼になってきたのでOSのUI全体を大幅に拡大してるとか、持っているノートPCのOSの拡大率が最初から大きいとか(TinkPadェ)…すべての人に自由を与えられてこそのアクセシビリティなので、可能な限りがんばります。




