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

NVDA屏幕阅读器重复朗读按钮内嵌套元素为按钮的无障碍缺陷修复请求

修复NVDA重复朗读按钮的无障碍缺陷

我碰到过好几个类似的无障碍问题,你的情况核心是HTML语义错误加上不合理的嵌套结构,导致屏幕阅读器(比如NVDA)把按钮内部的元素也误识别为按钮的一部分,进而重复朗读信息。咱们一步一步来解决:

问题根源分析

  • 违反HTML语义规范:<button> 标签内部不能嵌套其他交互式控件(比如你的<input type="radio">),这会让屏幕阅读器混淆控件的角色和功能。
  • 冗余的嵌套块级元素:<button> 里的<section><p>等元素会被NVDA当成按钮的子内容重复朗读,叠加按钮本身的识别信息,造成重复。
  • 重复的交互绑定:按钮和内部的单选框都绑定了selectAnswer事件,既不符合逻辑,也会干扰屏幕阅读器的交互识别。

解决方案步骤

  1. 重构结构,移除button嵌套交互式控件:把外层的<button>换成非交互式容器(比如<div>),让单选按钮成为独立的可交互控件——单选按钮本身就具备选择功能,不需要包裹在按钮里。
  2. 保证键盘可访问性:给容器添加tabindex="0",并监听keydown事件处理Enter和空格键的触发,模拟按钮的键盘交互行为。
  3. 优化无障碍属性:确保单选按钮的nameidaria-label属性正确关联,让屏幕阅读器能清晰识别单选组和每个选项的状态。
  4. 用CSS模拟按钮样式:保留原有的按钮视觉样式,让容器看起来和原来的按钮一致。

修改后的代码

<div 
  class="btn" 
  [ngClass]="{
    'border-color-Red': answer?.AnswerId == 1, 
    'border-color-Orange': answer?.AnswerId == 2, 
    'border-color-Yellow': answer?.AnswerId == 3, 
    'border-color-LightGreen': answer?.AnswerId == 4, 
    'border-color-Green': answer?.AnswerId == 5
  }"
  (click)="selectAnswer(assesment, answer)"
  tabindex="0"
  (keydown)="handleKeydown($event, assesment, answer)"
>
  <section class="d-flex">
    <section class="header" [ngClass]="{
      'color-Red': assesment?.SelectedAnswerId == 1 && answer?.AnswerId == 1, 
      'color-Orange': assesment?.SelectedAnswerId == 2 && answer?.AnswerId == 2, 
      'color-Yellow': assesment?.SelectedAnswerId == 3 && answer?.AnswerId == 3, 
      'color-LightGreen': assesment?.SelectedAnswerId == 4 && answer?.AnswerId == 4, 
      'color-Green': assesment?.SelectedAnswerId == 5 && answer?.AnswerId == 5, 
      'color-pink': answer?.AnswerId == 1, 
      'color-light-orange':answer?.AnswerId == 2, 
      'color-light-yellow':answer?.AnswerId == 3, 
      'color-light-yellowgreen':answer?.AnswerId == 4, 
      'color-green-light':answer?.AnswerId == 5
    }">
      <h6 class="font-caption-alt">{{answer.AnswerId}} = {{answer.Answer}}</h6>
    </section>
    <section class="body">
      <p>{{answer.AnswerDesciption}}</p>
    </section>
    <section class="footer">
      <label class="font-caption">
        <input 
          type="radio" 
          id="answer-{{assesment?.Id}}-select-{{answer.Id}}" 
          [attr.aria-label]="isSelectedAnswer(assesment, answer?.AnswerId) ? 'Selected: ' + answer.Answer : 'Not selected: ' + answer.Answer"
          name="{{assesment?.Id}}-select" 
          [checked]="isSelectedAnswer(assesment, answer?.AnswerId)" 
          tabindex="-1" <!-- 让单选框不单独聚焦,聚焦到外层容器 -->
        />
        <label for="answer-{{assesment?.Id}}-select-{{answer.Id}}"></label>
        Select
      </label>
    </section>
  </section>
</div>

关键改动说明

  • 把外层<button>换成<div>,避免语义冲突。
  • <div>添加tabindex="0"使其可聚焦,同时添加keydown事件处理Enter和空格键,模拟按钮的键盘交互:
    handleKeydown(event: KeyboardEvent, assesment: any, answer: any) {
      if (event.key === 'Enter' || event.key === ' ') {
        event.preventDefault();
        this.selectAnswer(assesment, answer);
      }
    }
    
  • 把内部单选框的tabindex设为-1,避免它被单独聚焦,确保用户通过外层容器进行交互。
  • 优化aria-label,让屏幕阅读器能清晰读出选项的状态和内容,而不是模糊的“Selected Radio”。
  • 移除了单选框上的(click)事件,统一通过外层容器的点击和键盘事件处理选择逻辑。

这样修改后,NVDA会正确识别这个单选选项容器,仅朗读一次选项的完整信息,不会重复识别内部元素为按钮,同时保持了原有的视觉样式和交互功能。

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

火山引擎 最新活动