mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 14:52:16 +00:00
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:
parent
b651bfe99a
commit
f6a958e457
@ -678,6 +678,8 @@ void NotificationController::WillRefresh(mozilla::TimeStamp aTime) {
|
||||
"isn't created!");
|
||||
}
|
||||
|
||||
mDocument->ProcessPendingUpdates();
|
||||
|
||||
nsTArray<CacheData> cache;
|
||||
|
||||
// Process rendered text change notifications.
|
||||
|
@ -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 =
|
||||
|
@ -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);
|
||||
|
||||
/**
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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)) {
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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(
|
||||
|
@ -41,10 +41,7 @@ addAccessibleTask(
|
||||
{
|
||||
COMBOBOX_LIST: [
|
||||
{
|
||||
GROUPING: [
|
||||
{ COMBOBOX_OPTION: [{ TEXT_LEAF: [] }] },
|
||||
{ COMBOBOX_OPTION: [{ TEXT_LEAF: [] }] },
|
||||
],
|
||||
GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }],
|
||||
},
|
||||
{
|
||||
COMBOBOX_OPTION: [],
|
||||
|
@ -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 }
|
||||
|
@ -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);
|
||||
|
@ -3,4 +3,3 @@ support-files =
|
||||
!/accessible/tests/mochitest/*.js
|
||||
|
||||
[test_list.html]
|
||||
[test_select.html]
|
||||
|
@ -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>
|
@ -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();
|
||||
|
@ -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",
|
||||
|
@ -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();
|
||||
|
@ -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: [ ],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -45,12 +45,8 @@
|
||||
{ COMBOBOX: [
|
||||
{ COMBOBOX_LIST: [
|
||||
{ GROUPING: [
|
||||
{ COMBOBOX_OPTION: [
|
||||
{ TEXT_LEAF: [] },
|
||||
] },
|
||||
{ COMBOBOX_OPTION: [
|
||||
{ TEXT_LEAF: [] },
|
||||
] },
|
||||
{ COMBOBOX_OPTION: [ ] },
|
||||
{ COMBOBOX_OPTION: [ ] },
|
||||
]},
|
||||
{ COMBOBOX_OPTION: [] },
|
||||
] },
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,6 +88,7 @@ class HTMLSelectEventListener final : public nsStubMutationObserver,
|
||||
int32_t aFromIndex, int32_t* aFoundIndex = nullptr) const;
|
||||
|
||||
void ComboboxMightHaveChanged();
|
||||
void OptionValueMightHaveChanged(nsIContent* aMutatingNode);
|
||||
|
||||
~HTMLSelectEventListener();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user