From c9d434d38ba65b6f312f4ff0787553511e9969d5 Mon Sep 17 00:00:00 2001 From: Gijs Kruitbosch Date: Thu, 20 Mar 2025 15:54:31 +0000 Subject: [PATCH] Bug 1953575 - add 'copy link' to sharing menu on macOS and offer on non-mac/windows as an alternative to sharing, r=mossop,fluent-reviewers,bolsson Differential Revision: https://phabricator.services.mozilla.com/D241487 --- .../content/test/contextMenu/browser.toml | 3 + .../browser_contextmenu_share_linux.js | 48 ++++++++++ .../browser_contextmenu_share_macosx.js | 53 ++++++++--- .../browser_contextmenu_share_win.js | 7 +- browser/locales/en-US/browser/browser.ftl | 3 + browser/modules/SharingUtils.sys.mjs | 93 +++++++++++++------ widget/nsIWindowsUIUtils.idl | 2 +- 7 files changed, 161 insertions(+), 48 deletions(-) create mode 100644 browser/base/content/test/contextMenu/browser_contextmenu_share_linux.js diff --git a/browser/base/content/test/contextMenu/browser.toml b/browser/base/content/test/contextMenu/browser.toml index 8d68475f1399..a471902f1bf4 100644 --- a/browser/base/content/test/contextMenu/browser.toml +++ b/browser/base/content/test/contextMenu/browser.toml @@ -60,6 +60,9 @@ skip-if = ["os == 'linux' && socketprocess_networking"] ["browser_contextmenu_save_blocked.js"] skip-if = ["os == 'linux' && socketprocess_networking"] +["browser_contextmenu_share_linux.js"] +run-if = ["os == 'linux'"] + ["browser_contextmenu_share_macosx.js"] https_first_disabled = true support-files = ["browser_contextmenu_shareurl.html"] diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_share_linux.js b/browser/base/content/test/contextMenu/browser_contextmenu_share_linux.js new file mode 100644 index 000000000000..dabeecf46c7b --- /dev/null +++ b/browser/base/content/test/contextMenu/browser_contextmenu_share_linux.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BASE = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const TEST_URL = BASE + "browser_contextmenu_shareurl.html"; + +/** + * Test the "Share" item in the tab contextmenu on Linux. + */ +add_task(async function test_contextmenu_share_linux() { + await BrowserTestUtils.withNewTab(TEST_URL, async () => { + await openTabContextMenu(gBrowser.selectedTab); + + let contextMenu = document.getElementById("tabContextMenu"); + let contextMenuClosedPromise = BrowserTestUtils.waitForPopupEvent( + contextMenu, + "hidden" + ); + let itemCreated = contextMenu.querySelector(".share-tab-url-item"); + ok(itemCreated, "Got Share item on Linux"); + await SimpleTest.promiseClipboardChange(TEST_URL, () => + contextMenu.activateItem(itemCreated) + ); + ok(true, "Copied to clipboard."); + + await contextMenuClosedPromise; + }); +}); + +/** + * Helper for opening the toolbar context menu. + */ +async function openTabContextMenu(tab) { + info("Opening tab context menu"); + let contextMenu = document.getElementById("tabContextMenu"); + let openTabContextMenuPromise = BrowserTestUtils.waitForPopupEvent( + contextMenu, + "shown" + ); + + EventUtils.synthesizeMouseAtCenter(tab, { type: "contextmenu" }); + await openTabContextMenuPromise; +} diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_share_macosx.js b/browser/base/content/test/contextMenu/browser_contextmenu_share_macosx.js index fe1de7065d30..00026903a6d3 100644 --- a/browser/base/content/test/contextMenu/browser_contextmenu_share_macosx.js +++ b/browser/base/content/test/contextMenu/browser_contextmenu_share_macosx.js @@ -69,25 +69,26 @@ add_task(async function test_contextmenu_share_macosx() { ok(getSharingProvidersSpy.calledOnce, "getSharingProviders called"); info( - "Check we have a service and one extra menu item for the More... button" + "Check we have copy link, a service and one extra menu item for the More... button" ); let popup = contextMenu.querySelector(".share-tab-url-item").menupopup; - let items = popup.querySelectorAll("menuitem"); - is(items.length, 2, "There should be 2 sharing services."); + let items = Array.from(popup.querySelectorAll("menuitem")); + is(items.length, 3, "There should be 3 sharing services."); info("Click on the sharing service"); let menuPopupClosedPromised = BrowserTestUtils.waitForPopupEvent( contextMenu, "hidden" ); - let shareButton = items[0]; - is( - shareButton.label, - mockShareData[0].menuItemTitle, + let shareButton = items.find( + t => t.label == mockShareData[0].menuItemTitle + ); + ok( + shareButton, "Share button's label should match the service's menu item title. " ); is( - shareButton.getAttribute("share-name"), + shareButton?.getAttribute("share-name"), mockShareData[0].name, "Share button's share-name value should match the service's name. " ); @@ -103,23 +104,45 @@ add_task(async function test_contextmenu_share_macosx() { is(url, TEST_URL, "Shared correct URL"); is(title, "Sharing URL", "Shared the correct title."); - info("Test the More... button"); + info("Test the copy link button"); contextMenu = await openTabContextMenu(gBrowser.selectedTab); await openMenuPopup(contextMenu); // Since the tab context menu was collapsed previously, the popup needs to get the // providers again. ok(getSharingProvidersSpy.calledTwice, "getSharingProviders called again"); popup = contextMenu.querySelector(".share-tab-url-item").menupopup; - items = popup.querySelectorAll("menuitem"); - is(items.length, 2, "There should be 2 sharing services."); - - info("Click on the More Button"); - let moreButton = items[1]; + items = Array.from(popup.querySelectorAll("menuitem")); + is(items.length, 3, "There should be 3 sharing services."); + info("Click on the Copy Link item"); + let copyLinkItem = items.find( + item => item.dataset.l10nId == "menu-share-copy-link" + ); menuPopupClosedPromised = BrowserTestUtils.waitForPopupEvent( contextMenu, "hidden" ); - popup.activateItem(moreButton); + await SimpleTest.promiseClipboardChange(TEST_URL, () => + popup.activateItem(copyLinkItem) + ); + await menuPopupClosedPromised; + + info("Test the More... item"); + contextMenu = await openTabContextMenu(gBrowser.selectedTab); + await openMenuPopup(contextMenu); + // Since the tab context menu was collapsed previously, the popup needs to get the + // providers again. + is(getSharingProvidersSpy.callCount, 3, "getSharingProviders called again"); + popup = contextMenu.querySelector(".share-tab-url-item").menupopup; + items = popup.querySelectorAll("menuitem"); + is(items.length, 3, "There should be 3 sharing services."); + + info("Click on the More item"); + let moreMenuitem = items[2]; + menuPopupClosedPromised = BrowserTestUtils.waitForPopupEvent( + contextMenu, + "hidden" + ); + popup.activateItem(moreMenuitem); await menuPopupClosedPromised; ok(openSharingPreferencesSpy.calledOnce, "openSharingPreferences called"); }); diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_share_win.js b/browser/base/content/test/contextMenu/browser_contextmenu_share_win.js index e315ce7c94df..91f4e18281ff 100644 --- a/browser/base/content/test/contextMenu/browser_contextmenu_share_win.js +++ b/browser/base/content/test/contextMenu/browser_contextmenu_share_win.js @@ -8,12 +8,11 @@ const { sinon } = ChromeUtils.importESModule( ); const BASE = getRootDirectory(gTestPath).replace( "chrome://mochitests/content", - // eslint-disable-next-line @microsoft/sdl/no-insecure-url - "http://example.com" + "https://example.com" ); const TEST_URL = BASE + "browser_contextmenu_shareurl.html"; -// Setup spies for observing function calls from MacSharingService +// Setup spies for observing function calls from WindowsUIUtils. let shareUrlSpy = sinon.spy(); SharingUtils.testOnlyMockUIUtils({ @@ -23,7 +22,7 @@ SharingUtils.testOnlyMockUIUtils({ QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowsUIUtils]), }); -registerCleanupFunction(async function () { +registerCleanupFunction(function () { SharingUtils.testOnlyMockUIUtils(null); }); diff --git a/browser/locales/en-US/browser/browser.ftl b/browser/locales/en-US/browser/browser.ftl index 037e1defa100..716a072166fa 100644 --- a/browser/locales/en-US/browser/browser.ftl +++ b/browser/locales/en-US/browser/browser.ftl @@ -983,6 +983,9 @@ panel-save-update-password = Password # "More" item in macOS share menu menu-share-more = .label = More… +menu-share-copy-link = + .label = Copy Link + .accesskey = L ui-tour-info-panel-close = .tooltiptext = Close diff --git a/browser/modules/SharingUtils.sys.mjs b/browser/modules/SharingUtils.sys.mjs index 9244b61067b5..967ccec0a66d 100644 --- a/browser/modules/SharingUtils.sys.mjs +++ b/browser/modules/SharingUtils.sys.mjs @@ -7,6 +7,8 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { BrowserUtils } from "resource://gre/modules/BrowserUtils.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +const APPLE_COPY_LINK = "com.apple.share.CopyLink.invite"; + let lazy = {}; XPCOMUtils.defineLazyServiceGetters(lazy, { @@ -26,20 +28,16 @@ class SharingUtilsCls { return; } - // We only support "share URL" on macOS and on Windows: - if (AppConstants.platform != "macosx" && AppConstants.platform != "win") { - return; - } - let shareURL = insertAfterEl.nextElementSibling; if (!shareURL?.matches(".share-tab-url-item")) { shareURL = this.#createShareURLMenuItem(insertAfterEl); } shareURL.browserToShare = Cu.getWeakReference(browser); - if (AppConstants.platform == "win") { - // We disable the item on Windows, as there's no submenu. - // On macOS, we handle this inside the menupopup. + if (AppConstants.platform != "macosx") { + // On macOS, we keep the item enabled and handle enabled state + // inside the menupopup. + // Everywhere else, we disable the item, as there's no submenu. shareURL.hidden = !BrowserUtils.getShareableURL(browser.currentURI); } } @@ -51,18 +49,21 @@ class SharingUtilsCls { let menu = insertAfterEl.parentNode; let shareURL = null; let document = insertAfterEl.ownerDocument; - if (AppConstants.platform == "win") { - shareURL = this.#buildShareURLItem(document); - } else if (AppConstants.platform == "macosx") { - shareURL = this.#buildShareURLMenu(document); + if (AppConstants.platform != "win" && AppConstants.platform != "macosx") { + shareURL = this.#buildCopyLinkItem(document); + } else { + if (AppConstants.platform == "win") { + shareURL = this.#buildShareURLItem(document); + } else if (AppConstants.platform == "macosx") { + shareURL = this.#buildShareURLMenu(document); + } + let l10nID = + menu.id == "tabContextMenu" + ? "tab-context-share-url" + : "menu-file-share-url"; + document.l10n.setAttributes(shareURL, l10nID); } - shareURL.className = "share-tab-url-item"; - - let l10nID = - menu.id == "tabContextMenu" - ? "tab-context-share-url" - : "menu-file-share-url"; - document.l10n.setAttributes(shareURL, l10nID); + shareURL.classList.add("share-tab-url-item"); menu.insertBefore(shareURL, insertAfterEl.nextSibling); return shareURL; @@ -88,6 +89,32 @@ class SharingUtilsCls { return menu; } + /** + * Return a menuitem that only copies the link. Useful for + * OSes where we do not yet have full share support, like Linux. + * + * We currently also use this on macOS because for some reason Apple does not + * provide the share service option for this. + */ + #buildCopyLinkItem(document) { + let shareURLMenuItem = document.createXULElement("menuitem"); + document.l10n.setAttributes(shareURLMenuItem, "menu-share-copy-link"); + shareURLMenuItem.classList.add("share-copy-link"); + + if (AppConstants.platform == "macosx") { + shareURLMenuItem.classList.add("menuitem-iconic"); + shareURLMenuItem.setAttribute( + "image", + "chrome://global/skin/icons/link.svg" + ); + } else { + // On macOS the command handling happens by virtue of the submenu + // command event listener. + shareURLMenuItem.addEventListener("command", this); + } + return shareURLMenuItem; + } + /** * Get the sharing data for a given DOM node. */ @@ -136,6 +163,16 @@ class SharingUtilsCls { let currentURI = gURLBar.makeURIReadable(urlToShare).displaySpec; let services = lazy.MacSharingService.getSharingProviders(currentURI); + // Apple seems reluctant to provide copy link as a feature. Add it at the + // start if it's not there. + if (!services.some(s => s.name == APPLE_COPY_LINK)) { + let item = this.#buildCopyLinkItem(document); + if (!shouldEnable) { + item.setAttribute("disabled", "true"); + } + menuPopup.appendChild(item); + } + services.forEach(share => { let item = document.createXULElement("menuitem"); item.classList.add("menuitem-iconic"); @@ -181,16 +218,16 @@ class SharingUtilsCls { let { urlToShare, titleToShare } = this.getDataToShare(target); let currentURI = gURLBar.makeURIReadable(urlToShare).displaySpec; - if (AppConstants.platform == "win") { + if (event.target.classList.contains("share-copy-link")) { + BrowserUtils.copyLink(currentURI, titleToShare); + } else if (AppConstants.platform == "win") { lazy.WindowsUIUtils.shareUrl(currentURI, titleToShare); - return; - } - - // On macOSX platforms - let shareName = event.target.getAttribute("share-name"); - - if (shareName) { - lazy.MacSharingService.shareUrl(shareName, currentURI, titleToShare); + } else { + // On macOSX platforms + let shareName = event.target.getAttribute("share-name"); + if (shareName) { + lazy.MacSharingService.shareUrl(shareName, currentURI, titleToShare); + } } } diff --git a/widget/nsIWindowsUIUtils.idl b/widget/nsIWindowsUIUtils.idl index e0904fe4a72b..12d0b7dc4233 100644 --- a/widget/nsIWindowsUIUtils.idl +++ b/widget/nsIWindowsUIUtils.idl @@ -44,5 +44,5 @@ interface nsIWindowsUIUtils : nsISupports /** * Share URL */ - void shareUrl(in AString shareTitle, in AString urlToShare); + void shareUrl(in AString urlToShare, in AString shareTitle); };