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:
Eitan Isaacson
2025-01-23 16:36:57 +00:00
parent b389ad5fbb
commit a23db6bc64
5 changed files with 146 additions and 22 deletions

View File

@@ -275,16 +275,16 @@ const nsDependentSubstring AssociatedElementsIterator::NextID() {
return Substring(mIDs, idStartIdx, mCurrIdx++ - idStartIdx);
}
nsIContent* AssociatedElementsIterator::NextElem() {
dom::Element* AssociatedElementsIterator::NextElem() {
while (true) {
const nsDependentSubstring id = NextID();
if (id.IsEmpty()) break;
nsIContent* refContent = GetElem(id);
dom::Element* refContent = GetElem(id);
if (refContent) return refContent;
}
while (nsIContent* element = mElements.SafeElementAt(mElemIdx++)) {
while (dom::Element* element = mElements.SafeElementAt(mElemIdx++)) {
if (nsCoreUtils::IsDescendantOfAnyShadowIncludingAncestor(element,
mContent)) {
return element;
@@ -317,7 +317,7 @@ dom::Element* AssociatedElementsIterator::GetElem(
}
LocalAccessible* AssociatedElementsIterator::Next() {
nsIContent* nextEl = nullptr;
dom::Element* nextEl = nullptr;
while ((nextEl = NextElem())) {
LocalAccessible* acc = mDoc->GetAccessible(nextEl);
if (acc) {

View File

@@ -225,7 +225,7 @@ class AssociatedElementsIterator : public AccIterable {
/**
* Return next element.
*/
nsIContent* NextElem();
dom::Element* NextElem();
/**
* Return the element with the given ID.

View File

@@ -1018,6 +1018,18 @@ void DocAccessible::ElementStateChanged(dom::Document* aDocument,
new AccStateChangeEvent(accessible, states::READONLY, !isEditable);
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)) {
@@ -2460,9 +2472,24 @@ void DocAccessible::DoARIAOwnsRelocation(LocalAccessible* aOwner) {
nsTArray<RefPtr<LocalAccessible>>* owned =
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);
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);
auto insertIdx = aOwner->ChildCount() - owned->Length() + idx;

View File

@@ -6,6 +6,8 @@
/* import-globals-from ../../mochitest/role.js */
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
/* import-globals-from ../../mochitest/states.js */
loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
requestLongerTimeout(2);
@@ -606,3 +608,95 @@ addAccessibleTask(`<div id='a'></div>`, async function (browser, accDoc) {
});
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"
);
}
);

View File

@@ -4,7 +4,7 @@
"use strict";
async function testComboBox(browser, accDoc, suppressPopupInValueTodo = false) {
async function testComboBox(browser, accDoc) {
const box = getNativeInterface(accDoc, "box");
is(box.getAttributeValue("AXRole"), "AXComboBox");
is(box.getAttributeValue("AXValue"), "peach", "Initial value correct");
@@ -20,20 +20,10 @@ async function testComboBox(browser, accDoc, suppressPopupInValueTodo = false) {
});
await expandedChanged;
if (suppressPopupInValueTodo) {
todo(
!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"
);
ok(!didBoxValueChange, "Value of combobox did not change when it was opened");
is(box.getAttributeValue("AXValue"), "peach", "After popup value correct");
}
}
addAccessibleTask(
`
@@ -74,8 +64,21 @@ addAccessibleTask(
`,
async (browser, accDoc) => {
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);
}
);