You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

嵌套菜单列表及子菜单项未被屏幕阅读器识别的技术问询

Fixing Screen Reader Recognition for Nested Menus

Hey there! Let's break down why your current menu setup isn't being picked up correctly by screen readers and walk through the fixes step by step.

First, let's spot the issues in your original code:

  • Your top-level role="menu" container has mixed content: an <a> tag (your "Menu1" title) directly alongside role="menuitem" elements. Screen readers expect direct children of a menu to be menuitems—anything else breaks the semantic structure.
  • There's no clear association between top-level menu items and their submenus. Screen readers need to know "this item opens a submenu" and "this submenu belongs to that top-level item".
  • You're missing critical ARIA attributes that signal submenu presence and state (like whether it's expanded or collapsed).
  • No keyboard interaction logic—screen reader users rely on keyboard navigation (arrows, Enter, Esc) to use menus, which your current code doesn't support.

Here's the step-by-step solution:

1. Fix the Semantic Role Structure

  • The top-level container stays as role="menu", but every direct child must be a role="menuitem" (your top-level menu items like Menu1/Menu2).
  • Each top-level menuitem will contain both the menu title and its submenu (which gets its own role="menu").
  • Ditch the outer role="application"—it's unnecessary for menus and can actually confuse screen readers by overriding default page semantics.

2. Add Critical ARIA Attributes

  • aria-haspopup="true": Tells screen readers this menu item opens a submenu.
  • aria-expanded="false/true": Dynamically updates to show if the submenu is open or closed.
  • aria-labelledby: Links the submenu to its parent menu item, so screen readers announce "Submenu for Menu1" instead of just a random menu.

3. Implement Keyboard Navigation (Non-Negotiable for Accessibility)

Menus need to support these keyboard interactions:

  • Tab: Enter/exit the menu system.
  • Enter/Space: Toggle the submenu open/closed, or activate a menu item link.
  • Arrow Up/Down: Navigate between sibling menu items (top-level or submenu).
  • Arrow Right: Open a submenu and focus the first item inside it.
  • Arrow Left: Close the submenu and return focus to the parent item.
  • Esc: Close all open submenus and return focus to the last focused element outside the menu.

Corrected Code Example (AngularJS)

<div role="menu" class="loContainer">
  <!-- Top-level Menu 1 -->
  <div 
    role="menuitem" 
    aria-haspopup="true" 
    aria-expanded="{{menuStates.menu1}}"
    id="menu1-trigger"
    ng-keydown="handleMenuKeydown($event, 'menu1')"
    tabindex="0"
  >
    <span class="loTitle">Menu1</span>
    <!-- Submenu for Menu1 -->
    <div 
      role="menu" 
      aria-labelledby="menu1-trigger"
      class="submenu"
      ng-show="menuStates.menu1"
    >
      <div role="menuitem" tabindex="0" ng-keydown="handleSubmenuKeydown($event)">
        <a href="" ng-click="setTopicIdx($index)">Submenu1</a>
      </div>
      <div role="menuitem" tabindex="0" ng-keydown="handleSubmenuKeydown($event)">
        <a href="" ng-click="setTopicIdx($index)">Submenu2</a>
      </div>
    </div>
  </div>

  <!-- Top-level Menu 2 -->
  <div 
    role="menuitem" 
    aria-haspopup="true" 
    aria-expanded="{{menuStates.menu2}}"
    id="menu2-trigger"
    ng-keydown="handleMenuKeydown($event, 'menu2')"
    tabindex="0"
  >
    <span class="loTitle">Menu2</span>
    <div 
      role="menu" 
      aria-labelledby="menu2-trigger"
      class="submenu"
      ng-show="menuStates.menu2"
    >
      <div role="menuitem" tabindex="0" ng-keydown="handleSubmenuKeydown($event)">
        <a href="" ng-click="setTopicIdx($index)">SubmenuA</a>
      </div>
    </div>
  </div>
</div>

AngularJS Controller Logic

angular.module('yourAppName').controller('MenuController', function($scope) {
  // Track submenu expansion states
  $scope.menuStates = {
    menu1: false,
    menu2: false
  };

  // Toggle submenu open/close
  $scope.toggleSubmenu = function(menuName) {
    $scope.menuStates[menuName] = !$scope.menuStates[menuName];
  };

  // Handle top-level menu keyboard events
  $scope.handleMenuKeydown = function(event, menuName) {
    event.preventDefault();
    switch(event.key) {
      case 'Enter':
      case ' ':
        $scope.toggleSubmenu(menuName);
        if ($scope.menuStates[menuName]) {
          // Focus first submenu item when opening
          document.querySelector(`#${menuName}-trigger .submenu [role="menuitem"]`).focus();
        }
        break;
      case 'ArrowRight':
        $scope.toggleSubmenu(menuName);
        document.querySelector(`#${menuName}-trigger .submenu [role="menuitem"]`).focus();
        break;
      case 'ArrowLeft':
        if ($scope.menuStates[menuName]) {
          $scope.toggleSubmenu(menuName);
        }
        break;
      // Add ArrowUp/ArrowLeft logic to navigate between top-level items
    }
  };

  // Handle submenu keyboard events
  $scope.handleSubmenuKeydown = function(event) {
    event.preventDefault();
    const items = Array.from(event.currentTarget.parentElement.querySelectorAll('[role="menuitem"]'));
    const currentIdx = items.indexOf(event.currentTarget);

    switch(event.key) {
      case 'ArrowUp':
        const prevItem = items[currentIdx - 1] || items[items.length - 1];
        prevItem.focus();
        break;
      case 'ArrowDown':
        const nextItem = items[currentIdx + 1] || items[0];
        nextItem.focus();
        break;
      case 'ArrowLeft':
        // Close submenu and return focus to parent menu item
        const parentTrigger = event.currentTarget.closest('[aria-haspopup="true"]');
        parentTrigger.focus();
        $scope.menuStates[parentTrigger.id.replace('-trigger', '')] = false;
        break;
      case 'Esc':
        // Close all submenus
        $scope.menuStates.menu1 = false;
        $scope.menuStates.menu2 = false;
        // Return focus to last element outside menu (adjust as needed)
        document.querySelector('.loContainer').previousElementSibling?.focus();
        break;
    }
  };
});

Final Notes

  • Test with screen readers like NVDA, VoiceOver, or JAWS to verify the experience—this is the best way to catch edge cases.
  • Ensure your CSS hides submenus properly when closed (using display: none or visibility: hidden—don't just move it off-screen without hiding it from assistive tech).

内容的提问来源于stack exchange,提问作者alyson_216

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 03:37:49