Bug 1543377 - Create message-bar and message-bar-stack WebComponents. r=mstriemer,robwu
authorLuca Greco <lgreco@mozilla.com>
Mon, 06 May 2019 18:35:44 +0000
changeset 472747 e70459131e5a6186b3e08995b42a08c754a2229e
parent 472746 9bee4151da91814b2369971779cb8a248e25dadc
child 472748 274e9115edf07409a34b56f128ac5e117b53d877
push id35978
push usershindli@mozilla.com
push dateTue, 07 May 2019 09:44:39 +0000
treeherdermozilla-central@7aee5a30dd15 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmstriemer, robwu
bugs1543377
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1543377 - Create message-bar and message-bar-stack WebComponents. r=mstriemer,robwu Differential Revision: https://phabricator.services.mozilla.com/D27547
toolkit/mozapps/extensions/content/aboutaddons.css
toolkit/mozapps/extensions/content/aboutaddons.html
toolkit/mozapps/extensions/content/message-bar.css
toolkit/mozapps/extensions/content/message-bar.js
toolkit/mozapps/extensions/jar.mn
toolkit/mozapps/extensions/test/browser/browser.ini
toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js
--- a/toolkit/mozapps/extensions/content/aboutaddons.css
+++ b/toolkit/mozapps/extensions/content/aboutaddons.css
@@ -4,16 +4,26 @@
 }
 
 #main {
   margin-inline-start: 28px;
   margin-bottom: 28px;
   max-width: var(--section-width);
 }
 
+#abuse-reports-messages {
+  margin-inline-start: 28px;
+  max-width: var(--section-width);
+}
+
+/* The margin between message bars. */
+#abuse-reports-messages > * {
+  margin-bottom: 8px;
+}
+
 /* List sections */
 
 .list-section-heading {
   font-size: 17px;
   font-weight: 600;
   margin-block: 16px;
 }
 
--- a/toolkit/mozapps/extensions/content/aboutaddons.html
+++ b/toolkit/mozapps/extensions/content/aboutaddons.html
@@ -3,19 +3,22 @@
   <head>
     <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
     <link rel="stylesheet" href="chrome://mozapps/content/extensions/aboutaddons.css">
 
     <link rel="localization" href="branding/brand.ftl">
     <link rel="localization" href="toolkit/about/aboutAddons.ftl">
 
     <script src="chrome://mozapps/content/extensions/aboutaddonsCommon.js"></script>
+    <script src="chrome://mozapps/content/extensions/message-bar.js"></script>
     <script src="chrome://mozapps/content/extensions/aboutaddons.js"></script>
   </head>
   <body>
+    <message-bar-stack id="abuse-reports-messages" reverse max-message-bar-count="3">
+    </message-bar-stack>
     <div id="main">
     </div>
 
     <template name="addon-options">
       <panel-list>
         <panel-item action="toggle-disabled"></panel-item>
         <panel-item data-l10n-id="remove-addon-button" action="remove"></panel-item>
         <panel-item data-l10n-id="install-update-button" action="install-update" badged></panel-item>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/message-bar.css
@@ -0,0 +1,160 @@
+:host {
+  --info-icon-url: url("chrome://global/skin/icons/info.svg");
+  --warn-icon-url: url("chrome://global/skin/icons/warning.svg");
+  --check-icon-url: url("chrome://global/skin/icons/check.svg");
+  --error-icon-url: url("chrome://global/skin/icons/error.svg");
+  --close-icon-url: url("chrome://global/skin/icons/close.svg");
+  --icon-size: 16px;
+}
+
+/* MessageBar colors by message type */
+
+:host([type=warning]) {
+  background-color: var(--yellow-50);
+  color: var(--yellow-90);
+}
+
+:host([type=success]) {
+  background-color: #30e60b;
+  color: #003706;
+}
+
+:host([type=error]) {
+  background: #d70022;
+  color: #ffffff;
+}
+
+:host {
+  border-radius: 4px;
+  /* Colors used by default, and for [type=generic] message bars.*/
+  background-color: var(--grey-20);
+  color: var(--grey-90);
+}
+
+/* Make the host to behave as a block by default, but allow hidden to hide it. */
+:host(:not([hidden])) {
+  display: block;
+}
+
+.container {
+  background: inherit;
+  color: inherit;
+}
+
+/* MessageBar Grid Layout */
+
+.container {
+  padding-top: 2px;
+  padding-bottom: 2px;
+
+  padding-inline-start: 4px;
+
+  min-height: 32px;
+  border-radius: 4px;
+  font-size: 13px;
+  font-weight: 400;
+  line-height: 1.4;
+
+  display: flex;
+  /* Ensure that the message bar shadow dom elements are vertically aligned. */
+  align-items: center;
+}
+
+:host([dismissable]) .container {
+  /* Add padding on the end of the container when the bar is dismissable. */
+  padding-inline-end: 4px;
+}
+
+.icon {
+  flex-shrink: 0;
+}
+
+.content {
+  margin-inline-end: 4px;
+  flex-grow: 1;
+  display: flex;
+  /* Ensure that the message bar content is vertically aligned. */
+  align-items: center;
+  /* Ensure that the message bar content is wrapped. */
+  word-break: break-word;
+}
+
+button.close {
+  flex-shrink: 0;
+}
+
+::slotted(button) {
+  /* Enforce micro-button width. */
+  min-width: -moz-fit-content !important;
+}
+
+/* MessageBar icon style */
+
+.icon {
+  padding: 4px;
+  width: var(--icon-size);
+  height: var(--icon-size);
+}
+
+.icon {
+  -moz-appearance: none;
+  -moz-context-properties: fill, fill-opacity;
+  color: inherit !important;
+  fill: currentColor;
+  fill-opacity: 0;
+}
+
+:host([type=success]) .icon {
+  fill-opacity: 1;
+}
+
+:host(:not([type])) .icon::after, :host([type=generic]) .icon::after {
+  content: var(--info-icon-url);
+}
+
+:host([type=warning]) .icon::after {
+  content: var(--warn-icon-url);
+}
+
+:host([type=success]) .icon::after {
+  content: var(--check-icon-url);
+}
+
+:host([type=error]) .icon::after {
+  content: var(--error-icon-url);
+}
+
+/* Close icon styles */
+
+:host(:not([dismissable])) button.close {
+  display: none;
+}
+
+button.close {
+  padding: 4px;
+  width: var(--icon-size);
+  height: var(--icon-size);
+  min-width: -moz-fit-content;
+}
+
+button.close {
+  background: var(--close-icon-url) no-repeat center center;
+  -moz-context-properties: fill, fill-opacity;
+  color: inherit !important;
+  fill: currentColor;
+  fill-opacity: 0;
+  min-width: auto;
+  min-height: auto;
+  width: var(--icon-size);
+  height: var(--icon-size);
+  margin: 0;
+  padding: 0;
+}
+
+button.close:hover {
+  fill-opacity: 0.1;
+}
+
+button.close:hover:active {
+  fill-opacity: 0.2;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/message-bar.js
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+/* eslint max-len: ["error", 80] */
+
+"use strict";
+
+class MessageBarStackElement extends HTMLElement {
+  constructor() {
+    super();
+    this._observer = null;
+    const shadowRoot = this.attachShadow({mode: "open"});
+    shadowRoot.append(this.constructor.template.content.cloneNode(true));
+  }
+
+  connectedCallback() {
+    // Close any message bar that should be allowed based on the
+    // maximum number of message bars.
+    this.closeMessageBars();
+
+    // Observe mutations to close older bars when new ones have been
+    // added.
+    this._observer = new MutationObserver(() => {
+      this._observer.disconnect();
+      this.closeMessageBars();
+      this._observer.observe(this, {childList: true});
+    });
+    this._observer.observe(this, {childList: true});
+  }
+
+  disconnectedCallback() {
+    this._observer.disconnect();
+    this._observer = null;
+  }
+
+  closeMessageBars() {
+    const {maxMessageBarCount} = this;
+    if (maxMessageBarCount > 1) {
+      // Remove the older message bars if the stack reached the
+      // maximum number of message bars allowed.
+      while (this.childElementCount > maxMessageBarCount) {
+        this.firstElementChild.remove();
+      }
+    }
+  }
+
+  get maxMessageBarCount() {
+    return parseInt(this.getAttribute("max-message-bar-count"), 10);
+  }
+
+  static get template() {
+    const template = document.createElement("template");
+
+    const style = document.createElement("style");
+    // Render the stack in the reverse order if the stack has the
+    // reverse attribute set.
+    style.textContent = `
+      :host {
+        display: block;
+      }
+      :host([reverse]) > slot {
+        display: flex;
+        flex-direction: column-reverse;
+      }
+    `;
+    template.content.append(style);
+    template.content.append(document.createElement("slot"));
+
+    Object.defineProperty(this, "template", {
+      value: template,
+    });
+
+    return template;
+  }
+}
+
+class MessageBarElement extends HTMLElement {
+  constructor() {
+    super();
+    const shadowRoot = this.attachShadow({mode: "open"});
+    const content = this.constructor.template.content.cloneNode(true);
+    shadowRoot.append(content);
+    this._closeIcon.addEventListener(
+      "click", () => this.remove(), {once: true});
+  }
+
+  disconnectedCallback() {
+    this.dispatchEvent(new CustomEvent("message-bar:close"));
+  }
+
+  get _closeIcon() {
+    return this.shadowRoot.querySelector("button.close");
+  }
+
+  static get template() {
+    const template = document.createElement("template");
+
+    const style = document.createElement("style");
+    style.textContent = `
+      @import "chrome://global/skin/in-content/common.css";
+      @import "chrome://mozapps/content/extensions/message-bar.css";
+    `;
+    template.content.append(style);
+
+    // A container for the entire message bar content,
+    // most of the css rules needed to provide the
+    // expected message bar layout is applied on this
+    // element.
+    const container = document.createElement("div");
+    container.setAttribute("class", "container");
+    template.content.append(container);
+
+    const icon = document.createElement("span");
+    icon.setAttribute("class", "icon");
+    container.append(icon);
+
+    const barcontent = document.createElement("span");
+    barcontent.setAttribute("class", "content");
+    barcontent.append(document.createElement("slot"));
+    container.append(barcontent);
+
+    const closeIcon = document.createElement("button");
+    closeIcon.setAttribute("class", "close");
+    container.append(closeIcon);
+
+    Object.defineProperty(this, "template", {
+      value: template,
+    });
+
+    return template;
+  }
+}
+
+customElements.define("message-bar", MessageBarElement);
+customElements.define("message-bar-stack", MessageBarStackElement);
--- a/toolkit/mozapps/extensions/jar.mn
+++ b/toolkit/mozapps/extensions/jar.mn
@@ -17,11 +17,13 @@ toolkit.jar:
   content/mozapps/extensions/blocklist.js                       (content/blocklist.js)
   content/mozapps/extensions/pluginPrefs.xul                    (content/pluginPrefs.xul)
   content/mozapps/extensions/pluginPrefs.js                     (content/pluginPrefs.js)
   content/mozapps/extensions/OpenH264-license.txt               (content/OpenH264-license.txt)
   content/mozapps/extensions/aboutaddons.html                   (content/aboutaddons.html)
   content/mozapps/extensions/aboutaddons.js                     (content/aboutaddons.js)
   content/mozapps/extensions/aboutaddonsCommon.js               (content/aboutaddonsCommon.js)
   content/mozapps/extensions/aboutaddons.css                    (content/aboutaddons.css)
+  content/mozapps/extensions/message-bar.css                    (content/message-bar.css)
+  content/mozapps/extensions/message-bar.js                     (content/message-bar.js)
   content/mozapps/extensions/panel-list.css                     (content/panel-list.css)
   content/mozapps/extensions/panel-item.css                     (content/panel-item.css)
 #endif
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -72,16 +72,17 @@ skip-if = os == "linux" && !debug # Bug 
 [browser_extension_sideloading_permission.js]
 [browser_file_xpi_no_process_switch.js]
 skip-if = true # Bug 1449071 - Frequent failures
 [browser_globalwarnings.js]
 [browser_gmpProvider.js]
 skip-if = os == 'linux' && !debug # Bug 1398766
 [browser_html_detail_view.js]
 [browser_html_list_view.js]
+[browser_html_message_bar.js]
 [browser_html_plugins.js]
 skip-if = (os == 'win' && processor == 'aarch64') # aarch64 has no plugin support, bug 1525174 and 1547495
 [browser_html_recent_updates.js]
 [browser_html_updates.js]
 [browser_inlinesettings_browser.js]
 skip-if = os == 'mac' || os == 'linux' # Bug 1483347
 [browser_installssl.js]
 skip-if = verify
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+/* eslint max-len: ["error", 80] */
+
+let htmlAboutAddonsWindow;
+
+function clickElement(el) {
+  el.dispatchEvent(new CustomEvent("click"));
+}
+
+function createMessageBar(messageBarStack, {attrs, children, onclose} = {}) {
+  const win = messageBarStack.ownerGlobal;
+  const messageBar = win.document.createElement("message-bar");
+  if (attrs) {
+    for (const [k, v] of Object.entries(attrs)) {
+      messageBar.setAttribute(k, v);
+    }
+  }
+  if (children) {
+    if (Array.isArray(children)) {
+      messageBar.append(...children);
+    } else {
+      messageBar.append(children);
+    }
+  }
+  messageBar.addEventListener("message-bar:close", onclose, {once: true});
+  messageBarStack.append(messageBar);
+  return messageBar;
+}
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["extensions.htmlaboutaddons.enabled", true],
+    ],
+  });
+
+  htmlAboutAddonsWindow = await loadInitialView("extension");
+  registerCleanupFunction(async () => {
+    await closeView(htmlAboutAddonsWindow);
+  });
+});
+
+add_task(async function test_message_bar_stack() {
+  const win = htmlAboutAddonsWindow;
+
+  let messageBarStack = win.document.querySelector("message-bar-stack");
+
+  ok(messageBarStack, "Got a message-bar-stack in HTML about:addons page");
+
+  is(messageBarStack.maxMessageBarCount, 3,
+     "Got the expected max-message-bar-count property");
+
+  is(messageBarStack.childElementCount, 0,
+     "message-bar-stack is initially empty");
+});
+
+add_task(async function test_create_message_bar_create_and_onclose() {
+  const win = htmlAboutAddonsWindow;
+  const messageBarStack = win.document.querySelector("message-bar-stack");
+
+  let messageEl = win.document.createElement("span");
+  messageEl.textContent = "A message bar text";
+  let buttonEl = win.document.createElement("button");
+  buttonEl.textContent = "An action button";
+
+  let messageBar;
+  let onceMessageBarClosed = new Promise(resolve => {
+    messageBar = createMessageBar(messageBarStack, {
+      children: [messageEl, buttonEl],
+      onclose: resolve,
+    });
+  });
+
+  is(messageBarStack.childElementCount, 1,
+     "message-bar-stack has a child element");
+  is(messageBarStack.firstElementChild, messageBar,
+     "newly created message-bar added as message-bar-stack child element");
+
+  const slot = messageBar.shadowRoot.querySelector("slot");
+  is(slot.assignedNodes()[0], messageEl,
+     "Got the expected span element assigned to the message-bar slot");
+  is(slot.assignedNodes()[1], buttonEl,
+     "Got the expected button element assigned to the message-bar slot");
+
+  info("Click the close icon on the newly created message-bar");
+  clickElement(messageBar.shadowRoot.querySelector("button.close"));
+
+  info("Expect the onclose function to be called");
+  await onceMessageBarClosed;
+
+  is(messageBarStack.childElementCount, 0,
+     "message-bar-stack has no child elements");
+});
+
+add_task(async function test_max_message_bar_count() {
+  const win = htmlAboutAddonsWindow;
+  const messageBarStack = win.document.querySelector("message-bar-stack");
+
+  info("Create a new message-bar");
+  let messageElement = document.createElement("span");
+  messageElement = "message bar label";
+
+  let onceMessageBarClosed = new Promise(resolve => {
+    createMessageBar(messageBarStack, {
+      children: messageElement,
+      onclose: resolve,
+    });
+  });
+
+  is(messageBarStack.childElementCount, 1,
+     "message-bar-stack has the expected number of children");
+
+  info("Create 3 more message bars");
+  const allBarsPromises = [];
+  for (let i = 2; i <= 4; i++) {
+    allBarsPromises.push(new Promise(resolve => {
+      createMessageBar(messageBarStack, {
+        attrs: {dismissable: ""},
+        children: [messageElement, i],
+        onclose: resolve,
+      });
+    }));
+  }
+
+  info("Expect first message-bar to closed automatically");
+  await onceMessageBarClosed;
+
+  is(messageBarStack.childElementCount, 3,
+     "message-bar-stack has the expected number of children");
+
+  info("Click on close icon for the second message-bar");
+  clickElement(messageBarStack.firstElementChild._closeIcon);
+
+  info("Expect the second message-bar to be closed");
+  await allBarsPromises[0];
+
+  is(messageBarStack.childElementCount, 2,
+     "message-bar-stack has the expected number of children");
+
+  info("Clear the entire message-bar-stack content");
+  messageBarStack.textContent = "";
+
+  info("Expect all the created message-bar to be closed automatically");
+  await Promise.all(allBarsPromises);
+
+  is(messageBarStack.childElementCount, 0,
+     "message-bar-stack has no child elements");
+});