Bug 1869966 part 3: When calculating a DOM point from a TextLeafPoint for a non-text node, use the child index within the parent, not an offset within the node itself. r=eeejay

For text nodes, DOM points use the character offset within the node itself, just like accessibility does.
For non-text nodes, DOM points use the child index within the parent to specify the position before or after the node.
Previously, TextLeafPoint::ToDOMPoint always used an offset within the node itself.
This meant that when the (exclusive) end point of a range was an image, we converted to a DOM point of offset 0 within the image.
As far as DOM is concerned, this includes the image.
This patch adjusts ToDOMPoint as appropriate.

As part of this, ToDOMPoint now returns a uint32_t instead of an int32_t.
This is because DOM offsets are actually uint32_t and calculating int32_t child indices is deprecated.

Differential Revision: https://phabricator.services.mozilla.com/D247873
This commit is contained in:
James Teh
2025-05-06 22:13:58 +00:00
committed by jteh@mozilla.com
parent 5c140ae138
commit 123bc5b115
3 changed files with 47 additions and 21 deletions

View File

@@ -536,7 +536,7 @@ static dom::Selection* GetDOMSelection(const nsIContent* aStartContent,
return startFrameSel ? &startFrameSel->NormalSelection() : nullptr; return startFrameSel ? &startFrameSel->NormalSelection() : nullptr;
} }
std::pair<nsIContent*, int32_t> TextLeafPoint::ToDOMPoint( std::pair<nsIContent*, uint32_t> TextLeafPoint::ToDOMPoint(
bool aIncludeGenerated) const { bool aIncludeGenerated) const {
if (!(*this) || !mAcc->IsLocal()) { if (!(*this) || !mAcc->IsLocal()) {
MOZ_ASSERT_UNREACHABLE("Invalid point"); MOZ_ASSERT_UNREACHABLE("Invalid point");
@@ -573,26 +573,37 @@ std::pair<nsIContent*, int32_t> TextLeafPoint::ToDOMPoint(
} }
} }
if (!mAcc->IsTextLeaf() && !mAcc->IsHTMLBr() && !mAcc->HasChildren()) { if (mAcc->IsTextLeaf()) {
// If this is not a text leaf it can be an empty editable container, // For text nodes, DOM uses a character offset within the node.
// whitespace, or an empty doc. In any case, the offset inside should be 0. return {content, RenderedToContentOffset(mAcc->AsLocal(), mOffset)};
MOZ_ASSERT(mOffset == 0);
if (RefPtr<TextControlElement> textControlElement =
TextControlElement::FromNodeOrNull(content)) {
// This is an empty input, use the shadow root's element.
if (RefPtr<TextEditor> textEditor = textControlElement->GetTextEditor()) {
if (textEditor->IsEmpty()) {
MOZ_ASSERT(mOffset == 0);
return {textEditor->GetRoot(), 0};
}
}
}
return {content, 0};
} }
return {content, RenderedToContentOffset(mAcc->AsLocal(), mOffset)}; if (!mAcc->IsHyperText()) {
// For non-text nodes (e.g. images), DOM points use the child index within
// the parent.
nsIContent* parent = content->GetParent();
MOZ_ASSERT(parent);
auto childIndex = parent->ComputeIndexOf(content);
MOZ_ASSERT(childIndex);
return {parent, *childIndex};
}
// This could be an empty editable container, whitespace or an empty doc. In
// any case, the offset inside should be 0.
MOZ_ASSERT(mOffset == 0);
if (RefPtr<TextControlElement> textControlElement =
TextControlElement::FromNodeOrNull(content)) {
// This is an empty input, use the shadow root's element.
if (RefPtr<TextEditor> textEditor = textControlElement->GetTextEditor()) {
if (textEditor->IsEmpty()) {
MOZ_ASSERT(mOffset == 0);
return {textEditor->GetRoot(), 0};
}
}
}
return {content, 0};
} }
static bool IsLineBreakContinuation(nsTextFrame* aContinuation) { static bool IsLineBreakContinuation(nsTextFrame* aContinuation) {

View File

@@ -188,7 +188,7 @@ class TextLeafPoint final {
/** /**
* Translate given TextLeafPoint into a DOM point. * Translate given TextLeafPoint into a DOM point.
*/ */
MOZ_CAN_RUN_SCRIPT std::pair<nsIContent*, int32_t> ToDOMPoint( MOZ_CAN_RUN_SCRIPT std::pair<nsIContent*, uint32_t> ToDOMPoint(
bool aIncludeGenerated = true) const; bool aIncludeGenerated = true) const;
private: private:

View File

@@ -21,7 +21,7 @@ function checkSelection(root, ranges) {
* Test IAccessibleTextSelectionContainer::setSelections. * Test IAccessibleTextSelectionContainer::setSelections.
*/ */
addAccessibleTask( addAccessibleTask(
`<p id="p">ab<a id="link" href="/">cd</a>ef</p>`, `<p id="p">ab<a id="link" href="/">cd</a>ef<img id="img" src="https://example.com/a11y/accessible/tests/mochitest/moz.png" alt="g"></p>`,
async function testSetSelections(browser, docAcc) { async function testSetSelections(browser, docAcc) {
docAcc.QueryInterface(nsIAccessibleText); docAcc.QueryInterface(nsIAccessibleText);
await runPython(` await runPython(`
@@ -66,6 +66,21 @@ addAccessibleTask(
await selected; await selected;
checkSelection(docAcc, [[link, 1, p, 4]]); checkSelection(docAcc, [[link, 1, p, 4]]);
info("Selecting f");
selected = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, p);
await runPython(`
docSel.setSelections(1, byref(IA2TextSelection(p, 4, p, 5, False)))
`);
await selected;
checkSelection(docAcc, [[p, 4, p, 5]]);
// DOM treats an end point of (img, 0) as including the image. Ensure we
// used a DOM child index.
await invokeContentTask(browser, [], () => {
const sel = content.getSelection();
is(sel.focusNode.id, "p", "DOM selection focus node correct");
is(sel.focusOffset, 3, "DOM selection focus offset correct");
});
info("Selecting a, c"); info("Selecting a, c");
selected = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, link); selected = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, link);
await runPython(` await runPython(`