一覧WB-15-1 · アクセシビリティと操作性

キーボード操作とフォーカス管理

リンク・名前欄・無効なボタン・詳細ボタンを用意します。右の画面でTabとShift+Tabを押して移動を確認し、Enterで詳細を開きます。最後にEscapeで閉じ、元のボタンへ戻ったことを確かめます。キー操作ごとに、左の説明と右の記録が進みます。

説明

キーボード操作では、現在のフォーカス位置と、Tabを押した後の移動先を確認します。見た目がボタンでも、HTMLの要素や状態によって操作方法が変わります。ダイアログを開くとフォーカスを内部へ移し、閉じると元の操作位置へ戻します。

リンク・名前欄・無効なボタン・詳細ボタンを用意します。右の画面でTabとShift+Tabを押して移動を確認し、Enterで詳細を開きます。最後にEscapeで閉じ、元のボタンへ戻ったことを確かめます。キー操作ごとに、左の説明と右の記録が進みます。

1操作対象を用意する

リンクやボタンを並べたkeyboardHTMLから文書を作り、ヘルプへフォーカスを置きます。

(globalThis.doc=createDocument(keyboardHTML)用意したHTMLから文書を作る,doc.getElementById("open").onclick=()=>doc.getElementById("dialog").showModal()モーダルとして開く,doc.getElementById("close").onclick=()=>doc.getElementById("dialog").close()ダイアログを閉じる,watchKeys(),doc.getElementById("help").focus()最初の操作位置,{active:doc.activeElement.id,disabled:doc.getElementById("disabled").disabled,label:doc.getElementById("name").labels[0].textContent})

ヘルプの周囲にフォーカスの枠が付きます。名前欄にはlabelが対応し、準備中のボタンはdisabledです。開く処理はネイティブのdialog.showModalを使い、ダイアログ内の閉じるボタンにautofocusを付けています。

2リンクから名前欄へ

ヘルプにあるフォーカスを、Tabで次へ移します。

Tab表示された対象でキーを押す

名前欄へ移ります。readonlyは値の編集を止めますが、フォーカス移動の対象から外す指定ではありません。表示上の順序とDOMの順序を自然にそろえると、移動先を予測しやすくなります。

3無効なボタンを飛ばす

名前欄でTabを押します。

Tab表示された対象でキーを押す

準備中のdisabledボタンを飛ばし、詳細へ移ります。disabledは操作不可の状態で、通常のTab順にも入りません。無効な理由を伝える必要がある場合は、見える説明も用意します。

4逆方向に移動する

詳細でShift+Tabを押します。

Shift+Tab表示された対象でキーを押す

名前欄へ戻ります。逆方向でも準備中のボタンは対象外です。Tab順を調整するために正のtabindexを大量に付けると、画面やDOMの順序との不一致が起きやすくなります。

5詳細へ戻る

名前欄からTabで詳細へ進みます。

Tab表示された対象でキーを押す

詳細にフォーカスが戻りました。次はこのボタンをキーボードで起動します。ネイティブのbuttonはEnterなどの標準操作を備えています。

6Enterでダイアログを開く

詳細にフォーカスがある状態でEnterを押します。

Enter表示された対象でキーを押す

ダイアログが開き、閉じるボタンへフォーカスが移ります。showModalは背後の文書を通常の操作対象から外し、ダイアログを前面へ置きます。単にdialogにopen属性を付ける表示とは異なります。

7ダイアログ内の次のボタン

閉じるボタンでTabを押します。

Tab表示された対象でキーを押す

ダイアログ内の戻るボタンへ移ります。開いている間は、背後の名前欄や保存ボタンへ操作が抜けないようにします。

8ダイアログ内を逆に移動する

戻るボタンでShift+Tabを押します。

Shift+Tab表示された対象でキーを押す

閉じるボタンへ戻ります。操作対象が複数ある場合にも、フォーカス位置が見えることと、順方向・逆方向の両方で移動できることを確認します。

9Escapeで閉じて元へ戻る

Escapeを押してダイアログを閉じます。

Escape表示された対象でキーを押す

ダイアログが閉じ、起動元の詳細ボタンへ戻ります。閉じた後に操作位置を見失わないよう、開始時の位置と終了時の戻り先を対応させます。

10要素と状態を確認する

操作した要素の種類・tabIndex・disabledを一覧にします。

["help","name","disabled","open","last"].map(id=>{const e=doc.getElementById(id);return{id,element:e.localNameネイティブ要素の種類,tabIndex:e.tabIndexプロパティ値,disabled:Boolean(e.disabled)無効状態}})

disabledボタンのtabIndexプロパティも0ですが、実際のTab移動では飛ばされました。tabIndexの数値だけで操作可能性の全条件は判断できません。要素の種類・無効状態・非表示・モーダルの状態を合わせ、実際のキー操作でも確認します。

まとめ

TabとShift+Tabの移動先を、実際のフォーカス表示で確認しました。

readonlyとdisabledは異なり、tabIndexの値だけでは実際の移動対象を決められません。

モーダルでは内部へ移動し、閉じた後に起動元へ戻れるようにします。

操作履歴と解説を振り返れます。

ブラウザ / 操作・表示

1
(globalThis.doc=createDocument(keyboardHTML),doc.getElementById("open").onclick=()=>doc.getElementById("dialog").showModal(),doc.getElementById("close").onclick=()=>doc.getElementById("dialog").close(),watchKeys(),doc.getElementById("help").focus(),{active:doc.activeElement.id,disabled:doc.getElementById("disabled").disabled,label:doc.getElementById("name").labels[0].textContent})
{
  "active": "help",
  "disabled": true,
  "label": "名前"
}
2
Tab
{
  "active": "name",
  "dialogOpen": false
}
3
Tab
{
  "active": "open",
  "dialogOpen": false
}
4
Shift+Tab
{
  "active": "name",
  "dialogOpen": false
}
5
Tab
{
  "active": "open",
  "dialogOpen": false
}
6
Enter
{
  "active": "close",
  "dialogOpen": true
}
7
Tab
{
  "active": "second",
  "dialogOpen": true
}
8
Shift+Tab
{
  "active": "close",
  "dialogOpen": true
}
9
Escape
{
  "active": "open",
  "dialogOpen": false
}
10
["help","name","disabled","open","last"].map(id=>{const e=doc.getElementById(id);return{id,element:e.localName,tabIndex:e.tabIndex,disabled:Boolean(e.disabled)}})
[
  {
    "id": "help",
    "element": "a",
    "tabIndex": 0,
    "disabled": false
  },
  {
    "id": "name",
    "element": "input",
    "tabIndex": 0,
    "disabled": false
  },
  {
    "id": "disabled",
    "element": "button",
    "tabIndex": 0,
    "disabled": true
  },
  {
    "id": "open",
    "element": "button",
    "tabIndex": 0,
    "disabled": false
  },
  {
    "id": "last",
    "element": "button",
    "tabIndex": 0,
    "disabled": false
  }
]

演習完了