嵌套菜单列表及子菜单项未被屏幕阅读器识别的技术问询
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 alongsiderole="menuitem"elements. Screen readers expect direct children of amenuto bemenuitems—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 arole="menuitem"(your top-level menu items like Menu1/Menu2). - Each top-level
menuitemwill contain both the menu title and its submenu (which gets its ownrole="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: noneorvisibility: hidden—don't just move it off-screen without hiding it from assistive tech).
内容的提问来源于stack exchange,提问作者alyson_216
相关产品推荐
相关产品推荐

