Bug 1870783 part 4: Fire state change events on all popover invokers. r=eeejay

A popover can have multiple invoker buttons.
Previously, we only fired a state change event on the button which was invoked.
This meant that the cached expanded/collapsed state of any other invokers was stale.
To facilitate this:

1. Add popovertarget to DocAccessible's kRelationAttrs so that we track reverse relationships. This will also later be used for exposing relations.
2. When an Accessible is shown or hidden, if it is a popover, use RelatedAccIterator to get all the invokers and fire appropriate state change events on each of them.
3. This also means we can get rid of nsAccessibilityService::PopovertargetMaybeChanged, since this explicit notification from DOM is no longer useful.
4. Add popovertarget to LocalAccessible::AttributeChangesState so that we fire an event for the expandable/expanded/collapsed state change if appropriate when that attribute is changed.

Differential Revision: https://phabricator.services.mozilla.com/D199842
This commit is contained in:
James Teh 2024-02-12 06:24:15 +00:00
Родитель adc647d267
Коммит f2b94d8123
7 изменённых файлов: 70 добавлений и 57 удалений

Просмотреть файл

@ -716,19 +716,6 @@ void nsAccessibilityService::TableLayoutGuessMaybeChanged(
}
}
void nsAccessibilityService::PopovertargetMaybeChanged(PresShell* aPresShell,
nsIContent* aContent) {
DocAccessible* document = GetDocAccessible(aPresShell);
if (!document) {
return;
}
if (LocalAccessible* acc = document->GetAccessible(aContent)) {
RefPtr<AccEvent> expandedChangeEvent =
new AccStateChangeEvent(acc, states::EXPANDED);
document->FireDelayedEvent(expandedChangeEvent);
}
}
void nsAccessibilityService::ComboboxOptionMaybeChanged(
PresShell* aPresShell, nsIContent* aMutatingNode) {
DocAccessible* document = GetDocAccessible(aPresShell);

Просмотреть файл

@ -180,12 +180,6 @@ class nsAccessibilityService final : public mozilla::a11y::DocManager,
void TableLayoutGuessMaybeChanged(mozilla::PresShell* aPresShell,
nsIContent* aContent);
/**
* Notifies when an element's popovertarget shows/hides.
*/
void PopovertargetMaybeChanged(mozilla::PresShell* aPresShell,
nsIContent* aContent);
/**
* Notifies when a combobox <option> text or label changes.
*/

Просмотреть файл

@ -58,15 +58,12 @@ using namespace mozilla::a11y;
////////////////////////////////////////////////////////////////////////////////
// Static member initialization
static nsStaticAtom* const kRelationAttrs[] = {nsGkAtoms::aria_labelledby,
nsGkAtoms::aria_describedby,
nsGkAtoms::aria_details,
nsGkAtoms::aria_owns,
nsGkAtoms::aria_controls,
nsGkAtoms::aria_flowto,
nsGkAtoms::aria_errormessage,
nsGkAtoms::_for,
nsGkAtoms::control};
static nsStaticAtom* const kRelationAttrs[] = {
nsGkAtoms::aria_labelledby, nsGkAtoms::aria_describedby,
nsGkAtoms::aria_details, nsGkAtoms::aria_owns,
nsGkAtoms::aria_controls, nsGkAtoms::aria_flowto,
nsGkAtoms::aria_errormessage, nsGkAtoms::_for,
nsGkAtoms::control, nsGkAtoms::popovertarget};
static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs);
@ -2006,6 +2003,22 @@ bool InsertIterator::Next() {
return false;
}
void DocAccessible::MaybeFireEventsForChangedPopover(LocalAccessible *aAcc) {
dom::Element* el = aAcc->Elm();
if (!el || !el->IsHTMLElement() || !el->HasAttr(nsGkAtoms::popover)) {
return; // Not a popover.
}
// A popover has just been inserted into or removed from the a11y tree, which
// means it just appeared or disappeared. Fire expanded state changes on its
// invokers.
RelatedAccIterator invokers(mDoc, el, nsGkAtoms::popovertarget);
while (Accessible* invoker = invokers.Next()) {
RefPtr<AccEvent> expandedChangeEvent =
new AccStateChangeEvent(invoker->AsLocal(), states::EXPANDED);
FireDelayedEvent(expandedChangeEvent);
}
}
void DocAccessible::ProcessContentInserted(
LocalAccessible* aContainer, const nsTArray<nsCOMPtr<nsIContent>>* aNodes) {
// Process insertions if the container accessible is still in tree.
@ -2065,6 +2078,7 @@ void DocAccessible::ProcessContentInserted(
CreateSubtree(iter.Child());
mt.AfterInsertion(iter.Child());
inserted = true;
MaybeFireEventsForChangedPopover(iter.Child());
continue;
}
@ -2603,6 +2617,7 @@ void DocAccessible::CacheChildrenInSubtree(LocalAccessible* aRoot,
}
void DocAccessible::UncacheChildrenInSubtree(LocalAccessible* aRoot) {
MaybeFireEventsForChangedPopover(aRoot);
aRoot->mStateFlags |= eIsNotInDocument;
RemoveDependentIDsFor(aRoot);

Просмотреть файл

@ -779,6 +779,8 @@ class DocAccessible : public HyperTextAccessible,
*/
void MaybeHandleChangeToHiddenNameOrDescription(nsIContent* aChild);
void MaybeFireEventsForChangedPopover(LocalAccessible* aAcc);
PresShell* mPresShell;
// Exclusively owned by IPDL so don't manually delete it!

Просмотреть файл

@ -1304,7 +1304,8 @@ bool LocalAccessible::AttributeChangesState(nsAtom* aAttribute) {
aAttribute == nsGkAtoms::aria_busy ||
aAttribute == nsGkAtoms::aria_multiline ||
aAttribute == nsGkAtoms::aria_multiselectable ||
aAttribute == nsGkAtoms::contenteditable;
aAttribute == nsGkAtoms::contenteditable ||
aAttribute == nsGkAtoms::popovertarget;
}
void LocalAccessible::DOMAttributeChanged(int32_t aNameSpaceID,

Просмотреть файл

@ -490,6 +490,7 @@ addAccessibleTask(
`
<button id="show-popover-btn" popovertarget="mypopover" popovertargetaction="show">Show popover</button>
<button id="hide-popover-btn" popovertarget="mypopover" popovertargetaction="hide">Hide popover</button>
<button id="toggle">toggle</button>
<div id="mypopover" popover>
Popover content
<button id="hide-inside" popovertarget="mypopover" popovertargetaction="hide">Hide inside popover</button>
@ -500,23 +501,52 @@ addAccessibleTask(
const hide = findAccessibleChildByID(docAcc, "hide-popover-btn");
testStates(show, STATE_COLLAPSED, 0);
testStates(hide, STATE_COLLAPSED, 0);
info("Expanding popover");
let onShowing = waitForEvent(EVENT_STATE_CHANGE, show);
await show.doAction(0);
let showingEvent = await onShowing;
testStates(showingEvent.accessible, STATE_EXPANDED, 0);
const hideInside = findAccessibleChildByID(
showingEvent.accessible,
"hide-inside"
const toggle = findAccessibleChildByID(docAcc, "toggle");
testStates(
toggle,
0,
0,
STATE_EXPANDED | STATE_COLLAPSED,
EXT_STATE_EXPANDABLE
);
info("Setting toggle's popovertarget");
let stateChanged = waitForStateChange(
toggle,
EXT_STATE_EXPANDABLE,
true,
true
);
await invokeContentTask(browser, [], () => {
content.document
.getElementById("toggle")
.setAttribute("popovertarget", "mypopover");
});
await stateChanged;
// Changes to the popover should fire events on all invokers.
const changeEvents = [
[EVENT_STATE_CHANGE, show],
[EVENT_STATE_CHANGE, hide],
[EVENT_STATE_CHANGE, toggle],
];
info("Expanding popover");
let onShowing = waitForEvents(changeEvents);
await show.doAction(0);
await onShowing;
testStates(show, STATE_EXPANDED, 0);
testStates(hide, STATE_EXPANDED, 0);
testStates(toggle, STATE_EXPANDED, 0);
const hideInside = findAccessibleChildByID(show, "hide-inside");
testStates(hideInside, 0, 0, STATE_EXPANDED | STATE_COLLAPSED, 0);
info("Collapsing popover");
let onHiding = waitForEvent(EVENT_STATE_CHANGE, hide);
let onHiding = waitForEvents(changeEvents);
await hide.doAction(0);
let hidingEvent = await onHiding;
testStates(hidingEvent.accessible, STATE_COLLAPSED, 0);
await onHiding;
testStates(hide, STATE_COLLAPSED, 0);
testStates(show, STATE_COLLAPSED, 0);
testStates(toggle, STATE_COLLAPSED, 0);
},
{ chrome: true, topLevel: true, remoteIframe: true }
);

Просмотреть файл

@ -93,10 +93,6 @@
#include "mozilla/dom/ElementBinding.h"
#include "mozilla/dom/ElementInternals.h"
#ifdef ACCESSIBILITY
# include "nsAccessibilityService.h"
#endif
using namespace mozilla;
using namespace mozilla::dom;
@ -2865,18 +2861,6 @@ void nsGenericHTMLFormControlElementWithState::HandlePopoverTargetAction() {
} else if (shouldShow) {
target->ShowPopoverInternal(this, IgnoreErrors());
}
#ifdef ACCESSIBILITY
// Notify the accessibility service about the change.
if (shouldHide || shouldShow) {
if (RefPtr<Document> doc = GetComposedDoc()) {
if (PresShell* presShell = doc->GetPresShell()) {
if (nsAccessibilityService* accService = GetAccService()) {
accService->PopovertargetMaybeChanged(presShell, this);
}
}
}
}
#endif
}
void nsGenericHTMLFormControlElementWithState::GetInvokeAction(