tab forcus – モーダル対応

この記事を読むのにかかる時間 1未満

概要

アクセシビリティの観点から、モーダルのあるページではtab移動をjsを使って制御する必要がある。
(1)モーダルを開くと、tab移動がモーダル内でループする
(2)モーダルを閉じると、tabの位置が下のカーソル位置に戻る
(3)escキーでモーダルを閉じる
これらの処理を記述していない場合、タブはモーダルの背景を移動することになるため、使えないことになる。

jsでは、モーダルの開閉処理時に、以下のtabフォーカス処理を追加して実装する。

(1)モーダルを開くと、tab移動がモーダル内でループする
モーダルを開いた際にtab移動を制御する関数:handleModalTabTrapを使ってループさせる処理を実装

(2)モーダルを閉じると、tabの位置が下のカーソル位置に戻る
モーダルを開く前にカーソルの当たった位置を記憶する変数:lastFocuseTargetを用意しておく。
モーダルを閉じた時にそのlastFocuseTargetにカーソルを当てる。

(3)escキーでモーダルを閉じる
ecsキーで押した時の挙動をtabキーのハンドリングに含めることで、モーダルを閉じることができる。

コード

グローバル変数の準備

var lastFocuseTarget = null; //モーダルに入る直前のフォーカスポイント。モーダルを閉じた時に元に戻る時に使用
var KeyHandler = null; //tabキーの操作があったかを記憶する変数

モーダルを閉じる関数に追加する処理

function closeModal() {

  ・・・モーダルを閉じる処理・・・

 //イベントリスナーの解除
  if (KeyHandler) {
    $(document).off('keydown', KeyHandler);
    KeyHandler = null;
  }
 //モーダルを閉じた後にフォーカスを元の位置に戻す
  if (lastFocuseTarget && typeof lastFocuseTarget.focus === 'function') {
    try { $(lastFocuseTarget).focus(); }
    catch (err) {$(document.body).focus(); } 
  } else {
    $('button, a').blur(); //blurでフォーカスを外す
  }
}

モーダルのセット関数

function setModal(){
  $('body').on('click','.cmn_modal_btn', (e) => {
  ・・・モーダルを開く処理・・・
    lastFocuseTarget = document.activeElement; //現在フォーカスが当たっているエレメントを保管
    setModalTabFocus($(`#${target}`));  } //モーダル用のtabフォーカス関数を実行

  // モーダルを閉じる
  $('body').on('click','.cmn_modal_close, .cmn_modal_bg', (e) => {
    closeModal();
    return false;
  });
}

モーダル内にいる時のループ処理

function handleModalTabTrap(e, $modal) {
  var nodes = getFocusableElements($modal[0]); //$modal[0]はjQueryで抽出したhtmlコードそのもの
  
  var first = nodes[0];
  var last = nodes[nodes.length - 1];
  var active = document.activeElement; //現在アクティブなエレメント
  
  if (e.shiftKey) { //シフトキーを押したとき(tabが逆方向に移動するとき)
    if (active === first || active === $modal[0]) { //最初のアイテムにフォーカスが当たっているとき(移動する前)
      e.preventDefault(); //キーの移動動作を停止
      $(last).focus();
    }
  } else { //tabの純方法
    if (active === last) { //最後のアイテムにフォーカスが当たっているとき(移動する前)
      e.preventDefault(); //キーの移動動作を停止
      $(first).focus();
    }
  }
}

モーダル内にいる時のtabとescキーの処理

function setModalTabFocus($modal) {
  if (!$modal.length) return;
  
  var focusable = getFocusableElements($modal[0]); //関数で抽出した、フォーカス可能なアイテムの配列。
  $(focusable[0]).focus();
  
  KeyHandler = function(e) {
    var key = e.key || e.keyCode;
    
    if (key === 'Escape' || key === 'Esc' || key === 27) {
      e.preventDefault();
      closeModal(); //モーダルを閉じる処理を実行
      return;
    }
    
    if (key === 'Tab' || key === 9) {
      handleModalTabTrap(e, $modal); //モーダル内のタブの処理
    }
  };
  
  $(document).on('keydown', KeyHandler);
}

フォーカス可能なエレメントを抽出する関数

function getFocusableElements(container) {
  var selectors = 'a[href], input:not([disabled]):not([type="hidden"]), select:not([disabled]),
         textarea:not([disabled]), button:not([disabled]), iframe, [tabindex]:not([tabindex="-1"])'; //主なtabターゲット
  var $container = $(container);
  if (!$container.length) return []; //エラー対応
  return $container.find(selectors).filter(':visible').toArray();
  //モーダルhtml内でtabターゲットが見つかれば配列に保管。:visibleは見えているものを対象(display:noneなどは対象にならない)
}