Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
AX: Content within iframes with ARIA roles is not accessible
https://bugs.webkit.org/show_bug.cgi?id=259719
rdar://104611075

Reviewed by Chris Fleizach.

This happened because `AccessibilityRenderObject::isAttachment` returned
false for `RenderWidget`s associated with objects that have ARIA roles,
which then caused `AccessibilityRenderObject::addAttachmentChildren()` to
return early without inserting any of the iframe's content.

This behavior (isAttachment returning false for things with an ARIA role)
was added 15 years ago in commit:

ca5f21d

There is no test for this behavior, and I can't discern why it's
necessary. So this patch removes it, in turn fixing the bug.

* LayoutTests/accessibility/iframe-with-role-expected.txt: Added.
* LayoutTests/accessibility/iframe-with-role.html: Added.
* LayoutTests/platform/glib/TestExpectations: Skip new test.
* LayoutTests/platform/ios/TestExpectations: Enable new test.
* LayoutTests/platform/ios/accessibility/iframe-with-role-expected.txt: Added.
* LayoutTests/platform/mac-wk1/accessibility/iframe-with-role-expected.txt: Added.
* Source/WebCore/accessibility/AccessibilityRenderObject.cpp:
(WebCore::AccessibilityRenderObject::isAttachment const):

Canonical link: https://commits.webkit.org/266506@main
  • Loading branch information
twilco committed Aug 2, 2023
1 parent 786e20b commit da39768
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 8 deletions.
34 changes: 34 additions & 0 deletions LayoutTests/accessibility/iframe-with-role-expected.txt
@@ -0,0 +1,34 @@
This test ensures that the content within an iframe with an ARIA role is accessible.

Traversing initial body content.

AXRole: AXStaticText
AXValue: Text before iframe

AXRole: AXScrollArea

AXRole: AXWebArea

AXRole: AXButton

AXRole: AXStaticText
AXValue: Text after iframe

document.getElementById('iframe').removeAttribute('role');

AXRole: AXStaticText
AXValue: Text before iframe

AXRole: AXScrollArea

AXRole: AXWebArea

AXRole: AXButton

AXRole: AXStaticText
AXValue: Text after iframe

PASS successfullyParsed is true

TEST COMPLETE
Text before iframe Text after iframe
56 changes: 56 additions & 0 deletions LayoutTests/accessibility/iframe-with-role.html
@@ -0,0 +1,56 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<script src="../resources/accessibility-helper.js"></script>
<script src="../resources/js-test.js"></script>
</head>
<body onload="runTest()">

Text before iframe
<iframe id="iframe" srcdoc="<body><button>Click me (inside iframe)</button></body>"></iframe>
Text after iframe

<script>
window.jsTestIsAsync = true;

var output = "This test ensures that the content within an iframe with an ARIA role is accessible.\n\n";

function traverseContent() {
const rootWebArea = accessibilityController.rootElement.childAtIndex(0);
let searchResult = null;
while (true) {
searchResult = rootWebArea.uiElementForSearchPredicate(searchResult, true, "AXAnyTypeSearchKey", "", false);
if (!searchResult)
break;

const role = searchResult.role;
output += `\n${role}`;
if (role.includes("StaticText")) {
let textContent = accessibilityController.platformName === "ios" ? searchResult.description : searchResult.stringValue;
output += `\n${textContent}`;
}
output += "\n";
}
}

function runTest() {
if (!window.accessibilityController)
return;

output += "Traversing initial body content.\n";
traverseContent();

output += `\n${evalAndReturn("document.getElementById('iframe').removeAttribute('role');")}`;
setTimeout(async function() {
// Wait an arbitrary amount of time for the AX tree to update.
await sleep(100);
traverseContent();

debug(output);
finishJSTest();
}, 0);
}
</script>
</body>
</html>

1 change: 1 addition & 0 deletions LayoutTests/platform/glib/TestExpectations
Expand Up @@ -505,6 +505,7 @@ accessibility/search-traversal-after-role-change.html [ Skip ]
accessibility/table-search-traversal.html [ Skip ]
accessibility/dynamically-changing-iframe-remains-accessible.html [ Skip ]
accessibility/ignore-modals-without-any-content.html [ Skip ]
accessibility/iframe-with-role.html [ Skip ]

# Missing `AccessibilityUIElement::stringDescriptionOfAttributeValue` implementation.
accessibility/visible-character-range-basic.html [ Skip ]
Expand Down
1 change: 1 addition & 0 deletions LayoutTests/platform/ios/TestExpectations
Expand Up @@ -2190,6 +2190,7 @@ accessibility/element-haspopup.html [ Pass ]
accessibility/empty-text-under-element-cached.html [ Pass ]
accessibility/heading-level.html [ Pass ]
accessibility/html5-required-attribute.html [ Pass ]
accessibility/iframe-with-role.html [ Pass ]
accessibility/input-type-hidden-in-aria-hidden-false.html [ Pass ]
accessibility/invalid-inputs.html [ Pass ]
accessibility/insert-text-into-password-field.html [ Pass ]
Expand Down
@@ -0,0 +1,26 @@
This test ensures that the content within an iframe with an ARIA role is accessible.

Traversing initial body content.

StaticText
AXLabel: Text before iframe

Button

StaticText
AXLabel: Text after iframe

document.getElementById('iframe').removeAttribute('role');

StaticText
AXLabel: Text before iframe

Button

StaticText
AXLabel: Text after iframe

PASS successfullyParsed is true

TEST COMPLETE
Text before iframe Text after iframe
@@ -0,0 +1,30 @@
This test ensures that the content within an iframe with an ARIA role is accessible.

Traversing initial body content.

AXRole: AXStaticText
AXValue: Text before iframe

AXRole: AXWebArea

AXRole: AXButton

AXRole: AXStaticText
AXValue: Text after iframe

document.getElementById('iframe').removeAttribute('role');

AXRole: AXStaticText
AXValue: Text before iframe

AXRole: AXWebArea

AXRole: AXButton

AXRole: AXStaticText
AXValue: Text after iframe

PASS successfullyParsed is true

TEST COMPLETE
Text before iframe Text after iframe
12 changes: 4 additions & 8 deletions Source/WebCore/accessibility/AccessibilityRenderObject.cpp
Expand Up @@ -562,19 +562,15 @@ AccessibilityObject* AccessibilityRenderObject::parentObject() const

bool AccessibilityRenderObject::isAttachment() const
{
RenderBoxModelObject* renderer = renderBoxModelObject();
if (!renderer)
return false;

auto* widget = this->widget();
// WebKit2 plugins need to be treated differently than attachments, so return false here.
// Only WebKit1 plugins have an associated platformWidget.
if (is<PluginViewBase>(widget()) && !widget()->platformWidget())
if (is<PluginViewBase>(widget) && !widget->platformWidget())
return false;

auto* renderer = this->renderer();
// Widgets are the replaced elements that we represent to AX as attachments
bool isWidget = renderer->isWidget();

return isWidget && ariaRoleAttribute() == AccessibilityRole::Unknown;
return renderer && renderer->isWidget();
}

bool AccessibilityRenderObject::isOffScreen() const
Expand Down

0 comments on commit da39768

Please sign in to comment.