Bug 1578494 - separate accessible roles into the ones that are expected to be interactive from keyboard accessible ones, better account for ARIA comboboxes and listboxes. r=nchevobbe

Differential Revision: https://phabricator.services.mozilla.com/D45215

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Yura Zenevich 2019-09-11 14:09:03 +00:00
Родитель fa78314c8f
Коммит 9e5e47e8ff
3 изменённых файлов: 113 добавлений и 16 удалений

Просмотреть файл

@ -54,32 +54,39 @@ const SHOW_LONG_DESC_ACTION = "showlongdesc";
const FOCUS_PSEUDO_CLASS = ":focus";
const MOZ_FOCUSRING_PSEUDO_CLASS = ":-moz-focusring";
const INTERACTIVE_ROLES = new Set([
const KEYBOARD_FOCUSABLE_ROLES = new Set([
Ci.nsIAccessibleRole.ROLE_BUTTONMENU,
Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM,
Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION,
Ci.nsIAccessibleRole.ROLE_COMBOBOX,
Ci.nsIAccessibleRole.ROLE_COMBOBOX_OPTION,
Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX,
Ci.nsIAccessibleRole.ROLE_ENTRY,
Ci.nsIAccessibleRole.ROLE_LINK,
Ci.nsIAccessibleRole.ROLE_LISTBOX,
Ci.nsIAccessibleRole.ROLE_MENUITEM,
Ci.nsIAccessibleRole.ROLE_OPTION,
Ci.nsIAccessibleRole.ROLE_PAGETAB,
Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT,
Ci.nsIAccessibleRole.ROLE_PARENT_MENUITEM,
Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
Ci.nsIAccessibleRole.ROLE_RICH_OPTION,
Ci.nsIAccessibleRole.ROLE_SLIDER,
Ci.nsIAccessibleRole.ROLE_SPINBUTTON,
Ci.nsIAccessibleRole.ROLE_SUMMARY,
Ci.nsIAccessibleRole.ROLE_SWITCH,
Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
]);
const INTERACTIVE_ROLES = new Set([
...KEYBOARD_FOCUSABLE_ROLES,
Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM,
Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION,
Ci.nsIAccessibleRole.ROLE_COMBOBOX_OPTION,
Ci.nsIAccessibleRole.ROLE_MENUITEM,
Ci.nsIAccessibleRole.ROLE_OPTION,
Ci.nsIAccessibleRole.ROLE_OUTLINE,
Ci.nsIAccessibleRole.ROLE_OUTLINEITEM,
Ci.nsIAccessibleRole.ROLE_PAGETAB,
Ci.nsIAccessibleRole.ROLE_PARENT_MENUITEM,
Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
Ci.nsIAccessibleRole.ROLE_RICH_OPTION,
]);
/**
* Determine if a node is dead or is not an element node.
*
@ -295,7 +302,7 @@ function interactiveRule(accessible) {
* when enabled, audit report object otherwise.
*/
function focusableRule(accessible) {
if (!INTERACTIVE_ROLES.has(accessible.role)) {
if (!KEYBOARD_FOCUSABLE_ROLES.has(accessible.role)) {
return null;
}
@ -315,6 +322,21 @@ function focusableRule(accessible) {
return null;
}
let ariaRoles;
try {
ariaRoles = accessible.attributes.getStringProperty("xml-roles");
} catch (e) {
// No xml-roles. nsPersistentProperties throws if the attribute for a key
// is not found.
}
if (
ariaRoles &&
(ariaRoles.includes("combobox") || ariaRoles.includes("listbox"))
) {
// Do not force ARIA combobox or listbox to be focusable.
return null;
}
return { score: FAIL, issue: INTERACTIVE_NOT_FOCUSABLE };
}
@ -331,7 +353,11 @@ function focusableRule(accessible) {
* interactive role, audit report object otherwise.
*/
function semanticsRule(accessible) {
if (INTERACTIVE_ROLES.has(accessible.role)) {
if (
INTERACTIVE_ROLES.has(accessible.role) ||
// Visible listboxes will have focusable state when inside comboboxes.
accessible.role === Ci.nsIAccessibleRole.ROLE_COMBOBOX_LIST
) {
return null;
}

Просмотреть файл

@ -129,6 +129,41 @@ add_task(async function() {
"#img-4",
{ score: FAIL, issue: MOUSE_INTERACTIVE_ONLY },
],
["Focusable button with aria-haspopup.", "#buttonmenu-1", null],
[
"Not focusable aria button with aria-haspopup.",
"#buttonmenu-2",
{
score: FAIL,
issue: INTERACTIVE_NOT_FOCUSABLE,
},
],
["Focusable checkbox.", "#checkbox-1", null],
["Focusable select element size > 1", "#listbox-1", null],
["Focusable select element with one option", "#combobox-1", null],
["Focusable select element with no options", "#combobox-2", null],
["Focusable select element with two options", "#combobox-3", null],
[
"Non-focusable aria combobox with one aria option.",
"#editcombobox-1",
null,
],
["Non-focusable aria combobox with no options.", "#editcombobox-2", null],
["Focusable aria combobox with no options.", "#editcombobox-3", null],
[
"Non-focusable aria switch",
"#switch-1",
{
score: FAIL,
issue: INTERACTIVE_NOT_FOCUSABLE,
},
],
["Focusable aria switch", "#switch-2", null],
[
"Combobox list that is visible (has focusable state)",
"#owned_listbox",
null,
],
];
for (const [description, selector, expected] of tests) {
@ -144,16 +179,27 @@ add_task(async function() {
}
info("Text leaf inside a link (jump action is propagated to the text link)");
const node = await walker.querySelector(walker.rootNode, "#link-5");
const parent = await a11yWalker.getAccessibleFor(node);
const front = (await parent.children())[0];
const audit = await front.audit({ types: [KEYBOARD] });
let node = await walker.querySelector(walker.rootNode, "#link-5");
let parent = await a11yWalker.getAccessibleFor(node);
let front = (await parent.children())[0];
let audit = await front.audit({ types: [KEYBOARD] });
Assert.deepEqual(
audit[KEYBOARD],
null,
"Text leafs are excluded from semantics rule."
);
info("Combobox list that is invisible");
node = await walker.querySelector(walker.rootNode, "#combobox-1");
parent = await a11yWalker.getAccessibleFor(node);
front = (await parent.children())[0];
audit = await front.audit({ types: [KEYBOARD] });
Assert.deepEqual(
audit[KEYBOARD],
null,
"Combobox lists (invisible) are excluded from semantics rule."
);
await accessibility.disable();
await waitForA11yShutdown();
await target.destroy();

Просмотреть файл

@ -40,5 +40,30 @@
<img id="img-2" longdesc="https://example.com" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
<img id="img-3" longdesc="https://example.com" onclick="console.log('foo');" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
<img id="img-4" onclick="console.log('foo');" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
<button id="buttonmenu-1" aria-haspopup="true">I have a popup</button>
<div role="button" id="buttonmenu-2" aria-haspopup="true">I have a popup</div>
<input id="checkbox-1" type="checkbox" name="hello" />
<select id="listbox-1" size="2">
<option id="lb_orange">orange</option>
<option id="lb_apple">apple</option>
</select>
<select id="combobox-1"></select>
<select id="combobox-2"><option>One</option></select>
<select id="combobox-3">
<option id="cb_orange">orange</option>
<option id="cb_apple">apple</option>
</select>
<div id="editcombobox-1" role="combobox"><span role="option">One</span></div>
<span id="editcombobox-2"role="combobox"></span>
<span id="editcombobox-3"role="combobox" tabindex="0"></span>
<span id="switch-1" role="switch"></span>
<span id="switch-2" role="switch" tabindex="0"></span>
<div aria-label="Tag" role="combobox" aria-expanded="true" aria-owns="owned_listbox" aria-haspopup="listbox">
<input type="text" aria-autocomplete="list" aria-controls="owned_listbox" aria-activedescendant="selected_option">
</div>
<ul role="listbox" id="owned_listbox">
<li role="option">Zebra</li>
<li role="option" id="selected_option">Zoom</li>
</ul>
</body>
</html>