Skip to content

Commit c28221c

Browse files
committed
AX: Elements with the popovertarget attribute should expose expanded state to assistive technologies
https://bugs.webkit.org/show_bug.cgi?id=257666 rdar://105425310 Reviewed by Chris Fleizach. Per w3c/html-aam#481, buttons with the `popovertarget` attribute and valid associated popover should expose expanded state to assistive technologies. This commit implements that, and also submits a notification to ATs when a popover is expanded and collapsed. * LayoutTests/accessibility/mac/expanded-notification-expected.txt: * LayoutTests/accessibility/mac/expanded-notification.html: Popover testcase added. * Source/WebCore/accessibility/AXObjectCache.cpp: (WebCore::AXObjectCache::onPopoverTargetToggle): * Source/WebCore/accessibility/AXObjectCache.h: * Source/WebCore/accessibility/AccessibilityNodeObject.cpp: (WebCore::AccessibilityNodeObject::popoverTargetElement const): * Source/WebCore/accessibility/AccessibilityNodeObject.h: * Source/WebCore/accessibility/AccessibilityObject.cpp: (WebCore::AccessibilityObject::supportsExpanded const): (WebCore::AccessibilityObject::isExpanded const): * Source/WebCore/accessibility/AccessibilityObject.h: (WebCore::AccessibilityObject::popoverTargetElement const): * Source/WebCore/html/HTMLFormControlElement.cpp: (WebCore::HTMLFormControlElement::handlePopoverTargetAction const): Canonical link: https://commits.webkit.org/264852@main
1 parent 51ee501 commit c28221c

9 files changed

+83
-35
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
This tests that aria-expanded changes will send notifications.
1+
This tests that expanded notifications will be sent when the appropriate changes occur.
22
Initial expanded status: false
33
Received notification: AXExpandedChanged
44
Expanded status: true
55
Received notification: AXExpandedChanged
66
Expanded status: false
7+
PASS: accessibilityController.accessibleElementById('show-popover-btn').isExpanded === false
8+
PASS: accessibilityController.accessibleElementById('hide-popover-btn').isExpanded === false
9+
Received notification: AXExpandedChanged
10+
Expanded status: true
11+
PASS: accessibilityController.accessibleElementById('hide-popover-btn').isExpanded === true
12+
Received notification: AXExpandedChanged
13+
Expanded status: false
14+
PASS: accessibilityController.accessibleElementById('show-popover-btn').isExpanded === false
715

816
PASS successfullyParsed is true
917

1018
TEST COMPLETE
11-
19+
Show popover Hide popover

LayoutTests/accessibility/mac/expanded-notification.html

+40-30
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,56 @@
88

99
<button id="button" aria-expanded="false">
1010

11+
<button id="show-popover-btn" popovertarget="mypopover" popovertargetaction="show">Show popover</button>
12+
<button id="hide-popover-btn" popovertarget="mypopover" popovertargetaction="hide">Hide popover</button>
13+
<div id="mypopover" popover>Popover content</div>
14+
1115
<script>
12-
let output = "This tests that aria-expanded changes will send notifications.\n";
16+
let output = "This tests that expanded notifications will be sent when the appropriate changes occur.\n";
1317

14-
let notificationCount = 0;
15-
function notificationCallback(element, notification) {
16-
if (notification == "AXExpandedChanged") {
17-
notificationCount++;
18+
let notificationCount = 0;
19+
function notificationCallback(element, notification) {
20+
if (notification == "AXExpandedChanged") {
21+
notificationCount++;
1822

19-
output += `Received notification: ${notification}\n`;
20-
output += `Expanded status: ${element.isExpanded}\n`;
21-
}
23+
output += `Received notification: ${notification}\n`;
24+
output += `Expanded status: ${element.isExpanded}\n`;
2225
}
26+
}
2327

24-
if (window.accessibilityController) {
25-
window.jsTestIsAsync = true;
28+
if (window.accessibilityController) {
29+
window.jsTestIsAsync = true;
2630

27-
accessibilityController.addNotificationListener(notificationCallback);
28-
let button = accessibilityController.accessibleElementById("button");
29-
output += `Initial expanded status: ${button.isExpanded}\n`;
31+
accessibilityController.addNotificationListener(notificationCallback);
32+
let button = accessibilityController.accessibleElementById("button");
33+
output += `Initial expanded status: ${button.isExpanded}\n`;
3034

31-
document.getElementById("button").setAttribute("aria-expanded", "true");
32-
setTimeout(async () => {
33-
await waitFor(() => {
34-
return button.isExpanded;
35-
});
35+
document.getElementById("button").setAttribute("aria-expanded", "true");
36+
setTimeout(async () => {
37+
await waitFor(() => button.isExpanded);
3638

37-
document.getElementById("button").setAttribute("aria-expanded", "false");
38-
await waitFor(() => {
39-
return !button.isExpanded;
40-
});
39+
document.getElementById("button").setAttribute("aria-expanded", "false");
40+
await waitFor(() => !button.isExpanded);
41+
await waitFor(() => notificationCount == 2);
4142

42-
await waitFor(() => {
43-
return notificationCount == 2;
44-
});
43+
// Now test popover.
44+
output += expect("accessibilityController.accessibleElementById('show-popover-btn').isExpanded", "false");
45+
output += expect("accessibilityController.accessibleElementById('hide-popover-btn').isExpanded", "false");
4546

46-
debug(output);
47-
accessibilityController.removeNotificationListener();
48-
finishJSTest();
49-
}, 0);
50-
}
47+
document.getElementById("show-popover-btn").click();
48+
await waitFor(() => notificationCount == 3);
49+
// We expanded the popover via #show-popover-btn, but #hide-popover-btn (which is also linked to the popover) should be considered expanded now as well.
50+
output += await expectAsync("accessibilityController.accessibleElementById('hide-popover-btn').isExpanded", "true");
51+
52+
document.getElementById("hide-popover-btn").click();
53+
await waitFor(() => notificationCount == 4);
54+
output += await expectAsync("accessibilityController.accessibleElementById('show-popover-btn').isExpanded", "false");
55+
56+
debug(output);
57+
accessibilityController.removeNotificationListener();
58+
finishJSTest();
59+
}, 0);
60+
}
5161
</script>
5262
</body>
5363
</html>

Source/WebCore/accessibility/AXObjectCache.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -1472,6 +1472,11 @@ void AXObjectCache::onFocusChange(Node* oldNode, Node* newNode)
14721472
handleFocusedUIElementChanged(oldNode, newNode);
14731473
}
14741474

1475+
void AXObjectCache::onPopoverTargetToggle(const HTMLFormControlElement& popoverInvokerElement)
1476+
{
1477+
postNotification(get(const_cast<HTMLFormControlElement*>(&popoverInvokerElement)), &document(), AXExpandedChanged);
1478+
}
1479+
14751480
void AXObjectCache::deferMenuListValueChange(Element* element)
14761481
{
14771482
if (!element)

Source/WebCore/accessibility/AXObjectCache.h

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class AXObjectCache : public CanMakeWeakPtr<AXObjectCache>, public CanMakeChecke
182182
void childrenChanged(RenderObject*, RenderObject* newChild = nullptr);
183183
void childrenChanged(AccessibilityObject*);
184184
void onFocusChange(Node* oldFocusedNode, Node* newFocusedNode);
185+
void onPopoverTargetToggle(const HTMLFormControlElement&);
185186
void onScrollbarFrameRectChange(const Scrollbar&);
186187
void onSelectedChanged(Node*);
187188
void onTextSecurityChanged(HTMLInputElement&);

Source/WebCore/accessibility/AccessibilityNodeObject.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,12 @@ Element* AccessibilityNodeObject::anchorElement() const
11071107
return nullptr;
11081108
}
11091109

1110+
Element* AccessibilityNodeObject::popoverTargetElement() const
1111+
{
1112+
WeakPtr formControlElement = dynamicDowncast<HTMLFormControlElement>(node());
1113+
return formControlElement ? formControlElement->popoverTargetElement() : nullptr;
1114+
}
1115+
11101116
AccessibilityObject* AccessibilityNodeObject::internalLinkElement() const
11111117
{
11121118
// We don't currently support ARIA links as internal link elements, so exit early if anchorElement() is not a native HTMLAnchorElement.

Source/WebCore/accessibility/AccessibilityNodeObject.h

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class AccessibilityNodeObject : public AccessibilityObject {
121121
Element* actionElement() const override;
122122
Element* mouseButtonListener(MouseButtonListenerResultFilter = ExcludeBodyElement) const;
123123
Element* anchorElement() const override;
124+
Element* popoverTargetElement() const final;
124125
AccessibilityObject* internalLinkElement() const;
125126
void addRadioButtonGroupMembers(AccessibilityChildrenVector& linkedUIElements) const;
126127
void addRadioButtonGroupChildren(AXCoreObject&, AccessibilityChildrenVector&) const;

Source/WebCore/accessibility/AccessibilityObject.cpp

+8-1
Original file line numberDiff line numberDiff line change
@@ -3269,6 +3269,10 @@ bool AccessibilityObject::supportsPressed() const
32693269

32703270
bool AccessibilityObject::supportsExpanded() const
32713271
{
3272+
// If this object can toggle an HTML popover, it supports the reporting of its expanded state (which is based on the expanded / collapsed state of that popover).
3273+
if (popoverTargetElement())
3274+
return true;
3275+
32723276
switch (roleValue()) {
32733277
case AccessibilityRole::Button:
32743278
case AccessibilityRole::CheckBox:
@@ -3322,8 +3326,11 @@ bool AccessibilityObject::isExpanded() const
33223326
return parent->isExpanded();
33233327
}
33243328

3325-
if (supportsExpanded())
3329+
if (supportsExpanded()) {
3330+
if (WeakPtr popoverTargetElement = this->popoverTargetElement())
3331+
return popoverTargetElement->isPopoverShowing();
33263332
return equalLettersIgnoringASCIICase(getAttribute(aria_expandedAttr), "true"_s);
3333+
}
33273334

33283335
return false;
33293336
}

Source/WebCore/accessibility/AccessibilityObject.h

+1
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ class AccessibilityObject : public AXCoreObject, public CanMakeWeakPtr<Accessibi
423423
static AccessibilityObject* anchorElementForNode(Node*);
424424
static AccessibilityObject* headingElementForNode(Node*);
425425
virtual Element* anchorElement() const { return nullptr; }
426+
virtual Element* popoverTargetElement() const { return nullptr; }
426427
bool supportsPressAction() const override;
427428
Element* actionElement() const override { return nullptr; }
428429
virtual LayoutRect boundingBoxRect() const { return { }; }

Source/WebCore/html/HTMLFormControlElement.cpp

+11-2
Original file line numberDiff line numberDiff line change
@@ -395,11 +395,20 @@ void HTMLFormControlElement::handlePopoverTargetAction() const
395395

396396
auto action = popoverTargetAction();
397397
bool canHide = action == hideAtom() || action == toggleAtom();
398+
bool shouldHide = canHide && target->popoverData()->visibilityState() == PopoverVisibilityState::Showing;
398399
bool canShow = action == showAtom() || action == toggleAtom();
399-
if (canHide && target->popoverData()->visibilityState() == PopoverVisibilityState::Showing)
400+
bool shouldShow = canShow && target->popoverData()->visibilityState() == PopoverVisibilityState::Hidden;
401+
402+
if (shouldHide)
400403
target->hidePopover();
401-
else if (canShow && target->popoverData()->visibilityState() == PopoverVisibilityState::Hidden)
404+
else if (shouldShow)
402405
target->showPopover(this);
406+
407+
if (shouldHide || shouldShow) {
408+
// Accessibility needs to know that the invoker (this) toggled popover visibility state.
409+
if (auto* cache = document().existingAXObjectCache())
410+
cache->onPopoverTargetToggle(*this);
411+
}
403412
}
404413

405414
// FIXME: We should remove the quirk once <rdar://problem/47334655> is fixed.

0 commit comments

Comments
 (0)