実践Webアクセシビリティ

アクセシブルなドロップダウンメニューの実装:キーボード操作とARIA活用

Tags: アクセシビリティ, ARIA, キーボード操作, JavaScript, HTML, ナビゲーション

はじめに

ドロップダウンメニューは、限られたスペースに多くのナビゲーションリンクやオプションを配置するためにウェブサイトで頻繁に使用されるUIコンポーネントです。しかし、適切に実装されないと、キーボードユーザーやスクリーンリーダーユーザーなど、多くの利用者にとって操作が困難になる可能性があります。

この記事では、HTML、CSS、JavaScript、そしてARIA属性を組み合わせて、すべての利用者が容易に操作できるアクセシブルなドロップダウンメニューを実装する具体的な方法を解説します。

ドロップダウンメニューのアクセシビリティ課題

一般的なドロップダウンメニューの実装では、以下のようなアクセシビリティに関する課題が見られます。

これらの課題により、キーボードのみで操作するユーザー、視覚障害がありスクリーンリーダーを利用するユーザー、運動機能障害を持つユーザーなどが、ウェブサイトのナビゲーションを効果的に利用できなくなります。

なぜドロップダウンメニューのアクセシビリティが重要なのか

Web Content Accessibility Guidelines (WCAG) には、操作可能(Operable)に関する複数の達成基準があります。特にキーボード操作に関する基準は、ドロップダウンメニューのようなインタラクティブなコンポーネントにおいて重要です。

アクセシブルなドロップダウンメニューを実装することは、これらの達成基準を満たし、より多くのユーザーがサイトを快適に利用できるようにするために不可欠です。

アクセシブルなドロップダウンメニューの基本構造(HTML)

セマンティックなHTML構造は、アクセシビリティの基盤となります。ナビゲーションメニューにはnav要素、リスト構造にはulli要素を使用することが推奨されます。

以下に、基本的なHTML構造の例を示します。

<nav aria-label="メインナビゲーション">
  <ul>
    <li><a href="/">ホーム</a></li>
    <li class="has-submenu">
      <button 
        aria-haspopup="true" 
        aria-expanded="false" 
        aria-controls="submenu-about"
      >
        会社情報
      </button>
      <ul id="submenu-about" class="submenu">
        <li><a href="/about/philosophy">経営理念</a></li>
        <li><a href="/about/history">沿革</a></li>
        <li><a href="/about/access">アクセス</a></li>
      </ul>
    </li>
    <li><a href="/products">製品情報</a></li>
    <li class="has-submenu">
      <button 
        aria-haspopup="true" 
        aria-expanded="false" 
        aria-controls="submenu-contact"
      >
        お問い合わせ
      </button>
      <ul id="submenu-contact" class="submenu">
        <li><a href="/contact/form">フォームで問い合わせ</a></li>
        <li><a href="/contact/faq">よくある質問</a></li>
      </ul>
    </li>
  </ul>
</nav>

この構造では、以下の点を考慮しています。

キーボード操作への対応

アクセシブルなドロップダウンメニューでは、マウスを使わずにキーボードだけで開閉、移動、選択ができる必要があります。

1. メニューを開閉するキーボード操作

2. メニュー項目を移動するキーボード操作

3. メニューを閉じるキーボード操作

ARIA属性の活用

前述のHTML構造で紹介したARIA属性は、ドロップダウンメニューの状態や役割を補助技術に正確に伝えるために不可欠です。

JavaScriptによる実装(開閉とキーボード操作の制御)

JavaScriptを使用して、ドロップダウンメニューの開閉ロジックとキーボードイベントのハンドリングを実装します。

document.addEventListener('DOMContentLoaded', () => {
  const nav = document.querySelector('nav[aria-label="メインナビゲーション"]');
  if (!nav) return;

  const subMenuTriggers = nav.querySelectorAll('.has-submenu > button');

  subMenuTriggers.forEach(trigger => {
    const subMenuId = trigger.getAttribute('aria-controls');
    const subMenu = document.getElementById(subMenuId);

    if (!subMenu) return;

    // 初期状態設定 (CSSでデフォルト非表示にしている前提)
    trigger.setAttribute('aria-expanded', 'false');
    subMenu.style.display = 'none'; // スタイルでの制御例

    // メニュー開閉関数
    const toggleSubMenu = (isOpen) => {
      trigger.setAttribute('aria-expanded', isOpen);
      subMenu.style.display = isOpen ? 'block' : 'none'; // スタイルでの制御例

      // メニューを開いた場合、最初の項目にフォーカスを移動
      if (isOpen) {
        const firstItem = subMenu.querySelector('a, button');
        if (firstItem) {
          firstItem.focus();
        }
      } else {
        // メニューを閉じた場合、トリガーにフォーカスを戻す (Escキー以外の場合に検討)
        // ここではEscキーのハンドリングでフォーカス制御を行うため省略
      }
    };

    // クリックイベントで開閉
    trigger.addEventListener('click', () => {
      const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
      toggleSubMenu(!isExpanded);

      // 他の開いているメニューを閉じる処理を追加することも多い
      subMenuTriggers.forEach(otherTrigger => {
          if (otherTrigger !== trigger && otherTrigger.getAttribute('aria-expanded') === 'true') {
              const otherSubMenuId = otherTrigger.getAttribute('aria-controls');
              const otherSubMenu = document.getElementById(otherSubMenuId);
              if(otherSubMenu) {
                  otherTrigger.setAttribute('aria-expanded', 'false');
                  otherSubMenu.style.display = 'none';
              }
          }
      });
    });

    // キーボードイベントのハンドリング
    trigger.addEventListener('keydown', (event) => {
      const isExpanded = trigger.getAttribute('aria-expanded') === 'true';

      switch (event.key) {
        case ' ': // Space
        case 'Enter':
          event.preventDefault(); // デフォルトのボタン動作を抑止
          toggleSubMenu(!isExpanded);
          break;
        case 'ArrowDown': // 下矢印キー
          event.preventDefault();
          if (!isExpanded) {
            toggleSubMenu(true); // メニューが開いていなければ開く
          } else {
            // 既に開いている場合は、最初の項目にフォーカスはtoggleSubMenu内で実施
          }
          break;
        // 他のキーボード操作 (例: 左右矢印で隣のメニューへ移動) もここに追加
      }
    });

    // サブメニュー内のキーボードイベントハンドリング (主に矢印キー、Escキー)
    subMenu.addEventListener('keydown', (event) => {
      const focusableItems = Array.from(subMenu.querySelectorAll('a, button')).filter(item => item.offsetParent !== null); // 表示されているフォーカス可能な要素を取得
      const focusedIndex = focusableItems.indexOf(document.activeElement);

      switch (event.key) {
        case 'ArrowDown': // 下矢印キー
          event.preventDefault();
          if (focusedIndex < focusableItems.length - 1) {
            focusableItems[focusedIndex + 1].focus(); // 次の項目へ
          } else {
            // 最後の項目の場合、先頭に戻るか、何もしないか選択
            // focusableItems[0].focus(); // 先頭に戻る例
          }
          break;
        case 'ArrowUp': // 上矢印キー
          event.preventDefault();
          if (focusedIndex > 0) {
            focusableItems[focusedIndex - 1].focus(); // 前の項目へ
          } else {
             // 最初の項目の場合、最後の項目に戻るか、何もしないか選択
             // focusableItems[focusableItems.length - 1].focus(); // 最後に戻る例
          }
          break;
        case 'Escape': // Escキー
          event.preventDefault();
          toggleSubMenu(false); // メニューを閉じる
          trigger.focus(); // トリガーにフォーカスを戻す
          break;
        case 'Tab': // Tabキー
          // Tabによる移動はデフォルトのブラウザ動作に任せる
          // shift+Tabの場合は前の要素へ移動
          if (event.shiftKey && focusedIndex === 0) {
            // 最初の項目でShift+Tabの場合、メニューを閉じてトリガーにフォーカスを戻す
            toggleSubMenu(false);
            trigger.focus();
            event.preventDefault(); // デフォルトのShift+Tab動作を抑止
          } else if (!event.shiftKey && focusedIndex === focusableItems.length - 1) {
            // 最後の項目でTabの場合、メニューを閉じる
            toggleSubMenu(false);
            // 次の要素へのフォーカス移動はブラウザに任せる
          }
          // それ以外の場合はデフォルトのTab/Shift+Tab動作
          break;
      }
    });

    // サブメニュー外をクリックしたら閉じる処理
    document.addEventListener('click', (event) => {
        const isClickInside = nav.contains(event.target);
        const isExpanded = trigger.getAttribute('aria-expanded') === 'true';

        if (!isClickInside && isExpanded) {
            toggleSubMenu(false);
        }
    });

    // サブメニュー外にフォーカスが移動したら閉じる処理 (Tab/Shift+Tabでの移動を考慮)
    document.addEventListener('focusin', (event) => {
        const isFocusInside = nav.contains(event.target);
        const isExpanded = trigger.getAttribute('aria-expanded') === 'true';

        if (!isFocusInside && isExpanded) {
            toggleSubMenu(false);
        }
    });

  }); // end forEach subMenuTriggers
});

このJavaScriptコードは、以下の機能を提供します。

実装時の注意点

実装したアクセシビリティのテスト方法

実装したドロップダウンメニューがアクセシブルであるかを検証するには、以下の方法を組み合わせて行うことが重要です。

まとめ

アクセシブルなドロップダウンメニューの実装は、HTMLのセマンティクス、CSSでの状態管理、JavaScriptでのインタラクション制御、そしてARIA属性によるセマンティクスの補強を組み合わせることで実現できます。特にキーボード操作とフォーカス管理、ARIA属性による状態伝達は、多くのユーザーがウェブサイトのナビゲーションを円滑に利用するために不可欠です。

この記事で紹介したコード例や手順を参考に、ご自身のプロジェクトでアクセシブルなドロップダウンメニューの実装に取り組んでみてください。実装後は、様々な方法で入念なテストを行い、すべての利用者が快適に利用できることを確認することが重要です。アクセシビリティ対応は一度行えば終わりではなく、継続的な改善が求められる取り組みです。