Bug 1901459 part 3: Implement ITextRangeProvider::GetChildren. r=nlapre

Differential Revision: https://phabricator.services.mozilla.com/D215759
This commit is contained in:
James Teh 2024-07-09 02:52:16 +00:00
Родитель 2451b5f8bb
Коммит ede1495f62
3 изменённых файлов: 145 добавлений и 6 удалений

Просмотреть файл

@ -970,3 +970,52 @@ addUiaTask(
}
}
);
/**
* Test the TextRange pattern's GetChildren method.
*/
addUiaTask(
`<div id="editable" contenteditable role="textbox">ab <span id="cdef" role="button"><span>cd</span> <a id="ef" href="/">ef</a> </span><img id="g" src="https://example.com/a11y/accessible/tests/mochitest/moz.png" alt="g"></div>`,
async function testTextRangeGetChildren() {
info("Getting editable DocumentRange");
await runPython(`
doc = getDocUia()
editable = findUiaByDomId(doc, "editable")
text = getUiaPattern(editable, "Text")
global r
r = text.DocumentRange
`);
await isUiaElementArray(
`r.GetChildren()`,
["cdef", "g"],
"Children are correct"
);
info("Expanding to word");
await runPython(`r.ExpandToEnclosingUnit(TextUnit_Word)`);
// Range is now "ab ".
await isUiaElementArray(`r.GetChildren()`, [], "Children are correct");
info("Moving 1 word");
await runPython(`r.Move(TextUnit_Word, 1)`);
// Range is now "cd ".
await isUiaElementArray(`r.GetChildren()`, [], "Children are correct");
info("Moving 1 word");
await runPython(`r.Move(TextUnit_Word, 1)`);
// Range is now "ef ". The range includes the link but is not completely
// enclosed by the link.
await isUiaElementArray(`r.GetChildren()`, ["ef"], "Children are correct");
info("Moving end -1 character");
await runPython(
`r.MoveEndpointByUnit(TextPatternRangeEndpoint_End, TextUnit_Character, -1)`
);
// Range is now "ef". The range encloses the link, so there are no children.
await isUiaElementArray(`r.GetChildren()`, [], "Children are correct");
info("Moving 1 word");
await runPython(`r.Move(TextUnit_Word, 1)`);
// Range is now the embedded object character for the img (g). The range is
// completely enclosed by the image.
// The IA2 -> UIA proxy gets this wrong.
if (gIsUiaEnabled) {
await isUiaElementArray(`r.GetChildren()`, [], "Children are correct");
}
}
);

Просмотреть файл

@ -58,6 +58,36 @@ static void RemoveExcludedAccessiblesFromRange(TextLeafRange& aRange) {
}
}
static bool IsUiaEmbeddedObject(const Accessible* aAcc) {
// "For UI Automation, an embedded object is any element that has non-textual
// boundaries such as an image, hyperlink, table, or document type"
// https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-textpattern-and-embedded-objects-overview
if (aAcc->IsText()) {
return false;
}
switch (aAcc->Role()) {
case roles::CONTENT_DELETION:
case roles::CONTENT_INSERTION:
case roles::EMPHASIS:
case roles::LANDMARK:
case roles::MARK:
case roles::NAVIGATION:
case roles::NOTE:
case roles::PARAGRAPH:
case roles::REGION:
case roles::SECTION:
case roles::STRONG:
case roles::SUBSCRIPT:
case roles::SUPERSCRIPT:
case roles::TEXT:
case roles::TEXT_CONTAINER:
return false;
default:
break;
}
return true;
}
// UiaTextRange
UiaTextRange::UiaTextRange(TextLeafRange& aRange) {
@ -454,6 +484,68 @@ UiaTextRange::GetChildren(__RPC__deref_out_opt SAFEARRAY** aRetVal) {
return E_INVALIDARG;
}
*aRetVal = nullptr;
TextLeafRange range = GetRange();
if (!range) {
return CO_E_OBJNOTCONNECTED;
}
RemoveExcludedAccessiblesFromRange(range);
Accessible* startAcc = range.Start().mAcc;
Accessible* endAcc = range.End().mAcc;
Accessible* common = startAcc->GetClosestCommonInclusiveAncestor(endAcc);
if (!common) {
return S_OK;
}
// Get all the direct children of `common` from `startAcc` through `endAcc`.
// Find the index of the direct child containing startAcc.
int32_t startIndex = -1;
if (startAcc == common) {
startIndex = 0;
} else {
Accessible* child = startAcc;
for (;;) {
Accessible* parent = child->Parent();
if (parent == common) {
startIndex = child->IndexInParent();
break;
}
child = parent;
}
MOZ_ASSERT(startIndex >= 0);
}
// Find the index of the direct child containing endAcc.
int32_t endIndex = -1;
if (endAcc == common) {
endIndex = static_cast<int32_t>(common->ChildCount()) - 1;
} else {
Accessible* child = endAcc;
for (;;) {
Accessible* parent = child->Parent();
if (parent == common) {
endIndex = child->IndexInParent();
break;
}
child = parent;
}
MOZ_ASSERT(endIndex >= 0);
}
// Now get the children between startIndex and endIndex.
// We guess 30 children because:
// 1. It's unlikely that a client would call GetChildren on a very large range
// because GetChildren is normally only called when reporting content and
// reporting the entire content of a massive range in one hit isn't ideal for
// performance.
// 2. A client is more likely to query the content of a line, paragraph, etc.
// 3. It seems unlikely that there would be more than 30 children in a line or
// paragraph, especially because we're only including children that are
// considered embedded objects by UIA.
AutoTArray<Accessible*, 30> children;
for (int32_t i = startIndex; i <= endIndex; ++i) {
Accessible* child = common->ChildAt(static_cast<uint32_t>(i));
if (IsUiaEmbeddedObject(child)) {
children.AppendElement(child);
}
}
*aRetVal = AccessibleArrayToUiaArray(children);
return S_OK;
}

Просмотреть файл

@ -1458,12 +1458,10 @@ long uiaRawElmProvider::GetLiveSetting() const {
}
SAFEARRAY* a11y::AccessibleArrayToUiaArray(const nsTArray<Accessible*>& aAccs) {
if (aAccs.IsEmpty()) {
// The UIA documentation is unclear about this, but the UIA client
// framework seems to treat a null value the same as an empty array. This
// is also what Chromium does.
return nullptr;
}
// The UIA client framework seems to treat a null value the same as an empty
// array most of the time, but not always. In particular, Narrator breaks if
// ITextRangeProvider::GetChildren returns null instead of an empty array.
// Therefore, don't return null for an empty array.
SAFEARRAY* uias = SafeArrayCreateVector(VT_UNKNOWN, 0, aAccs.Length());
LONG indices[1] = {0};
for (Accessible* acc : aAccs) {