Bug 1577381: Correct accessibility exposure for optgroups in content select dropdowns. r=eeejay,NeilDeakin

For remote content documents, select dropdowns (for <select size="1">) are rendered in the parent process using a XUL menupopup.
This means that the accessibility code for HTML selects doesn't apply.

In the menupopup, the optgroup is a sibling of its contained options.
For accessibility, we want to preserve the hierarchy such that the options are inside the optgroup.
We do this using aria-owns on the optgroup item.

This required some tweaks to XULMenuitemAccessible, as it couldn't previously handle grouping Accessibles between the menupopup and its items.

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
James Teh 2019-09-27 02:50:59 +00:00
parent 964bceeb91
commit 1adf6ac726
4 changed files with 101 additions and 6 deletions

View File

@ -20,6 +20,8 @@ support-files =
[browser_caching_states.js]
[browser_caching_value.js]
[browser_contentSelectDropdown.js]
# Events tests
[browser_events_announcement.js]
skip-if = e10s && os == 'win' # Bug 1288839

View File

@ -0,0 +1,65 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* import-globals-from ../../mochitest/role.js */
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
const snippet = `
<select id="select">
<option>o1</option>
<optgroup label="g1">
<option>g1o1</option>
<option>g1o2</option>
</optgroup>
<optgroup label="g2">
<option>g2o1</option>
<option>g2o2</option>
</optgroup>
<option>o2</option>
</select>
`;
addAccessibleTask(snippet, async function(browser, accDoc) {
await invokeFocus(browser, "select");
// Expand the select. A dropdown item should get focus.
// Note that the dropdown is rendered in the parent process.
let focused = waitForEvent(
EVENT_FOCUS,
event => event.accessible.role == ROLE_COMBOBOX_OPTION,
"Dropdown item focused after select expanded"
);
await BrowserTestUtils.synthesizeKey(
"KEY_ArrowDown",
{ altKey: true },
browser
);
let event = await focused;
let dropdown = event.accessible.parent;
let selectedOptionChildren = [];
if (MAC) {
// Checkmark is part of the Mac menu styling.
selectedOptionChildren = [{ STATICTEXT: [] }];
}
let tree = {
COMBOBOX_LIST: [
{ COMBOBOX_OPTION: selectedOptionChildren },
{ GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }] },
{ GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }] },
{ COMBOBOX_OPTION: [] },
],
};
testAccessibleTree(dropdown, tree);
// Collapse the select. Focus should return to the select.
focused = waitForEvent(
EVENT_FOCUS,
"select",
"select focused after collapsed"
);
await BrowserTestUtils.synthesizeKey("KEY_Escape", {}, browser);
await focused;
});

View File

@ -221,8 +221,10 @@ role XULMenuitemAccessible::NativeRole() const {
nsCOMPtr<nsIDOMXULContainerElement> xulContainer = Elm()->AsXULContainer();
if (xulContainer) return roles::PARENT_MENUITEM;
if (mParent && mParent->Role() == roles::COMBOBOX_LIST)
Accessible* widget = ContainerWidget();
if (widget && widget->Role() == roles::COMBOBOX_LIST) {
return roles::COMBOBOX_OPTION;
}
if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
nsGkAtoms::radio, eCaseMatters))
@ -287,11 +289,19 @@ Accessible* XULMenuitemAccessible::ContainerWidget() const {
if (menuFrame) {
nsMenuParent* menuParent = menuFrame->GetMenuParent();
if (menuParent) {
if (menuParent->IsMenuBar()) // menubar menu
return mParent;
// a menupoup or parent menu item
if (menuParent->IsMenu()) return mParent;
nsBoxFrame* frame = nullptr;
if (menuParent->IsMenuBar()) { // menubar menu
frame = static_cast<nsMenuBarFrame*>(menuParent);
} else if (menuParent->IsMenu()) { // a menupopup or parent menu item
frame = static_cast<nsMenuPopupFrame*>(menuParent);
}
if (frame) {
nsIContent* content = frame->GetContent();
if (content) {
MOZ_ASSERT(mDoc);
return mDoc->GetAccessible(content);
}
}
// otherwise it's different kind of popups (like panel or tooltip), it
// shouldn't be a real case.

View File

@ -408,11 +408,15 @@ var SelectParentHelper = {
) {
let element = menulist.menupopup;
let ariaOwns = "";
for (let option of options) {
let isOptGroup = option.tagName == "OPTGROUP";
let item = element.ownerDocument.createXULElement(
isOptGroup ? "menucaption" : "menuitem"
);
if (isOptGroup) {
item.setAttribute("role", "group");
}
let style = uniqueOptionStyles[option.styleIndex];
item.setAttribute("label", option.textContent);
@ -487,6 +491,16 @@ var SelectParentHelper = {
item.removeAttribute("customoptionstyling");
}
if (parentElement) {
// In the menupopup, the optgroup is a sibling of its contained options.
// For accessibility, we want to preserve the hierarchy such that the
// options are inside the optgroup. We do this using aria-owns on the
// parent.
item.id = "ContentSelectDropdownOption" + nthChildIndex;
item.setAttribute("aria-level", "2");
ariaOwns += item.id + " ";
}
element.appendChild(item);
nthChildIndex++;
@ -536,6 +550,10 @@ var SelectParentHelper = {
}
}
if (parentElement && ariaOwns) {
parentElement.setAttribute("aria-owns", ariaOwns);
}
// Check if search pref is enabled, if this is the first time iterating through
// the dropdown, and if the list is long enough for a search element to be added.
if (