Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
AX: aria-flowto doesn't work for display:contents elements
https://bugs.webkit.org/show_bug.cgi?id=255974
rdar://problem/108542797

Reviewed by Chris Fleizach.

Move the necessary implementations from AccessibilityRenderObject to
AcceAccessibilityNodeObject.

This patch also changes aria-flowto.html to wait after the dynamic JS
changes it does which allows it to start passing in ITM.

* LayoutTests/accessibility-isolated-tree/TestExpectations:
* LayoutTests/accessibility/aria-flowto-expected.txt:
* LayoutTests/accessibility/aria-flowto.html:
* Source/WebCore/accessibility/AccessibilityNodeObject.cpp:
(WebCore::AccessibilityNodeObject::internalLinkElement const):
(WebCore::AccessibilityNodeObject::addRadioButtonGroupChildren const):
(WebCore::AccessibilityNodeObject::addRadioButtonGroupMembers const):
(WebCore::AccessibilityNodeObject::linkedObjects const):
* Source/WebCore/accessibility/AccessibilityNodeObject.h:
* Source/WebCore/accessibility/AccessibilityObject.h:
* Source/WebCore/accessibility/AccessibilityObjectInterface.h:
* Source/WebCore/accessibility/AccessibilityRenderObject.cpp:
(WebCore::AccessibilityRenderObject::internalLinkElement const): Deleted.
(WebCore::AccessibilityRenderObject::addRadioButtonGroupChildren const): Deleted.
(WebCore::AccessibilityRenderObject::addRadioButtonGroupMembers const): Deleted.
(WebCore::AccessibilityRenderObject::linkedObjects const): Deleted.
* Source/WebCore/accessibility/AccessibilityRenderObject.h:

Canonical link: https://commits.webkit.org/263425@main
  • Loading branch information
twilco committed Apr 26, 2023
1 parent fbebacb commit e5e7a5d
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 104 deletions.
1 change: 0 additions & 1 deletion LayoutTests/accessibility-isolated-tree/TestExpectations
Expand Up @@ -59,6 +59,5 @@ accessibility/math-multiscript-attributes.html [ Failure ]
accessibility/aria-labelledby-on-password-input.html [ Failure ]

accessibility/aria-selected-menu-items.html [ Failure ]
accessibility/aria-flowto.html [ Failure ]
accessibility/aria-owns-hierarchy-remap.html [ Failure ]
accessibility/element-reflection-ariaactivedescendant.html [ Failure ]
4 changes: 4 additions & 0 deletions LayoutTests/accessibility/aria-flowto-expected.txt
Expand Up @@ -4,6 +4,8 @@ PASS: item.ariaFlowToElementAtIndex(0).role === 'AXRole: AXButton'
PASS: item.ariaFlowToElementAtIndex(0).title === 'AXTitle: BUTTON'
PASS: item.ariaFlowToElementAtIndex(1).role === 'AXRole: AXRadioButton'
PASS: item.ariaFlowToElementAtIndex(1).title === 'AXTitle: RADIO BUTTON'
PASS: displayContentsImg.ariaFlowToElementAtIndex(0).role === 'AXRole: AXButton'
PASS: displayContentsImg.ariaFlowToElementAtIndex(0).title === 'AXTitle: Foo button'

Removing id 'extra' from #item1's aria-flowto.
PASS: item.ariaFlowToElementAtIndex(0).role === 'AXRole: AXRadioButton'
Expand All @@ -15,5 +17,7 @@ TEST COMPLETE
Item 1
Item 2
Item 3
Foo img
BUTTON
RADIO BUTTON
Foo button
51 changes: 28 additions & 23 deletions LayoutTests/accessibility/aria-flowto.html
Expand Up @@ -10,33 +10,38 @@
<div>Item 2</div>
<div>Item 3</div>

<div tabindex=0 id="display-contents-img" role="img" aria-flowto="extra3" style="display:contents">Foo img</div>

<div id="extra" role="button">BUTTON</div>
<div id="extra2" role="radio">RADIO BUTTON</div>
<div id="extra3" role="button">Foo button</div>

<script>
var testOutput = "This tests that aria-flowto correctly identifies the right elements.\n\n";

if (window.accessibilityController) {
window.jsTestIsAsync = true;
var item;
setTimeout(function() {
item = accessibilityController.accessibleElementById("item1");
testOutput += expect("item.ariaFlowToElementAtIndex(0).role", "'AXRole: AXButton'");
testOutput += expect("item.ariaFlowToElementAtIndex(0).title", "'AXTitle: BUTTON'");
testOutput += expect("item.ariaFlowToElementAtIndex(1).role", "'AXRole: AXRadioButton'");
testOutput += expect("item.ariaFlowToElementAtIndex(1).title", "'AXTitle: RADIO BUTTON'");

testOutput += "\nRemoving id 'extra' from #item1's aria-flowto.\n";

document.getElementById("item1").setAttribute("aria-flowto", "extra2");

testOutput += expect("item.ariaFlowToElementAtIndex(0).role", "'AXRole: AXRadioButton'");
testOutput += expect("item.ariaFlowToElementAtIndex(0).title", "'AXTitle: RADIO BUTTON'");

debug(testOutput);
finishJSTest();
}, 0);
}
var output = "This tests that aria-flowto correctly identifies the right elements.\n\n";

if (window.accessibilityController) {
window.jsTestIsAsync = true;
var item, displayContentsImg;
setTimeout(async function() {
item = accessibilityController.accessibleElementById("item1");
displayContentsImg = accessibilityController.accessibleElementById("display-contents-img");
output += expect("item.ariaFlowToElementAtIndex(0).role", "'AXRole: AXButton'");
output += expect("item.ariaFlowToElementAtIndex(0).title", "'AXTitle: BUTTON'");
output += expect("item.ariaFlowToElementAtIndex(1).role", "'AXRole: AXRadioButton'");
output += expect("item.ariaFlowToElementAtIndex(1).title", "'AXTitle: RADIO BUTTON'");
output += expect("displayContentsImg.ariaFlowToElementAtIndex(0).role", "'AXRole: AXButton'");
output += expect("displayContentsImg.ariaFlowToElementAtIndex(0).title", "'AXTitle: Foo button'");

output += "\nRemoving id 'extra' from #item1's aria-flowto.\n";

document.getElementById("item1").setAttribute("aria-flowto", "extra2");
output += await expectAsync("item.ariaFlowToElementAtIndex(0).role", "'AXRole: AXRadioButton'");
output += await expectAsync("item.ariaFlowToElementAtIndex(0).title", "'AXTitle: RADIO BUTTON'");

debug(output);
finishJSTest();
}, 0);
}
</script>
</body>
</html>
Expand Down
71 changes: 71 additions & 0 deletions Source/WebCore/accessibility/AccessibilityNodeObject.cpp
Expand Up @@ -1083,6 +1083,77 @@ Element* AccessibilityNodeObject::anchorElement() const
return nullptr;
}

AccessibilityObject* AccessibilityNodeObject::internalLinkElement() const
{
// We don't currently support ARIA links as internal link elements, so exit early if anchorElement() is not a native HTMLAnchorElement.
WeakPtr anchor = dynamicDowncast<HTMLAnchorElement>(anchorElement());
if (!anchor)
return nullptr;

auto linkURL = anchor->href();
auto fragmentIdentifier = linkURL.fragmentIdentifier();
if (fragmentIdentifier.isEmpty())
return nullptr;

// Check if URL is the same as current URL
auto* document = this->document();
if (!document || !equalIgnoringFragmentIdentifier(document->url(), linkURL))
return nullptr;

auto linkedNode = document->findAnchor(fragmentIdentifier);
if (!linkedNode)
return nullptr;

// The element we find may not be accessible, so find the first accessible object.
return firstAccessibleObjectFromNode(linkedNode);
}

void AccessibilityNodeObject::addRadioButtonGroupChildren(AXCoreObject& parent, AccessibilityChildrenVector& linkedUIElements) const
{
for (const auto& child : parent.children()) {
if (child->roleValue() == AccessibilityRole::RadioButton)
linkedUIElements.append(child);
else
addRadioButtonGroupChildren(*child, linkedUIElements);
}
}

void AccessibilityNodeObject::addRadioButtonGroupMembers(AccessibilityChildrenVector& linkedUIElements) const
{
if (roleValue() != AccessibilityRole::RadioButton)
return;

WeakPtr node = this->node();
if (auto* input = dynamicDowncast<HTMLInputElement>(node.get())) {
for (auto& radioSibling : input->radioButtonGroup()) {
if (auto* object = axObjectCache()->getOrCreate(radioSibling.ptr()))
linkedUIElements.append(object);
}
} else {
// If we didn't find any radio button siblings with the traditional naming, lets search for a radio group role and find its children.
for (auto* parent = parentObject(); parent; parent = parent->parentObject()) {
if (parent->roleValue() == AccessibilityRole::RadioGroup)
addRadioButtonGroupChildren(*parent, linkedUIElements);
}
}
}

AXCoreObject::AccessibilityChildrenVector AccessibilityNodeObject::linkedObjects() const
{
auto linkedObjects = flowToObjects();

if (isLink()) {
if (auto* linkedAXElement = internalLinkElement())
linkedObjects.append(linkedAXElement);
}

if (roleValue() == AccessibilityRole::RadioButton)
addRadioButtonGroupMembers(linkedObjects);

linkedObjects.appendVector(controlledObjects());
return linkedObjects;
}

static bool isNodeActionElement(Node* node)
{
if (is<HTMLInputElement>(*node)) {
Expand Down
4 changes: 4 additions & 0 deletions Source/WebCore/accessibility/AccessibilityNodeObject.h
Expand Up @@ -120,6 +120,10 @@ class AccessibilityNodeObject : public AccessibilityObject {
Element* actionElement() const override;
Element* mouseButtonListener(MouseButtonListenerResultFilter = ExcludeBodyElement) const;
Element* anchorElement() const override;
AccessibilityObject* internalLinkElement() const;
void addRadioButtonGroupMembers(AccessibilityChildrenVector& linkedUIElements) const;
void addRadioButtonGroupChildren(AXCoreObject&, AccessibilityChildrenVector&) const;
AccessibilityChildrenVector linkedObjects() const override;
AccessibilityObject* menuForMenuButton() const;

virtual void changeValueByPercent(float percentChange);
Expand Down
76 changes: 0 additions & 76 deletions Source/WebCore/accessibility/AccessibilityRenderObject.cpp
Expand Up @@ -959,82 +959,6 @@ Path AccessibilityRenderObject::elementPath() const
return Path();
}

AccessibilityObject* AccessibilityRenderObject::internalLinkElement() const
{
auto element = anchorElement();

// Right now, we do not support ARIA links as internal link elements
if (!is<HTMLAnchorElement>(element))
return nullptr;
auto& anchor = downcast<HTMLAnchorElement>(*element);

auto linkURL = anchor.href();
auto fragmentIdentifier = linkURL.fragmentIdentifier();
if (fragmentIdentifier.isEmpty())
return nullptr;

// check if URL is the same as current URL
auto documentURL = m_renderer->document().url();
if (!equalIgnoringFragmentIdentifier(documentURL, linkURL))
return nullptr;

auto linkedNode = m_renderer->document().findAnchor(fragmentIdentifier);
if (!linkedNode)
return nullptr;

// The element we find may not be accessible, so find the first accessible object.
return firstAccessibleObjectFromNode(linkedNode);
}

void AccessibilityRenderObject::addRadioButtonGroupChildren(AXCoreObject* parent, AccessibilityChildrenVector& linkedUIElements) const
{
for (const auto& child : parent->children()) {
if (child->roleValue() == AccessibilityRole::RadioButton)
linkedUIElements.append(child);
else
addRadioButtonGroupChildren(child.get(), linkedUIElements);
}
}

void AccessibilityRenderObject::addRadioButtonGroupMembers(AccessibilityChildrenVector& linkedUIElements) const
{
if (roleValue() != AccessibilityRole::RadioButton)
return;

Node* node = this->node();
if (is<HTMLInputElement>(node)) {
HTMLInputElement& input = downcast<HTMLInputElement>(*node);
for (auto& radioSibling : input.radioButtonGroup()) {
if (AccessibilityObject* object = axObjectCache()->getOrCreate(radioSibling.ptr()))
linkedUIElements.append(object);
}
} else {
// If we didn't find any radio button siblings with the traditional naming, lets search for a radio group role and find its children.
for (AccessibilityObject* parent = parentObject(); parent; parent = parent->parentObject()) {
if (parent->roleValue() == AccessibilityRole::RadioGroup)
addRadioButtonGroupChildren(parent, linkedUIElements);
}
}
}

AXCoreObject::AccessibilityChildrenVector AccessibilityRenderObject::linkedObjects() const
{
auto linkedObjects = flowToObjects();

if (isLink()) {
auto* linkedAXElement = internalLinkElement();
if (linkedAXElement)
linkedObjects.append(linkedAXElement);
}

if (roleValue() == AccessibilityRole::RadioButton)
addRadioButtonGroupMembers(linkedObjects);

linkedObjects.appendVector(controlledObjects());

return linkedObjects;
}

#if ENABLE(APPLE_PAY)
String AccessibilityRenderObject::applePayButtonDescription() const
{
Expand Down
4 changes: 0 additions & 4 deletions Source/WebCore/accessibility/AccessibilityRenderObject.h
Expand Up @@ -80,7 +80,6 @@ class AccessibilityRenderObject : public AccessibilityNodeObject {
AccessibilityObject* parentObject() const override;
AccessibilityObject* parentObjectIfExists() const override;
AccessibilityObject* observableObject() const override;
AccessibilityChildrenVector linkedObjects() const override;
AccessibilityObject* titleUIElement() const override;

// Should be called on the root accessibility object to kick off a hit test.
Expand Down Expand Up @@ -172,9 +171,6 @@ class AccessibilityRenderObject : public AccessibilityNodeObject {
Path elementPath() const override;

LayoutRect checkboxOrRadioRect() const;
void addRadioButtonGroupMembers(AccessibilityChildrenVector& linkedUIElements) const;
void addRadioButtonGroupChildren(AXCoreObject*, AccessibilityChildrenVector&) const;
AccessibilityObject* internalLinkElement() const;
AXCoreObject* accessibilityImageMapHitTest(HTMLAreaElement*, const IntPoint&) const;
AccessibilityObject* accessibilityParentForImageMap(HTMLMapElement*) const;
AXCoreObject* elementAccessibilityHitTest(const IntPoint&) const override;
Expand Down

0 comments on commit e5e7a5d

Please sign in to comment.