Bug 1951720: Don't exclude 0 width whitespace text nodes from the accessibility tree if they're at the end of a wrapped line. r=morgan

In bug 1951067, I pruned all 0 width whitespace text nodes from the accessibility tree.
However, it turns out that whitespace text nodes at the end of wrapped lines can also be 0 width.
These have semantic importance, since otherwise, words in separate inline nodes can be merged together without a space.
To fix this, explicitly check that the node is before a hard line break, not a soft (wrapped) line break.

Differential Revision: https://phabricator.services.mozilla.com/D242290
This commit is contained in:
James Teh
2025-03-21 02:15:18 +00:00
parent 28a04d1c5f
commit 510cf177bc
5 changed files with 55 additions and 14 deletions

View File

@@ -823,11 +823,10 @@ void NotificationController::WillRefresh(mozilla::TimeStamp aTime) {
if (textAcc) {
// Remove the TextLeafAccessible if:
// 1. The rendered text is empty; or
// 2. The text is just a space, but its layout frame has a width of 0,
// so it isn't visible. This can happen if there is whitespace before an
// invisible element at the end of a block.
// 2. The text is invisible, semantically irrelevant whitespace before a
// hard line break.
if (text.mString.IsEmpty() ||
(text.mString.EqualsLiteral(" ") && textFrame->GetRect().IsEmpty())) {
nsCoreUtils::IsTrimmedWhitespaceBeforeHardLineBreak(textFrame)) {
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eTree | logging::eText)) {
logging::MsgBegin("TREE", "text node lost its content; doc: %p",

View File

@@ -1306,7 +1306,7 @@ LocalAccessible* nsAccessibilityService::CreateAccessible(
// Ignore not rendered text nodes and whitespace text nodes between table
// cells.
if (text.mString.IsEmpty() ||
(text.mString.EqualsLiteral(" ") && frame->GetRect().IsEmpty()) ||
nsCoreUtils::IsTrimmedWhitespaceBeforeHardLineBreak(frame) ||
(aContext->IsTableRow() &&
nsCoreUtils::IsWhitespaceString(text.mString))) {
if (aIsSubtreeHidden) *aIsSubtreeHidden = true;

View File

@@ -661,3 +661,19 @@ Element* nsCoreUtils::GetAriaActiveDescendantElement(Element* aElement) {
return nullptr;
}
bool nsCoreUtils::IsTrimmedWhitespaceBeforeHardLineBreak(nsIFrame* aFrame) {
if (!aFrame->GetRect().IsEmpty() ||
!aFrame->HasAnyStateBits(TEXT_END_OF_LINE)) {
return false;
}
// Normally, accessibility calls nsIFrame::GetRenderedText with
// TrailingWhitespace::NoTrim. Using TrailingWhitespace::Trim instead trims 0
// width whitespace before a hard line break, resulting in an empty string if
// that is all the frame contains. Note that TrailingWhitespace::Trim does
// *not* trim whitespace before a soft line break (wrapped line).
nsIFrame::RenderedText text = aFrame->GetRenderedText(
0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText,
nsIFrame::TrailingWhitespace::Trim);
return text.mString.IsEmpty();
}

View File

@@ -336,6 +336,17 @@ class nsCoreUtils {
nsINode* aStartAncestor);
static Element* GetAriaActiveDescendantElement(Element* aElement);
/**
* Return true if the given text frame is 0 width whitespace before a hard
* line break. This is not visible and is semantically irrelevant. This can
* happen if there is whitespace before an invisible element at the end of a
* block. For example:
* <div><span>a</span> <span hidden>b</span></div>
* This results in a text node for "a" and a text node for " ". This function
* will return true for the latter node.
*/
static bool IsTrimmedWhitespaceBeforeHardLineBreak(nsIFrame* aFrame);
};
#endif

View File

@@ -69,24 +69,29 @@ addAccessibleTask(
);
/**
* Test whitespace before a hidden element at the end of a block.
* Test whitespace before hard and soft line breaks.
*/
addAccessibleTask(
`<div id="container"><span>a</span> <span id="b" hidden>b</span></div>`,
async function testBeforeHiddenElementAtEnd(browser, docAcc) {
const container = findAccessibleChildByID(docAcc, "container");
testAccessibleTree(container, {
`
<div id="hardContainer"><span>a</span> <span id="b" hidden>b</span></div>
<div id="softContainer" style="width: 1ch; font-family: monospace;">
<span>c</span> <span>d</span>
</div>
`,
async function testBeforeLineBreaks(browser, docAcc) {
const hardContainer = findAccessibleChildByID(docAcc, "hardContainer");
testAccessibleTree(hardContainer, {
role: ROLE_SECTION,
children: [{ role: ROLE_TEXT_LEAF, name: "a" }],
});
info("Showing b");
let reordered = waitForEvent(EVENT_REORDER, container);
let reordered = waitForEvent(EVENT_REORDER, hardContainer);
await invokeContentTask(browser, [], () => {
content.document.getElementById("b").hidden = false;
});
await reordered;
testAccessibleTree(container, {
testAccessibleTree(hardContainer, {
role: ROLE_SECTION,
children: [
{ role: ROLE_TEXT_LEAF, name: "a" },
@@ -96,15 +101,25 @@ addAccessibleTask(
});
info("Hiding b");
reordered = waitForEvent(EVENT_REORDER, container);
reordered = waitForEvent(EVENT_REORDER, hardContainer);
await invokeContentTask(browser, [], () => {
content.document.getElementById("b").hidden = true;
});
await reordered;
testAccessibleTree(container, {
testAccessibleTree(hardContainer, {
role: ROLE_SECTION,
children: [{ role: ROLE_TEXT_LEAF, name: "a" }],
});
const softContainer = findAccessibleChildByID(docAcc, "softContainer");
testAccessibleTree(softContainer, {
role: ROLE_SECTION,
children: [
{ role: ROLE_TEXT_LEAF, name: "c" },
{ role: ROLE_TEXT_LEAF, name: " " },
{ role: ROLE_TEXT_LEAF, name: "d" },
],
});
},
{ chrome: true, topLevel: true }
);