diff --git a/accessible/base/ARIAMap.cpp b/accessible/base/ARIAMap.cpp index 96cd0f8e3a4e..b097f28748a4 100644 --- a/accessible/base/ARIAMap.cpp +++ b/accessible/base/ARIAMap.cpp @@ -14,9 +14,11 @@ #include "States.h" #include "nsAttrName.h" +#include "nsGenericHTMLElement.h" #include "nsWhitespaceTokenizer.h" #include "mozilla/BinarySearch.h" +#include "mozilla/dom/Document.h" #include "mozilla/dom/Element.h" #include "nsUnicharUtils.h" @@ -1602,11 +1604,27 @@ uint8_t aria::AttrCharacteristicsFor(nsAtom* aAtom) { return 0; } -bool aria::HasDefinedARIAHidden(nsIContent* aContent) { +bool aria::IsValidARIAHidden(nsIContent* aContent) { return aContent && aContent->IsElement() && nsAccUtils::ARIAAttrValueIs(aContent->AsElement(), nsGkAtoms::aria_hidden, nsGkAtoms::_true, - eCaseMatters); + eCaseMatters) && + !ShouldIgnoreARIAHidden(aContent); +} + +bool aria::ShouldIgnoreARIAHidden(nsIContent* aContent) { + if (!aContent) { + return false; + } + + dom::Document* doc = aContent->OwnerDoc(); + bool isValidElementType = (aContent == doc->GetDocumentElement()); + + if (auto docBody = doc->GetBody()) { + isValidElementType |= (aContent == docBody->AsContent()); + } + + return isValidElementType && doc->IsTopLevelContentDocument(); } const nsRoleMapEntry* aria::GetRoleMap(const nsStaticAtom* aAriaRole) { diff --git a/accessible/base/ARIAMap.h b/accessible/base/ARIAMap.h index 984072e26779..e0e04cca07a9 100644 --- a/accessible/base/ARIAMap.h +++ b/accessible/base/ARIAMap.h @@ -305,9 +305,16 @@ uint64_t UniversalStatesFor(dom::Element* aElement); uint8_t AttrCharacteristicsFor(nsAtom* aAtom); /** - * Return true if the element has defined aria-hidden. + * Return true if the element has defined aria-hidden + * and should not be ignored per ShouldIgnoreARIAHidden. */ -bool HasDefinedARIAHidden(nsIContent* aContent); +bool IsValidARIAHidden(nsIContent* aContent); + +/** + * Return true if the element should render its subtree + * regardless of the presence of aria-hidden. + */ +bool ShouldIgnoreARIAHidden(nsIContent* aContent); /** * Get the role map entry for a given ARIA role. diff --git a/accessible/base/nsAccessibilityService.cpp b/accessible/base/nsAccessibilityService.cpp index df9e88724d13..843b06443a3f 100644 --- a/accessible/base/nsAccessibilityService.cpp +++ b/accessible/base/nsAccessibilityService.cpp @@ -1184,7 +1184,7 @@ LocalAccessible* nsAccessibilityService::CreateAccessible( if (!aNode->IsContent()) return nullptr; nsIContent* content = aNode->AsContent(); - if (aria::HasDefinedARIAHidden(content)) { + if (aria::IsValidARIAHidden(content)) { if (aIsSubtreeHidden) { *aIsSubtreeHidden = true; } diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp index c5e365af00a3..21e8042a3f09 100644 --- a/accessible/generic/DocAccessible.cpp +++ b/accessible/generic/DocAccessible.cpp @@ -862,7 +862,7 @@ void DocAccessible::AttributeChanged(dom::Element* aElement, // Update the accessible tree on aria-hidden change. Make sure to not create // a tree under aria-hidden='true'. if (aAttribute == nsGkAtoms::aria_hidden) { - if (aria::HasDefinedARIAHidden(aElement)) { + if (aria::IsValidARIAHidden(aElement)) { ContentRemoved(aElement); } else { ContentInserted(aElement, aElement->GetNextSibling()); @@ -1206,7 +1206,7 @@ LocalAccessible* DocAccessible::GetAccessibleOrContainer( for (nsINode* currNode : dom::InclusiveFlatTreeAncestors(*start)) { // No container if is inside of aria-hidden subtree. if (aNoContainerIfPruned && currNode->IsElement() && - aria::HasDefinedARIAHidden(currNode->AsElement())) { + aria::IsValidARIAHidden(currNode->AsElement())) { return nullptr; } diff --git a/accessible/tests/browser/tree/browser.toml b/accessible/tests/browser/tree/browser.toml index 1ab6f8e2a254..59c408d46796 100644 --- a/accessible/tests/browser/tree/browser.toml +++ b/accessible/tests/browser/tree/browser.toml @@ -28,6 +28,8 @@ skip-if = [ ["browser_link.js"] +["browser_test_aria_hidden.js"] + ["browser_searchbar.js"] ["browser_select.js"] diff --git a/accessible/tests/browser/tree/browser_test_aria_hidden.js b/accessible/tests/browser/tree/browser_test_aria_hidden.js new file mode 100644 index 000000000000..4a7ee2036d78 --- /dev/null +++ b/accessible/tests/browser/tree/browser_test_aria_hidden.js @@ -0,0 +1,329 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +/** + * Verify loading a root doc with aria-hidden renders the document. + * Non-root doc elements, like embedded iframes, should continue + * to respect aria-hidden when present. This test ONLY tests + * tab documents, it should not run in iframes. There is a separate + * test for iframes below. + */ +addAccessibleTask( + ` +

I am some content in a document

+ `, + async function testTabDocument(browser, docAcc) { + const originalTree = { DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }] }; + testAccessibleTree(docAcc, originalTree); + }, + { + chrome: true, + topLevel: true, + iframe: false, + remoteIframe: false, + contentDocBodyAttrs: { "aria-hidden": "true" }, + } +); + +/** + * Verify adding aria-hidden to root doc elements has no effect. + * Non-root doc elements, like embedded iframes, should continue + * to respect aria-hidden when applied. This test ONLY tests + * tab documents, it should not run in iframes. There is a separate + * test for iframes below. + */ +addAccessibleTask( + ` +

I am some content in a document

+ `, + async function testTabDocumentMutation(browser, docAcc) { + const originalTree = { DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }] }; + + testAccessibleTree(docAcc, originalTree); + info("Adding aria-hidden=true to content doc"); + const unexpectedEvents = { unexpected: [[EVENT_REORDER, docAcc]] }; + await contentSpawnMutation(browser, unexpectedEvents, function () { + const b = content.document.body; + b.setAttribute("aria-hidden", "true"); + }); + + testAccessibleTree(docAcc, originalTree); + }, + { chrome: true, topLevel: true, iframe: false, remoteIframe: false } +); + +/** + * Verify loading an iframe doc with aria-hidden doesn't render the document. + * This test ONLY tests iframe documents, it should not run in tab docs. + * There is a separate test for tab docs above. + */ +addAccessibleTask( + ` +

I am some content in a document

+ `, + async function testIframeDocument(browser, docAcc, topLevel) { + const originalTree = { DOCUMENT: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }] }; + testAccessibleTree(topLevel, originalTree); + }, + { + chrome: false, + topLevel: false, + iframe: true, + remoteIframe: true, + iframeDocBodyAttrs: { "aria-hidden": "true" }, + } +); + +/** + * Verify adding aria-hidden to iframe doc elements removes + * their subtree. This test ONLY tests iframe documents, it + * should not run in tab documents. There is a separate test for + * tab documents above. + */ +addAccessibleTask( + ` +

I am some content in a document

+ `, + async function testIframeDocumentMutation(browser, docAcc, topLevel) { + const originalTree = { + DOCUMENT: [ + { + INTERNAL_FRAME: [ + { + DOCUMENT: [ + { + PARAGRAPH: [ + { + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }, + ], + }; + + testAccessibleTree(topLevel, originalTree); + info("Adding aria-hidden=true to content doc"); + await contentSpawnMutation( + browser, + { expected: [[EVENT_REORDER, docAcc]] }, + function () { + const b = content.document.body; + b.setAttribute("aria-hidden", "true"); + } + ); + const newTree = { + DOCUMENT: [ + { + INTERNAL_FRAME: [ + { + DOCUMENT: [], + }, + ], + }, + ], + }; + testAccessibleTree(topLevel, newTree); + }, + { chrome: false, topLevel: false, iframe: true, remoteIframe: true } +); + +// // /////////////////////////////// +// // //////////////////// SVG Tests +// // ////////////////////////////// + +const SVG_DOCUMENT_ID = "rootSVG"; +const HIDDEN_SVG_URI = + "data:image/svg+xml,%3Csvg%20id%3D%22rootSVG%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20aria-hidden%3D%22true%22%3E%3Ctext%20x%3D%2210%22%20y%3D%2250%22%20font-size%3D%2230%22%20id%3D%22textSVG%22%3EMy%20SVG%3C%2Ftext%3E%3C%2Fsvg%3E"; +const SVG_URI = + "data:image/svg+xml,%3Csvg%20id%3D%22rootSVG%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctext%20x%3D%2210%22%20y%3D%2250%22%20font-size%3D%2230%22%20id%3D%22textSVG%22%3EMy%20SVG%3C%2Ftext%3E%3C%2Fsvg%3E"; + +/** + * Verify loading an SVG document with aria-hidden=true renders the + * entire document subtree. + * Non-root svg elements, like those in embedded iframes, should + * continue to respect aria-hidden when applied. + */ +addAccessibleTask( + `hello world`, + async function testSVGDocument(browser) { + let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID); + info("Loading SVG"); + browser.loadURI(Services.io.newURI(HIDDEN_SVG_URI), { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + await loaded; + + const tree = { + DOCUMENT: [ + { + TEXT_CONTAINER: [ + { + TEXT_LEAF: [], + }, + ], + }, + ], + }; + const root = getRootAccessible(document); + const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID); + testAccessibleTree(svgRoot, tree); + }, + { chrome: true, topLevel: true, iframe: false, remoteIframe: false } +); + +/////////// +///// TODO: Bug 1960416 +////////// +// /** +// * Verify loading an SVG document with aria-hidden=true +// * in an iframe does not render the document subtree. +// */ +// addAccessibleTask( +// `hello world`, +// async function testSVGIframeDocument(browser) { +// info("Loading SVG"); +// const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID); +// await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID, HIDDEN_SVG_URI], (_id,_uri) => { +// content.document.getElementById(_id).src = _uri; +// }); +// await loaded; + +// const tree = { +// DOCUMENT: [], +// }; +// const root = getRootAccessible(document); +// const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID); +// testAccessibleTree(svgRoot, tree); +// }, +// { chrome: false, topLevel: false, iframe: true, remoteIframe: true } +// ); + +/** + * Verify adding aria-hidden to root svg elements has no effect. + * Non-root svg elements, like those in embedded iframes, should + * continue to respect aria-hidden when applied. + */ +addAccessibleTask( + `hello world`, + async function testSVGDocumentMutation(browser) { + let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID); + info("Loading SVG"); + browser.loadURI(Services.io.newURI(SVG_URI), { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + await loaded; + + const originalTree = { + DOCUMENT: [ + { + TEXT_CONTAINER: [ + { + TEXT_LEAF: [], + }, + ], + }, + ], + }; + const root = getRootAccessible(document); + const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID); + testAccessibleTree(svgRoot, originalTree); + info("Adding aria-hidden=true to svg"); + // XXX Bug 1959547: We incorrectly get a reorder + // here. The tree should be unaffected by this attribute, + // but it seems like it isn't! Below we'll verify that + // the tree isn't removed, despite this reorder. + const unexpectedEvents = { expected: [[EVENT_REORDER, SVG_DOCUMENT_ID]] }; + info("Adding aria-hidden"); + await contentSpawnMutation( + browser, + unexpectedEvents, + function (_id) { + const d = content.document.getElementById(_id); + d.setAttribute("aria-hidden", "true"); + }, + [SVG_DOCUMENT_ID] + ); + // XXX Bug 1959547: We end up with an extra node in the + // tree after adding aria-hidden. It seems like SVG root + // element is splitting off / no longer behaves as the + // document...? + const newTree = { + DOCUMENT: [ + { + DIAGRAM: [ + { + TEXT_CONTAINER: [ + { + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }; + testAccessibleTree(svgRoot, newTree); + }, + { chrome: true, topLevel: true, iframe: false, remoteIframe: false } +); + +/** + * Verify adding aria-hidden to root svg elements in iframes removes + * the svg subtree. + */ +addAccessibleTask( + `hello world`, + async function testSVGIframeDocumentMutation(browser) { + info("Loading SVG"); + const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID); + await SpecialPowers.spawn( + browser, + [DEFAULT_IFRAME_ID, SVG_URI], + (contentId, _uri) => { + content.document.getElementById(contentId).src = _uri; + } + ); + await loaded; + const originalTree = { + DOCUMENT: [ + { + TEXT_CONTAINER: [ + { + TEXT_LEAF: [], + }, + ], + }, + ], + }; + const svgRoot = findAccessibleChildByID( + getRootAccessible(document), + SVG_DOCUMENT_ID + ); + testAccessibleTree(svgRoot, originalTree); + + info("Adding aria-hidden=true to svg"); + const events = { expected: [[EVENT_REORDER, SVG_DOCUMENT_ID]] }; + await contentSpawnMutation( + browser, + events, + function (_id) { + const d = content.document.getElementById(_id); + d.setAttribute("aria-hidden", "true"); + }, + [SVG_DOCUMENT_ID] + ); + + const newTree = { DOCUMENT: [] }; + testAccessibleTree(svgRoot, newTree); + }, + { chrome: false, topLevel: false, iframe: true, remoteIframe: true } +);