Skip to content

Commit 30687a5

Browse files
committed
AX: Per spec, aria-controls should be exposed via AXLinkedUIElements and not AXARIAControls
https://bugs.webkit.org/show_bug.cgi?id=250625 rdar://problem/104265235 Reviewed by Chris Fleizach. Per spec (https://www.w3.org/TR/core-aam-1.2), aria-controls should be exposed via AXLinkedUIElements and not AXARIAControls (which is not a specced attribute). This patch implements that and removes AXARIAControls entirely. This patch also fixes a bug where AXPropertyName::IsSelected is not updated when aria-controls changes. New testcases are added to aria-controls-with-tabs.html to cover this. We still don't pass aria-controls-with-tabs.html in ITM because we need to update AXPropertyName::IsSelected when focus changes. That change will come in a later patch. * LayoutTests/accessibility-isolated-tree/TestExpectations: * LayoutTests/accessibility/aria-controls-with-tabs-expected.txt: * LayoutTests/accessibility/aria-controls-with-tabs.html: * LayoutTests/accessibility/mac/relationships-in-frames.html: Wait for dynamic change to aria-controls to propagate to the isolated tree. * Source/WebCore/accessibility/AXLogger.cpp: (WebCore::operator<<): * Source/WebCore/accessibility/AXObjectCache.cpp: (WebCore::AXObjectCache::handleAttributeChange): (WebCore::AXObjectCache::updateIsolatedTree): * Source/WebCore/accessibility/AXObjectCache.h: * Source/WebCore/accessibility/AccessibilityRenderObject.cpp: (WebCore::AccessibilityRenderObject::linkedObjects const): * Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm: (-[WebAccessibilityObjectWrapper accessibilityAttributeValue:]): * Tools/DumpRenderTree/mac/AccessibilityUIElementMac.mm: (AccessibilityUIElement::ariaControlsElementAtIndex): * Tools/WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.mm: (WTR::AccessibilityUIElement::ariaControlsElementAtIndex): Canonical link: https://commits.webkit.org/258922@main
1 parent 5c38a23 commit 30687a5

File tree

11 files changed

+82
-71
lines changed

11 files changed

+82
-71
lines changed

LayoutTests/accessibility-isolated-tree/TestExpectations

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Timeouts
2+
accessibility/aria-controls-with-tabs.html [ Timeout ]
23
accessibility/frame-disconnect-textmarker-cache-crash.html [ Timeout ]
34
accessibility/slider-with-lost-renderer.html [ Timeout ]
45

56
# Text failures
6-
accessibility/aria-controls-with-tabs.html [ Failure ]
77
accessibility/aria-current.html [ Failure ]
88
accessibility/aria-roles.html [ Failure ]
99
accessibility/dialog-showModal.html [ Failure ]
Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1-
Crust
2-
Veges
3-
Test
4-
5-
Select Crust
6-
7-
Select Crust
8-
91
This tests that the aria tab item becomes selected if either aria-selected is used, or if aria-controls points to an item that contains KB focus.
102

11-
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
3+
PASS: tab2.isSelected === true
124

5+
Removing aria-controls from #tab_2
6+
PASS: tab2.isSelected === false
7+
8+
Resetting #tab_2 aria-controls to be '#panel_2'
9+
PASS: tab2.isSelected === true
10+
PASS: tab1.isSelected === false
11+
PASS: tab2.isSelected === false
12+
PASS: tab1.isSelected === true
1313

14-
PASS tab2.isSelected is true
15-
PASS tab1.isSelected is false
16-
PASS tab2.isSelected is false
17-
PASS tab1.isSelected is true
1814
PASS successfullyParsed is true
1915

2016
TEST COMPLETE
17+
Crust
18+
Veges
19+
Test
2120

21+
Select Crust
22+
23+
Select Crust
Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,65 @@
11
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
22
<html>
33
<head>
4-
<script src="../resources/js-test-pre.js"></script>
4+
<script src="../resources/js-test.js"></script>
5+
<script src="../resources/accessibility-helper.js"></script>
56
</head>
6-
<body id="body">
7+
<body>
78

89
<ul id="tablist_1" role="tablist">
9-
<li id="tab_1" role="tab" tabindex="-1" class="">Crust</li>
10-
<li id="tab_2" role="tab" tabindex="-1" aria-controls="panel_2" class="">Veges</li>
10+
<li id="tab_1" role="tab" tabindex="-1" class="">Crust</li>
11+
<li id="tab_2" role="tab" tabindex="-1" aria-controls="panel_2" class="">Veges</li>
1112
</ul>
1213

1314
<h3 tabindex=0 id="elementOutsideTabs">Test</h3>
1415

1516
<div id="panel_1" role="tabpanel" >
16-
<h3 tabindex=0>Select Crust</h3>
17+
<h3 tabindex=0>Select Crust</h3>
1718
</div>
1819

1920
<div id="panel_2" role="tabpanel" >
20-
<h2 id="itemInPanel2" tabindex=0>Select Crust</h2>
21+
<h2 id="itemInPanel2" tabindex=0>Select Crust</h2>
2122
</div>
2223

23-
24-
<p id="description"></p>
25-
<div id="console"></div>
26-
2724
<script>
28-
29-
description("This tests that the aria tab item becomes selected if either aria-selected is used, or if aria-controls points to an item that contains KB focus.");
30-
31-
if (window.accessibilityController) {
32-
33-
var tabList = accessibilityController.accessibleElementById('tablist_1');
34-
var tab1 = tabList.childAtIndex(0);
35-
var tab2 = tabList.childAtIndex(1);
36-
37-
// we set KB focus to something in panel_2, which means that tab2 should become selected
38-
// because it aria-controls panel_2
39-
var panel2Item = document.getElementById("itemInPanel2");
40-
panel2Item.focus();
41-
42-
shouldBe("tab2.isSelected", "true");
43-
44-
// reset KB focus and verify that neither tab is selected
45-
document.getElementById("elementOutsideTabs").focus();
46-
shouldBe("tab1.isSelected", "false");
47-
shouldBe("tab2.isSelected", "false");
48-
49-
// Now we set aria-selected to be true on tab1 so that it should become selected
50-
document.getElementById("tab_1").setAttribute("aria-selected", "true");
51-
shouldBe("tab1.isSelected", "true");
52-
}
53-
25+
var output = "This tests that the aria tab item becomes selected if either aria-selected is used, or if aria-controls points to an item that contains KB focus.\n\n";
26+
27+
if (window.accessibilityController) {
28+
window.jsTestIsAsync = true;
29+
30+
var tabList = accessibilityController.accessibleElementById('tablist_1');
31+
var tab1 = tabList.childAtIndex(0);
32+
var tab2 = tabList.childAtIndex(1);
33+
34+
var panel2Item = document.getElementById("itemInPanel2");
35+
panel2Item.focus();
36+
setTimeout(async () => {
37+
// Set KB focus to something in #panel_2, which means that #tab2 should become selected because it aria-controls #panel_2.
38+
output += await expectAsync("tab2.isSelected", "true");
39+
40+
output += "\nRemoving aria-controls from #tab_2\n";
41+
document.getElementById("tab_2").removeAttribute("aria-controls");
42+
// #tab_2 no longer aria-controls something with KB focus, so it should become unselected.
43+
output += await expectAsync("tab2.isSelected", "false");
44+
45+
output += "\nResetting #tab_2 aria-controls to be '#panel_2'\n";
46+
document.getElementById("tab_2").setAttribute("aria-controls", "panel_2");
47+
output += await expectAsync("tab2.isSelected", "true");
48+
49+
// Reset KB focus and verify that neither tab is selected.
50+
document.getElementById("elementOutsideTabs").focus();
51+
output += expect("tab1.isSelected", "false");
52+
output += expect("tab2.isSelected", "false");
53+
54+
// Now we set aria-selected to be true on tab1 so that it should become selected
55+
document.getElementById("tab_1").setAttribute("aria-selected", "true");
56+
output += expect("tab1.isSelected", "true");
57+
58+
debug(output);
59+
finishJSTest();
60+
}, 0);
61+
}
5462
</script>
55-
56-
<script src="../resources/js-test-post.js"></script>
5763
</body>
5864
</html>
65+

LayoutTests/accessibility/mac/relationships-in-frames.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
output += `aria-controls textbox->listbox : ${controlled.isEqual(listbox)}\n`;
2929

3030
frame1.contentDocument.getElementById("controller").setAttribute("aria-controls", "listbox2");
31-
controlled = controller.ariaControlsElementAtIndex(0);
31+
await waitFor(() => {
32+
controlled = controller.ariaControlsElementAtIndex(0);
33+
return controlled.domIdentifier === "listbox2";
34+
});
3235
output += `controller controls role after change: ${controlled.domIdentifier}\n`;
3336
debug(output);
3437
finishJSTest();

Source/WebCore/accessibility/AXLogger.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,9 @@ TextStream& operator<<(TextStream& stream, AXObjectCache::AXNotification notific
415415
case AXObjectCache::AXNotification::AXColumnSpanChanged:
416416
stream << "AXColumnSpanChanged";
417417
break;
418+
case AXObjectCache::AXNotification::AXControlledObjectsChanged:
419+
stream << "AXControlledObjectsChanged";
420+
break;
418421
case AXObjectCache::AXNotification::AXCurrentStateChanged:
419422
stream << "AXCurrentStateChanged";
420423
break;

Source/WebCore/accessibility/AXObjectCache.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2085,6 +2085,8 @@ void AXObjectCache::handleAttributeChange(Element* element, const QualifiedName&
20852085
postNotification(element, AXIsAtomicChanged);
20862086
else if (attrName == aria_busyAttr)
20872087
postNotification(element, AXObjectCache::AXElementBusyChanged);
2088+
else if (attrName == aria_controlsAttr)
2089+
postNotification(element, AXControlledObjectsChanged);
20882090
else if (attrName == aria_valuenowAttr || attrName == aria_valuetextAttr)
20892091
postNotification(element, AXObjectCache::AXValueChanged);
20902092
else if (attrName == aria_labelAttr || attrName == aria_labeledbyAttr || attrName == aria_labelledbyAttr)
@@ -3759,6 +3761,7 @@ void AXObjectCache::updateIsolatedTree(const Vector<std::pair<RefPtr<Accessibili
37593761
case AXActiveDescendantChanged:
37603762
case AXRoleChanged:
37613763
case AXColumnSpanChanged:
3764+
case AXControlledObjectsChanged:
37623765
case AXDescribedByChanged:
37633766
case AXDropEffectChanged:
37643767
case AXElementBusyChanged:

Source/WebCore/accessibility/AXObjectCache.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ class AXObjectCache : public CanMakeWeakPtr<AXObjectCache>
288288
AXColumnCountChanged,
289289
AXColumnIndexChanged,
290290
AXColumnSpanChanged,
291+
AXControlledObjectsChanged,
291292
AXCurrentStateChanged,
292293
AXDescribedByChanged,
293294
AXDisabledStateChanged,

Source/WebCore/accessibility/AccessibilityRenderObject.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,8 +1072,6 @@ void AccessibilityRenderObject::addRadioButtonGroupMembers(AccessibilityChildren
10721072
}
10731073
}
10741074

1075-
// Linked ui elements could be all the related radio buttons in a group
1076-
// or an internal anchor connection.
10771075
AXCoreObject::AccessibilityChildrenVector AccessibilityRenderObject::linkedObjects() const
10781076
{
10791077
auto linkedObjects = flowToObjects();
@@ -1087,6 +1085,8 @@ AXCoreObject::AccessibilityChildrenVector AccessibilityRenderObject::linkedObjec
10871085
if (roleValue() == AccessibilityRole::RadioButton)
10881086
addRadioButtonGroupMembers(linkedObjects);
10891087

1088+
linkedObjects.appendVector(controlledObjects());
1089+
10901090
return linkedObjects;
10911091
}
10921092

Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,6 @@
232232
#define NSAccessibilityDocumentEncodingAttribute @"AXDocumentEncoding"
233233
#endif
234234

235-
#ifndef NSAccessibilityAriaControlsAttribute
236-
#define NSAccessibilityAriaControlsAttribute @"AXARIAControls"
237-
#endif
238-
239235
#define NSAccessibilityDOMIdentifierAttribute @"AXDOMIdentifier"
240236
#define NSAccessibilityDOMClassListAttribute @"AXDOMClassList"
241237

@@ -2647,10 +2643,6 @@ - (id)accessibilityAttributeValue:(NSString*)attributeName
26472643
if ([attributeName isEqualToString:NSAccessibilityDocumentEncodingAttribute])
26482644
return backingObject->documentEncoding();
26492645

2650-
// Aria controls element
2651-
if ([attributeName isEqualToString:NSAccessibilityAriaControlsAttribute])
2652-
return makeNSArray(backingObject->controlledObjects());
2653-
26542646
if ([attributeName isEqualToString:NSAccessibilityFocusableAncestorAttribute]) {
26552647
AXCoreObject* object = backingObject->focusableAncestor();
26562648
return object ? object->wrapper() : nil;

Tools/DumpRenderTree/mac/AccessibilityUIElementMac.mm

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -375,13 +375,10 @@ - (void)_accessibilitySetValue:(id)value forAttribute:(NSString*)attributeName;
375375

376376
AccessibilityUIElement AccessibilityUIElement::ariaControlsElementAtIndex(unsigned index)
377377
{
378-
BEGIN_AX_OBJC_EXCEPTIONS
379-
NSArray* ariaControls = [m_element accessibilityAttributeValue:@"AXARIAControls"];
380-
if (index < [ariaControls count])
381-
return [ariaControls objectAtIndex:index];
382-
END_AX_OBJC_EXCEPTIONS
383-
384-
return nullptr;
378+
// Per spec, aria-controls is exposed via AXLinkedUIElements on the Mac.
379+
// Note that a few other things are exposed via AXLinkedUIElements (aria-flowto), so this function
380+
// may provide unexpected results for tests that use a combination of these attributes.
381+
return linkedUIElementAtIndex(index);
385382
}
386383

387384
AccessibilityUIElement AccessibilityUIElement::disclosedRowAtIndex(unsigned index)

0 commit comments

Comments
 (0)