如何创建屏幕 reader 可访问 AJAX 区域和 DOM insert/remove 更新?
How to create screen reader accessible AJAX regions and DOM insert/remove updates?
我的用户交互过程如下:
- 用户会看到一个包含城市列表的下拉列表
- 在select一个城市之后,一个AJAX请求获取城市中的所有建筑物,并将它们插入到一个div (这个AJAX 请求 returns 复选框列表)
- 用户然后可以 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 插件上进行了测试。
我的用户交互过程如下:
- 用户会看到一个包含城市列表的下拉列表
- 在select一个城市之后,一个AJAX请求获取城市中的所有建筑物,并将它们插入到一个div (这个AJAX 请求 returns 复选框列表)
- 用户然后可以 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 插件上进行了测试。