diff --git a/browser/actors/ClickHandlerChild.jsm b/browser/actors/ClickHandlerChild.jsm index 4f5c2b0b7ea1..3c33deddd248 100644 --- a/browser/actors/ClickHandlerChild.jsm +++ b/browser/actors/ClickHandlerChild.jsm @@ -23,6 +23,12 @@ ChromeUtils.defineModuleGetter( "resource://gre/modules/E10SUtils.jsm" ); +ChromeUtils.defineModuleGetter( + this, + "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm" +); + class ClickHandlerChild extends JSWindowActorChild { handleEvent(event) { if ( @@ -59,7 +65,9 @@ class ClickHandlerChild extends JSWindowActorChild { } } - let [href, node, principal] = this._hrefAndLinkNodeForClickEvent(event); + let [href, node, principal] = BrowserUtils.hrefAndLinkNodeForClickEvent( + event + ); let csp = ownerDoc.csp; if (csp) { @@ -133,65 +141,4 @@ class ClickHandlerChild extends JSWindowActorChild { this.sendAsyncMessage("Content:Click", json); } } - - /** - * Extracts linkNode and href for the current click target. - * - * @param event - * The click event. - * @return [href, linkNode, linkPrincipal]. - * - * @note linkNode will be null if the click wasn't on an anchor - * element. This includes SVG links, because callers expect |node| - * to behave like an element, which SVG links (XLink) don't. - */ - _hrefAndLinkNodeForClickEvent(event) { - let content = this.contentWindow; - function isHTMLLink(aNode) { - // Be consistent with what nsContextMenu.js does. - return ( - (aNode instanceof content.HTMLAnchorElement && aNode.href) || - (aNode instanceof content.HTMLAreaElement && aNode.href) || - aNode instanceof content.HTMLLinkElement - ); - } - - let node = event.composedTarget; - while (node && !isHTMLLink(node)) { - node = node.flattenedTreeParentNode; - } - - if (node) { - return [node.href, node, node.ownerDocument.nodePrincipal]; - } - - // If there is no linkNode, try simple XLink. - let href, baseURI; - node = event.composedTarget; - while (node && !href) { - if ( - node.nodeType == content.Node.ELEMENT_NODE && - (node.localName == "a" || - node.namespaceURI == "http://www.w3.org/1998/Math/MathML") - ) { - href = - node.getAttribute("href") || - node.getAttributeNS("http://www.w3.org/1999/xlink", "href"); - if (href) { - baseURI = node.ownerDocument.baseURIObject; - break; - } - } - node = node.flattenedTreeParentNode; - } - - // In case of XLink, we don't return the node we got href from since - // callers expect -like elements. - // Note: makeURI() will throw if aUri is not a valid URI. - return [ - href ? Services.io.newURI(href, null, baseURI).spec : null, - null, - node && node.ownerDocument.nodePrincipal, - ]; - } } diff --git a/browser/actors/ClickHandlerParent.jsm b/browser/actors/ClickHandlerParent.jsm index 0f658fccf7b8..89363074ed14 100644 --- a/browser/actors/ClickHandlerParent.jsm +++ b/browser/actors/ClickHandlerParent.jsm @@ -55,6 +55,11 @@ class ClickHandlerParent extends JSWindowActorParent { // This is heavily based on contentAreaClick from browser.js (Bug 903016) // The data is set up in a way to look like an Event. let browser = this.manager.browsingContext.top.embedderElement; + if (!browser) { + // Can be null if the tab disappeared by the time we got the message. + // Just bail. + return; + } let window = browser.ownerGlobal; if (!data.href) { diff --git a/toolkit/actors/AutoScrollChild.jsm b/toolkit/actors/AutoScrollChild.jsm index fcaf3866eee9..ecbc28903792 100644 --- a/toolkit/actors/AutoScrollChild.jsm +++ b/toolkit/actors/AutoScrollChild.jsm @@ -3,9 +3,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +var EXPORTED_SYMBOLS = ["AutoScrollChild"]; + const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); -var EXPORTED_SYMBOLS = ["AutoScrollChild"]; +ChromeUtils.defineModuleGetter( + this, + "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm" +); class AutoScrollChild extends JSWindowActorChild { constructor() { @@ -25,11 +31,12 @@ class AutoScrollChild extends JSWindowActorChild { this.autoscrollLoop = this.autoscrollLoop.bind(this); } - isAutoscrollBlocker(node) { + isAutoscrollBlocker(event) { let mmPaste = Services.prefs.getBoolPref("middlemouse.paste"); let mmScrollbarPosition = Services.prefs.getBoolPref( "middlemouse.scrollbarPosition" ); + let node = event.originalTarget; let content = node.ownerGlobal; // If the node is in editable document or content, we don't want to start @@ -45,34 +52,27 @@ class AutoScrollChild extends JSWindowActorChild { } } - while (node) { - if ( - (node instanceof content.HTMLAnchorElement || - node instanceof content.HTMLAreaElement) && - node.hasAttribute("href") - ) { - return true; - } + // Don't start if we're on a link. + let [href] = BrowserUtils.hrefAndLinkNodeForClickEvent(event); + if (href) { + return true; + } - if ( - mmPaste && - (node instanceof content.HTMLInputElement || - node instanceof content.HTMLTextAreaElement) - ) { - return true; - } + // Or if we're pasting into an input field of sorts. + if ( + mmPaste && + node.closest("input,textarea")?.constructor.name.startsWith("HTML") + ) { + return true; + } - if ( - node instanceof content.XULElement && - ((mmScrollbarPosition && - (node.localName == "scrollbar" || - node.localName == "scrollcorner")) || - node.localName == "treechildren") - ) { - return true; - } - - node = node.parentNode; + // Or if we're on a scrollbar or XUL + if ( + (mmScrollbarPosition && + node.closest("scrollbar,scrollcorner") instanceof content.XULElement) || + node.closest("treechildren") instanceof content.XULElement + ) { + return true; } return false; } @@ -377,7 +377,7 @@ class AutoScrollChild extends JSWindowActorChild { if ( this.canStartAutoScrollWith(event) && !this._scrollable && - !this.isAutoscrollBlocker(event.originalTarget) + !this.isAutoscrollBlocker(event) ) { this.startScroll(event); } diff --git a/toolkit/content/tests/browser/browser.ini b/toolkit/content/tests/browser/browser.ini index e33bcedde48f..c072e7e66434 100644 --- a/toolkit/content/tests/browser/browser.ini +++ b/toolkit/content/tests/browser/browser.ini @@ -23,6 +23,7 @@ support-files = [browser_autoscroll_disabled.js] skip-if = true # Bug 1312652 [browser_autoscroll_disabled_on_editable_content.js] +[browser_autoscroll_disabled_on_links.js] [browser_delay_autoplay_media.js] tags = audiochannel skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 diff --git a/toolkit/content/tests/browser/browser_autoscroll_disabled_on_links.js b/toolkit/content/tests/browser/browser_autoscroll_disabled_on_links.js new file mode 100644 index 000000000000..efd502fb1a5e --- /dev/null +++ b/toolkit/content/tests/browser/browser_autoscroll_disabled_on_links.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_autoscroll_links() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["general.autoScroll", true], + ["middlemouse.contentLoadURL", false], + ["test.events.async.enabled", false], + ], + }); + + let autoScroller; + function onPopupShown(aEvent) { + if (aEvent.originalTarget.id != "autoscroller") { + return false; + } + autoScroller = aEvent.originalTarget; + return true; + } + window.addEventListener("popupshown", onPopupShown, { capture: true }); + registerCleanupFunction(() => { + window.removeEventListener("popupshown", onPopupShown, { capture: true }); + }); + function popupIsNotClosed() { + return autoScroller && autoScroller.state != "closed"; + } + + async function promiseNativeMouseMiddleButtonDown(aBrowser) { + await EventUtils.promiseNativeMouseEvent({ + type: "mousemove", + target: aBrowser, + atCenter: true, + }); + return EventUtils.promiseNativeMouseEvent({ + type: "mousedown", + target: aBrowser, + atCenter: true, + button: 1, // middle button + }); + } + async function promiseNativeMouseMiddleButtonUp(aBrowser) { + return EventUtils.promiseNativeMouseEvent({ + type: "mouseup", + target: aBrowser, + atCenter: true, + button: 1, // middle button + }); + } + function promiseWaitForAutoScrollerClosed() { + if (!autoScroller || autoScroller.state == "closed") { + return Promise.resolve(); + } + let result = BrowserTestUtils.waitForEvent( + autoScroller, + "popuphidden", + { capture: true }, + () => { + return true; + } + ); + EventUtils.synthesizeKey("KEY_Escape"); + return result; + } + + async function testMarkup(markup) { + return BrowserTestUtils.withNewTab( + "https://example.com/browser/toolkit/content/tests/browser/file_empty.html", + async function(browser) { + await SpecialPowers.spawn(browser, [markup], html => { + // eslint-disable-next-line no-unsanitized/property + content.document.body.innerHTML = html; + content.document.documentElement.scrollTop = 1; + content.document.documentElement.scrollTop; // Flush layout. + }); + await promiseNativeMouseMiddleButtonDown(browser); + try { + await TestUtils.waitForCondition( + popupIsNotClosed, + "Wait for timeout of popup", + 100, + 10 + ); + ok(false, "Autoscroll shouldn't be started on " + markup); + } catch (e) { + ok( + typeof e == "string" && e.includes(" - timed out after 10 tries."), + `Autoscroll shouldn't be started on ${markup} (${ + typeof e == "string" ? e : e.message + })` + ); + } finally { + await promiseNativeMouseMiddleButtonUp(browser); + let waitForAutoScrollEnd = promiseWaitForAutoScrollerClosed(); + await waitForAutoScrollEnd; + } + } + ); + } + + await testMarkup( + 'Click me' + ); + + await testMarkup(` + + + + + `); + + await testMarkup(` + + + + + + + + + + `); +}); diff --git a/toolkit/modules/BrowserUtils.jsm b/toolkit/modules/BrowserUtils.jsm index 3fc567c8c865..ae23c213d47d 100644 --- a/toolkit/modules/BrowserUtils.jsm +++ b/toolkit/modules/BrowserUtils.jsm @@ -140,6 +140,71 @@ var BrowserUtils = { "moz-extension" == scheme ); }, + + /** + * Extracts linkNode and href for a click event. + * + * @param event + * The click event. + * @return [href, linkNode, linkPrincipal]. + * + * @note linkNode will be null if the click wasn't on an anchor + * element. This includes SVG links, because callers expect |node| + * to behave like an element, which SVG links (XLink) don't. + */ + hrefAndLinkNodeForClickEvent(event) { + // We should get a window off the event, and bail if not: + let content = event.view || event.composedTarget?.ownerGlobal; + if (!content?.HTMLAnchorElement) { + return null; + } + function isHTMLLink(aNode) { + // Be consistent with what nsContextMenu.js does. + return ( + (aNode instanceof content.HTMLAnchorElement && aNode.href) || + (aNode instanceof content.HTMLAreaElement && aNode.href) || + aNode instanceof content.HTMLLinkElement + ); + } + + let node = event.composedTarget; + while (node && !isHTMLLink(node)) { + node = node.flattenedTreeParentNode; + } + + if (node) { + return [node.href, node, node.ownerDocument.nodePrincipal]; + } + + // If there is no linkNode, try simple XLink. + let href, baseURI; + node = event.composedTarget; + while (node && !href) { + if ( + node.nodeType == content.Node.ELEMENT_NODE && + (node.localName == "a" || + node.namespaceURI == "http://www.w3.org/1998/Math/MathML") + ) { + href = + node.getAttribute("href") || + node.getAttributeNS("http://www.w3.org/1999/xlink", "href"); + if (href) { + baseURI = node.ownerDocument.baseURIObject; + break; + } + } + node = node.flattenedTreeParentNode; + } + + // In case of XLink, we don't return the node we got href from since + // callers expect -like elements. + // Note: makeURI() will throw if aUri is not a valid URI. + return [ + href ? Services.io.newURI(href, null, baseURI).spec : null, + null, + node && node.ownerDocument.nodePrincipal, + ]; + }, }; XPCOMUtils.defineLazyPreferenceGetter(