Skip to content

Commit 5b80ea6

Browse files
rahimabdiRahim Abdi
authored andcommitted
AX: Ensure native HTML radio buttons on macOS maintain accurate group count when group membership changes
https://bugs.webkit.org/show_bug.cgi?id=297929 rdar://159221583 Reviewed by Tyler Wilcock. When the membership of a native HTML radio group changes (e.g., adding one or more radio buttons to a group via user interaction), AccessibilityNodeObject::radioButtonGroup() determines group membership when the AX object is first created. However, any membership changes must also be propagated to the platform accessibility API so that the backing AX object used for querying current AX state (mirrored in the AX isolated tree) is kept up-to-date. This PR ensures that upon radio group membership change, a new AXNotification::RadioGroupMembershipChanged is dispatched for every radio in the affected group. In this manner, assistive technologies will convey the accurate group count "X of Y" for existing members, where X is the radio's current position in the group and Y is the group count. Note: the cardinality of radio buttons is not derived from AXProperty::PosInSet nor AXProperty::SetSize because these attributes are not queried by the platform accessibility API for this purpose. For native radio buttons, the "X of Y" assistive technology announcement is exposed via NSAccessibilityLinkedUIElementsAttribute which calls into AXCoreObject::linkedObjects(). * LayoutTests/accessibility/radio-button-group-dynamic-changes-expected.txt: Added. * LayoutTests/accessibility/radio-button-group-dynamic-changes.html: Added. * LayoutTests/platform/glib/TestExpectations: skip radio-button-group-dynamic-changes.html. * Source/WebCore/accessibility/AXLogger.cpp: (WebCore::operator<<): * Source/WebCore/accessibility/AXNotifications.h: * Source/WebCore/accessibility/AXObjectCache.cpp: (WebCore::AXObjectCache::onRadioGroupMembershipChanged): (WebCore::AXObjectCache::updateIsolatedTree): * Source/WebCore/accessibility/AXObjectCache.h: * Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.h: * Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp: (WebCore::AXIsolatedTree::updateNodeProperties): (WebCore::createIsolatedObjectData): * Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h: * Source/WebCore/dom/RadioButtonGroups.cpp: (WebCore::RadioButtonGroups::addButton): (WebCore::RadioButtonGroups::removeButton): Canonical link: https://commits.webkit.org/299585@main
1 parent ad64bde commit 5b80ea6

11 files changed

Lines changed: 113 additions & 6 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
This test verifies AX radio group member count is updated for existing members when a new radio button is added to a group.
2+
3+
Verify radio 2a's first linked element is itself:
4+
PASS: radio2a.linkedUIElementAtIndex(0).isEqual(radio2a) === true
5+
6+
Verify radio 2a has no second linked element:
7+
PASS: !radio2a.linkedUIElementAtIndex(1) === true
8+
9+
Verify radio2b dynamically added to DOM successfully:
10+
PASS: !!accessibilityController.accessibleElementById('radio2b') === true
11+
12+
Verify radio 2a's first linked element is itself:
13+
PASS: radio2a.linkedUIElementAtIndex(0).isEqual(accessibilityController.accessibleElementById('radio2a')) === true
14+
15+
Verify radio 2a's second linked element is now radio 2b:
16+
PASS: radio2a.linkedUIElementAtIndex(1).isEqual(accessibilityController.accessibleElementById('radio2b')) === true
17+
18+
PASS successfullyParsed is true
19+
20+
TEST COMPLETE
21+
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<!DOCTYPE HTML>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<script src="../resources/accessibility-helper.js"></script>
6+
<script src="../resources/js-test.js"></script>
7+
</head>
8+
<body>
9+
<div id ="content">
10+
<input id="radio1" type="radio" name="group1">
11+
<input id="radio2" type="radio" name="group1">
12+
<input id="radio2a" type="radio" name="group2">
13+
</div>
14+
15+
<script>
16+
if (window.accessibilityController) {
17+
window.jsTestIsAsync = true;
18+
var output = "This test verifies AX radio group member count is updated for existing members when a new radio button is added to a group.\n";
19+
20+
document.getElementById("radio2").addEventListener("change", () => {
21+
document.getElementById("radio2a").insertAdjacentHTML(
22+
"afterend",
23+
'<input id="radio2b" type="radio" name="group2">'
24+
);
25+
});
26+
27+
function focusRadio2() {
28+
document.getElementById("radio2").checked = false;
29+
document.getElementById("radio2").click();
30+
}
31+
32+
var radio2a = accessibilityController.accessibleElementById("radio2a");
33+
34+
output += "\nVerify radio 2a's first linked element is itself: \n";
35+
output += expect("radio2a.linkedUIElementAtIndex(0).isEqual(radio2a)", "true");
36+
output += "\nVerify radio 2a has no second linked element: \n";
37+
output += expect("!radio2a.linkedUIElementAtIndex(1)", "true");
38+
39+
setTimeout(async function() {
40+
await focusRadio2();
41+
42+
output += "\nVerify radio2b dynamically added to DOM successfully: \n";
43+
output += await expectAsync("!!accessibilityController.accessibleElementById('radio2b')", "true");
44+
45+
output += "\nVerify radio 2a's first linked element is itself: \n";
46+
output += await expectAsync("radio2a.linkedUIElementAtIndex(0).isEqual(accessibilityController.accessibleElementById('radio2a'))", "true");
47+
output += "\nVerify radio 2a's second linked element is now radio 2b: \n";
48+
output += await expectAsync("radio2a.linkedUIElementAtIndex(1).isEqual(accessibilityController.accessibleElementById('radio2b'))", "true");
49+
50+
debug(output);
51+
finishJSTest();
52+
}, 0);
53+
}
54+
55+
</script>
56+
57+
</body>
58+
</html>

LayoutTests/platform/glib/TestExpectations

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,7 @@ webkit.org/b/228915 accessibility/selected-state-changed-notifications.html [ Ti
955955
# Not supported
956956
accessibility/embedded-image-description.html [ Skip ]
957957
accessibility/aria-keyshortcuts.html [ Skip ]
958+
accessibility/radio-button-group-dynamic-changes.html [ Skip ]
958959

959960
# Not supported. Skipped also in mac-wk1 and win ports.
960961
accessibility/nested-textareas-value-changed-notifications.html [ Skip ]

Source/WebCore/accessibility/AXLogger.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,8 +1090,8 @@ TextStream& operator<<(WTF::TextStream& stream, AXProperty property)
10901090
case AXProperty::PreventKeyboardDOMEventDispatch:
10911091
stream << "PreventKeyboardDOMEventDispatch";
10921092
break;
1093-
case AXProperty::RadioButtonGroup:
1094-
stream << "RadioButtonGroup";
1093+
case AXProperty::RadioButtonGroupMembers:
1094+
stream << "RadioButtonGroupMembers";
10951095
break;
10961096
case AXProperty::RelativeFrame:
10971097
stream << "RelativeFrame";

Source/WebCore/accessibility/AXNotifications.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ namespace WebCore {
120120
macro(PressDidSucceed) \
121121
macro(PressDidFail) \
122122
macro(PressedStateChanged) \
123+
macro(RadioGroupMembershipChanged) \
123124
macro(ReadOnlyStatusChanged) \
124125
macro(RequiredStatusChanged) \
125126
macro(SortDirectionChanged) \

Source/WebCore/accessibility/AXObjectCache.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1929,6 +1929,19 @@ void AXObjectCache::onDetailsSummarySlotChange(const HTMLDetailsElement& details
19291929
}
19301930
}
19311931

1932+
void AXObjectCache::onRadioGroupMembershipChanged(HTMLElement& radio)
1933+
{
1934+
if (auto* radioElement = dynamicDowncast<HTMLInputElement>(radio)) {
1935+
for (auto& sibling : radioElement->radioButtonGroup()) {
1936+
if (sibling.ptr() == &radio)
1937+
continue;
1938+
1939+
if (auto* axObject = get(sibling.ptr()))
1940+
postNotification(axObject, &sibling->document(), AXNotification::RadioGroupMembershipChanged);
1941+
}
1942+
}
1943+
}
1944+
19321945
static bool isContentVisibilityHidden(const RenderStyle& style)
19331946
{
19341947
return style.usedContentVisibility() == ContentVisibility::Hidden;
@@ -4941,6 +4954,9 @@ void AXObjectCache::updateIsolatedTree(const Vector<std::pair<Ref<AccessibilityO
49414954
case AXNotification::IdAttributeChanged:
49424955
tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::IdentifierAttribute });
49434956
break;
4957+
case AXNotification::RadioGroupMembershipChanged:
4958+
tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::RadioButtonGroupMembers });
4959+
break;
49444960
case AXNotification::ReadOnlyStatusChanged:
49454961
tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::CanSetValueAttribute });
49464962
break;

Source/WebCore/accessibility/AXObjectCache.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ class AXObjectCache final : public CanMakeWeakPtr<AXObjectCache>, public CanMake
302302
void onFocusChange(Element* oldElement, Element* newElement);
303303
void onInertOrVisibilityChange(RenderElement&);
304304
void onPopoverToggle(const HTMLElement&);
305+
void onRadioGroupMembershipChanged(HTMLElement&);
305306
void onScrollbarFrameRectChange(const Scrollbar&);
306307
void onSelectedOptionChanged(Element&);
307308
void onSelectedOptionChanged(RenderObject&, int);

Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ class AXIsolatedObject final : public AXCoreObject {
325325
AXIsolatedObject* accessibilityHitTest(const IntPoint&) const final;
326326
AXIsolatedObject* focusedUIElement() const final;
327327
AXIsolatedObject* internalLinkElement() const final { return objectAttributeValue(AXProperty::InternalLinkElement); }
328-
AccessibilityChildrenVector radioButtonGroup() const final { return tree()->objectsForIDs(vectorAttributeValue<AXID>(AXProperty::RadioButtonGroup)); }
328+
AccessibilityChildrenVector radioButtonGroup() const final { return tree()->objectsForIDs(vectorAttributeValue<AXID>(AXProperty::RadioButtonGroupMembers)); }
329329
AXIsolatedObject* scrollBar(AccessibilityOrientation) final;
330330
const String placeholderValue() const final { return stringAttributeValue(AXProperty::PlaceholderValue); }
331331
String abbreviation() const final { return stringAttributeValue(AXProperty::Abbreviation); }

Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,9 @@ void AXIsolatedTree::updateNodeProperties(AccessibilityObject& axObject, const A
787787
case AXProperty::CellScope:
788788
properties.append({ AXProperty::CellScope, axObject.cellScope().isolatedCopy() });
789789
break;
790+
case AXProperty::RadioButtonGroupMembers:
791+
properties.append({ AXProperty::RadioButtonGroupMembers, axIDs(axObject.radioButtonGroup()) });
792+
break;
790793
case AXProperty::ScreenRelativePosition:
791794
properties.append({ AXProperty::ScreenRelativePosition, axObject.screenRelativePosition() });
792795
break;
@@ -2072,8 +2075,7 @@ IsolatedObjectData createIsolatedObjectData(const Ref<AccessibilityObject>& axOb
20722075
setProperty(AXProperty::IsTree, object.isTree());
20732076
if (object.isRadioButton()) {
20742077
setProperty(AXProperty::NameAttribute, object.nameAttribute().isolatedCopy());
2075-
// FIXME: This property doesn't get updated when a page changes dynamically.
2076-
setObjectVectorProperty(AXProperty::RadioButtonGroup, object.radioButtonGroup());
2078+
setObjectVectorProperty(AXProperty::RadioButtonGroupMembers, object.radioButtonGroup());
20772079
}
20782080

20792081
if (object.isImage())

Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ enum class AXProperty : uint16_t {
261261
#endif
262262
PosInSet,
263263
PreventKeyboardDOMEventDispatch,
264-
RadioButtonGroup,
264+
RadioButtonGroupMembers,
265265
RelativeFrame,
266266
RemoteFrameOffset,
267267
RemoteFramePlatformElement,

0 commit comments

Comments
 (0)