diff --git a/accessible/base/NotificationController.cpp b/accessible/base/NotificationController.cpp index f9fc625c0219..1a31014f9a50 100644 --- a/accessible/base/NotificationController.cpp +++ b/accessible/base/NotificationController.cpp @@ -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", diff --git a/accessible/base/nsAccessibilityService.cpp b/accessible/base/nsAccessibilityService.cpp index 88ed7332b4aa..844af4980d4f 100644 --- a/accessible/base/nsAccessibilityService.cpp +++ b/accessible/base/nsAccessibilityService.cpp @@ -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; diff --git a/accessible/base/nsCoreUtils.cpp b/accessible/base/nsCoreUtils.cpp index de8320f0f459..fa76cc481c9a 100644 --- a/accessible/base/nsCoreUtils.cpp +++ b/accessible/base/nsCoreUtils.cpp @@ -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(); +} diff --git a/accessible/base/nsCoreUtils.h b/accessible/base/nsCoreUtils.h index fe46bf4fdf7d..67a5889834b0 100644 --- a/accessible/base/nsCoreUtils.h +++ b/accessible/base/nsCoreUtils.h @@ -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: + *
a
+ * 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 diff --git a/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js b/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js index 293477d2cc73..5f0a3f676e1d 100644 --- a/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js +++ b/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js @@ -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( - `
a
`, - async function testBeforeHiddenElementAtEnd(browser, docAcc) { - const container = findAccessibleChildByID(docAcc, "container"); - testAccessibleTree(container, { + ` +
a
+
+ c d +
+ `, + 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 } );