Bug 1744009 - Accessibility fixes for new combobox layout code. r=eeejay

In terms of the C++ code, this patch does basically one thing, which is
allowing creating option / optgroup accessibles without a frame for
comboboxes, and tracking mutations like layout does.

It seems this should be straight-forward, but handling mutations got a
bit complicated. We don't want to forcibly re-create accessibles, so we
want to re-use the PruneOrInsertSubtree logic that ContentInserted uses.

But determining whether we need to create the accessible requires
having flushed styles, so I added a ScheduleAccessibilitySubtreeUpdate
API to trigger that from WillRefresh once style and layout are
up-to-date.

The rest of the test updates should be sort of straight-forward. They
reflect two changes:

 * <option> accessibles are leaves now (so they don't have text
   children). Note that we still have the right native name and so on,
   using the same logic we use to render the label.

 * In 1proc tests, the focus no longer goes to the <option>, and uses
   the same code-path that e10s does (moving focus to a <menulist> in
   the parent process). Since that wasn't easy to test for (afaict) and
   we have browser tests to cover that
   (browser_treeupdate_select_dropdown.js, etc), I've decided to just
   remove the tests that relied on the previous code-path, as they were
   testing for a codepath that users weren't hitting anyways.

I've tested this with JAWS and Orca and behavior seems unchanged to my
knowledge.

Differential Revision: https://phabricator.services.mozilla.com/D133098
This commit is contained in:
Emilio Cobos Álvarez 2022-01-17 11:10:05 +00:00
parent b651bfe99a
commit f6a958e457
22 changed files with 175 additions and 246 deletions

View File

@ -678,6 +678,8 @@ void NotificationController::WillRefresh(mozilla::TimeStamp aTime) {
"isn't created!");
}
mDocument->ProcessPendingUpdates();
nsTArray<CacheData> cache;
// Process rendered text change notifications.

View File

@ -484,6 +484,22 @@ void nsAccessibilityService::ContentRangeInserted(PresShell* aPresShell,
}
}
void nsAccessibilityService::ScheduleAccessibilitySubtreeUpdate(
PresShell* aPresShell, nsIContent* aContent) {
DocAccessible* document = GetDocAccessible(aPresShell);
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eTree)) {
logging::MsgBegin("TREE", "schedule update; doc: %p", document);
logging::Node("content node", aContent);
logging::MsgEnd();
}
#endif
if (document) {
document->ScheduleTreeUpdate(aContent);
}
}
void nsAccessibilityService::ContentRemoved(PresShell* aPresShell,
nsIContent* aChildNode) {
DocAccessible* document = GetDocAccessible(aPresShell);
@ -518,6 +534,27 @@ void nsAccessibilityService::TableLayoutGuessMaybeChanged(
}
}
void nsAccessibilityService::ComboboxOptionMaybeChanged(
PresShell* aPresShell, nsIContent* aMutatingNode) {
DocAccessible* document = GetDocAccessible(aPresShell);
if (!document) {
return;
}
for (nsIContent* cur = aMutatingNode; cur; cur = cur->GetParent()) {
if (cur->IsHTMLElement(nsGkAtoms::option)) {
if (LocalAccessible* accessible = document->GetAccessible(cur)) {
document->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE,
accessible);
break;
}
if (cur->IsHTMLElement(nsGkAtoms::select)) {
break;
}
}
}
}
void nsAccessibilityService::UpdateText(PresShell* aPresShell,
nsIContent* aContent) {
DocAccessible* document = GetDocAccessible(aPresShell);
@ -882,7 +919,7 @@ LocalAccessible* nsAccessibilityService::CreateAccessible(
if (!frame || !frame->StyleVisibility()->IsVisible()) {
// display:contents element doesn't have a frame, but retains the semantics.
// All its children are unaffected.
if (nsCoreUtils::IsDisplayContents(content)) {
if (nsCoreUtils::CanCreateAccessibleWithoutFrame(content)) {
const MarkupMapInfo* markupMap = GetMarkupMapInfoForNode(content);
if (markupMap && markupMap->new_func) {
RefPtr<LocalAccessible> newAcc =

View File

@ -166,6 +166,14 @@ class nsAccessibilityService final : public mozilla::a11y::DocManager,
void ContentRangeInserted(mozilla::PresShell* aPresShell,
nsIContent* aStartChild, nsIContent* aEndChild);
/**
* Triggers a re-evaluation of the a11y tree of aContent after the next
* refresh. This is important because whether we create accessibles may
* depend on the frame tree / style.
*/
void ScheduleAccessibilitySubtreeUpdate(mozilla::PresShell* aPresShell,
nsIContent* aStartChild);
/**
* Notification used to update the accessible tree when content is removed.
*/
@ -177,6 +185,12 @@ class nsAccessibilityService final : public mozilla::a11y::DocManager,
void TableLayoutGuessMaybeChanged(mozilla::PresShell* aPresShell,
nsIContent* aContent);
/**
* Notifies when a combobox <option> text or label changes.
*/
void ComboboxOptionMaybeChanged(mozilla::PresShell*,
nsIContent* aMutatingNode);
void UpdateText(mozilla::PresShell* aPresShell, nsIContent* aContent);
/**

View File

@ -580,8 +580,26 @@ void nsCoreUtils::DispatchAccEvent(RefPtr<nsIAccessibleEvent> event) {
}
bool nsCoreUtils::IsDisplayContents(nsIContent* aContent) {
return aContent && aContent->IsElement() &&
aContent->AsElement()->IsDisplayContents();
auto* element = Element::FromNodeOrNull(aContent);
return element && element->IsDisplayContents();
}
bool nsCoreUtils::CanCreateAccessibleWithoutFrame(nsIContent* aContent) {
auto* element = Element::FromNodeOrNull(aContent);
if (!element) {
return false;
}
if (!element->HasServoData() || Servo_Element_IsDisplayNone(element)) {
// Out of the flat tree or in a display: none subtree.
return false;
}
if (element->IsDisplayContents()) {
return true;
}
// We don't have a frame, but we're not display: contents either.
// For now, only create accessibles for <option>/<optgroup> as our combobox
// select code depends on it.
return element->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup);
}
bool nsCoreUtils::IsDocumentVisibleConsideringInProcessAncestors(

View File

@ -318,6 +318,7 @@ class nsCoreUtils {
static void DispatchAccEvent(RefPtr<nsIAccessibleEvent> aEvent);
static bool IsDisplayContents(nsIContent* aContent);
static bool CanCreateAccessibleWithoutFrame(nsIContent* aContent);
/**
* Return whether the document and all its in-process ancestors are visible in

View File

@ -131,6 +131,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible,
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingUpdates)
for (const auto& ar : tmp->mARIAOwnsHash.Values()) {
for (uint32_t i = 0; i < ar->Length(); i++) {
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mARIAOwnsHash entry item");
@ -148,6 +149,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, LocalAccessible)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mInvalidationList)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingUpdates)
NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
tmp->mARIAOwnsHash.Clear();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
@ -457,6 +459,7 @@ void DocAccessible::Shutdown() {
mAnchorJumpElm = nullptr;
mInvalidationList.Clear();
mPendingUpdates.Clear();
for (auto iter = mAccessibleCache.Iter(); !iter.Done(); iter.Next()) {
LocalAccessible* accessible = iter.Data();
@ -1164,6 +1167,25 @@ void DocAccessible::ContentInserted(nsIContent* aStartChildNode,
mNotificationController->ScheduleContentInsertion(container, list);
}
void DocAccessible::ScheduleTreeUpdate(nsIContent* aContent) {
if (mPendingUpdates.Contains(aContent)) {
return;
}
mPendingUpdates.AppendElement(aContent);
mNotificationController->ScheduleProcessing();
}
void DocAccessible::ProcessPendingUpdates() {
auto updates = std::move(mPendingUpdates);
for (auto update : updates) {
if (update->GetComposedDoc() != mDocumentNode) {
continue;
}
// The pruning logic will take care of avoiding unnecessary notifications.
ContentInserted(update, update->GetNextSibling());
}
}
bool DocAccessible::PruneOrInsertSubtree(nsIContent* aRoot) {
bool insert = false;
@ -1178,7 +1200,7 @@ bool DocAccessible::PruneOrInsertSubtree(nsIContent* aRoot) {
// then remove their accessibles and subtrees.
while (nsIContent* childNode = iter.GetNextChild()) {
if (!childNode->GetPrimaryFrame() &&
!nsCoreUtils::IsDisplayContents(childNode)) {
!nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) {
ContentRemoved(childNode);
}
}
@ -1189,7 +1211,7 @@ bool DocAccessible::PruneOrInsertSubtree(nsIContent* aRoot) {
for (nsIContent* childNode = aRoot->GetFirstChild(); childNode;
childNode = childNode->GetNextSibling()) {
if (!childNode->GetPrimaryFrame() &&
!nsCoreUtils::IsDisplayContents(childNode)) {
!nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) {
ContentRemoved(childNode);
}
}
@ -1218,7 +1240,7 @@ bool DocAccessible::PruneOrInsertSubtree(nsIContent* aRoot) {
// As well as removing the a11y subtree, we must also remove Accessibles
// for DOM descendants, since some of these might be relocated Accessibles
// and their DOM nodes are now hidden as well.
if (!frame && !nsCoreUtils::IsDisplayContents(aRoot)) {
if (!frame && !nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) {
ContentRemoved(aRoot);
return false;
}
@ -1280,7 +1302,8 @@ bool DocAccessible::PruneOrInsertSubtree(nsIContent* aRoot) {
} else {
// If there is no current accessible, and the node has a frame, or is
// display:contents, schedule it for insertion.
if (aRoot->GetPrimaryFrame() || nsCoreUtils::IsDisplayContents(aRoot)) {
if (aRoot->GetPrimaryFrame() ||
nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) {
// This may be a new subtree, the insertion process will recurse through
// its descendants.
if (!GetAccessibleOrDescendant(aRoot)) {

View File

@ -335,6 +335,11 @@ class DocAccessible : public HyperTextAccessibleWrap,
*/
void ContentInserted(nsIContent* aStartChildNode, nsIContent* aEndChildNode);
/**
* @see nsAccessibilityService::ScheduleAccessibilitySubtreeUpdate
*/
void ScheduleTreeUpdate(nsIContent* aContent);
/**
* Update the tree on content removal.
*/
@ -490,6 +495,11 @@ class DocAccessible : public HyperTextAccessibleWrap,
*/
void ProcessInvalidationList();
/**
* Process mPendingUpdates
*/
void ProcessPendingUpdates();
/**
* Called from NotificationController to process this doc's
* mMaybeBoundsChanged list. Sends a cache update for each acc in this
@ -718,6 +728,11 @@ class DocAccessible : public HyperTextAccessibleWrap,
nsTArray<RefPtr<LocalAccessible>>>
mARIAOwnsHash;
/**
* Keeps a list of pending subtrees to update post-refresh.
*/
nsTArray<RefPtr<nsIContent>> mPendingUpdates;
/**
* Used to process notification from core and accessible events.
*/

View File

@ -16,6 +16,7 @@
#include "nsCOMPtr.h"
#include "mozilla/dom/HTMLOptionElement.h"
#include "mozilla/dom/HTMLOptGroupElement.h"
#include "mozilla/dom/HTMLSelectElement.h"
#include "nsComboboxControlFrame.h"
#include "nsContainerFrame.h"
@ -121,22 +122,20 @@ role HTMLSelectOptionAccessible::NativeRole() const {
}
ENameValueFlag HTMLSelectOptionAccessible::NativeName(nsString& aName) const {
// CASE #1 -- great majority of the cases
// find the label attribute - this is what the W3C says we should use
mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aName);
if (!aName.IsEmpty()) return eNameOK;
// CASE #2 -- no label parameter, get the first child,
// use it if it is a text node
LocalAccessible* firstChild = LocalFirstChild();
nsIContent* text = firstChild ? firstChild->GetContent() : nullptr;
if (text && text->IsText()) {
nsTextEquivUtils::AppendTextEquivFromTextContent(text, &aName);
aName.CompressWhitespace();
if (auto* option = dom::HTMLOptionElement::FromNode(mContent)) {
option->GetAttr(nsGkAtoms::label, aName);
if (!aName.IsEmpty()) {
return eNameOK;
}
option->GetText(aName);
return eNameFromSubtree;
}
if (auto* group = dom::HTMLOptGroupElement::FromNode(mContent)) {
group->GetLabel(aName);
return aName.IsEmpty() ? eNameOK : eNameFromSubtree;
}
return eNameOK;
MOZ_ASSERT_UNREACHABLE("What content do we have?");
return eNameFromSubtree;
}
void HTMLSelectOptionAccessible::DOMAttributeChanged(

View File

@ -41,10 +41,7 @@ addAccessibleTask(
{
COMBOBOX_LIST: [
{
GROUPING: [
{ COMBOBOX_OPTION: [{ TEXT_LEAF: [] }] },
{ COMBOBOX_OPTION: [{ TEXT_LEAF: [] }] },
],
GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }],
},
{
COMBOBOX_OPTION: [],

View File

@ -40,6 +40,7 @@ addAccessibleTask(
const EventUtils = ContentTaskUtils.getEventUtils(content);
EventUtils.synthesizeKey("VK_DOWN", { altKey: true }, content);
});
info("Waiting for parent focus");
let event = await focused;
let dropdown = event.accessible.parent;
@ -64,13 +65,8 @@ addAccessibleTask(
"select",
"select focused after collapsed"
);
await invokeContentTask(browser, [], () => {
const { ContentTaskUtils } = ChromeUtils.import(
"resource://testing-common/ContentTaskUtils.jsm"
);
const EventUtils = ContentTaskUtils.getEventUtils(content);
EventUtils.synthesizeKey("VK_ESCAPE", {}, content);
});
EventUtils.synthesizeKey("VK_ESCAPE", {}, window);
info("Waiting for child focus");
await focused;
},
{ iframe: true, remoteIframe: true }

View File

@ -32,42 +32,6 @@
new focusChecker("lb_apple"),
],
},
{
ID: "combobox",
actionIndex: 0,
actionName: "open",
events: CLICK_EVENTS,
eventSeq: [
new focusChecker("cb_orange"),
],
},
{
ID: "cb_apple",
actionIndex: 0,
actionName: "select",
events: CLICK_EVENTS,
eventSeq: [
new focusChecker("combobox"),
],
},
{
ID: "combobox",
actionIndex: 0,
actionName: "open",
events: CLICK_EVENTS,
eventSeq: [
new focusChecker("cb_apple"),
],
},
{
ID: "combobox",
actionIndex: 0,
actionName: "close",
events: CLICK_EVENTS,
eventSeq: [
new focusChecker("combobox"),
],
},
];
testActions(actionsArray);

View File

@ -3,4 +3,3 @@ support-files =
!/accessible/tests/mochitest/*.js
[test_list.html]
[test_select.html]

View File

@ -1,78 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Accessible boundaries when page is zoomed</title>
<link rel="stylesheet" type="text/css"
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script type="application/javascript"
src="../common.js"></script>
<script type="application/javascript"
src="../role.js"></script>
<script type="application/javascript"
src="../layout.js"></script>
<script type="application/javascript"
src="../events.js"></script>
<script type="application/javascript">
function openComboboxNCheckBounds(aID) {
this.combobox = getAccessible(aID);
this.comboboxList = this.combobox.firstChild;
this.comboboxOption = this.comboboxList.firstChild;
this.eventSeq = [
new invokerChecker(EVENT_FOCUS, this.comboboxOption),
];
this.invoke = function openComboboxNCheckBounds_invoke() {
getNode(aID).focus();
synthesizeKey("VK_DOWN", { altKey: true });
};
this.finalCheck = function openComboboxNCheckBounds_invoke() {
testBounds(this.comboboxOption);
};
this.getID = function openComboboxNCheckBounds_getID() {
return "open combobox and test boundaries";
};
}
// gA11yEventDumpToConsole = true;
var gQueue = null;
function doTest() {
// Combobox
testBounds("combobox");
// Option boundaries matches to combobox boundaries when collapsed.
var selectBounds = getBoundsForDOMElm("combobox");
testBounds("option1", selectBounds);
// Open combobox and test option boundaries.
gQueue = new eventQueue();
gQueue.push(new openComboboxNCheckBounds("combobox"));
gQueue.invoke(); // Will call SimpleTest.finish();
}
SimpleTest.waitForExplicitFinish();
addA11yLoadEvent(doTest);
</script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test">
</pre>
<select id="combobox">
<option id="option1">item1</option>
<option>item2</option>
</select>
</body>
</html>

View File

@ -100,26 +100,6 @@
synthesizeKey("VK_DOWN");
await p;
p = waitForEvent(EVENT_FOCUS, "cb_apple");
// current selected item is focused when combobox is expanded
synthesizeKey("VK_DOWN", { altKey: true });
await p;
p = waitForEvents({
expected: [
[EVENT_SELECTION, "cb_orange"],
stateChangeEventArgs("cb_orange", EXT_STATE_ACTIVE, true, true),
],
});
// selected item is focused for expanded combobox
synthesizeKey("VK_UP");
await p;
p = waitForEvent(EVENT_FOCUS, "combobox");
// collapsed combobx keeps a focus
synthesizeKey("VK_ESCAPE");
await p;
// no focus events for unfocused list controls when current item is
// changed
@ -141,14 +121,14 @@
p = waitForEvents({
expected: [
[EVENT_SELECTION, "cb_apple"],
stateChangeEventArgs("cb_apple", EXT_STATE_ACTIVE, true, true),
[EVENT_SELECTION, "cb_orange"],
stateChangeEventArgs("cb_orange", EXT_STATE_ACTIVE, true, true),
],
unexpected: [[EVENT_FOCUS]],
});
// An unfocused selectable combobox gets selection change events,
// and active state change events, but not focus.
getNode("cb_apple").selected = true;
getNode("cb_orange").selected = true;
await p;
SimpleTest.finish();

View File

@ -31,17 +31,11 @@
function doTests() {
gQueue = new eventQueue();
// open combobox
gQueue.push(new synthClick("combobox",
new invokerChecker(EVENT_FOCUS, "cb1_item1")));
gQueue.push(new synthDownKey("cb1_item1",
selChangeSeq("cb1_item1", "cb1_item2")));
// closed combobox
gQueue.push(new synthEscapeKey("combobox",
new invokerChecker(EVENT_FOCUS, "combobox")));
gQueue.push(new synthDownKey("cb1_item2",
selChangeSeq("cb1_item2", "cb1_item3")));
gQueue.push(new synthFocus("combobox"));
gQueue.push(new synthDownKey("cb1_item1",
selChangeSeq("cb1_item1", "cb1_item2")));
// listbox
gQueue.push(new synthClick("lb1_item1",

View File

@ -18,36 +18,9 @@
src="../events.js"></script>
<script type="application/javascript">
function openComboboxNCheckStates(aID) {
this.combobox = getAccessible(aID);
this.comboboxList = this.combobox.firstChild;
this.comboboxOption = this.comboboxList.firstChild;
this.eventSeq = [
new invokerChecker(EVENT_FOCUS, this.comboboxOption),
];
this.invoke = function openComboboxNCheckStates_invoke() {
getNode(aID).focus();
synthesizeKey("VK_DOWN", { altKey: true });
};
this.finalCheck = function openComboboxNCheckStates_invoke() {
// Expanded state on combobox.
testStates(this.combobox, STATE_EXPANDED);
// Floating state on combobox list.
testStates(this.comboboxList, STATE_FLOATING);
};
this.getID = function openComboboxNCheckStates_getID() {
return "open combobox and test states";
};
}
// gA11yEventDumpToConsole = true;
var gQueue = null;
function doTest() {
// combobox
var combobox = getAccessible("combobox");
@ -60,11 +33,11 @@
var opt1 = comboboxList.firstChild;
testStates(opt1, STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE,
EXT_STATE_ACTIVE, STATE_FOCUSED, 0);
0, STATE_FOCUSED, 0);
var opt2 = comboboxList.lastChild;
testStates(opt2, STATE_SELECTABLE | STATE_FOCUSABLE, 0, STATE_SELECTED, 0,
STATE_FOCUSED, EXT_STATE_ACTIVE);
STATE_FOCUSED, 0);
// collapsed combobox
testStates("collapsedcombobox",
@ -123,10 +96,7 @@
// STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE,
// EXT_STATE_ACTIVE);
// open combobox
gQueue = new eventQueue();
gQueue.push(new openComboboxNCheckStates("combobox"));
gQueue.invoke(); // Will call */SimpleTest.finish();
SimpleTest.finish();
}
SimpleTest.waitForExplicitFinish();

View File

@ -64,34 +64,18 @@
role: ROLE_GROUPING,
children: [
{
role: ROLE_STATICTEXT,
role: ROLE_COMBOBOX_OPTION,
children: [ ],
},
{
role: ROLE_COMBOBOX_OPTION,
children: [
{
role: ROLE_TEXT_LEAF,
children: [ ],
},
],
},
{
role: ROLE_COMBOBOX_OPTION,
children: [
{
role: ROLE_TEXT_LEAF,
},
],
},
],
},
{
role: ROLE_COMBOBOX_OPTION,
children: [
{
role: ROLE_TEXT_LEAF,
},
],
children: [ ],
},
],
},

View File

@ -45,12 +45,8 @@
{ COMBOBOX: [
{ COMBOBOX_LIST: [
{ GROUPING: [
{ COMBOBOX_OPTION: [
{ TEXT_LEAF: [] },
] },
{ COMBOBOX_OPTION: [
{ TEXT_LEAF: [] },
] },
{ COMBOBOX_OPTION: [ ] },
{ COMBOBOX_OPTION: [ ] },
]},
{ COMBOBOX_OPTION: [] },
] },

View File

@ -39,12 +39,8 @@
var tree =
{ COMBOBOX: [
{ COMBOBOX_LIST: [
{ COMBOBOX_OPTION: [
{ TEXT_LEAF: [] },
] },
{ COMBOBOX_OPTION: [
{ TEXT_LEAF: [] },
] },
{ COMBOBOX_OPTION: [ ] },
{ COMBOBOX_OPTION: [ ] },
] },
] };
testAccessibleTree(this.select, tree);

View File

@ -90,7 +90,7 @@ class HTMLOptionElement final : public nsGenericHTMLElement {
}
}
void GetLabel(DOMString& aLabel) {
void GetLabel(nsAString& aLabel) {
if (!GetAttr(kNameSpaceID_None, nsGkAtoms::label, aLabel)) {
GetText(aLabel);
}

View File

@ -278,6 +278,16 @@ int32_t HTMLSelectEventListener::ItemsPerPage() const {
return AssertedCast<int32_t>(size - 1u);
}
void HTMLSelectEventListener::OptionValueMightHaveChanged(
nsIContent* aMutatingNode) {
#ifdef ACCESSIBILITY
if (nsAccessibilityService* acc = PresShell::GetAccessibilityService()) {
acc->ComboboxOptionMaybeChanged(mElement->OwnerDoc()->GetPresShell(),
aMutatingNode);
}
#endif
}
void HTMLSelectEventListener::AttributeChanged(dom::Element* aElement,
int32_t aNameSpaceID,
nsAtom* aAttribute,
@ -285,6 +295,8 @@ void HTMLSelectEventListener::AttributeChanged(dom::Element* aElement,
const nsAttrValue* aOldValue) {
if (aElement->IsHTMLElement(nsGkAtoms::option) &&
aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::label) {
// A11y has its own mutation listener for this so no need to do
// OptionValueMightHaveChanged().
ComboboxMightHaveChanged();
}
}
@ -292,6 +304,7 @@ void HTMLSelectEventListener::AttributeChanged(dom::Element* aElement,
void HTMLSelectEventListener::CharacterDataChanged(
nsIContent* aContent, const CharacterDataChangeInfo&) {
if (nsContentUtils::IsInSameAnonymousTree(mElement, aContent)) {
OptionValueMightHaveChanged(aContent);
ComboboxMightHaveChanged();
}
}
@ -299,28 +312,36 @@ void HTMLSelectEventListener::CharacterDataChanged(
void HTMLSelectEventListener::ContentRemoved(nsIContent* aChild,
nsIContent* aPreviousSibling) {
if (nsContentUtils::IsInSameAnonymousTree(mElement, aChild)) {
OptionValueMightHaveChanged(aChild);
ComboboxMightHaveChanged();
}
}
void HTMLSelectEventListener::ContentAppended(nsIContent* aFirstNewContent) {
if (nsContentUtils::IsInSameAnonymousTree(mElement, aFirstNewContent)) {
OptionValueMightHaveChanged(aFirstNewContent);
ComboboxMightHaveChanged();
}
}
void HTMLSelectEventListener::ContentInserted(nsIContent* aChild) {
if (nsContentUtils::IsInSameAnonymousTree(mElement, aChild)) {
OptionValueMightHaveChanged(aChild);
ComboboxMightHaveChanged();
}
}
void HTMLSelectEventListener::ComboboxMightHaveChanged() {
if (nsIFrame* f = mElement->GetPrimaryFrame()) {
PresShell* ps = f->PresShell();
// nsComoboxControlFrame::Reflow updates the selected text. AddOption /
// RemoveOption / etc takes care of keeping the displayed index up to date.
f->PresShell()->FrameNeedsReflow(f, IntrinsicDirty::StyleChange,
NS_FRAME_IS_DIRTY);
ps->FrameNeedsReflow(f, IntrinsicDirty::StyleChange, NS_FRAME_IS_DIRTY);
#ifdef ACCESSIBILITY
if (nsAccessibilityService* acc = PresShell::GetAccessibilityService()) {
acc->ScheduleAccessibilitySubtreeUpdate(ps, mElement);
}
#endif
}
}

View File

@ -88,6 +88,7 @@ class HTMLSelectEventListener final : public nsStubMutationObserver,
int32_t aFromIndex, int32_t* aFoundIndex = nullptr) const;
void ComboboxMightHaveChanged();
void OptionValueMightHaveChanged(nsIContent* aMutatingNode);
~HTMLSelectEventListener();