记住我们已经构建了这个自动完成组件(https://alligator.io/vuejs/vue-autocomplete-component/)吗?虽然大多数用户能够使用它,但需要辅助技术浏览网络的残疾人不会使用它。
在本文中,我们将学习如何使用ARIA属性来使我们的自动完成成为一个完全可访问的属性。
可访问丰富的互联网应用程序(ARIA)
您是否曾尝试过使用辅助技术浏览网络? 大多数操作系统都配备了集成解决方案,在 MacOS 中,您可以打开 VoiceOver,按一下cmd + F5
,在 Windows 中,您可以通过按一下Windows 标志键 + Ctrl + Enter
来启动 Narrator。
当我们使用上述其中一个与 此自动完成组件时,它会告诉我们自动完成是一个文本字段,不会告诉我们选项列表。
ARIA 规范定义了如何使网页内容能够被残疾人使用,通过提供一组属性,使辅助技术软件能够理解内容的语义。
标签 主题
你会惊讶于一个简单的标签可以提高可用性。
让我们快速将我们的组件设置为 应用程序并使用 VoiceOver与它进行交互。
1[label app.vue]
2<template>
3<div id="app">
4 <div>
5 <label>Choose a fruit:</label>
6
7 <autocomplete
8 :items="[ 'Apple', 'Banana', 'Orange', 'Mango', 'Pear', 'Peach', 'Grape', 'Tangerine', 'Pineapple']"
9 />
10 </div>
11</div>
12</template>
当我们允许VoiceOver与我们的组件进行交互时,我们只知道文本字段的存在,但我们不知道标签的用途是什么,因为该标签没有被辅助技术收集。
通过添加aria-label
或aria-labelledby
属性,我们将允许用户知道这个输入的用途。
请注意,您可以选择提供aria-label
,但由于大多数自动完成的组件附近有标签元素,所以我会利用这一点:
1[label components/autocomplete.vue]
2<script>
3export default {
4 ...
5 props {
6 ...
7 ariaLabelledBy: {
8 type: String,
9 required: true,
10 },
11 };
12};
13</script>
14<template>
15 ...
16 <input
17 type="text"
18 v-model="search"
19 @input="onChange"
20 :aria-labelledby="ariaLabelledBy"
21 />
22 ...
23</template>
我已经使它成为一个必要的属性,以确保没有人忘记添加它. 如果你的应用程序永远不会有围绕组件的标签元素,那么可以更明智地使用aria-label
属性。
我们只需要在我们的标签中添加一个ID
,并提供它作为一个标签:
1[label app.vue]
2<template>
3<div id="app">
4 <div>
5 <label id="fruitLabel">Choose a fruit:</label>
6
7 <autocomplete
8 :items="[ 'Apple', 'Banana', 'Orange', 'Mango', 'Pear', 'Peach', 'Grape', 'Tangerine', 'Pineapple']"
9 aria-labelled-by="fruitlabel"
10 />
11 </div>
12</div>
13</template>
现在辅助技术能够告诉我们,输入文本的意图是选择一个水果:
阿里亚特征
虽然标签可以极大地提高可用性,但它们还不够,用户仍然不知道这是一个自动完成元素。
让我们先了解角色
属性是如何工作的。
角色定义元素的元素类型. 在 这里中,您可以检查所有不同类型的角色。
更适合我们的自动装配是combobox
之一:
包含单行文本框和其他元素(如列表框或网格)的复合小工具,可动态出现,以帮助用户设置文本框的值。
由于我们组件中的文本输入会显示预期值的结果列表,我们还需要在文本框元素中设置 aria-autocomplete
属性。
aria-autocomplete
属性允许三个不同的值,一个inline
值,定义值完成将发生在文本输入中和一个列表
值,这意味着值将在一个单独的元素中存在,而文本输入旁边出现或一个两者
值,这意味着将显示一个值列表,当显示时,列表中的一个值将自动选择,并在文本输入中可见。
由于我们的选项列表位于一个单独的元素中,我们需要使用列表
值。
仅此属性本身就不知道我们的值列表在文档中的位置,所以我们需要通过使用 aria-controls
属性来指定。
我们还需要确保我们的自动完成被识别为 aria-haspopup
属性,并且在结果列表可见时,我们的容器有一个 aria-expanded
属性集。
最后但不是最重要的是,我们还需要将角色
属性添加到我们的输入
与搜索框
值,到ul
元素与列表框
和每个li
与角色
值。
借助这些属性,辅助技术软件现在可以理解我们正在向用户呈现一个 combobox,它将显示建议值的列表。
1[label components/autocomplete.vue]
2<template>
3 <div
4 class="autocomplete"
5 role="combobox"
6 aria-haspopup="listbox"
7 aria-owns="autocomplete-results"
8 :aria-expanded="isOpen"
9 >
10 <input
11 type="text"
12 @input="onChange"
13 v-model="search"
14 @keyup.down="onArrowDown" @keyup.up="onArrowUp" @keyup.enter="onEnter" aria-multiline="false"
15 role="searchbox"
16 aria-autocomplete="list"
17 aria-controls="autocomplete-results"
18 aria-activedescendant=""
19 :aria-labelledby="ariaLabelledBy"
20 />
21 <ul
22 id="autocomplete-results"
23 v-show="isOpen"
24 class="autocomplete-results"
25 role="listbox"
26 >
27 <li class="loading" v-if="isLoading">
28 Loading results...
29 </li>
30 <li
31 v-else
32 v-for="(result, i) in results"
33 :key="i"
34 @click="setResult(result)" class="autocomplete-result"
35 :class="{ 'is-active': i === arrowCounter }"
36 role="option"
37 >
38 {{ result }}
39 </li>
40 </ul>
41 </div>
42</template>
箭头支持
记得我们在我们的自动完成组件中添加了键盘支持吗?我们需要使用ARIA属性来管理它。
为了让辅助技术知道当我们使用箭头键时选择哪个选项,我们需要设置两个属性:
需要在输入字段中设置 aria-activedescendant
,它将持有视觉识别为具有键盘焦点的选项的ID。
而 aria-selected
则需要在li
属性中设置为视觉上作为选定的选项。
我们需要在我们的组件中更新的一件重要事情是倾听器,为了帮助辅助技术正确识别哪个选项是活跃的,我们需要倾听关闭
事件而不是关闭
事件。
你可以看到完整的源代码在下面的片段或在这个 codepen。
1[label components/autocomplete.vue]
2<script>
3 export default {
4 name: 'autocomplete',
5 props: {
6 items: {
7 type: Array,
8 required: false,
9 default: () => [],
10 },
11 isAsync: {
12 type: Boolean,
13 required: false,
14 default: false,
15 },
16 ariaLabelledBy: {
17 type: String,
18 required: true
19 }
20 },
21
22 data() {
23 return {
24 isOpen: false,
25 results: [],
26 search: '',
27 isLoading: false,
28 arrowCounter: 0,
29 activedescendant: ''
30 };
31 },
32
33 methods: {
34 onChange() {
35 this.$emit('input', this.search);
36 if (this.isAsync) {
37 this.isLoading = true;
38 } else {
39 this.filterResults();
40 }
41 },
42
43 filterResults() {
44 this.results = this.items.filter((item) => {
45 return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
46 });
47 },
48 setResult(result) {
49 this.search = result;
50 this.isOpen = false;
51 },
52 onArrowDown(evt) {
53 if (this.isOpen) {
54 if (this.arrowCounter < this.results.length) {
55 this.arrowCounter = this.arrowCounter + 1;
56 this.setActiveDescendent();
57 }
58 }
59 },
60 onArrowUp() {
61 if (this.isOpen) {
62 if (this.arrowCounter > 0) {
63 this.arrowCounter = this.arrowCounter -1;
64 this.setActiveDescendent();
65 }
66 }
67 },
68 onEnter() {
69 this.search = this.results[this.arrowCounter];
70 this.arrowCounter = -1;
71 },
72 handleClickOutside(evt) {
73 if (!this.$el.contains(evt.target)) {
74 this.isOpen = false;
75 this.arrowCounter = -1;
76 }
77 },
78 setActiveDescendant() {
79 this.activedescendant = this.getId(this.arrowCounter);
80 },
81 getId(index) {
82 return `result-item-${index}`;
83 },
84 isSelected(i) {
85 return i === this.arrowCounter;
86 },
87 },
88 watch: {
89 items: function (val, oldValue) {
90 // actually compare them
91 if (val.length !== oldValue.length) {
92 this.results = val;
93 this.isLoading = false;
94 }
95 },
96 },
97 mounted() {
98 document.addEventListener('click', this.handleClickOutside)
99 },
100 destroyed() {
101 document.removeEventListener('click', this.handleClickOutside)
102 }
103 };
104</script>
105</script>
106<template>
107 <div
108 class="autocomplete"
109 role="combobox"
110 aria-haspopup="listbox"
111 aria-owns="autocomplete-results"
112 :aria-expanded="isOpen"
113 >
114 <input
115 type="text"
116 @input="onChange"
117 @focus="onFocus"
118 v-model="search"
119 @keydown.down="onArrowDown"
120 @keydown.up="onArrowUp"
121 @keydown.enter="onEnter"
122 role="searchbox"
123 aria-autocomplete="list"
124 aria-controls="autocomplete-results"
125 :aria-labelledby="ariaLabelledBy"
126 :aria-activedescendant="activedescendant"
127 />
128 <ul
129 id="autocomplete-results"
130 v-show="isOpen"
131 class="autocomplete-results"
132 role="listbox"
133 >
134 <li
135 class="loading"
136 v-if="isLoading"
137 >
138 Loading results...
139 </li>
140 <li
141 v-else
142 v-for="(result, i) in results"
143 :key="i"
144 @click="setResult(result)"
145 class="autocomplete-result"
146 :class="{ 'is-active': isSelected(i) }"
147 role="option"
148 :id="getId(i)"
149 :aria-selected="isSelected(i)"
150 >
151 {{ result }}
152 </li>
153 </ul>
154 </div>
155</template>
自动完成可用性Cheatsheet
在这里,您可以找到一个 cheatsheet,其中包含您需要实现自动完成的所有ARIA属性。
QQ 元素 QQ 属性 QQ 值 QQ 使用量 QQ QQ:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-: `--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
<$>[注] IDREF:引用元素的ID属性 <$>