Bug 1580962 - Show a SUMO link when an add-on can't be removed r=rpl,fluent-reviewers,flod

Differential Revision: https://phabricator.services.mozilla.com/D48128

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mark Striemer 2019-10-08 16:27:43 +00:00
parent 8a4e19f7d0
commit be025da362
5 changed files with 131 additions and 31 deletions

View File

@ -393,6 +393,8 @@ addon-options-button =
## Add-on actions
report-addon-button = Report
remove-addon-button = Remove
# The link will always be shown after the other text.
remove-addon-disabled-button = Cant Be Removed <a data-l10n-name="link">Why?</a>
disable-addon-button = Disable
enable-addon-button = Enable
preferences-addon-button =

View File

@ -235,7 +235,7 @@
<template name="panel-item">
<link rel="stylesheet" href="chrome://mozapps/content/extensions/panel-item.css">
<button><slot></slot></button>
<button><slot></slot></button><slot name="support-link"></slot>
</template>
<template name="taar-notice">

View File

@ -715,26 +715,22 @@ class PanelList extends HTMLElement {
// If the menu is opened with the mouse, the active element might be
// somewhere else in the document. In that case we should ignore it
// to avoid walking unrelated DOM nodes.
this.walker.currentNode = this.contains(document.activeElement)
this.focusWalker.currentNode = this.contains(document.activeElement)
? document.activeElement
: this;
let nextItem = moveForward
? this.walker.nextNode()
: this.walker.previousNode();
? this.focusWalker.nextNode()
: this.focusWalker.previousNode();
// If the next item wasn't found, try looping to the top/bottom.
if (!nextItem) {
this.walker.currentNode = this;
this.focusWalker.currentNode = this;
if (moveForward) {
nextItem = this.walker.firstChild();
nextItem = this.focusWalker.firstChild();
} else {
nextItem = this.walker.lastChild();
nextItem = this.focusWalker.lastChild();
}
}
if (nextItem) {
nextItem.focus();
}
break;
} else if (e.key === "Escape") {
let { triggeringEvent } = this;
@ -768,19 +764,44 @@ class PanelList extends HTMLElement {
}
}
get walker() {
if (!this._walker) {
this._walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT, {
acceptNode: node => {
if (node.disabled || node.hidden || node.localName !== "panel-item") {
return NodeFilter.FILTER_REJECT;
}
/**
* A TreeWalker that can be used to focus elements. The returned element will
* be the element that has gained focus based on the requested movement
* through the tree.
*
* Example:
*
* this.focusWalker.currentNode = this;
* // Focus and get the first focusable child.
* let focused = this.focusWalker.nextNode();
* // Focus the second focusable child.
* this.focusWalker.nextNode();
*/
get focusWalker() {
if (!this._focusWalker) {
this._focusWalker = document.createTreeWalker(
this,
NodeFilter.SHOW_ELEMENT,
{
acceptNode: node => {
// No need to look at hidden nodes.
if (node.hidden) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
},
});
// Focus the node, if it worked then this is the node we want.
node.focus();
if (node === document.activeElement) {
return NodeFilter.FILTER_ACCEPT;
}
// Continue into child nodes if the parent couldn't be focused.
return NodeFilter.FILTER_SKIP;
},
}
);
}
return this._walker;
return this._focusWalker;
}
async onShow() {
@ -797,11 +818,8 @@ class PanelList extends HTMLElement {
triggeringEvent &&
triggeringEvent.mozInputSource === MouseEvent.MOZ_SOURCE_KEYBOARD
) {
this.walker.currentNode = this;
let firstItem = this.walker.nextNode();
if (firstItem) {
firstItem.focus();
}
this.focusWalker.currentNode = this;
this.focusWalker.nextNode();
}
this.sendEvent("shown");
@ -895,7 +913,27 @@ class AddonOptions extends HTMLElement {
setElementState(el, card, addon, updateInstall) {
switch (el.getAttribute("action")) {
case "remove":
el.hidden = !hasPermission(addon, "uninstall");
if (hasPermission(addon, "uninstall")) {
// Regular add-on that can be uninstalled.
el.disabled = false;
el.hidden = false;
document.l10n.setAttributes(el, "remove-addon-button");
} else if (addon.isBuiltin) {
// Likely the built-in themes, can't be removed, that's fine.
el.hidden = true;
} else {
// Likely sideloaded, mention that it can't be removed with a link.
el.hidden = false;
el.disabled = true;
if (!el.querySelector('[slot="support-link"]')) {
let link = document.createElement("a", { is: "support-link" });
link.setAttribute("data-l10n-name", "link");
link.setAttribute("support-page", "cant-remove-addon");
link.setAttribute("slot", "support-link");
el.appendChild(link);
document.l10n.setAttributes(el, "remove-addon-disabled-button");
}
}
break;
case "report":
el.hidden = !isAbuseReportSupported(addon);

View File

@ -1,3 +1,12 @@
:host {
display: flex;
align-items: center;
}
::slotted(a) {
margin-inline-end: 12px;
}
:host([checked]) {
--icon: url("chrome://global/skin/icons/check.svg");
-moz-context-properties: fill;

View File

@ -8,6 +8,11 @@ AddonTestUtils.initMochitest(this);
let promptService;
const SUPPORT_URL = Services.urlFormatter.formatURL(
Services.prefs.getStringPref("app.support.baseURL")
);
const REMOVE_SUMO_URL = SUPPORT_URL + "cant-remove-addon";
const SECTION_INDEXES = {
enabled: 0,
disabled: 1,
@ -34,7 +39,10 @@ function waitForThemeChange(list) {
return BrowserTestUtils.waitForEvent(list, "move", () => ++moveCount == 2);
}
add_task(async function enableHtmlViews() {
let mockProvider;
add_task(async function setup() {
mockProvider = new MockProvider();
promptService = mockPromptService();
Services.telemetry.clearEvents();
});
@ -128,6 +136,9 @@ add_task(async function testExtensionList() {
"remove-addon-button",
"The button has the remove label"
);
// There is a support link when the add-on isn't removeable, verify we don't
// always include one.
ok(!removeButton.querySelector("a"), "There isn't a link in the item");
// Remove but cancel.
let cancelled = BrowserTestUtils.waitForEvent(card, "remove-cancelled");
@ -736,6 +747,48 @@ add_task(async function testBuiltInThemeButtons() {
await closeView(win);
});
add_task(async function testSideloadRemoveButton() {
const id = "sideload@mochi.test";
mockProvider.createAddons([
{
id,
name: "Sideloaded",
permissions: 0,
},
]);
let win = await loadInitialView("extension");
let doc = win.document;
let card = getCardByAddonId(doc, id);
let moreOptionsPanel = card.querySelector("panel-list");
let panelOpened = BrowserTestUtils.waitForEvent(moreOptionsPanel, "shown");
moreOptionsPanel.show();
await panelOpened;
// Verify the remove button is visible with a SUMO link.
let removeButton = card.querySelector('[action="remove"]');
ok(removeButton.disabled, "Remove is disabled");
ok(!removeButton.hidden, "Remove is visible");
let sumoLink = removeButton.querySelector("a");
ok(sumoLink, "There's a link");
is(
doc.l10n.getAttributes(removeButton).id,
"remove-addon-disabled-button",
"The can't remove text is shown"
);
sumoLink.focus();
is(doc.activeElement, sumoLink, "The link can be focused");
let newTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, REMOVE_SUMO_URL);
sumoLink.click();
BrowserTestUtils.removeTab(await newTabOpened);
await closeView(win);
});
add_task(async function testOnlyTypeIsShown() {
let win = await loadInitialView("theme");
let doc = win.document;
@ -809,8 +862,6 @@ add_task(async function testExtensionGenericIcon() {
});
add_task(async function testSectionHeadingKeys() {
let mockProvider = new MockProvider();
mockProvider.createAddons([
{
id: "test-theme",