commit e987524d34436cacf823d319d71bab33f162ba1c
parent b391bb8e0f50c50f8d5c8712809d2102e8328cc8
Author: Jan-Niklas Jaeschke <jjaschke@mozilla.com>
Date: Fri, 19 Dec 2025 09:07:32 +0000
Bug 2006040, part 2 - Scroll asynchronously also for window.find(). r=emilio
Same as part 1, but for window.find().
Differential Revision: https://phabricator.services.mozilla.com/D276835
Diffstat:
3 files changed, 188 insertions(+), 29 deletions(-)
diff --git a/toolkit/components/find/nsWebBrowserFind.cpp b/toolkit/components/find/nsWebBrowserFind.cpp
@@ -300,23 +300,23 @@ nsWebBrowserFind::SetMatchDiacritics(bool aMatchDiacritics) {
return NS_OK;
}
-void nsWebBrowserFind::SetSelectionAndScroll(nsPIDOMWindowOuter* aWindow,
- nsRange* aRange) {
+already_AddRefed<Selection> nsWebBrowserFind::UpdateSelection(
+ nsPIDOMWindowOuter* aWindow, nsRange* aRange) {
RefPtr<Document> doc = aWindow->GetDoc();
if (!doc) {
- return;
+ return nullptr;
}
PresShell* presShell = doc->GetPresShell();
if (!presShell) {
- return;
+ return nullptr;
}
nsCOMPtr<nsIContent> content =
nsIContent::FromNodeOrNull(aRange->GetStartContainer());
nsIFrame* const frameForStartContainer = content->GetPrimaryFrame();
if (!frameForStartContainer) {
- return;
+ return nullptr;
}
// since the match could be an anonymous textnode inside a
@@ -326,7 +326,7 @@ void nsWebBrowserFind::SetSelectionAndScroll(nsPIDOMWindowOuter* aWindow,
if (!content->IsInNativeAnonymousSubtree()) {
nsIFrame* f = content->GetPrimaryFrame();
if (!f) {
- return;
+ return nullptr;
}
if (f->IsTextInputFrame()) {
tcFrame = f;
@@ -341,7 +341,7 @@ void nsWebBrowserFind::SetSelectionAndScroll(nsPIDOMWindowOuter* aWindow,
RefPtr<Selection> selection =
selCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
if (!selection) {
- return;
+ return nullptr;
}
selection->RemoveAllRanges(IgnoreErrors());
selection->AddRangeAndSelectFramesAndNotifyListeners(*aRange, IgnoreErrors());
@@ -356,18 +356,7 @@ void nsWebBrowserFind::SetSelectionAndScroll(nsPIDOMWindowOuter* aWindow,
nsIFocusManager::FLAG_NOSCROLL, getter_AddRefs(result));
}
}
-
- // Scroll if necessary to make the selection visible:
- // Must be the last thing to do - bug 242056
-
- // After ScrollSelectionIntoView(), the pending notifications might be
- // flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
- // FIXME(emilio): Any reason this couldn't do selection->ScrollIntoView()
- // directly, rather than re-requesting the selection?
- selCon->ScrollSelectionIntoView(
- SelectionType::eNormal, nsISelectionController::SELECTION_WHOLE_SELECTION,
- ScrollAxis(WhereToScroll::Center), ScrollAxis(), ScrollFlags::None,
- SelectionScrollMode::SyncFlush);
+ return selection.forget();
}
nsresult nsWebBrowserFind::SetRangeAroundDocument(nsRange* aSearchRange,
@@ -633,15 +622,28 @@ nsresult nsWebBrowserFind::SearchInFrame(nsPIDOMWindowOuter* aWindow,
if (NS_SUCCEEDED(rv) && foundRange) {
*aDidFind = true;
- // Reveal hidden-until-found and closed details elements for the match.
- // https://html.spec.whatwg.org/#interaction-with-details-and-hidden=until-found
- if (RefPtr startNode = foundRange->GetStartContainer()) {
- startNode->QueueAncestorRevealingAlgorithm();
- }
sel->RemoveAllRanges(IgnoreErrors());
- // Beware! This may flush notifications via synchronous
- // ScrollSelectionIntoView.
- SetSelectionAndScroll(aWindow, foundRange);
+ RefPtr<Selection> scrollSelection = UpdateSelection(aWindow, foundRange);
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "nsWebBrowserFind::RevealAndScroll",
+ [foundRange = RefPtr{foundRange},
+ scrollSelection = RefPtr{scrollSelection}]()
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA {
+ // Reveal hidden-until-found and closed details elements.
+ // https://html.spec.whatwg.org/#interaction-with-details-and-hidden=until-found
+ if (RefPtr startNode = foundRange->GetStartContainer()) {
+ startNode->AncestorRevealingAlgorithm(IgnoreErrors());
+ }
+
+ // Scroll to make the selection visible
+ if (scrollSelection) {
+ scrollSelection->ScrollIntoView(
+ nsISelectionController::SELECTION_WHOLE_SELECTION,
+ ScrollAxis(WhereToScroll::Center), ScrollAxis(),
+ ScrollFlags::None, SelectionScrollMode::SyncFlush);
+ }
+ }));
}
return rv;
diff --git a/toolkit/components/find/nsWebBrowserFind.h b/toolkit/components/find/nsWebBrowserFind.h
@@ -63,8 +63,8 @@ class nsWebBrowserFind : public nsIWebBrowserFind,
MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult OnFind(nsPIDOMWindowOuter* aFoundWindow);
- MOZ_CAN_RUN_SCRIPT_BOUNDARY void SetSelectionAndScroll(
- nsPIDOMWindowOuter* aWindow, nsRange* aRange);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY already_AddRefed<mozilla::dom::Selection>
+ UpdateSelection(nsPIDOMWindowOuter* aWindow, nsRange* aRange);
nsresult GetSearchLimits(nsRange* aSearchRange, nsRange* aStartPt,
nsRange* aEndPt, mozilla::dom::Document* aDoc,
diff --git a/toolkit/content/tests/browser/browser_findbar_hidden_reveal.js b/toolkit/content/tests/browser/browser_findbar_hidden_reveal.js
@@ -159,3 +159,160 @@ add_task(async function test_findbar_reveal_closed_details() {
await BrowserTestUtils.removeTab(tab);
});
+
+add_task(async function test_window_find_reveal_hidden_until_found() {
+ const TEST_PAGE = `data:text/html,
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ body { margin: 0; }
+ .spacer { height: 200vh; }
+ </style>
+ </head>
+ <body>
+ <div class="spacer">Top content</div>
+ <div hidden="until-found">
+ <p id="hidden-target">WindowFindHiddenText</p>
+ </div>
+ <div class="spacer">Bottom content</div>
+ </body>
+ </html>`;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ let browser = tab.linkedBrowser;
+
+ // Verify initial state: element is hidden and page is not scrolled
+ let initialState = await SpecialPowers.spawn(browser, [], () => {
+ let hiddenDiv = content.document.querySelector('[hidden="until-found"]');
+ let target = content.document.getElementById("hidden-target");
+ return {
+ isHidden: hiddenDiv.hidden,
+ scrollY: content.scrollY,
+ targetVisible: target.checkVisibility(),
+ };
+ });
+
+ ok(initialState.isHidden, "Element should initially be hidden");
+ is(initialState.scrollY, 0, "Page should not be scrolled initially");
+ ok(!initialState.targetVisible, "Target should not be visible initially");
+
+ // Set up event listener for the beforematch event and use window.find()
+ let result = await SpecialPowers.spawn(browser, [], () => {
+ return new Promise(resolve => {
+ let hiddenDiv = content.document.querySelector('[hidden="until-found"]');
+ hiddenDiv.addEventListener(
+ "beforematch",
+ () => {
+ // Wait one frame for scroll to complete
+ content.requestAnimationFrame(() => {
+ let target = content.document.getElementById("hidden-target");
+ let parent = target.parentElement;
+ resolve({
+ found: true,
+ hasHiddenAttr: parent.hasAttribute("hidden"),
+ scrollY: content.scrollY,
+ targetVisible: target.checkVisibility(),
+ });
+ });
+ },
+ { once: true }
+ );
+
+ // Use window.find() to search
+ content.find("WindowFindHiddenText");
+ });
+ });
+
+ ok(result.found, "window.find() should find the text");
+ ok(
+ !result.hasHiddenAttr,
+ "Hidden attribute should be removed after window.find()"
+ );
+ Assert.greater(
+ result.scrollY,
+ 0,
+ "Page should be scrolled after window.find()"
+ );
+ ok(result.targetVisible, "Target should be visible after window.find()");
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_window_find_reveal_closed_details() {
+ const TEST_PAGE = `data:text/html,
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ body { margin: 0; }
+ .spacer { height: 200vh; }
+ </style>
+ </head>
+ <body>
+ <div class="spacer">Top content</div>
+ <details id="details-target">
+ <summary>Click to expand</summary>
+ <p id="details-content">WindowFindDetailsText</p>
+ </details>
+ <div class="spacer">Bottom content</div>
+ </body>
+ </html>`;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ let browser = tab.linkedBrowser;
+
+ // Verify initial state: details is closed and page is not scrolled
+ let initialState = await SpecialPowers.spawn(browser, [], () => {
+ let details = content.document.getElementById("details-target");
+ let target = content.document.getElementById("details-content");
+ return {
+ isOpen: details.open,
+ scrollY: content.scrollY,
+ targetVisible: target.checkVisibility(),
+ };
+ });
+
+ ok(!initialState.isOpen, "Details should initially be closed");
+ is(initialState.scrollY, 0, "Page should not be scrolled initially");
+ ok(!initialState.targetVisible, "Target should not be visible initially");
+
+ // Set up event listener for the toggle event and use window.find()
+ let result = await SpecialPowers.spawn(browser, [], () => {
+ return new Promise(resolve => {
+ let details = content.document.getElementById("details-target");
+ details.addEventListener(
+ "toggle",
+ () => {
+ // Wait one frame for scroll to complete
+ content.requestAnimationFrame(() => {
+ let target = content.document.getElementById("details-content");
+ resolve({
+ found: true,
+ isOpen: details.open,
+ scrollY: content.scrollY,
+ targetVisible: target.checkVisibility(),
+ });
+ });
+ },
+ { once: true }
+ );
+
+ // Use window.find() to search
+ content.find("WindowFindDetailsText");
+ });
+ });
+
+ ok(result.found, "window.find() should find the text");
+ ok(result.isOpen, "Details should be opened after window.find()");
+ Assert.greater(
+ result.scrollY,
+ 0,
+ "Page should be scrolled after window.find()"
+ );
+ ok(result.targetVisible, "Target should be visible after window.find()");
+
+ await BrowserTestUtils.removeTab(tab);
+});