You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

无障碍下拉菜单(Accessibility dropdown menu)键盘导航异常问题咨询

无障碍下拉菜单的键盘交互解决方案

这个问题在无障碍菜单实现里挺常见的,核心是要手动处理键盘导航逻辑,同时遵循WAI-ARIA规范,让原生键盘行为和自定义菜单交互兼容。我给你一套完整的实现方案,包含HTML结构、样式和JavaScript逻辑,每部分都有说明:

1. 基础HTML结构(遵循WAI-ARIA规范)

首先要确保结构语义化,同时添加必要的ARIA属性让屏幕阅读器识别菜单角色:

<div class="dropdown">
  <button 
    id="dropdownTrigger"
    class="dropdown-trigger"
    aria-haspopup="true"
    aria-expanded="false"
  >
    导航菜单
  </button>
  <ul 
    id="dropdownMenu"
    class="dropdown-menu"
    role="menu"
    aria-labelledby="dropdownTrigger"
    hidden
  >
    <li role="none">
      <a 
        href="/home"
        role="menuitem"
        tabindex="-1"
      >
        首页
      </a>
    </li>
    <li role="none">
      <a 
        href="/profile"
        role="menuitem"
        tabindex="-1"
      >
        个人中心
      </a>
    </li>
    <li role="none">
      <a 
        href="/settings"
        role="menuitem"
        tabindex="-1"
      >
        设置
      </a>
    </li>
  </ul>
</div>

关键结构说明:

  • <button>作为触发器:原生button默认支持键盘聚焦和Enter/Space触发,比div更符合无障碍要求。
  • aria-haspopup="true":告诉屏幕阅读器这个按钮会弹出一个菜单。
  • aria-expanded:动态切换true/false,表示菜单当前是否展开。
  • 菜单容器<ul>添加role="menu",每个选项<a>添加role="menuitem":明确菜单的角色关系。
  • tabindex="-1":让菜单选项默认不被tab键直接聚焦,只有在菜单展开后,通过上下箭头导航才能获得焦点,从根源解决tab直接跳第一个链接的问题。
  • hidden属性:默认隐藏菜单,比纯CSS隐藏更友好(屏幕阅读器会忽略hidden元素)。

2. 基础CSS样式

实现菜单的显示/隐藏逻辑,以及聚焦状态的可视化(这对键盘导航用户至关重要,必须要有):

.dropdown {
  position: relative;
  display: inline-block;
}

.dropdown-trigger {
  padding: 8px 16px;
  cursor: pointer;
}

.dropdown-menu {
  position: absolute;
  top: 100%;
  left: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  background: white;
  border: 1px solid #ccc;
  min-width: 150px;
}

.dropdown-menu[hidden] {
  display: none;
}

.dropdown-menu a {
  display: block;
  padding: 8px 16px;
  text-decoration: none;
  color: #333;
}

/* 聚焦状态样式,必须要有 */
.dropdown-menu a:focus {
  outline: 2px solid #005fcc;
  background: #f0f7ff;
}

3. JavaScript交互逻辑(核心解决键盘导航问题)

需要处理按钮和菜单选项的键盘事件,实现tab导航、上下箭头切换、回车/空格跳转、ESC关闭等功能:

const trigger = document.getElementById('dropdownTrigger');
const menu = document.getElementById('dropdownMenu');
const menuItems = Array.from(menu.querySelectorAll('[role="menuitem"]'));

// 切换菜单显示/隐藏
function toggleMenu() {
  const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
  trigger.setAttribute('aria-expanded', !isExpanded);
  menu.hidden = isExpanded;
  
  // 如果菜单展开,聚焦到第一个选项
  if (!isExpanded) {
    menuItems[0].focus();
  } else {
    trigger.focus();
  }
}

// 处理菜单选项的键盘导航
function handleMenuItemKeydown(e) {
  const currentIndex = menuItems.indexOf(document.activeElement);
  
  switch(e.key) {
    case 'ArrowDown':
      e.preventDefault();
      const nextIndex = (currentIndex + 1) % menuItems.length;
      menuItems[nextIndex].focus();
      break;
    case 'ArrowUp':
      e.preventDefault();
      const prevIndex = (currentIndex - 1 + menuItems.length) % menuItems.length;
      menuItems[prevIndex].focus();
      break;
    case 'Enter':
    case ' ':
      // 触发链接跳转
      e.currentTarget.click();
      // 跳转后关闭菜单
      toggleMenu();
      break;
    case 'Escape':
      e.preventDefault();
      toggleMenu();
      break;
    case 'Tab':
      // 按tab键时关闭菜单,让焦点正常跳转到页面下一个/上一个元素
      toggleMenu();
      break;
  }
}

// 处理触发器的键盘事件
function handleTriggerKeydown(e) {
  switch(e.key) {
    case 'ArrowDown':
      e.preventDefault();
      toggleMenu();
      break;
    case 'Escape':
      if (trigger.getAttribute('aria-expanded') === 'true') {
        toggleMenu();
      }
      break;
  }
}

// 绑定事件
trigger.addEventListener('click', toggleMenu);
trigger.addEventListener('keydown', handleTriggerKeydown);
menuItems.forEach(item => {
  item.addEventListener('keydown', handleMenuItemKeydown);
});

// 点击菜单外部关闭菜单
document.addEventListener('click', (e) => {
  if (!trigger.contains(e.target) && !menu.contains(e.target)) {
    if (trigger.getAttribute('aria-expanded') === 'true') {
      toggleMenu();
    }
  }
});

核心逻辑说明:

  • tab键处理:菜单选项默认tabindex="-1",所以tab不会直接跳到它们;当在菜单内按tab时,关闭菜单,让焦点正常跳转到页面的下一个元素,符合用户预期。
  • 上下箭头导航:在菜单内按上下箭头时,循环切换焦点到上一个/下一个选项,阻止默认页面滚动行为。
  • 回车/空格触发跳转:模拟点击链接,同时关闭菜单,确保跳转后界面状态正确。
  • ESC键关闭:无论在触发器还是菜单内,按ESC都能关闭菜单并将焦点返回触发器。
  • 点击外部关闭:提升用户体验,符合常规下拉菜单的交互逻辑。

测试要点

实现后记得测试以下场景:

  1. 按tab键聚焦到触发器,按Enter/Space打开菜单,焦点自动跳到第一个选项。
  2. 在菜单内按上下箭头,焦点在选项间循环切换,能看到清晰的聚焦样式。
  3. 按Enter/Space,跳转到对应链接,菜单自动关闭。
  4. 按ESC,菜单关闭,焦点回到触发器。
  5. 按tab键,菜单关闭,焦点跳转到页面下一个可聚焦元素。
  6. 用屏幕阅读器(如NVDA、VoiceOver)测试,确保能正确识别菜单角色和状态变化。

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

火山引擎 最新活动