Bug 1550099 - Create basic custom element for a modal input. r=MattN
authorJared Wein <jwein@mozilla.com>
Tue, 14 May 2019 20:07:00 +0000
changeset 532684 c9f9e36d2af737667e3efd6ae89f34803fad7cd7
parent 532683 22be0302af4d107d45d7715def74909b2a0a7377
child 532685 abde24c1d7eee05672ed2b12e35d41869421b7db
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
bugs1550099
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 1550099 - Create basic custom element for a modal input. r=MattN Differential Revision: https://phabricator.services.mozilla.com/D30957
browser/components/aboutlogins/content/aboutLogins.html
browser/components/aboutlogins/content/components/modal-input.css
browser/components/aboutlogins/content/components/modal-input.js
browser/components/aboutlogins/jar.mn
browser/components/aboutlogins/tests/mochitest/mochitest.ini
browser/components/aboutlogins/tests/mochitest/test_modal_input.html
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -79,10 +79,17 @@
       <button class="cancel-button"></button>
     </template>
 
     <template id="login-filter-template">
       <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-filter.css">
       <input type="text"/>
     </template>
+
+    <template id="modal-input-template">
+      <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
+      <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/modal-input.css">
+      <span class="locked-value"></span>
+      <input type="text" class="unlocked-value"/>
+    </template>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/modal-input.css
@@ -0,0 +1,8 @@
+/* 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/. */
+
+:host([editing]) .locked-value,
+:host(:not([editing])) .unlocked-value {
+  display: none;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/modal-input.js
@@ -0,0 +1,85 @@
+/* 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 ModalInput extends HTMLElement {
+  static get LOCKED_PASSWORD_DISPLAY() {
+    return "••••••••";
+  }
+
+  connectedCallback() {
+    if (this.children.length) {
+      return;
+    }
+
+    let modalInputTemplate = document.querySelector("#modal-input-template");
+    this.attachShadow({mode: "open"})
+        .appendChild(modalInputTemplate.content.cloneNode(true));
+
+    if (this.hasAttribute("value")) {
+      this.value = this.getAttribute("value");
+    }
+
+    if (this.getAttribute("type") == "password") {
+      let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
+      unlockedValue.setAttribute("type", "password");
+    }
+  }
+
+  static get observedAttributes() {
+    return ["editing", "type", "value"];
+  }
+
+  attributeChangedCallback(attr, oldValue, newValue) {
+    if (!this.shadowRoot) {
+      return;
+    }
+
+    let lockedValue = this.shadowRoot.querySelector(".locked-value");
+    let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
+
+    switch (attr) {
+      case "editing": {
+        let isEditing = newValue !== null;
+        if (!isEditing) {
+          this.setAttribute("value", unlockedValue.value);
+        }
+        break;
+      }
+      case "type": {
+        if (newValue == "password") {
+          lockedValue.textContent = this.constructor.LOCKED_PASSWORD_DISPLAY;
+          unlockedValue.setAttribute("type", "password");
+        } else {
+          lockedValue.textContent = this.getAttribute("value");
+          unlockedValue.setAttribute("type", "text");
+        }
+        break;
+      }
+      case "value": {
+        this.value = newValue;
+        break;
+      }
+    }
+  }
+
+  get value() {
+    return this.hasAttribute("editing") ? this.shadowRoot.querySelector(".unlocked-value").value.trim()
+                                        : this.getAttribute("value") || "";
+  }
+
+  set value(val) {
+    if (this.getAttribute("value") != val) {
+      this.setAttribute("value", val);
+      return;
+    }
+    this.shadowRoot.querySelector(".unlocked-value").value = val;
+    let lockedValue = this.shadowRoot.querySelector(".locked-value");
+    if (this.getAttribute("type") == "password" && val && val.length) {
+      lockedValue.textContent = this.constructor.LOCKED_PASSWORD_DISPLAY;
+    } else {
+      lockedValue.textContent = val;
+    }
+  }
+}
+customElements.define("modal-input", ModalInput);
--- a/browser/components/aboutlogins/jar.mn
+++ b/browser/components/aboutlogins/jar.mn
@@ -6,12 +6,14 @@ 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/modal-input.css       (content/components/modal-input.css)
+  content/browser/aboutlogins/components/modal-input.js        (content/components/modal-input.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,14 +1,16 @@
 [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/modal-input.js
    ../../content/components/reflected-fluent-element.js
    aboutlogins_common.js
 
 [test_login_filter.html]
 [test_login_item.html]
 [test_login_list.html]
+[test_modal_input.html]
 [test_reflected_fluent_element.html]
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/mochitest/test_modal_input.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the modal-input component
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the modal-input component</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="modal-input.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 modal-input component **/
+
+let gModalInput;
+const TEST_INPUT_VALUE = "fakeValue";
+add_task(async function setup() {
+  let templateFrame = document.getElementById("templateFrame");
+  let displayEl = document.getElementById("display");
+  importDependencies(templateFrame, displayEl);
+
+  gModalInput = document.createElement("modal-input");
+  gModalInput.setAttribute("value", TEST_INPUT_VALUE);
+  displayEl.appendChild(gModalInput);
+});
+
+add_task(async function test_initial_state() {
+  ok(gModalInput, "modalInput exists");
+  is(gModalInput.shadowRoot.querySelector(".locked-value").textContent, TEST_INPUT_VALUE, "Values are set initially");
+  is(gModalInput.shadowRoot.querySelector(".unlocked-value").value, TEST_INPUT_VALUE, "Values are set initially");
+  is(getComputedStyle(gModalInput.shadowRoot.querySelector(".locked-value")).display, "inline", ".locked-value is visible by default");
+  is(getComputedStyle(gModalInput.shadowRoot.querySelector(".unlocked-value")).display, "none", ".unlocked-value is hidden by default");
+});
+
+add_task(async function test_editing_set_unset() {
+  let lockedValue = gModalInput.shadowRoot.querySelector(".locked-value");
+  let unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
+  gModalInput.setAttribute("editing", "");
+  is(getComputedStyle(lockedValue).display, "none", ".locked-value is hidden when editing");
+  is(getComputedStyle(unlockedValue).display, "inline", ".unlocked-value is visible when editing");
+
+  const NEW_VALUE = "editedValue";
+  SpecialPowers.wrap(unlockedValue).setUserInput(NEW_VALUE);
+  gModalInput.removeAttribute("editing");
+
+  is(lockedValue.textContent, NEW_VALUE, "Values are updated from edit");
+  is(unlockedValue.value, NEW_VALUE, "Values are updated from edit");
+  is(gModalInput.getAttribute("value"), NEW_VALUE, "The value attribute on the host element is updated from edit");
+  is(getComputedStyle(lockedValue).display, "inline", ".locked-value is visible when not editing");
+  is(getComputedStyle(unlockedValue).display, "none", ".unlocked-value is hidden when not editing");
+});
+
+add_task(async function test_password() {
+  gModalInput.setAttribute("type", "password");
+  let lockedValue = gModalInput.shadowRoot.querySelector(".locked-value");
+  let unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
+
+  is(lockedValue.textContent, gModalInput.constructor.LOCKED_PASSWORD_DISPLAY,
+     "type=password should display masked characters when locked");
+  is(unlockedValue.value, gModalInput.getAttribute("value"), "type=password should have actual value in .unlocked-value");
+  is(unlockedValue.getAttribute("type"), "password", "input[type=password] should be used for .unlocked-value with type=password");
+
+  gModalInput.removeAttribute("value");
+  is(lockedValue.textContent, "",
+     "type=password should display nothing when locked without a value (.locked-value)");
+  is(unlockedValue.value, "",
+     "type=password should display nothing when locked without a value (.unlocked-value)");
+});
+</script>
+
+</body>
+</html>