Bug 1549809 - Reduce duplication of reflected Fluent strings. r?MattN draft
authorpulselistener
Sun, 12 May 2019 01:50:07 +0000
changeset 1993929 beb84994d5090242e95d5814ffe573cb48aa0355
parent 1993928 2308394ad8ff640c72ee2675d9249fce7c97b0c4
child 1993930 429c32450a93177ed6337a3ccbca96a0a551506a
push id360349
push userreviewbot
push dateSun, 12 May 2019 01:50:36 +0000
treeherdertry@429c32450a93 [default view] [failures only]
reviewersMattN
bugs1549809
milestone68.0a1
Bug 1549809 - Reduce duplication of reflected Fluent strings. r?MattN 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 Differential Diff: PHID-DIFF-7gvbpcswotxtoj5qk74g
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
--- 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,14 +1,18 @@
 /* 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() {
+    super.connectedCallback();
+
     if (this.children.length) {
       return;
     }
 
     let loginFilterTemplate = document.querySelector("#login-filter-template");
     this.attachShadow({mode: "open"})
         .appendChild(loginFilterTemplate.content.cloneNode(true));
 
@@ -27,23 +31,18 @@ class LoginFilter extends HTMLElement {
       }
     }
   }
 
   static get observedAttributes() {
     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;
+  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,19 +1,23 @@
 /* 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() {
+    super.connectedCallback();
+
     if (this.children.length) {
       this.render();
       return;
     }
 
     let loginItemTemplate = document.querySelector("#login-item-template");
     this.attachShadow({mode: "open"})
         .appendChild(loginItemTemplate.content.cloneNode(true));
@@ -41,29 +45,16 @@ class LoginItem extends HTMLElement {
       "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;
-  }
-
   render() {
     let l10nArgs = {
       timeCreated: this._login.timeCreated || "",
       timeChanged: this._login.timePasswordChanged || "",
       timeUsed: this._login.timeLastUsed || "",
     };
     document.l10n.setAttributes(this, "login-item", l10nArgs);
     let hostnameNoScheme = this._login.hostname && new URL(this._login.hostname).hostname;
--- a/browser/components/aboutlogins/content/components/login-list.js
+++ b/browser/components/aboutlogins/content/components/login-list.js
@@ -1,22 +1,24 @@
 /* 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() {
+    super.connectedCallback();
+
     if (this.children.length) {
       return;
     }
     let loginListTemplate = document.querySelector("#login-list-template");
     this.attachShadow({mode: "open"})
         .appendChild(loginListTemplate.content.cloneNode(true));
     this.render();
 
@@ -70,30 +72,16 @@ class LoginList extends HTMLElement {
       }
     }
   }
 
   static get observedAttributes() {
     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;
-    }
-  }
-
   setLogins(logins) {
     let list = this.shadowRoot.querySelector("ol");
     list.textContent = "";
     this._logins = logins;
     this.render();
   }
 
   loginAdded(login) {
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/reflected-fluent-element.js
@@ -0,0 +1,59 @@
+/* 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.observedAttributes.includes(attr.name);
+  }
+
+  /* Apply any localized strings that Fluent may have applied to the element
+     before the custom element was defined. */
+  async connectedCallback() {
+    if (![...this.attributes].some(this._isReflectedAttributePresent.bind(this))) {
+      return;
+    }
+
+    // Let the subclass complete their connectedCallback so the
+    // shadow DOM will be constructed.
+    await Promise.resolve();
+
+    if (!this.shadowRoot) {
+      return;
+    }
+
+    for (let observedAttribute of this.constructor.observedAttributes) {
+      if (this.hasAttribute(observedAttribute)) {
+        if (this.handleSpecialCaseFluentString &&
+            this.handleSpecialCaseFluentString(observedAttribute)) {
+          continue;
+        }
+
+        let attrValue = this.getAttribute(observedAttribute);
+        // 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("." + observedAttribute);
+        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;
+    }
+
+    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,13 @@
 [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]
--- a/browser/components/aboutlogins/tests/mochitest/test_login_filter.html
+++ b/browser/components/aboutlogins/tests/mochitest/test_login_filter.html
@@ -2,16 +2,17 @@
 <html>
 <!--
 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="reflected-fluent-element.js"></script>
   <script src="login-filter.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_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">