Bug 1549809 - Reduce duplication of reflected Fluent strings. r=MattN,Pike
authorJared Wein <jwein@mozilla.com>
Tue, 14 May 2019 20:06:28 +0000
changeset 532683 22be0302af4d107d45d7715def74909b2a0a7377
parent 532682 ae45390d506114e7ffd97d35918e63b6912252e7
child 532684 c9f9e36d2af737667e3efd6ae89f34803fad7cd7
push id11270
push userrgurzau@mozilla.com
push dateWed, 15 May 2019 15:07:19 +0000
treeherdermozilla-beta@571bc76da583 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, Pike
bugs1549809
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 1549809 - Reduce duplication of reflected Fluent strings. r=MattN,Pike This patch also fixes a bug where the custom elements wouldn't display their localized text if the attributes were updated before the custom element was defined. Differential Revision: https://phabricator.services.mozilla.com/D30800
browser/components/aboutlogins/content/aboutLogins.html
browser/components/aboutlogins/content/components/login-filter.js
browser/components/aboutlogins/content/components/login-item.js
browser/components/aboutlogins/content/components/login-list.js
browser/components/aboutlogins/content/components/reflected-fluent-element.js
browser/components/aboutlogins/jar.mn
browser/components/aboutlogins/tests/mochitest/mochitest.ini
browser/components/aboutlogins/tests/mochitest/test_login_filter.html
browser/components/aboutlogins/tests/mochitest/test_login_item.html
browser/components/aboutlogins/tests/mochitest/test_login_list.html
browser/components/aboutlogins/tests/mochitest/test_reflected_fluent_element.html
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -4,16 +4,17 @@
 
 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'; script-src resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';"/>
     <title data-l10n-id="about-logins-page-title"></title>
     <link rel="localization" href="browser/aboutLogins.ftl">
+    <script defer="defer" src="chrome://browser/content/aboutlogins/components/reflected-fluent-element.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-filter.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-item.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-list.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-list-item.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/aboutLogins.js"></script>
     <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
     <link rel="stylesheet" type="text/css" href="chrome://browser/content/aboutlogins/aboutLogins.css">
   </head>
--- a/browser/components/aboutlogins/content/components/login-filter.js
+++ b/browser/components/aboutlogins/content/components/login-filter.js
@@ -1,49 +1,52 @@
 /* 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/. */
 
-class LoginFilter extends HTMLElement {
+/* globals ReflectedFluentElement */
+
+class LoginFilter extends ReflectedFluentElement {
   connectedCallback() {
     if (this.children.length) {
       return;
     }
 
     let loginFilterTemplate = document.querySelector("#login-filter-template");
     this.attachShadow({mode: "open"})
         .appendChild(loginFilterTemplate.content.cloneNode(true));
 
+    this.reflectFluentStrings();
+
     this.addEventListener("input", this);
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "input": {
         this.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
           bubbles: true,
           composed: true,
           detail: event.originalTarget.value,
         }));
         break;
       }
     }
   }
 
-  static get observedAttributes() {
+  static get reflectedFluentIDs() {
     return ["placeholder"];
   }
 
-  /* Fluent doesn't handle localizing into Shadow DOM yet so strings
-     need to get reflected in to their targeted element. */
-  attributeChangedCallback(attr, oldValue, newValue) {
-    if (!this.shadowRoot) {
-      return;
+  static get observedAttributes() {
+    return this.reflectedFluentIDs;
+  }
+
+  handleSpecialCaseFluentString(attrName) {
+    if (attrName != "placeholder") {
+      return false;
     }
 
-    switch (attr) {
-      case "placeholder":
-        this.shadowRoot.querySelector("input").placeholder = newValue;
-        break;
-    }
+    this.shadowRoot.querySelector("input").placeholder = this.getAttribute(attrName);
+    return true;
   }
 }
 customElements.define("login-filter", LoginFilter);
--- a/browser/components/aboutlogins/content/components/login-item.js
+++ b/browser/components/aboutlogins/content/components/login-item.js
@@ -1,67 +1,62 @@
 /* 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/. */
 
-class LoginItem extends HTMLElement {
+/* globals ReflectedFluentElement */
+
+class LoginItem extends ReflectedFluentElement {
   constructor() {
     super();
     this._login = {};
   }
 
   connectedCallback() {
     if (this.children.length) {
       this.render();
       return;
     }
 
     let loginItemTemplate = document.querySelector("#login-item-template");
     this.attachShadow({mode: "open"})
         .appendChild(loginItemTemplate.content.cloneNode(true));
 
+    this.reflectFluentStrings();
+
     for (let selector of [
       ".delete-button",
       ".save-changes-button",
       ".cancel-button",
     ]) {
       let button = this.shadowRoot.querySelector(selector);
       button.addEventListener("click", this);
     }
 
     window.addEventListener("AboutLoginsLoginSelected", this);
 
     this.render();
   }
 
-  static get observedAttributes() {
+  static get reflectedFluentIDs() {
     return [
       "cancel-button",
       "delete-button",
       "hostname-label",
       "password-label",
       "save-changes-button",
       "time-created",
       "time-changed",
       "time-used",
       "username-label",
     ];
   }
 
-  /* Fluent doesn't handle localizing into Shadow DOM yet so strings
-     need to get reflected in to their targeted element. */
-  attributeChangedCallback(attr, oldValue, newValue) {
-    if (!this.shadowRoot) {
-      return;
-    }
-
-    // Strings that are reflected to their shadowed element are assigned
-    // to an attribute name that matches a className on the element.
-    let shadowedElement = this.shadowRoot.querySelector("." + attr);
-    shadowedElement.textContent = newValue;
+  static get observedAttributes() {
+    return this.reflectedFluentIDs;
   }
 
   render() {
     let l10nArgs = {
       timeCreated: this._login.timeCreated || "",
       timeChanged: this._login.timePasswordChanged || "",
       timeUsed: this._login.timeLastUsed || "",
     };
--- a/browser/components/aboutlogins/content/components/login-list.js
+++ b/browser/components/aboutlogins/content/components/login-list.js
@@ -1,28 +1,31 @@
 /* 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/. */
 
-/* globals LoginListItem */
+/* globals ReflectedFluentElement, LoginListItem */
 
-class LoginList extends HTMLElement {
+class LoginList extends ReflectedFluentElement {
   constructor() {
     super();
     this._logins = [];
     this._selectedItem = null;
   }
 
   connectedCallback() {
     if (this.children.length) {
       return;
     }
     let loginListTemplate = document.querySelector("#login-list-template");
     this.attachShadow({mode: "open"})
         .appendChild(loginListTemplate.content.cloneNode(true));
+
+    this.reflectFluentStrings();
+
     this.render();
 
     window.addEventListener("AboutLoginsLoginSelected", this);
     window.addEventListener("AboutLoginsFilterLogins", this);
   }
 
   render() {
     let list = this.shadowRoot.querySelector("ol");
@@ -66,32 +69,22 @@ class LoginList extends HTMLElement {
         }
         this._selectedItem = this.shadowRoot.querySelector(`login-list-item[guid="${event.detail.guid}"]`);
         this._selectedItem.classList.toggle("selected", true);
         break;
       }
     }
   }
 
-  static get observedAttributes() {
+  static get reflectedFluentIDs() {
     return ["count"];
   }
 
-  /* Fluent doesn't handle localizing into Shadow DOM yet so strings
-     need to get reflected in to their targeted element. */
-  attributeChangedCallback(attr, oldValue, newValue) {
-    if (!this.shadowRoot) {
-      return;
-    }
-
-    switch (attr) {
-      case "count":
-        this.shadowRoot.querySelector(".count").textContent = newValue;
-        break;
-    }
+  static get observedAttributes() {
+    return this.reflectedFluentIDs;
   }
 
   setLogins(logins) {
     let list = this.shadowRoot.querySelector("ol");
     list.textContent = "";
     this._logins = logins;
     this.render();
   }
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/reflected-fluent-element.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+class ReflectedFluentElement extends HTMLElement {
+  _isReflectedAttributePresent(attr) {
+    return this.constructor.reflectedFluentIDs.includes(attr.name);
+  }
+
+  /* This function should be called to apply any localized strings that Fluent
+     may have applied to the element before the custom element was defined. */
+  reflectFluentStrings() {
+    for (let reflectedFluentID of this.constructor.reflectedFluentIDs) {
+      if (this.hasAttribute(reflectedFluentID)) {
+        if (this.handleSpecialCaseFluentString &&
+            this.handleSpecialCaseFluentString(reflectedFluentID)) {
+          continue;
+        }
+
+        let attrValue = this.getAttribute(reflectedFluentID);
+        // Strings that are reflected to their shadowed element are assigned
+        // to an attribute name that matches a className on the element.
+        let shadowedElement = this.shadowRoot.querySelector("." + reflectedFluentID);
+        shadowedElement.textContent = attrValue;
+      }
+    }
+  }
+
+  /* Fluent doesn't handle localizing into Shadow DOM yet so strings
+     need to get reflected in to their targeted element. */
+  attributeChangedCallback(attr, oldValue, newValue) {
+    if (!this.shadowRoot) {
+      return;
+    }
+
+    // Don't respond to attribute changes that aren't related to locale text.
+    if (!this.constructor.reflectedFluentIDs.includes(attr)) {
+      return;
+    }
+
+    if (this.handleSpecialCaseFluentString &&
+        this.handleSpecialCaseFluentString(attr)) {
+      return;
+    }
+
+    // Strings that are reflected to their shadowed element are assigned
+    // to an attribute name that matches a className on the element.
+    let shadowedElement = this.shadowRoot.querySelector("." + attr);
+    shadowedElement.textContent = newValue;
+  }
+}
+customElements.define("reflected-fluent-element", ReflectedFluentElement);
--- a/browser/components/aboutlogins/jar.mn
+++ b/browser/components/aboutlogins/jar.mn
@@ -6,11 +6,12 @@ browser.jar:
   content/browser/aboutlogins/components/login-filter.css      (content/components/login-filter.css)
   content/browser/aboutlogins/components/login-filter.js       (content/components/login-filter.js)
   content/browser/aboutlogins/components/login-item.css        (content/components/login-item.css)
   content/browser/aboutlogins/components/login-item.js         (content/components/login-item.js)
   content/browser/aboutlogins/components/login-list.css        (content/components/login-list.css)
   content/browser/aboutlogins/components/login-list.js         (content/components/login-list.js)
   content/browser/aboutlogins/components/login-list-item.css   (content/components/login-list-item.css)
   content/browser/aboutlogins/components/login-list-item.js    (content/components/login-list-item.js)
+  content/browser/aboutlogins/components/reflected-fluent-element.js  (content/components/reflected-fluent-element.js)
   content/browser/aboutlogins/aboutLogins.css   (content/aboutLogins.css)
   content/browser/aboutlogins/aboutLogins.js    (content/aboutLogins.js)
   content/browser/aboutlogins/aboutLogins.html  (content/aboutLogins.html)
--- a/browser/components/aboutlogins/tests/mochitest/mochitest.ini
+++ b/browser/components/aboutlogins/tests/mochitest/mochitest.ini
@@ -1,12 +1,14 @@
 [DEFAULT]
 support-files =
    ../../content/aboutLogins.html
    ../../content/components/login-filter.js
    ../../content/components/login-item.js
    ../../content/components/login-list.js
    ../../content/components/login-list-item.js
+   ../../content/components/reflected-fluent-element.js
    aboutlogins_common.js
 
 [test_login_filter.html]
 [test_login_item.html]
 [test_login_list.html]
+[test_reflected_fluent_element.html]
--- a/browser/components/aboutlogins/tests/mochitest/test_login_filter.html
+++ b/browser/components/aboutlogins/tests/mochitest/test_login_filter.html
@@ -3,16 +3,17 @@
 <!--
 Test the login-filter component
 -->
 <head>
   <meta charset="utf-8">
   <title>Test the login-filter component</title>
   <script src="/tests/SimpleTest/SimpleTest.js"></script>
   <script src="/tests/SimpleTest/EventUtils.js"></script>
+  <script src="reflected-fluent-element.js"></script>
   <script src="login-filter.js"></script>
   <script src="login-list-item.js"></script>
   <script src="login-list.js"></script>
   <script src="aboutlogins_common.js"></script>
 
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
--- a/browser/components/aboutlogins/tests/mochitest/test_login_item.html
+++ b/browser/components/aboutlogins/tests/mochitest/test_login_item.html
@@ -2,16 +2,17 @@
 <html>
 <!--
 Test the login-item component
 -->
 <head>
   <meta charset="utf-8">
   <title>Test the login-item component</title>
   <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="reflected-fluent-element.js"></script>
   <script src="login-item.js"></script>
   <script src="aboutlogins_common.js"></script>
 
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
   </p>
--- a/browser/components/aboutlogins/tests/mochitest/test_login_list.html
+++ b/browser/components/aboutlogins/tests/mochitest/test_login_list.html
@@ -2,16 +2,17 @@
 <html>
 <!--
 Test the login-list component
 -->
 <head>
   <meta charset="utf-8">
   <title>Test the login-list component</title>
   <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="reflected-fluent-element.js"></script>
   <script src="login-list-item.js"></script>
   <script src="login-list.js"></script>
   <script src="aboutlogins_common.js"></script>
 
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/mochitest/test_reflected_fluent_element.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the reflected-fluent-element component
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the reflected-fluent-element component</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="reflected-fluent-element.js"></script>
+  <script src="aboutlogins_common.js"></script>
+
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+  </p>
+<div id="content" style="display: none">
+  <iframe id="templateFrame" src="aboutLogins.html"
+          sandbox="allow-same-origin"></iframe>
+</div>
+<pre id="test">
+</pre>
+<script>
+/** Test the reflected-fluent-element component **/
+
+const TEST_STRINGS = {
+  loginFilter: {
+    placeholder: "Sample placeholder",
+  },
+  loginItem: {
+    "cancel-button": "Cancel",
+    "delete-button": "Delete",
+    "hostname-label": "Website Address",
+    "password-label": "Password",
+    "save-changes-button": "Save Changes",
+    // See stubFluentL10n for the following three
+    "time-created": "",
+    "time-changed": "",
+    "time-used": "",
+    "username-label": "Username",
+  },
+};
+
+let gLoginFilter;
+let gLoginItem;
+add_task(async function setup() {
+  stubFluentL10n({
+    "time-created": "timeCreated",
+    "time-changed": "timeChanged",
+    "time-used": "timeUsed",
+  });
+
+  let displayEl = document.getElementById("display");
+
+  // Create and append the login-filter element before its template
+  // is cloned the custom element defined.
+  gLoginFilter = document.createElement("login-filter");
+  gLoginFilter.setAttribute("placeholder", TEST_STRINGS.loginFilter.placeholder);
+  displayEl.appendChild(gLoginFilter);
+
+  // ... and do the same with the login-item.
+  gLoginItem = document.createElement("login-item");
+  for (let attrKey of Object.keys(TEST_STRINGS.loginItem)) {
+    gLoginItem.setAttribute(attrKey, TEST_STRINGS.loginItem[attrKey]);
+  }
+  displayEl.appendChild(gLoginItem);
+
+  let templateFrame = document.getElementById("templateFrame");
+  importDependencies(templateFrame, displayEl);
+
+  // The script needs to be inserted after the element and template are appended
+  // to match the environment of the locale text being applied before the custom
+  // element is defined.
+  for (let scriptSrc of ["login-filter.js", "login-item.js", "login-list.js"]) {
+    let scriptEl = document.createElement("script");
+    scriptEl.setAttribute("src", scriptSrc);
+    document.head.appendChild(scriptEl);
+  }
+});
+
+add_task(async function test_placeholder_on_login_filter() {
+  ok(gLoginFilter, "loginFilter exists");
+  await SimpleTest.promiseWaitForCondition(() => !!gLoginFilter.shadowRoot, "Wait for shadowRoot");
+  is(gLoginFilter.shadowRoot.querySelector("input").placeholder,
+     TEST_STRINGS.loginFilter.placeholder,
+     "Placeholder text should be present when set before the element is defined");
+});
+
+add_task(async function test_login_item() {
+  ok(gLoginItem, "loginItem exists");
+  await SimpleTest.promiseWaitForCondition(() => !!gLoginItem.shadowRoot, "Wait for shadowRoot");
+
+  for (let attrKey of Object.keys(TEST_STRINGS.loginItem)) {
+    let selector = "." + attrKey;
+    is(gLoginItem.shadowRoot.querySelector(selector).textContent,
+       TEST_STRINGS.loginItem[attrKey],
+       selector + " textContent should be present when set before the element is defined");
+  }
+});
+
+add_task(async function test_attribute_changed_callback() {
+  let displayEl = document.getElementById("display");
+  let loginList = document.createElement("login-list");
+  displayEl.appendChild(loginList);
+  await SimpleTest.promiseWaitForCondition(() => !!loginList.shadowRoot, "Wait for element to get templated");
+
+  loginList.setAttribute("count", "1234");
+  await SimpleTest.promiseWaitForCondition(() => loginList.shadowRoot.querySelector(".count").textContent.includes("1234"),
+                                           "Wait for text to get localized");
+  ok(loginList.shadowRoot.querySelector(".count").textContent.includes("1234"),
+     "The count attribute should be inherited by the .count span");
+});
+</script>
+
+</body>
+</html>