如何创建屏幕 reader 可访问 AJAX 区域和 DOM insert/remove 更新?

How to create screen reader accessible AJAX regions and DOM insert/remove updates?

我的用户交互过程如下:

  1. 用户会看到一个包含城市列表的下拉列表
  2. 在select一个城市之后,一个AJAX请求获取城市中的所有建筑物,并将它们插入到一个div (这个AJAX 请求 returns 复选框列表)
  3. 用户然后可以 check/uncheck 一个复选框来将城市添加到同一页面上的 table。 (动态inserts/removestable行)

这是我的 select 下拉菜单:

<label for="city-selector">Choose your favorite city?</label>
  <select name="select" size="1" id="city-selector" aria-controls="city-info">
  <option value="1">Amsterdam</option>
  <option value="2">Buenos Aires</option>
  <option value="3">Delhi</option>
  <option value="4">Hong Kong</option>
  <option value="5">London</option>
  <option value="6">Los Angeles</option>
  <option value="7">Moscow</option>
  <option value="8">Mumbai</option>
  <option value="9">New York</option>
  <option value="10">Sao Paulo</option>
  <option value="11">Tokyo</option>
</select>

这里是 ajax div 得到 empties/populated:

<div role="region" id="city-info" aria-live="polite">
<!-- AJAX CONTENT LOADED HERE -->
</div>

这是放置在 ajax div 中的复选框列表:

<fieldset id="building-selector" aria-controls="building-table">
  <legend>Select your favorite building:</legend>  
  <input id="fox-plaza" type="checkbox" name="buildings" value="fox-plaza">
  <label for="fox-plaza">Fox Plaza</label><br>
  <input id="chrysler-building" type="checkbox" name="buildings" value="chrysler-building">
  <label for="chrysler-building">Chrysler Building</label><br>
  <input id="empire-state-building" type="checkbox" name="buildings" value="empire-state-building">
  <label for="empire-state-building">Empire State Building</label><br>
</fieldset>

最后 table 包含他使用的城市 adds/removes

<table id="building-table" aria-live="polite">
  <caption>List of buildings you have selected</caption>

  <tr>
    <th scope="col">Building name</th>
    <th scope="col">Delete Building</th>
  </tr>

  <tr>
    <td>Empire State Building</td>
    <td><button>Delete</button> /td>
  </tr>

</table>

我认为使用 aria-controls="" 和 aria-live="" 是正确的,但这似乎不足以让屏幕 reader 检测到变化.事实上,我不知道我的标记中是否遗漏了某些内容,或者我是否需要触发任何警报事件或类似事件才能完成这项工作。

你描述的是一种情况,而不是问题。

只要您的屏幕阅读器可以在视觉焦点移到内容上(使用您的 AT 快捷键)时阅读内容,那么它就是预期的结果。

在您 select 一个城市后,屏幕阅读器不会自行移动到复选框列表。这是由于屏幕阅读器的视觉焦点与标准焦点不相关的方式。

aria-live 是要发布的内容,但不会让您与同一 div 内的任何内部控件进行交互,并且不会将视觉焦点移动到此元素。

在您的情况下,您应该描述表单的操作:

Chose a city in the list to select your favorite building

请考虑使用vue.js (light framework) or angular2(重型框架)来完成这个任务。它将非常简化您需要的那种 dom-js 交互。将它与 aria-live="polite" aria-atomic='true' 一起使用并且有效。这是 vue.js:

中针对您的案例的解决方案

https://jsfiddle.net/nuLjb4uc/8/

HTML:

在头部分放

<script type="text/javascript" src="https://vuejs.org/js/vue.min.js"></script>

在正文中

<script type="x-template" id="my-template">

<label for="city-selector">Choose your favorite city?</label>
  <select name="select" size="1" id="city-selector" aria-controls="city-info" @change="onCityChange($event)">
  <option disabled selected value>-- choose city --</option>
  <option v-for=" (index, city) of cities" value="{{index+1}}">{{ city }}</option>
</select>

<fieldset v-if="currentCityBuildings" id="building-selector" aria-controls="building-table">
  <legend>Select your favorite building:</legend>  

  <div v-for="building of currentCityBuildings" aria-live="polite" aria-atomic='true'>
    <input v-model="building.checked" id="{{ building.name }}" @click='selectBuilding(building)' value="{{ building.name }}"  type="checkbox" name="buildings">
    <label for="{{ building.name }}">{{ building.name }}</label><br>
  </div>
</fieldset>

<table v-if='checkedCityBuildings' id="building-table" aria-live="polite">
  <caption>List of buildings you have selected</caption>

  <tr>
    <th scope="col">Building name</th>
    <th scope="col">Delete Building</th>
  </tr>

  <tr v-for="building of checkedCityBuildings" aria-live="polite" aria-atomic='true'>
    <td>{{ building.name }}</td>
    <td><button @click='deleteBuilding(building)'>Delete</button></td>
  </tr>

</table>

</script>

<div id="app"></div>

JS:

new Vue({
  el: '#app',
  data: {
    cities: ['Amsterdam','Buenos Aires', 'Delhi', 'Hong Kong', 'London', 'Los Angeles', 'Moscow', 'Mumbai', 'New York', 'Sao Paulo', 'Tokyo' ],
    currentCityBuildings: null,
    checkedCityBuildings: null,
  },
  template: '#my-template',
  methods: {
    onCityChange: function(e) {
        var index = e.target.value-1;
        // instead of below line, you should call proper AJAX function which, when it get data, put them into this.checkedCityBuildings like 'simulateAJAX function do.
        setTimeout(this.simulateAJAX(this.cities[index]), 1000); 
    },

    simulateAJAX: function(city) {
      this.checkedCityBuildings = null;
      this.currentCityBuildings = [];
      this.currentCityBuildings.push({name:'fox-plaza-'+city, checked:false});
      this.currentCityBuildings.push({name:'chrysler-building-'+city, checked:false});
      this.currentCityBuildings.push({name:'empire-state-building-'+city, checked:false});
    },

    selectBuilding: function(building) {
      building.checked = !building.checked;
      this.checkedCityBuildings = [];
      for(var b of this.currentCityBuildings) {
        if(b.checked) this.checkedCityBuildings.push(b);
      }
    },

    deleteBuilding: function(building) {
        this.selectBuilding(building)
    },


  },

})

我在 Chrome + ChromeVox 插件上进行了测试。