Skip to content

Commit

Permalink
AX: Elements with the popovertarget attribute should expose expanded …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
twilco authored and mnutt committed Jun 19, 2023
1 parent b40f8c5 commit fed84e7
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 35 deletions.
12 changes: 10 additions & 2 deletions LayoutTests/accessibility/mac/expanded-notification-expected.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
This tests that aria-expanded changes will send notifications.
This tests that expanded notifications will be sent when the appropriate changes occur.
Initial expanded status: false
Received notification: AXExpandedChanged
Expanded status: true
Received notification: AXExpandedChanged
Expanded status: false
PASS: accessibilityController.accessibleElementById('show-popover-btn').isExpanded === false
PASS: accessibilityController.accessibleElementById('hide-popover-btn').isExpanded === false
Received notification: AXExpandedChanged
Expanded status: true
PASS: accessibilityController.accessibleElementById('hide-popover-btn').isExpanded === true
Received notification: AXExpandedChanged
Expanded status: false
PASS: accessibilityController.accessibleElementById('show-popover-btn').isExpanded === false

PASS successfullyParsed is true

TEST COMPLETE

Show popover Hide popover
70 changes: 40 additions & 30 deletions LayoutTests/accessibility/mac/expanded-notification.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,56 @@

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

<button id="show-popover-btn" popovertarget="mypopover" popovertargetaction="show">Show popover</button>
<button id="hide-popover-btn" popovertarget="mypopover" popovertargetaction="hide">Hide popover</button>
<div id="mypopover" popover>Popover content</div>

<script>
let output = "This tests that aria-expanded changes will send notifications.\n";
let output = "This tests that expanded notifications will be sent when the appropriate changes occur.\n";

let notificationCount = 0;
function notificationCallback(element, notification) {
if (notification == "AXExpandedChanged") {
notificationCount++;
let notificationCount = 0;
function notificationCallback(element, notification) {
if (notification == "AXExpandedChanged") {
notificationCount++;

output += `Received notification: ${notification}\n`;
output += `Expanded status: ${element.isExpanded}\n`;
}
output += `Received notification: ${notification}\n`;
output += `Expanded status: ${element.isExpanded}\n`;
}
}

if (window.accessibilityController) {
window.jsTestIsAsync = true;
if (window.accessibilityController) {
window.jsTestIsAsync = true;

accessibilityController.addNotificationListener(notificationCallback);
let button = accessibilityController.accessibleElementById("button");
output += `Initial expanded status: ${button.isExpanded}\n`;
accessibilityController.addNotificationListener(notificationCallback);
let button = accessibilityController.accessibleElementById("button");
output += `Initial expanded status: ${button.isExpanded}\n`;

document.getElementById("button").setAttribute("aria-expanded", "true");
setTimeout(async () => {
await waitFor(() => {
return button.isExpanded;
});
document.getElementById("button").setAttribute("aria-expanded", "true");
setTimeout(async () => {
await waitFor(() => button.isExpanded);

document.getElementById("button").setAttribute("aria-expanded", "false");
await waitFor(() => {
return !button.isExpanded;
});
document.getElementById("button").setAttribute("aria-expanded", "false");
await waitFor(() => !button.isExpanded);
await waitFor(() => notificationCount == 2);

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

debug(output);
accessibilityController.removeNotificationListener();
finishJSTest();
}, 0);
}
document.getElementById("show-popover-btn").click();
await waitFor(() => notificationCount == 3);
// 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.
output += await expectAsync("accessibilityController.accessibleElementById('hide-popover-btn').isExpanded", "true");

document.getElementById("hide-popover-btn").click();
await waitFor(() => notificationCount == 4);
output += await expectAsync("accessibilityController.accessibleElementById('show-popover-btn').isExpanded", "false");

debug(output);
accessibilityController.removeNotificationListener();
finishJSTest();
}, 0);
}
</script>
</body>
</html>
5 changes: 5 additions & 0 deletions Source/WebCore/accessibility/AXObjectCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,11 @@ void AXObjectCache::onFocusChange(Node* oldNode, Node* newNode)
handleFocusedUIElementChanged(oldNode, newNode);
}

void AXObjectCache::onPopoverTargetToggle(const HTMLFormControlElement& popoverInvokerElement)
{
postNotification(get(const_cast<HTMLFormControlElement*>(&popoverInvokerElement)), &document(), AXExpandedChanged);
}

void AXObjectCache::deferMenuListValueChange(Element* element)
{
if (!element)
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/accessibility/AXObjectCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class AXObjectCache : public CanMakeWeakPtr<AXObjectCache>, public CanMakeChecke
void childrenChanged(RenderObject*, RenderObject* newChild = nullptr);
void childrenChanged(AccessibilityObject*);
void onFocusChange(Node* oldFocusedNode, Node* newFocusedNode);
void onPopoverTargetToggle(const HTMLFormControlElement&);
void onScrollbarFrameRectChange(const Scrollbar&);
void onSelectedChanged(Node*);
void onTextSecurityChanged(HTMLInputElement&);
Expand Down
6 changes: 6 additions & 0 deletions Source/WebCore/accessibility/AccessibilityNodeObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,12 @@ Element* AccessibilityNodeObject::anchorElement() const
return nullptr;
}

Element* AccessibilityNodeObject::popoverTargetElement() const
{
WeakPtr formControlElement = dynamicDowncast<HTMLFormControlElement>(node());
return formControlElement ? formControlElement->popoverTargetElement() : 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.
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/accessibility/AccessibilityNodeObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class AccessibilityNodeObject : public AccessibilityObject {
Element* actionElement() const override;
Element* mouseButtonListener(MouseButtonListenerResultFilter = ExcludeBodyElement) const;
Element* anchorElement() const override;
Element* popoverTargetElement() const final;
AccessibilityObject* internalLinkElement() const;
void addRadioButtonGroupMembers(AccessibilityChildrenVector& linkedUIElements) const;
void addRadioButtonGroupChildren(AXCoreObject&, AccessibilityChildrenVector&) const;
Expand Down
9 changes: 8 additions & 1 deletion Source/WebCore/accessibility/AccessibilityObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3269,6 +3269,10 @@ bool AccessibilityObject::supportsPressed() const

bool AccessibilityObject::supportsExpanded() const
{
// 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).
if (popoverTargetElement())
return true;

switch (roleValue()) {
case AccessibilityRole::Button:
case AccessibilityRole::CheckBox:
Expand Down Expand Up @@ -3322,8 +3326,11 @@ bool AccessibilityObject::isExpanded() const
return parent->isExpanded();
}

if (supportsExpanded())
if (supportsExpanded()) {
if (WeakPtr popoverTargetElement = this->popoverTargetElement())
return popoverTargetElement->isPopoverShowing();
return equalLettersIgnoringASCIICase(getAttribute(aria_expandedAttr), "true"_s);
}

return false;
}
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/accessibility/AccessibilityObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ class AccessibilityObject : public AXCoreObject, public CanMakeWeakPtr<Accessibi
static AccessibilityObject* anchorElementForNode(Node*);
static AccessibilityObject* headingElementForNode(Node*);
virtual Element* anchorElement() const { return nullptr; }
virtual Element* popoverTargetElement() const { return nullptr; }
bool supportsPressAction() const override;
Element* actionElement() const override { return nullptr; }
virtual LayoutRect boundingBoxRect() const { return { }; }
Expand Down
13 changes: 11 additions & 2 deletions Source/WebCore/html/HTMLFormControlElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -395,11 +395,20 @@ void HTMLFormControlElement::handlePopoverTargetAction() const

auto action = popoverTargetAction();
bool canHide = action == hideAtom() || action == toggleAtom();
bool shouldHide = canHide && target->popoverData()->visibilityState() == PopoverVisibilityState::Showing;
bool canShow = action == showAtom() || action == toggleAtom();
if (canHide && target->popoverData()->visibilityState() == PopoverVisibilityState::Showing)
bool shouldShow = canShow && target->popoverData()->visibilityState() == PopoverVisibilityState::Hidden;

if (shouldHide)
target->hidePopover();
else if (canShow && target->popoverData()->visibilityState() == PopoverVisibilityState::Hidden)
else if (shouldShow)
target->showPopover(this);

if (shouldHide || shouldShow) {
// Accessibility needs to know that the invoker (this) toggled popover visibility state.
if (auto* cache = document().existingAXObjectCache())
cache->onPopoverTargetToggle(*this);
}
}

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

0 comments on commit fed84e7

Please sign in to comment.