Bug 1280188 - Don't relocate children in or out of editable content. r=nlapre
Depends on D234768 Differential Revision: https://phabricator.services.mozilla.com/D234769
This commit is contained in:
@@ -275,16 +275,16 @@ const nsDependentSubstring AssociatedElementsIterator::NextID() {
|
|||||||
return Substring(mIDs, idStartIdx, mCurrIdx++ - idStartIdx);
|
return Substring(mIDs, idStartIdx, mCurrIdx++ - idStartIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
nsIContent* AssociatedElementsIterator::NextElem() {
|
dom::Element* AssociatedElementsIterator::NextElem() {
|
||||||
while (true) {
|
while (true) {
|
||||||
const nsDependentSubstring id = NextID();
|
const nsDependentSubstring id = NextID();
|
||||||
if (id.IsEmpty()) break;
|
if (id.IsEmpty()) break;
|
||||||
|
|
||||||
nsIContent* refContent = GetElem(id);
|
dom::Element* refContent = GetElem(id);
|
||||||
if (refContent) return refContent;
|
if (refContent) return refContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (nsIContent* element = mElements.SafeElementAt(mElemIdx++)) {
|
while (dom::Element* element = mElements.SafeElementAt(mElemIdx++)) {
|
||||||
if (nsCoreUtils::IsDescendantOfAnyShadowIncludingAncestor(element,
|
if (nsCoreUtils::IsDescendantOfAnyShadowIncludingAncestor(element,
|
||||||
mContent)) {
|
mContent)) {
|
||||||
return element;
|
return element;
|
||||||
@@ -317,7 +317,7 @@ dom::Element* AssociatedElementsIterator::GetElem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LocalAccessible* AssociatedElementsIterator::Next() {
|
LocalAccessible* AssociatedElementsIterator::Next() {
|
||||||
nsIContent* nextEl = nullptr;
|
dom::Element* nextEl = nullptr;
|
||||||
while ((nextEl = NextElem())) {
|
while ((nextEl = NextElem())) {
|
||||||
LocalAccessible* acc = mDoc->GetAccessible(nextEl);
|
LocalAccessible* acc = mDoc->GetAccessible(nextEl);
|
||||||
if (acc) {
|
if (acc) {
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ class AssociatedElementsIterator : public AccIterable {
|
|||||||
/**
|
/**
|
||||||
* Return next element.
|
* Return next element.
|
||||||
*/
|
*/
|
||||||
nsIContent* NextElem();
|
dom::Element* NextElem();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the element with the given ID.
|
* Return the element with the given ID.
|
||||||
|
|||||||
@@ -1018,6 +1018,18 @@ void DocAccessible::ElementStateChanged(dom::Document* aDocument,
|
|||||||
new AccStateChangeEvent(accessible, states::READONLY, !isEditable);
|
new AccStateChangeEvent(accessible, states::READONLY, !isEditable);
|
||||||
FireDelayedEvent(event);
|
FireDelayedEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (aElement->HasAttr(nsGkAtoms::aria_owns)) {
|
||||||
|
// If this has aria-owns, update children that are relocated into here.
|
||||||
|
// If we are becoming editable, put them back into their original
|
||||||
|
// containers, if we are becoming readonly, acquire them.
|
||||||
|
mNotificationController->ScheduleRelocation(accessible);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a node inside of a newly editable subtree, it needs to be
|
||||||
|
// un-aria-owned. And inversely, if the node becomes uneditable, allow the
|
||||||
|
// node to be aria-owned.
|
||||||
|
RelocateARIAOwnedIfNeeded(aElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aStateMask.HasState(dom::ElementState::CHECKED)) {
|
if (aStateMask.HasState(dom::ElementState::CHECKED)) {
|
||||||
@@ -2460,9 +2472,24 @@ void DocAccessible::DoARIAOwnsRelocation(LocalAccessible* aOwner) {
|
|||||||
nsTArray<RefPtr<LocalAccessible>>* owned =
|
nsTArray<RefPtr<LocalAccessible>>* owned =
|
||||||
mARIAOwnsHash.GetOrInsertNew(aOwner);
|
mARIAOwnsHash.GetOrInsertNew(aOwner);
|
||||||
|
|
||||||
|
if (aOwner->Elm()->State().HasState(dom::ElementState::READWRITE)) {
|
||||||
|
// The container is editable.
|
||||||
|
PutChildrenBack(owned, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AssociatedElementsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns);
|
AssociatedElementsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns);
|
||||||
uint32_t idx = 0;
|
uint32_t idx = 0;
|
||||||
while (nsIContent* childEl = iter.NextElem()) {
|
while (dom::Element* childEl = iter.NextElem()) {
|
||||||
|
if (childEl->State().HasState(dom::ElementState::READWRITE)) {
|
||||||
|
nsINode* parentEl = childEl->GetFlattenedTreeParentNode();
|
||||||
|
if (parentEl->IsElement() && parentEl->AsElement()->State().HasState(
|
||||||
|
dom::ElementState::READWRITE)) {
|
||||||
|
// The child is inside of an editable subtree, don't relocate it.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LocalAccessible* child = GetAccessible(childEl);
|
LocalAccessible* child = GetAccessible(childEl);
|
||||||
auto insertIdx = aOwner->ChildCount() - owned->Length() + idx;
|
auto insertIdx = aOwner->ChildCount() - owned->Length() + idx;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
/* import-globals-from ../../mochitest/role.js */
|
/* import-globals-from ../../mochitest/role.js */
|
||||||
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
|
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
|
||||||
|
/* import-globals-from ../../mochitest/states.js */
|
||||||
|
loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
|
||||||
|
|
||||||
requestLongerTimeout(2);
|
requestLongerTimeout(2);
|
||||||
|
|
||||||
@@ -606,3 +608,95 @@ addAccessibleTask(`<div id='a'></div>`, async function (browser, accDoc) {
|
|||||||
});
|
});
|
||||||
is(getAccessibleDOMNodeID(a.firstChild), "b", "'a' owns relocated child");
|
is(getAccessibleDOMNodeID(a.firstChild), "b", "'a' owns relocated child");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Test to assure that aria-owned elements are not relocated into an editable subtree.
|
||||||
|
*/
|
||||||
|
addAccessibleTask(
|
||||||
|
`
|
||||||
|
<button id="btn">World</button>
|
||||||
|
<div contentEditable="true" id="textbox" role="textbox">
|
||||||
|
<p id="p" aria-owns="btn">Hello</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
async function (browser, accDoc) {
|
||||||
|
const p = findAccessibleChildByID(accDoc, "p");
|
||||||
|
const textbox = findAccessibleChildByID(accDoc, "textbox");
|
||||||
|
testStates(textbox, 0, EXT_STATE_EDITABLE, 0, 0);
|
||||||
|
|
||||||
|
isnot(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn");
|
||||||
|
is(textbox.value, "Hello");
|
||||||
|
|
||||||
|
let expectedEvents = Promise.all([
|
||||||
|
waitForStateChange(textbox, EXT_STATE_EDITABLE, false, true),
|
||||||
|
waitForEvent(EVENT_INNER_REORDER, p),
|
||||||
|
]);
|
||||||
|
await invokeContentTask(browser, [], () => {
|
||||||
|
content.document.getElementById("textbox").contentEditable = false;
|
||||||
|
});
|
||||||
|
await expectedEvents;
|
||||||
|
is(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn");
|
||||||
|
is(textbox.value, "Hello World");
|
||||||
|
|
||||||
|
expectedEvents = Promise.all([
|
||||||
|
waitForStateChange(textbox, EXT_STATE_EDITABLE, true, true),
|
||||||
|
waitForEvent(EVENT_INNER_REORDER, p),
|
||||||
|
]);
|
||||||
|
await invokeContentTask(browser, [], () => {
|
||||||
|
content.document.getElementById("textbox").contentEditable = true;
|
||||||
|
});
|
||||||
|
await expectedEvents;
|
||||||
|
isnot(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn");
|
||||||
|
is(textbox.value, "Hello");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Test to ensure that aria-owned elements are not relocated out of editable subtree.
|
||||||
|
*/
|
||||||
|
addAccessibleTask(
|
||||||
|
`
|
||||||
|
<div contentEditable="true" id="textbox" role="textbox">
|
||||||
|
<button id="btn">World</button>
|
||||||
|
</div>
|
||||||
|
<p id="p" aria-owns="btn">Hello</p>
|
||||||
|
<p id="p2" aria-owns="textbox"></p>
|
||||||
|
`,
|
||||||
|
async function (browser, accDoc) {
|
||||||
|
const p = findAccessibleChildByID(accDoc, "p");
|
||||||
|
const textbox = findAccessibleChildByID(accDoc, "textbox");
|
||||||
|
testStates(textbox, 0, EXT_STATE_EDITABLE, 0, 0);
|
||||||
|
|
||||||
|
is(
|
||||||
|
getAccessibleDOMNodeID(textbox.parent),
|
||||||
|
"p2",
|
||||||
|
"editable root can be relocated"
|
||||||
|
);
|
||||||
|
isnot(
|
||||||
|
getAccessibleDOMNodeID(p.lastChild),
|
||||||
|
"btn",
|
||||||
|
"editable element cannot be relocated"
|
||||||
|
);
|
||||||
|
is(textbox.value, "World");
|
||||||
|
|
||||||
|
let expectedEvents = Promise.all([
|
||||||
|
waitForStateChange(textbox, EXT_STATE_EDITABLE, false, true),
|
||||||
|
waitForEvent(EVENT_REORDER, p),
|
||||||
|
]);
|
||||||
|
await invokeContentTask(browser, [], () => {
|
||||||
|
content.document.getElementById("textbox").contentEditable = false;
|
||||||
|
});
|
||||||
|
await expectedEvents;
|
||||||
|
is(
|
||||||
|
getAccessibleDOMNodeID(p.lastChild),
|
||||||
|
"btn",
|
||||||
|
"'p' owns readonly relocated btn"
|
||||||
|
);
|
||||||
|
is(textbox.value, "");
|
||||||
|
is(
|
||||||
|
getAccessibleDOMNodeID(textbox.parent),
|
||||||
|
"p2",
|
||||||
|
"textbox is still relocated"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
async function testComboBox(browser, accDoc, suppressPopupInValueTodo = false) {
|
async function testComboBox(browser, accDoc) {
|
||||||
const box = getNativeInterface(accDoc, "box");
|
const box = getNativeInterface(accDoc, "box");
|
||||||
is(box.getAttributeValue("AXRole"), "AXComboBox");
|
is(box.getAttributeValue("AXRole"), "AXComboBox");
|
||||||
is(box.getAttributeValue("AXValue"), "peach", "Initial value correct");
|
is(box.getAttributeValue("AXValue"), "peach", "Initial value correct");
|
||||||
@@ -20,19 +20,9 @@ async function testComboBox(browser, accDoc, suppressPopupInValueTodo = false) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expandedChanged;
|
await expandedChanged;
|
||||||
if (suppressPopupInValueTodo) {
|
|
||||||
todo(
|
ok(!didBoxValueChange, "Value of combobox did not change when it was opened");
|
||||||
!didBoxValueChange,
|
|
||||||
"Value of combobox did not change when it was opened"
|
|
||||||
);
|
|
||||||
todo_is(box.getAttributeValue("AXValue"), "peach");
|
|
||||||
} else {
|
|
||||||
ok(
|
|
||||||
!didBoxValueChange,
|
|
||||||
"Value of combobox did not change when it was opened"
|
|
||||||
);
|
|
||||||
is(box.getAttributeValue("AXValue"), "peach", "After popup value correct");
|
is(box.getAttributeValue("AXValue"), "peach", "After popup value correct");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addAccessibleTask(
|
addAccessibleTask(
|
||||||
@@ -74,8 +64,21 @@ addAccessibleTask(
|
|||||||
`,
|
`,
|
||||||
async (browser, accDoc) => {
|
async (browser, accDoc) => {
|
||||||
info("Test ARIA 1.0 style combobox (entry aria-owns list)");
|
info("Test ARIA 1.0 style combobox (entry aria-owns list)");
|
||||||
// XXX: Bug 1912520
|
|
||||||
await testComboBox(browser, accDoc, true);
|
const box = getNativeInterface(accDoc, "box");
|
||||||
|
is(
|
||||||
|
box.getAttributeValue("AXChildren").length,
|
||||||
|
1,
|
||||||
|
"owned list is not relocated"
|
||||||
|
);
|
||||||
|
// XXX: Bug 1912520 - Remap from aria-owns to aria-controls
|
||||||
|
todo_is(
|
||||||
|
box.getAttributeValue("AXARIAControls").length,
|
||||||
|
1,
|
||||||
|
"box controls list"
|
||||||
|
);
|
||||||
|
|
||||||
|
await testComboBox(browser, accDoc);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user