Bug 1603166 - Fix focus ring moving to the wrong addressing input field after a new pill is created. r=mkmelin
authorAlessandro Castellani <alessandro@thunderbird.net>
Fri, 24 Jan 2020 17:51:27 -0800
changeset 37128 75ad64af9fe2e20cffc78ad78a6a42291fc115ec
parent 37127 2dff5349a09ff6b7ad3c5febf222bf8e9b7eb414
child 37129 9dde84974661e23ca475ebd5c322a0eee6ccfeaf
push id2552
push userclokep@gmail.com
push dateMon, 10 Feb 2020 21:24:16 +0000
treeherdercomm-beta@f95a6f4408a3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1603166
Bug 1603166 - Fix focus ring moving to the wrong addressing input field after a new pill is created. r=mkmelin
mail/base/content/mailWidgets.js
mail/components/compose/content/MsgComposeCommands.js
--- a/mail/base/content/mailWidgets.js
+++ b/mail/base/content/mailWidgets.js
@@ -1710,36 +1710,33 @@
       };
     }
 
     connectedCallback() {
       if (this.hasChildNodes() || this.delayConnectedCallback()) {
         return;
       }
 
-      // Used to store the html:input element from where the pill was generated.
-      this.originalInput;
-
       this.classList.add("address-pill");
       this.setAttribute("context", "emailAddressPillPopup");
       this.setAttribute("allowevents", "true");
 
       this.pillLabel = document.createXULElement("label");
       this.pillLabel.classList.add("pill-label");
 
       this.appendChild(this.pillLabel);
       this._setupEmailInput();
 
       this._setupEventListeners();
       this.initializeAttributeInheritance();
 
       // @implements {nsIObserver}
       this.inputObserver = {
         observe: (subject, topic, data) => {
-          if (topic == "autocomplete-did-enter-text") {
+          if (topic == "autocomplete-did-enter-text" && this.isEditing) {
             this.updatePill();
           }
         },
       };
 
       Services.obs.addObserver(
         this.inputObserver,
         "autocomplete-did-enter-text"
@@ -1784,38 +1781,46 @@
     get displayName() {
       return this.getAttribute("displayName");
     }
 
     set displayName(val) {
       this.setAttribute("displayName", val);
     }
 
+    get emailInput() {
+      return this.querySelector(`input[is="autocomplete-input"]`);
+    }
+
+    /**
+     * Get the main addressing input field the pill belongs to.
+     */
+    get rowInput() {
+      return this.closest(".address-container").querySelector(
+        `input[is="autocomplete-input"][recipienttype]`
+      );
+    }
+
     /**
      * Check if the pill is currently in "Edit Mode", meaning the label is
      * hidden and the html:input field is visible.
      *
      * @returns {boolean} True if the pill is currently being edited.
      */
     get isEditing() {
       return !this.emailInput.hasAttribute("hidden");
     }
 
     get fragment() {
       if (!this.constructor.hasOwnProperty("_fragment")) {
         this.constructor._fragment = MozXULElement.parseXULToFragment(`
           <html:input is="autocomplete-input"
                       type="text"
-                      class="${this.originalInput.getAttribute(
-                        "class"
-                      )} input-pill"
+                      class="input-pill"
                       disableonsend="true"
-                      aria-labelledby="${this.originalInput.getAttribute(
-                        "aria-labelledby"
-                      )}"
                       autocompletesearch="mydomain addrbook ldap news"
                       autocompletesearchparam="{}"
                       timeout="300"
                       maxrows="6"
                       completedefaultindex="true"
                       forcecomplete="true"
                       completeselectedindex="true"
                       minresultsforpopup="2"
@@ -1823,32 +1828,17 @@
                       hidden="hidden"/>
         `);
       }
       return document.importNode(this.constructor._fragment, true);
     }
 
     _setupEmailInput() {
       this.appendChild(this.fragment);
-
-      this.emailInput = this.querySelector(`input[is="autocomplete-input"]`);
       this.emailInput.value = this.fullAddress;
-      this.emailInput.classList.remove("mail-primary-input");
-      this.emailInput.classList.remove("news-primary-input");
-
-      let params = JSON.parse(
-        this.emailInput.getAttribute("autocompletesearchparam")
-      );
-      params.type = this.originalInput.getAttribute("recipienttype");
-      this.emailInput.setAttribute(
-        "autocompletesearchparam",
-        JSON.stringify(params)
-      );
-
-      this.appendChild(this.emailInput);
     }
 
     _setupEventListeners() {
       this.emailInput.addEventListener("keypress", event => {
         this.finishEditing(event);
       });
 
       this.emailInput.addEventListener("blur", () => {
@@ -1904,30 +1894,30 @@
       switch (key) {
         case "Escape":
           this.emailInput.value = this.fullAddress;
           this.resetPill();
           break;
         case "Delete":
         case "Backspace":
           if (!this.emailInput.value.trim() && !event.repeat) {
-            this.originalInput.focus();
+            this.rowInput.focus();
             this.remove();
           }
           break;
       }
     }
 
     updatePill() {
       let addresses = MailServices.headerParser.makeFromDisplayAddress(
         this.emailInput.value
       );
 
       if (!addresses[0]) {
-        this.originalInput.focus();
+        this.rowInput.focus();
         this.remove();
         return;
       }
 
       this.label = addresses[0].toString();
       this.emailAddress = addresses[0].email || "";
       this.fullAddress = addresses[0].toString();
       this.displayName = addresses[0].name || "";
@@ -1945,17 +1935,17 @@
       let isValid = this.isValidAddress(this.emailAddress);
       let listNames = MimeParser.parseHeaderField(
         this.fullAddress,
         MimeParser.HEADER_ADDRESS
       );
       let isMailingList =
         listNames.length > 0 &&
         MailServices.ab.mailListNameExists(listNames[0].name);
-      let isNewsgroup = this.originalInput.classList.contains("news-input");
+      let isNewsgroup = this.emailInput.classList.contains("news-input");
 
       this.classList.toggle(
         "error",
         !isValid && !isMailingList && !isNewsgroup
       );
 
       let emailCard = DisplayNameUtils.getCardForEmail(this.emailAddress);
       this.classList.toggle(
@@ -1963,17 +1953,17 @@
         isValid && !emailCard.card && !isMailingList && !isNewsgroup
       );
 
       this.style.removeProperty("max-width");
       this.style.removeProperty("min-width");
       this.classList.remove("editing");
       this.pillLabel.removeAttribute("hidden");
       this.emailInput.setAttribute("hidden", "hidden");
-      this.originalInput.focus();
+      this.rowInput.focus();
     }
 
     removeObserver() {
       Services.obs.removeObserver(
         this.inputObserver,
         "autocomplete-did-enter-text"
       );
     }
@@ -2115,18 +2105,18 @@
      *
      * @param {HTMLElement} element - The original autocomplete input that
      *   generated the pill.
      * @param {Array} address - The array containing the recipient's info.
      */
     createRecipientPill(element, address) {
       let pill = document.createXULElement("mail-address-pill");
 
-      pill.originalInput = element;
       pill.label = address.toString();
+
       pill.emailAddress = address.email || "";
       pill.fullAddress = address.toString();
       pill.displayName = address.name || "";
       pill.setAttribute("recipienttype", element.getAttribute("recipienttype"));
 
       let listNames = MimeParser.parseHeaderField(
         address.toString(),
         MimeParser.HEADER_ADDRESS
@@ -2156,16 +2146,39 @@
       pill.addEventListener("dblclick", event => {
         this.startEditing(pill, event);
       });
       pill.addEventListener("keypress", event => {
         this.handleKeyPress(pill, event);
       });
 
       element.closest(".address-container").insertBefore(pill, element);
+
+      // The emailInput attribute is accessible only after the pill has been
+      // appended to the DOM.
+      let classes = element.getAttribute("class").split(" ");
+      var fixedClassed = classes.filter(value => {
+        return value != "mail-primary-input" && value != "news-primary-input";
+      });
+      for (let css of fixedClassed) {
+        pill.emailInput.classList.add(css);
+      }
+      pill.emailInput.setAttribute(
+        "aria-labelledby",
+        element.getAttribute("aria-labelledby")
+      );
+
+      let params = JSON.parse(
+        pill.emailInput.getAttribute("autocompletesearchparam")
+      );
+      params.type = element.getAttribute("recipienttype");
+      pill.emailInput.setAttribute(
+        "autocompletesearchparam",
+        JSON.stringify(params)
+      );
     }
 
     /**
      * Move the focus on the first pill from the same .address-container.
      *
      * @param {XULElement} pill - The mail-address-pill element.
      * @param {Event} event - The DOM Event.
      */
@@ -2207,17 +2220,17 @@
           break;
 
         case "Home":
           pill.removeAttribute("selected");
           this.setFocusOnFirstPill(pill);
           break;
 
         case "End":
-          pill.originalInput.focus();
+          pill.rowInput.focus();
           break;
 
         case "Tab":
           for (let item of this.getSiblingPills(pill)) {
             item.removeAttribute("selected");
           }
           break;
 
@@ -2312,21 +2325,25 @@
 
     /**
      * When a "Delete" action is triggered, we need to check if other pills are
      * currently selected and delete them all.
      *
      * @param {XULElement} pill - The mail-address-pill element.
      */
     removePills(pill) {
+      // Store the addressing input that needs to receive focus before deleting
+      // the pill.
+      let rowInput = pill.rowInput;
       for (let item of this.getAllSelectedPills()) {
         item.remove();
       }
 
-      pill.originalInput.focus();
+      rowInput.focus();
+      // Manually remove the pill in case it wasn't part of the selected array.
       pill.remove();
 
       onRecipientsChanged();
       calculateHeaderHeight();
     }
 
     /**
      * Move the focus on the first pill from the same .address-container.
--- a/mail/components/compose/content/MsgComposeCommands.js
+++ b/mail/components/compose/content/MsgComposeCommands.js
@@ -3956,17 +3956,17 @@ function updateSendLock() {
       );
       let isMailingList =
         listNames.length > 0 &&
         MailServices.ab.mailListNameExists(listNames[0].name);
 
       if (
         isValidAddress(address.emailAddress) ||
         isMailingList ||
-        address.originalInput.classList.contains("news-input")
+        address.emailInput.classList.contains("news-input")
       ) {
         gSendLocked = false;
         break;
       }
     }
   }
 }