Commit aa5a3698 authored by Mark Striemer's avatar Mark Striemer
Browse files

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
parent 88c6d9a1
...@@ -16,7 +16,7 @@ async function isExtensionLocked(win, addonID) { ...@@ -16,7 +16,7 @@ async function isExtensionLocked(win, addonID) {
'panel-item[action="toggle-disabled"]' 'panel-item[action="toggle-disabled"]'
); );
let removeBtn = addonCard.querySelector('panel-item[action="remove"]'); let removeBtn = addonCard.querySelector('panel-item[action="remove"]');
ok(removeBtn.hidden, "Remove button should be hidden"); ok(removeBtn.disabled, "Remove button should be disabled");
ok(disableBtn.hidden, "Disable button should be hidden"); ok(disableBtn.hidden, "Disable button should be hidden");
} }
......
...@@ -393,6 +393,8 @@ addon-options-button = ...@@ -393,6 +393,8 @@ addon-options-button =
## Add-on actions ## Add-on actions
report-addon-button = Report report-addon-button = Report
remove-addon-button = Remove remove-addon-button = Remove
# The link will always be shown after the other text.
remove-addon-disabled-button = Can’t Be Removed <a data-l10n-name="link">Why?</a>
disable-addon-button = Disable disable-addon-button = Disable
enable-addon-button = Enable enable-addon-button = Enable
preferences-addon-button = preferences-addon-button =
......
...@@ -235,7 +235,7 @@ ...@@ -235,7 +235,7 @@
<template name="panel-item"> <template name="panel-item">
<link rel="stylesheet" href="chrome://mozapps/content/extensions/panel-item.css"> <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>
<template name="taar-notice"> <template name="taar-notice">
......
...@@ -715,26 +715,22 @@ class PanelList extends HTMLElement { ...@@ -715,26 +715,22 @@ class PanelList extends HTMLElement {
// If the menu is opened with the mouse, the active element might be // 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 // somewhere else in the document. In that case we should ignore it
// to avoid walking unrelated DOM nodes. // to avoid walking unrelated DOM nodes.
this.walker.currentNode = this.contains(document.activeElement) this.focusWalker.currentNode = this.contains(document.activeElement)
? document.activeElement ? document.activeElement
: this; : this;
let nextItem = moveForward let nextItem = moveForward
? this.walker.nextNode() ? this.focusWalker.nextNode()
: this.walker.previousNode(); : this.focusWalker.previousNode();
// If the next item wasn't found, try looping to the top/bottom. // If the next item wasn't found, try looping to the top/bottom.
if (!nextItem) { if (!nextItem) {
this.walker.currentNode = this; this.focusWalker.currentNode = this;
if (moveForward) { if (moveForward) {
nextItem = this.walker.firstChild(); nextItem = this.focusWalker.firstChild();
} else { } else {
nextItem = this.walker.lastChild(); nextItem = this.focusWalker.lastChild();
} }
} }
if (nextItem) {
nextItem.focus();
}
break; break;
} else if (e.key === "Escape") { } else if (e.key === "Escape") {
let { triggeringEvent } = this; let { triggeringEvent } = this;
...@@ -768,19 +764,44 @@ class PanelList extends HTMLElement { ...@@ -768,19 +764,44 @@ class PanelList extends HTMLElement {
} }
} }
get walker() { /**
if (!this._walker) { * A TreeWalker that can be used to focus elements. The returned element will
this._walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT, { * be the element that has gained focus based on the requested movement
acceptNode: node => { * through the tree.
if (node.disabled || node.hidden || node.localName !== "panel-item") { *
return NodeFilter.FILTER_REJECT; * 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() { async onShow() {
...@@ -797,11 +818,8 @@ class PanelList extends HTMLElement { ...@@ -797,11 +818,8 @@ class PanelList extends HTMLElement {
triggeringEvent && triggeringEvent &&
triggeringEvent.mozInputSource === MouseEvent.MOZ_SOURCE_KEYBOARD triggeringEvent.mozInputSource === MouseEvent.MOZ_SOURCE_KEYBOARD
) { ) {
this.walker.currentNode = this; this.focusWalker.currentNode = this;
let firstItem = this.walker.nextNode(); this.focusWalker.nextNode();
if (firstItem) {
firstItem.focus();
}
} }
this.sendEvent("shown"); this.sendEvent("shown");
...@@ -895,7 +913,27 @@ class AddonOptions extends HTMLElement { ...@@ -895,7 +913,27 @@ class AddonOptions extends HTMLElement {
setElementState(el, card, addon, updateInstall) { setElementState(el, card, addon, updateInstall) {
switch (el.getAttribute("action")) { switch (el.getAttribute("action")) {
case "remove": 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; break;
case "report": case "report":
el.hidden = !isAbuseReportSupported(addon); el.hidden = !isAbuseReportSupported(addon);
......
:host {
display: flex;
align-items: center;
}
::slotted(a) {
margin-inline-end: 12px;
}
:host([checked]) { :host([checked]) {
--icon: url("chrome://global/skin/icons/check.svg"); --icon: url("chrome://global/skin/icons/check.svg");
-moz-context-properties: fill; -moz-context-properties: fill;
......
...@@ -8,6 +8,11 @@ AddonTestUtils.initMochitest(this); ...@@ -8,6 +8,11 @@ AddonTestUtils.initMochitest(this);
let promptService; 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 = { const SECTION_INDEXES = {
enabled: 0, enabled: 0,
disabled: 1, disabled: 1,
...@@ -34,7 +39,10 @@ function waitForThemeChange(list) { ...@@ -34,7 +39,10 @@ function waitForThemeChange(list) {
return BrowserTestUtils.waitForEvent(list, "move", () => ++moveCount == 2); return BrowserTestUtils.waitForEvent(list, "move", () => ++moveCount == 2);
} }
add_task(async function enableHtmlViews() { let mockProvider;
add_task(async function setup() {
mockProvider = new MockProvider();
promptService = mockPromptService(); promptService = mockPromptService();
Services.telemetry.clearEvents(); Services.telemetry.clearEvents();
}); });
...@@ -128,6 +136,9 @@ add_task(async function testExtensionList() { ...@@ -128,6 +136,9 @@ add_task(async function testExtensionList() {
"remove-addon-button", "remove-addon-button",
"The button has the remove label" "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. // Remove but cancel.
let cancelled = BrowserTestUtils.waitForEvent(card, "remove-cancelled"); let cancelled = BrowserTestUtils.waitForEvent(card, "remove-cancelled");
...@@ -736,6 +747,48 @@ add_task(async function testBuiltInThemeButtons() { ...@@ -736,6 +747,48 @@ add_task(async function testBuiltInThemeButtons() {
await closeView(win); 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() { add_task(async function testOnlyTypeIsShown() {
let win = await loadInitialView("theme"); let win = await loadInitialView("theme");
let doc = win.document; let doc = win.document;
...@@ -809,8 +862,6 @@ add_task(async function testExtensionGenericIcon() { ...@@ -809,8 +862,6 @@ add_task(async function testExtensionGenericIcon() {
}); });
add_task(async function testSectionHeadingKeys() { add_task(async function testSectionHeadingKeys() {
let mockProvider = new MockProvider();
mockProvider.createAddons([ mockProvider.createAddons([
{ {
id: "test-theme", id: "test-theme",
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment