可访问性
概述
屏幕阅读器
屏幕阅读器是一种辅助应用程序,它为盲人或视力障碍用户解释屏幕上显示的内容。屏幕阅读器可以通过多种方式与用户交互,包括大声朗读甚至生成盲文输出。
谁使用屏幕阅读器?
许多人发现能够通过多种方式与计算机交互很有帮助。虽然 Google 不会统计使用屏幕阅读器的用户数量,但随着人口老龄化,越来越多的人将需要辅助技术。确保我们的应用程序对每个人都可访问非常重要。
屏幕阅读器如何工作?
屏幕阅读器监听由特定于平台的 API 触发的标准事件集。例如,当您的屏幕上弹出警报窗口时,Microsoft Windows 将使用 Microsoft Active Accessibility API 公开此事件,而 Linux 机器将使用 Linux Access Toolkit。 ChromeVox for Chrome 和 FireVox for Firefox 是基于最新 Web 技术构建的屏幕阅读器浏览器扩展。
GWT 和屏幕阅读器
Ajax 应用程序通常以屏幕阅读器难以正确解释的方式编写。例如,GWT 开发人员编写树小部件时,可能会使用一个经过修改的列表元素来模拟树控件的行为。但屏幕阅读器会将控件显示为列表——一个不正确的描述,使得树不可用。屏幕阅读器还会将 HTML span 或 div 元素视为普通的静态文本元素,无论是否存在用于用户交互的 JavaScript 事件处理程序;您可以很容易地想象这会导致问题。
ARIA
ARIA 是一种 W3C 规范,用于通过一组标准的 DOM 属性使富互联网应用程序可访问。它仍然很新,但网络上存在有用的资源。更多信息可以在 Google 可访问性网站 和 Chrome 可访问性扩展页面 上找到。
向 GWT 小部件添加可访问性支持涉及向 DOM 元素添加相关的属性,这些属性可以被浏览器用来在用户交互期间触发事件。屏幕阅读器可以对这些事件做出反应,以表示 GWT 小部件的功能。ARIA 规范中指定的 DOM 属性被分类为具有属性和状态的角色。
ARIA role
属性被添加到 HTML 元素中,以定义可以被阅读器应用程序/设备解释的小部件和页面结构元素,因此描述了小部件的行为方式。角色是静态的,在小部件的生命周期中不会改变。一些 ARIA 角色示例:tree
、menubar
、menuitem
和 tab
。小部件的 ARIA 角色表示为名为 role
的 DOM 属性,其值设置为 ARIA 角色字符串之一。
还存在 ARIA 属性和状态。ARIA 状态提供有关网页更改的信息,这些信息可用于警报、通知、导航帮助。状态会因用户交互而改变,开发人员应考虑在处理用户操作时更改小部件状态。例如,复选框小部件可能处于“选中”或“未选中”状态。状态是动态的,应在用户交互期间更新。一些 ARIA 状态示例:aria-disabled
、aria-pressed
和 aria-expanded
。ARIA 属性是小部件的一个特征,它会随着时间的推移而改变,但比 ARIA 状态改变得更少,并且通常不会因用户操作而改变。属性的示例是 aria-haspopup
、aria-label
和 aria-level
,它们是关于页面布局和小部件之间交互性的语义信息。ARIA 状态和属性本身就是 DOM 属性——例如,要表示一个切换按钮小部件已被按下,我们需要将属性 aria-pressed = 'true'
添加到小部件的 HTML 元素中。作为另一个示例,一个在获得焦点时具有附加弹出窗口的 textarea 小部件将具有属性 aria-haspopup = 'true'
。
小部件的角色决定了它支持的状态和属性集。例如,具有 list
角色的小部件不会公开 aria-pressed
状态,但将具有 aria-expanded
状态。
此外,可访问的小部件需要键盘支持。屏幕阅读器将朗读获得键盘焦点的元素,因此键盘可访问性可以通过响应键盘命令将焦点移动到不同的元素来实现。
GWT 和 ARIA
一旦 ARIA 角色、属性和状态被添加到 GWT 小部件的 DOM 中,浏览器就会向屏幕阅读器触发相应的事件。由于 ARIA 仍在不断发展中,浏览器可能不会为每个 ARIA 属性触发事件,而屏幕阅读器可能无法识别所有触发的事件。
最近,我们升级了 GWT 中的可访问性支持,并弃用了 Accessibility 类,该类包含 ARIA 角色和状态的子集。我们添加了一个 GWT ARIA 库,其中包含 ARIA 标准中定义的所有角色、状态和属性。角色在接口的层次结构中组织。对于每个角色,都有一组支持的状态和属性。在 Roles 工厂 中,每个角色都有一个 getter。对于每个角色,都存在用于获取、设置和移除状态和属性的访问器,或者像 tabindex
这样的额外属性。
ARIA 标准指定了状态和属性值的 HTML 属性类型。这些属性类型在新的库中得到支持,并进行编译时检查。已为像 ‘aria-dropeffect’ 属性类型、‘aria-checked’ 类型等的标记类型的 HTML 属性添加了枚举。在类中添加了一种用于 IDREF(S) 的通用类型。所有状态和属性都在 State 和 Property 类中定义。tabIndex
属性也已作为 ARIA 标准中的额外属性添加,我们已 将其添加到库中。我们鼓励用户通过从 Roles 工厂 获取角色来访问状态和属性,因为工厂会检查状态或属性是否受角色支持。
现在,许多 GWT 小部件具有键盘可访问性和 ARIA 属性。这些包括 CustomButton、Tree、TreeItem、MenuBar、MenuItem、TabBar 和 TabPanel。此外,所有从 FocusWidget 继承的小部件现在默认都有 tabindex,允许更好的键盘导航。
使小部件可访问
本页的其余部分描述了如何使用新的 ARIA 库向您的 GWT 应用程序和小部件添加可访问性支持。讨论了各种不同的技术;尚未存在使 AJAX 应用程序可访问的标准和通用的方法,但我们提供了一些建议。
添加 ARIA 角色
ARIA 属性 role
是小部件类型的指示;它描述了小部件的行为方式。角色是静态的,在小部件的生命周期中不应改变。小部件作者应该
- 从 Roles 工厂 类中为小部件选择正确的角色。
- 在构造时设置此属性。
以下是来自 CustomButton 小部件的示例。添加 button
角色指示辅助技术该小部件将像按钮一样工作。
protected CustomButton() {
...
// Add a11y role "button"
Roles.getButtonRole().set(getElement());
...
}
添加 ARIA 属性和状态
ARIA 状态是一个附加属性,它反映了小部件的当前状态,这是用户操作的结果;例如,复选框是否选中或未选中。状态应该在构造小部件时初始化,并在用户交互期间更新。
ARIA 属性是为小部件提供额外信息的附加属性;例如,小部件的标签是一个属性,在构建小部件时设置一次。一个属性值会改变的例子是 aria-activedescendant
,它用于提供关于组合小部件(如树或菜单)当前选中的子元素的信息。
请注意,
- 一个小部件可以有多个状态属性、一个或多个属性属性,以及一个角色属性。
- 状态属性是动态的,在小部件的生命周期中会发生变化。角色一旦设置就不会改变。
- 属性属性反映了使应用程序更具交互性的附加小部件功能,这些功能可能会随着时间的推移而改变或保持不变。
初始化状态和属性
一旦将特定的 ARIA 角色与一个小部件关联,就需要检查哪些状态和属性与该角色关联。例如,button
角色有两个状态属性
- `aria-disabled`
- 指示小部件存在,但用户不允许进行操作。
- `aria-pressed`
- 用于可切换的按钮,以指示其当前的按下状态。
在 CustomButton 小部件中,aria-pressed
和 aria-disabled
ARIA 状态的初始化方式如下
protected CustomButton() {
...
// Add a11y state "aria-pressed" and "aria-disabled"
Roles.getButtonRole().setAriaPressedState(getElement(), PressedValue.of(false));
Roles.getButtonRole().setAriaDisabledState(getElement(), false);
}
在用户交互期间更新状态
The CustomButton 小部件支持多个按钮面,为开发人员提供了更多样式控制。此外,与 Button 小部件不同,CustomButton 可以切换,就像 CustomButton 子类 ToggleButton 一样。附加到底层 DOM 元素的事件处理程序会在按下按钮时更新按钮面。我们需要切换 ARIA 状态 aria-pressed
,如下所示。
void toggleDown() {
// Update a11y state "aria-pressed"
Roles.getButtonRole().setAriaPressedState(getElement(), PressedValue.of(true));
}
void toggleUp() {
// Update a11y state "aria-pressed"
Roles.getButtonRole().setAriaPressedState(getElement(), PressedValue.of(false));
}
void setInactive() {
// Update a11y state "aria-disabled"
Roles.getButtonRole().setAriaDisabledState(getElement(), false);
}
void setActive() {
// Update a11y state "aria-disabled"
Roles.getButtonRole().setAriaDisabledState(getElement(), true);
}
重要的是要确保所有更改小部件状态的事件处理程序也更改 ARIA 状态。
添加键盘可访问性
键盘可访问性是启用 GWT 小部件访问的关键要求。在开发新小部件时,请确保从一开始就提供键盘可访问性;在稍后添加键盘可访问性可能很困难。屏幕阅读器会朗读具有键盘焦点的元素,因此可以通过响应键盘命令将焦点移动到不同的元素来实现键盘可访问性。
正确的键盘可访问性提供了以下最终用户行为
- 用户可以使用 Tab 键将焦点移动到小部件上。
- 当小部件获得焦点时,屏幕阅读器会解释在小部件上设置的 ARIA 角色和状态。
- 屏幕阅读器会朗读小部件及其文本内容的描述。
默认情况下,HTML DOM 中唯一可以接收键盘焦点的元素是锚点和表单字段。但是,将 DOM 属性 tabIndex
(请注意,这对应于 HTML 属性 tabindex
)设置为 0 会将这些元素置于默认的 Tab 键顺序中,从而使它们能够接收键盘焦点。将 tabIndex
设置为 -1 会将元素从 Tab 键顺序中移除,但仍允许元素通过编程方式接收键盘焦点。可以通过设置 tabindex
ARIA 属性为任何角色添加 Tab 键索引支持。下面的代码展示了如何将标题元素添加到页面的自然 Tab 键顺序中。如果您希望允许与标题进行进一步交互(例如,某种可点击标题),您可能需要这样做。
// Set tab index for a heading element
Roles.getHeadingRole().setTabindexExtraAttribute(heading.getElement(), 0);
在 GWT 中,扩展 FocusWidget 抽象类的任何小部件默认情况下都具有键盘可聚焦性。FocusWidget 抽象类包括一个 setFocus(boolean) 方法,该方法可用于通过编程方式设置小部件的焦点或删除焦点。FocusWidget 还包括一个 setTabIndex(int) 方法,该方法允许用户设置小部件的 DOM 属性 tabIndex
。
请记住,扩展 FocusWidget 并不保证小部件的可聚焦性。FocusWidget 的基本元素(传递给超类构造函数)必须是自然可聚焦的 HTML 元素。
对于不扩展 FocusWidget 抽象类的那些小部件,确保键盘可访问性可能更加困难。不同的浏览器以不同的方式设置焦点,并且并非所有浏览器都支持对任意元素设置焦点。您可以使用 FocusPanel 来包围需要接收键盘焦点的元素;只需确保在不同的浏览器上测试您的部件即可。
有关使用 tabIndex
属性的示例,请参见 MenuBar 小部件。根菜单是唯一应该在 Tab 键顺序中的菜单;其子菜单不在。为了实现这一点,在 MenuBar 的构造函数中将 Tab 键索引设置为 0,并且当新的 MenuBar 作为子菜单添加时,它们的 Tab 键索引属性将重置为 -1。
指示选择更改
某些小部件,例如 GWT 的 Tree 和 MenuBar 小部件,包含一个包含一组项目的容器。容器有一个自然可聚焦的 DOM 元素,但项目本身没有。可聚焦元素接收所有键盘输入,并在包含的项目中引起视觉变化以指示项目选择的变化。例如,GWT 的 Tree 小部件包含 TreeItems;这两个元素都是由 div
元素组成的。但是,Tree 还具有一个自然可聚焦的隐藏元素,该元素接收键盘事件。每当用户按下箭头键时,此元素都会处理事件并导致相应的 TreeItem 被突出显示。
虽然此技术对视力正常的用户有效,但它会对屏幕阅读器造成破坏。由于 TreeItems 本身在被选中时从未获得自然焦点,因此屏幕阅读器无法知道项目选择已发生变化。解决此问题的一种可能方法是使每个 TreeItem 自然可聚焦。不幸的是,TreeItems 可以包含的不仅仅是文本——它们可以包含其他小部件,这些小部件本身可以是可聚焦的。在这里,正确地委托焦点将非常复杂——每个 TreeItem 都必须处理其子小部件的所有关键事件,并决定是否将关键事件委托给其子小部件(用于与子小部件的交互),或者处理关键事件本身(用于树导航)。请记住,为每个项目连接键盘事件处理程序将变得很麻烦,因为树可能会变得非常大。可以通过依赖关键事件的自然事件冒泡,并让 Tree 小部件根部的元素负责接收事件来避免这样做。
解决此问题的另一种方法是使用 ARIA aria-activedescendant
属性。此属性设置在自然可聚焦的元素上,其值为当前选中项目的 HTML id。每当项目更改时,aria-activedescendant
值都应更新为新选中的项目的 id。屏幕阅读器会注意到值的更改并读取与 id 相对应的元素。下面是如何在 GWT Tree 和 TreeItem 小部件上使用此技术的示例。
首先,我们在 Tree 的根元素及其可聚焦元素上设置角色
// Called from Tree(...) constructor
private void init(TreeImages images, boolean useLeafImages) {
...
// Root element of Tree is a div
setElement(DOM.createDiv());
...
// Create naturally-focusable element
focusable = FocusPanel.impl.createFocusable();
...
// Hide element and append it to root div
DOM.setIntStyleAttribute(focusable, "zIndex", -1);
DOM.appendChild(getElement(), focusable);
// Listen for key events on the root element
sinkEvents(Event.MOUSEEVENTS | Event.ONCLICK | Event.KEYEVENTS);
...
// Add a11y role "tree" to the focusable element
Roles.getTreeRole().set(focusable);
}
每当项目选择更改时,都会在可聚焦元素上设置 aria-activedescendant
属性的值,并设置当前选中项目的 ARIA 状态和属性
// Called after a new item has been selected
private void updateAriaAttributes() {
// Get the element which contains the text (or widget) content within
// the currently-selected TreeItem
Element curSelectionContentElem = curSelection.getContentElem();
...
// Set the 'aria-level' state. To do this, we need to compute the level of
// the currently selected item.
Roles.getTreeitemRole().setAriaLevelProperty(curSelectionContentElem, curSelectionLevel + 1);
// Set other ARIA states
...
/ Update the 'aria-activedescendant' state for the focusable element to
// match the id of the currently selected item
Roles.getTreeRole().setAriaActivedescendantProperty(focusable,
IdReference.of(DOM.getElementAttribute(curSelectionContentElem, "id")));
}
虽然此代码片段中没有显示,但在创建 TreeItems 时,它们是由多个 div 构成的,其中只有一个包含我们希望屏幕阅读器解释的内容。此 div 被分配一个唯一的 DOM id(使用 DOM.createUniqueId() 方法生成),以及一个 treeitem
角色。这些属性没有设置在根 TreeItem div 上,因为它包含一个子图像,我们不希望读取该子图像。
此方法的注意事项
此方法的明显问题是需要为所有可能被选中的项目分配唯一的 DOM id。虽然这很容易实现,但为每个项目分配一个 DOM id 似乎很麻烦。
此外,使用 aria-activedescendant
状态存在一个细微问题。最初,此状态的预期用例是使用 div 实现一个列表框。每当父 div(具有自然焦点的 div)的 aria-activedescendant
值发生变化时,屏幕阅读器都会读出具有相应 id 的列表项的文本,忽略设置在选中项上的任何角色或状态。对于像列表框一样简单的小部件来说,这很好;选中项有足够的文本供用户理解选择了什么。但是,在 Tree 的情况下,选中项目的文本可能不足够。例如,选中项目位于树的哪个级别上?
一些屏幕阅读器已经开始不仅仅读出使用 aria-activedescendant
选择的项目的文本,而是像对待任何其他接收键盘焦点的元素一样解释该项目。但是,并非所有屏幕阅读器都这样做。
关联有意义的标签
网页通常会包含人类可读的描述性元素(例如 Label 和 HTML 小部件),这些元素解释了特定小部件的用途。但是,浏览器或屏幕阅读器可能无法理解小部件与其描述之间的关联。ARIA 定义了 aria-labelledby
属性,该属性可用于显式地将一个小部件与一个或多个描述性元素关联起来。
为了将标签与一个小部件关联,请确保所有描述性元素都具有唯一的 id。分配的 id 稍后可用于设置小部件的 aria-labelledby
状态以引用任何描述性元素的 id 值,从而将这些描述性元素与小部件关联起来。
自动朗读突出显示的内容
AJAX 组件通常会突出显示一个感兴趣的项目,而不会将键盘焦点移动到该项目。这在使用自动完成小部件等组件时会创造良好的最终用户体验;用户可以继续键入并获得可用选择集的进一步细化。由于屏幕阅读器传统上尝试朗读具有键盘焦点的项目,因此它们不会朗读突出显示的项目。ARIA 实时区域有助于使自动完成框等小部件对视力障碍用户可用。
工作原理
ARIA 角色 region
用于声明包含此类实时内容的区域,即,在没有键盘焦点的情况下动态更新的内容。此类区域上的 ARIA 状态 aria-live
指定此类更新的优先级;可以将其视为礼貌设置。以下代码示例应提供有关如何为自动完成小部件实施此技术的总体思路
初始化实时区域
在实例化 AutoCompleteWidget 构造函数中的相关 DOM 节点时添加 ARIA 角色 region
public AutoCompleteWidget() {
...
// Create a hidden div where we store the current item text for a
// screen reader to speak
ariaElement = DOM.createDiv();
DOM.setStyleAttribute(ariaElement, "display", "none");
Roles.getRegionRole(ariaElement);
Roles.getRegionRole().setAriaLiveProperty(ariaElement, LiveValue.ASSERTIVE);
DOM.appendChild(getElement(), ariaElement);
}
在这里,我们创建了一个隐藏的 div 元素,它包含要朗读的内容。我们已将其声明为 role = 'region'
和 live = 'assertive'
;后一种设置指定对此内容的更新具有最高优先级。接下来,我们设置了必要的关联,以便将作为用户键入 AutoCompleteWidget 的文本框中时返回的建议集放入隐藏的 div 中
// This method is called via a keyboard event handler
private void showSuggestions(Collection suggestions) {
if (suggestions.size() > 0) {
// Popupulate the visible suggestion pop-up widget with the new selections
// and show them
....
// Generate the hidden div content based on the suggestions
String hiddenDivText = "Suggestions ";
for (Suggestion curSuggestion : suggestions) {
hiddenDivText += " " + curSuggestion.getDisplayString();
}
DOM.setInnerText(ariaElement, hiddenDivText);
}
}
此方法的问题
使用这种技术,开发者可以完全控制屏幕阅读器朗读的文本。虽然这对于开发者来说似乎是一件好事,但对于屏幕阅读器用户来说并不理想。采用这种方法,自动完成小部件的开发者可能会决定屏幕阅读器应读取的不同的文本。例如,另一个屏幕阅读器可能会在每个建议前面加上“建议 x”,其中 x 是建议在列表中的索引。这会导致应用程序之间体验不一致。如果两个开发者都能使用 ARIA 角色、属性和状态,那么根据 ARIA 规范,将产生更一致的体验。
这种方法的另一个直接问题是国际化。大多数开发者会意识到建议列表需要翻译成不同的语言;该列表直接显示在屏幕上。但是,“建议”这个词作为动态区域的第一个词,很容易被忽略,因为它从未在视觉上显示给用户。这些描述性词语也必须翻译。如果可以利用 ARIA 角色和状态,那么与角色和状态相关的口语翻译将是屏幕阅读器的任务;开发者只需要负责翻译自己的内容。
小部件开发人员的一般建议
首先,尽可能使用原生的 HTML 控件。原生 HTML 控件被屏幕阅读器很好地理解。它们不需要 ARIA 角色和状态,这有两个主要好处
- ARIA 尚未得到所有主要浏览器的支持。屏幕阅读器和浏览器开发者已经完成了使 HTML 控件可访问的工作。
- 使用 div(例如)重新实现原生 HTML 控件会导致性能低下。例如,假设开发者使用 div 重新实现了一个列表框。适用于 `listitem` 角色的 ARIA 属性之一是 `aria-posinset`。此值指示项目在其父容器中的位置,对应于项目在列表框中的索引。问题是,每次向列表框添加或删除项目时,都必须遍历列表中的所有项目,调整其 `aria-posinset` 值。虽然可以做一些优化,但这仍然比原生 HTML select 元素慢得多。
如果无法使用原生 HTML 控件,并且必须构建自定义小部件,请记住,从一开始就开发一个可访问的小部件比在现有小部件上添加可访问性支持要容易得多。虽然添加 ARIA 角色、属性和状态相对容易,但确保在用户交互期间适当的元素获得键盘焦点可能会更加复杂。
确保测试新的小部件是否可访问!在 DOM 和屏幕阅读器之间转换有三个基本步骤
DOM
由于 ARIA 属性直接添加到 DOM,因此检查属性是否位于正确位置的简单方法是使用 DOM 检查器,例如 Firebug 或 Chrome 开发者工具。
事件
确保浏览器对 ARIA 属性、焦点变化和小部件本身的变化做出适当的事件响应非常重要。Microsoft 的一个工具名为 Accessible Event Watcher 或 AccEvent,可以让你检查正在触发的事件。
屏幕阅读器
最终,验证 GWT 小部件是否可访问的最佳方法是使用屏幕阅读器。一些屏幕阅读器可能无法监听浏览器触发的所有事件,或者它们可能期望将 ARIA 属性添加到 DOM 的特定位置。支持 ARIA 的最广泛使用的屏幕阅读器是 JAWS、Window-Eyes。新的浏览器屏幕阅读器是 FireVox(Firefox 的文本转语音插件)和 ChromeVox(Chrome 和 Chrome OS 的扩展程序)。