зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1699339: Fire name/description change event when text in a hidden aria-labelledby/describedby subtree changes. r=eeejay,morgan
We already handled this for visible aria-labelledby/describedby subtrees based on a11y events. However, when a subtree is hidden (whether via CSS or aria-hidden), it is completely removed from the a11y tree, so we can't use a11y events. Instead, when a node is added to the DOM, we walk its ancestors looking for an aria-labelledby/describedby target. We stop if the node or an ancestor has an Accessible, since that means it will be handled elsewhere. This also limits the number of ancestors we walk for each inserted node, thus decreasing the performance impact of this change. This doesn't catch all possible mutations in a hidden subtree (e.g. removals or direct text node changes), but this at least fixes a case in Gmail. Given performance risks, I think it makes sense to address specific cases as they arise. Differential Revision: https://phabricator.services.mozilla.com/D147559
This commit is contained in:
Родитель
7f54665fd0
Коммит
9d9377be5e
|
@ -860,7 +860,9 @@ void DocAccessible::ARIAActiveDescendantChanged(LocalAccessible* aAccessible) {
|
|||
}
|
||||
}
|
||||
|
||||
void DocAccessible::ContentAppended(nsIContent* aFirstNewContent) {}
|
||||
void DocAccessible::ContentAppended(nsIContent* aFirstNewContent) {
|
||||
MaybeHandleChangeToHiddenNameOrDescription(aFirstNewContent);
|
||||
}
|
||||
|
||||
void DocAccessible::ContentStateChanged(dom::Document* aDocument,
|
||||
nsIContent* aContent,
|
||||
|
@ -934,7 +936,9 @@ void DocAccessible::CharacterDataWillChange(nsIContent* aContent,
|
|||
void DocAccessible::CharacterDataChanged(nsIContent* aContent,
|
||||
const CharacterDataChangeInfo&) {}
|
||||
|
||||
void DocAccessible::ContentInserted(nsIContent* aChild) {}
|
||||
void DocAccessible::ContentInserted(nsIContent* aChild) {
|
||||
MaybeHandleChangeToHiddenNameOrDescription(aChild);
|
||||
}
|
||||
|
||||
void DocAccessible::ContentRemoved(nsIContent* aChildNode,
|
||||
nsIContent* aPreviousSiblingNode) {
|
||||
|
@ -2644,3 +2648,40 @@ void DocAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
|
|||
aName.AssignLiteral("click");
|
||||
}
|
||||
}
|
||||
|
||||
void DocAccessible::MaybeHandleChangeToHiddenNameOrDescription(
|
||||
nsIContent* aChild) {
|
||||
if (!HasLoadState(eTreeConstructed)) {
|
||||
return;
|
||||
}
|
||||
for (nsIContent* content = aChild; content; content = content->GetParent()) {
|
||||
if (HasAccessible(content)) {
|
||||
// This node isn't hidden. Events for name/description dependents will be
|
||||
// fired elsewhere.
|
||||
break;
|
||||
}
|
||||
nsAtom* id = content->GetID();
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
auto* providers =
|
||||
GetRelProviders(content->AsElement(), nsDependentAtomString(id));
|
||||
if (!providers) {
|
||||
continue;
|
||||
}
|
||||
for (auto& provider : *providers) {
|
||||
if (provider->mRelAttr != nsGkAtoms::aria_labelledby &&
|
||||
provider->mRelAttr != nsGkAtoms::aria_describedby) {
|
||||
continue;
|
||||
}
|
||||
LocalAccessible* dependentAcc = GetAccessible(provider->mContent);
|
||||
if (!dependentAcc) {
|
||||
continue;
|
||||
}
|
||||
FireDelayedEvent(provider->mRelAttr == nsGkAtoms::aria_labelledby
|
||||
? nsIAccessibleEvent::EVENT_NAME_CHANGE
|
||||
: nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE,
|
||||
dependentAcc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -766,6 +766,14 @@ class DocAccessible : public HyperTextAccessibleWrap,
|
|||
*/
|
||||
void TrackMovedAccessible(LocalAccessible* aAcc);
|
||||
|
||||
/**
|
||||
* For hidden subtrees, fire a name/description change event if the subtree
|
||||
* is a target of aria-labelledby/describedby.
|
||||
* This does nothing if it is called on a node which is not part of a hidden
|
||||
* aria-labelledby/describedby target.
|
||||
*/
|
||||
void MaybeHandleChangeToHiddenNameOrDescription(nsIContent* aChild);
|
||||
|
||||
PresShell* mPresShell;
|
||||
|
||||
// Exclusively owned by IPDL so don't manually delete it!
|
||||
|
|
|
@ -212,3 +212,35 @@ addAccessibleTask(
|
|||
},
|
||||
{ iframe: true, remoteIframe: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* Test that the description is updated when the content of a hidden aria-describedby
|
||||
* subtree changes.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<button id="button" aria-describedby="desc">
|
||||
<div id="desc" hidden>a</div>
|
||||
`,
|
||||
async function(browser, docAcc) {
|
||||
const button = findAccessibleChildByID(docAcc, "button");
|
||||
testDescr(button, "a");
|
||||
info("Changing desc textContent");
|
||||
let descChanged = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
|
||||
await invokeContentTask(browser, [], () => {
|
||||
content.document.getElementById("desc").textContent = "c";
|
||||
});
|
||||
await descChanged;
|
||||
testDescr(button, "c");
|
||||
info("Prepending text node to desc");
|
||||
descChanged = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
|
||||
await invokeContentTask(browser, [], () => {
|
||||
content.document
|
||||
.getElementById("desc")
|
||||
.prepend(content.document.createTextNode("b"));
|
||||
});
|
||||
await descChanged;
|
||||
testDescr(button, "bc");
|
||||
},
|
||||
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
|
||||
);
|
||||
|
|
|
@ -519,3 +519,35 @@ addAccessibleTask(
|
|||
},
|
||||
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* Test that the name is updated when the content of a hidden aria-labelledby
|
||||
* subtree changes.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<button id="button" aria-labelledby="label">
|
||||
<div id="label" hidden>a</div>
|
||||
`,
|
||||
async function(browser, docAcc) {
|
||||
const button = findAccessibleChildByID(docAcc, "button");
|
||||
testName(button, "a");
|
||||
info("Changing label textContent");
|
||||
let nameChanged = waitForEvent(EVENT_NAME_CHANGE, button);
|
||||
await invokeContentTask(browser, [], () => {
|
||||
content.document.getElementById("label").textContent = "c";
|
||||
});
|
||||
await nameChanged;
|
||||
testName(button, "c");
|
||||
info("Prepending text node to label");
|
||||
nameChanged = waitForEvent(EVENT_NAME_CHANGE, button);
|
||||
await invokeContentTask(browser, [], () => {
|
||||
content.document
|
||||
.getElementById("label")
|
||||
.prepend(content.document.createTextNode("b"));
|
||||
});
|
||||
await nameChanged;
|
||||
testName(button, "bc");
|
||||
},
|
||||
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
|
||||
);
|
||||
|
|
Загрузка…
Ссылка в новой задаче