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
This commit is contained in:
Gijs Kruitbosch
2025-03-20 15:54:31 +00:00
parent 28515892e4
commit c9d434d38b
7 changed files with 161 additions and 48 deletions

View File

@@ -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"]

View File

@@ -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;
}

View File

@@ -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");
});

View File

@@ -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);
});

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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);
};