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) { if (textAcc) {
// Remove the TextLeafAccessible if: // Remove the TextLeafAccessible if:
// 1. The rendered text is empty; or // 1. The rendered text is empty; or
// 2. The text is just a space, but its layout frame has a width of 0, // 2. The text is invisible, semantically irrelevant whitespace before a
// so it isn't visible. This can happen if there is whitespace before an // hard line break.
// invisible element at the end of a block.
if (text.mString.IsEmpty() || if (text.mString.IsEmpty() ||
(text.mString.EqualsLiteral(" ") && textFrame->GetRect().IsEmpty())) { nsCoreUtils::IsTrimmedWhitespaceBeforeHardLineBreak(textFrame)) {
#ifdef A11Y_LOG #ifdef A11Y_LOG
if (logging::IsEnabled(logging::eTree | logging::eText)) { if (logging::IsEnabled(logging::eTree | logging::eText)) {
logging::MsgBegin("TREE", "text node lost its content; doc: %p", 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 // Ignore not rendered text nodes and whitespace text nodes between table
// cells. // cells.
if (text.mString.IsEmpty() || if (text.mString.IsEmpty() ||
(text.mString.EqualsLiteral(" ") && frame->GetRect().IsEmpty()) || nsCoreUtils::IsTrimmedWhitespaceBeforeHardLineBreak(frame) ||
(aContext->IsTableRow() && (aContext->IsTableRow() &&
nsCoreUtils::IsWhitespaceString(text.mString))) { nsCoreUtils::IsWhitespaceString(text.mString))) {
if (aIsSubtreeHidden) *aIsSubtreeHidden = true; if (aIsSubtreeHidden) *aIsSubtreeHidden = true;

View File

@@ -661,3 +661,19 @@ Element* nsCoreUtils::GetAriaActiveDescendantElement(Element* aElement) {
return nullptr; 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); nsINode* aStartAncestor);
static Element* GetAriaActiveDescendantElement(Element* aElement); 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 #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( addAccessibleTask(
`<div id="container"><span>a</span> <span id="b" hidden>b</span></div>`, `
async function testBeforeHiddenElementAtEnd(browser, docAcc) { <div id="hardContainer"><span>a</span> <span id="b" hidden>b</span></div>
const container = findAccessibleChildByID(docAcc, "container"); <div id="softContainer" style="width: 1ch; font-family: monospace;">
testAccessibleTree(container, { <span>c</span> <span>d</span>
</div>
`,
async function testBeforeLineBreaks(browser, docAcc) {
const hardContainer = findAccessibleChildByID(docAcc, "hardContainer");
testAccessibleTree(hardContainer, {
role: ROLE_SECTION, role: ROLE_SECTION,
children: [{ role: ROLE_TEXT_LEAF, name: "a" }], children: [{ role: ROLE_TEXT_LEAF, name: "a" }],
}); });
info("Showing b"); info("Showing b");
let reordered = waitForEvent(EVENT_REORDER, container); let reordered = waitForEvent(EVENT_REORDER, hardContainer);
await invokeContentTask(browser, [], () => { await invokeContentTask(browser, [], () => {
content.document.getElementById("b").hidden = false; content.document.getElementById("b").hidden = false;
}); });
await reordered; await reordered;
testAccessibleTree(container, { testAccessibleTree(hardContainer, {
role: ROLE_SECTION, role: ROLE_SECTION,
children: [ children: [
{ role: ROLE_TEXT_LEAF, name: "a" }, { role: ROLE_TEXT_LEAF, name: "a" },
@@ -96,15 +101,25 @@ addAccessibleTask(
}); });
info("Hiding b"); info("Hiding b");
reordered = waitForEvent(EVENT_REORDER, container); reordered = waitForEvent(EVENT_REORDER, hardContainer);
await invokeContentTask(browser, [], () => { await invokeContentTask(browser, [], () => {
content.document.getElementById("b").hidden = true; content.document.getElementById("b").hidden = true;
}); });
await reordered; await reordered;
testAccessibleTree(container, { testAccessibleTree(hardContainer, {
role: ROLE_SECTION, role: ROLE_SECTION,
children: [{ role: ROLE_TEXT_LEAF, name: "a" }], 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 } { chrome: true, topLevel: true }
); );