tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mtoolkit/components/find/nsWebBrowserFind.cpp | 56+++++++++++++++++++++++++++++---------------------------
Mtoolkit/components/find/nsWebBrowserFind.h | 4++--
Mtoolkit/content/tests/browser/browser_findbar_hidden_reveal.js | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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); +});