Bug 1601740 - Wrap the recipients area of the compose window into a Custom Element. r=mkmelin
authorAlessandro Castellani <alessandro@thunderbird.net>
Fri, 03 Jan 2020 23:44:00 +0200
changeset 36982 1f8ec0da7271b230c9e4a088207e275bfa92a6f1
parent 36981 c39faa95a2606fbebc561272cf483300db8c9ce8
child 36983 d0bcd93317f907d7475f2722846af5c9ecf333f1
push id2543
push userclokep@gmail.com
push dateMon, 06 Jan 2020 23:26:40 +0000
treeherdercomm-beta@323890d47ddf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1601740
Bug 1601740 - Wrap the recipients area of the compose window into a Custom Element. r=mkmelin
mail/base/content/mailWidgets.js
mail/components/compose/content/MsgComposeCommands.js
mail/components/compose/content/addressingWidgetOverlay.js
mail/components/compose/content/messengercompose.xhtml
mail/themes/shared/mail/messengercompose.css
--- a/mail/base/content/mailWidgets.js
+++ b/mail/base/content/mailWidgets.js
@@ -10,17 +10,17 @@
 /* global openUILink */
 /* global MessageIdClick */
 /* global onClickEmailStar */
 /* global onClickEmailPresence */
 /* global gFolderDisplay */
 /* global UpdateEmailNodeDetails */
 /* global PluralForm */
 /* global UpdateExtraAddressProcessing */
-/* global getSiblingPills setFocusOnFirstPill getAllPills getAllSelectedPills onRecipientsChanged */
+/* global onRecipientsChanged */
 
 // Wrap in a block to prevent leaking to window scope.
 {
   const { Services } = ChromeUtils.import(
     "resource://gre/modules/Services.jsm"
   );
   const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
   const { MailServices } = ChromeUtils.import(
@@ -1778,22 +1778,18 @@
 
       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.pillDeleteImage = document.createXULElement("image");
-      this.pillDeleteImage.classList.add("delete-pill-icon");
-
       this.appendChild(this.pillLabel);
       this._setupEmailInput();
-      this.appendChild(this.pillDeleteImage);
 
       this._setupEventListeners();
       this.initializeAttributeInheritance();
 
       // @implements {nsIObserver}
       this.inputObserver = {
         observe: (subject, topic, data) => {
           if (topic == "autocomplete-did-enter-text") {
@@ -1902,215 +1898,47 @@
         "autocompletesearchparam",
         JSON.stringify(params)
       );
 
       this.appendChild(this.emailInput);
     }
 
     _setupEventListeners() {
-      this.addEventListener("click", this);
-      this.addEventListener("dblclick", this);
-      this.addEventListener("keypress", this);
-
       this.emailInput.addEventListener("keypress", event => {
         this.finishEditing(event);
       });
 
       this.emailInput.addEventListener("blur", () => {
         this.updatePill();
       });
-
-      this.pillDeleteImage.addEventListener("click", () => {
-        this.removePills();
-      });
-    }
-
-    handleEvent(event) {
-      switch (event.type) {
-        case "click":
-          this.checkSelected(event);
-          break;
-        case "dblclick":
-          this.startEditing(event);
-          break;
-        case "keypress":
-          this.handleKeyPress(event);
-          break;
-      }
-    }
-
-    handleKeyPress(event) {
-      if (this.isEditing) {
-        return;
-      }
-
-      switch (event.key) {
-        case " ":
-          this.checkSelected(event);
-          break;
-
-        case "Enter":
-        case "F2": // For Windows users
-          this.startEditing(event);
-          break;
-
-        case "Delete":
-        case "Backspace":
-          this.removePills();
-          break;
-
-        case "ArrowLeft":
-          if (this.previousElementSibling) {
-            this.previousElementSibling.focus();
-            this.checkKeyboardSelected(event, this.previousElementSibling);
-          }
-          break;
-
-        case "ArrowRight":
-          if (this.nextElementSibling.hasAttribute("hidden")) {
-            this.nextElementSibling.removeAttribute("hidden");
-            this.nextElementSibling.focus();
-            break;
-          }
-          this.nextElementSibling.focus();
-          this.checkKeyboardSelected(event, this.nextElementSibling);
-          break;
-
-        case "Home":
-          this.removeAttribute("selected");
-          setFocusOnFirstPill(this);
-          break;
-
-        case "End":
-          this.originalInput.focus();
-          break;
-
-        case "Tab":
-          for (let pill of getSiblingPills(this)) {
-            pill.removeAttribute("selected");
-          }
-          break;
-
-        case "a":
-          if (event.ctrlKey || event.metaKey) {
-            this.selectPills();
-          }
-          break;
-
-        case "c":
-          if (event.ctrlKey || event.metaKey) {
-            copyEmailNewsAddress(this);
-          }
-          break;
-
-        case "x":
-          if (event.ctrlKey || event.metaKey) {
-            copyEmailNewsAddress(this);
-            deleteAddressPill(this);
-          }
-          break;
-      }
-    }
-
-    selectPills() {
-      for (let pill of getSiblingPills(this)) {
-        pill.setAttribute("selected", "selected");
-      }
-    }
-
-    clearSelected() {
-      for (let pill of getAllPills()) {
-        pill.removeAttribute("selected");
-      }
-    }
-
-    checkSelected(event) {
-      if (
-        this.isEditing ||
-        (this.hasAttribute("selected") && event.which == 3)
-      ) {
-        return;
-      }
-
-      if (!event.ctrlKey && !event.metaKey && event.key != " ") {
-        this.clearSelected();
-      }
-
-      this.toggleAttribute("selected");
-      if (!this.hasAttribute("selected") && event.key != " ") {
-        this.blur();
-      } else {
-        this.focus();
-      }
-    }
-
-    checkKeyboardSelected(event, element) {
-      if (event.shiftKey) {
-        if (this.hasAttribute("selected") && element.hasAttribute("selected")) {
-          this.removeAttribute("selected");
-          return;
-        }
-
-        this.setAttribute("selected", "selected");
-        element.setAttribute("selected", "selected");
-      } else if (!event.ctrlKey) {
-        this.clearSelected();
-      }
-    }
-
-    /**
-     * When a "Delete" action is triggered, we need to check if other pills are
-     * currently selected and delete them all.
-     */
-    removePills() {
-      for (let pill of getAllSelectedPills()) {
-        pill.remove();
-      }
-
-      this.originalInput.focus();
-      this.remove();
-
-      onRecipientsChanged();
     }
 
     /**
      * Simple email address validation.
      *
      * @param {String} address - An email address.
      */
     isValidAddress(address) {
       return address.includes("@", 1) && !address.endsWith("@");
     }
 
     /**
-     * Convert the pill into "Edit Mode", meaning hiding the label and showing
-     * the html:input element.
-     *
-     * @param {Event} event - The DOM Event.
+     * Convert the pill into "Edit Mode" by hiding the label and showing the
+     * html:input element.
      */
-    startEditing(event) {
-      if (this.isEditing) {
-        event.stopPropagation();
-        return;
-      }
-
-      for (let pill of getAllPills()) {
-        pill.finishEditing();
-      }
-
+    startEditing() {
       // We need to set the min and max width before hiding and showing the
       // child nodes in order to prevent unwanted jumps in the resizing of the
       // edited pill. Both properties are necessary to handle flexbox.
       this.style.setProperty("max-width", `${this.clientWidth}px`);
       this.style.setProperty("min-width", `${this.clientWidth}px`);
 
       this.classList.add("editing");
       this.pillLabel.setAttribute("hidden", "true");
-      this.pillDeleteImage.setAttribute("hidden", "true");
       this.emailInput.removeAttribute("hidden");
       this.emailInput.focus();
 
       // In case the original address is shorter than the input field child node
       // force resize the pill container to prevent overflows.
       if (this.emailInput.clientWidth > this.clientWidth) {
         this.style.setProperty("max-width", `${this.emailInput.clientWidth}px`);
         this.style.setProperty("min-width", `${this.emailInput.clientWidth}px`);
@@ -2190,23 +2018,421 @@
         "warning",
         isValid && !emailCard.card && !isMailingList && !isNewsgroup
       );
 
       this.style.removeProperty("max-width");
       this.style.removeProperty("min-width");
       this.classList.remove("editing");
       this.pillLabel.removeAttribute("hidden");
-      this.pillDeleteImage.removeAttribute("hidden");
       this.emailInput.setAttribute("hidden", "hidden");
       this.originalInput.focus();
     }
 
     removeObserver() {
       Services.obs.removeObserver(
         this.inputObserver,
         "autocomplete-did-enter-text"
       );
     }
   }
 
   customElements.define("mail-address-pill", MailAddressPill);
+
+  /**
+   * The MailRecipientsArea widget is used to display the recipient rows in the
+   * header area of the messengercompose.xul window.
+   *
+   * @extends {MozXULElement}
+   */
+  class MailRecipientsArea extends MozXULElement {
+    connectedCallback() {
+      if (this.delayConnectedCallback() || this.hasConnected) {
+        return;
+      }
+      this.hasConnected = true;
+
+      this.highlightNonMatches = Services.prefs.getBoolPref(
+        "mail.autoComplete.highlightNonMatches"
+      );
+
+      for (let input of this.querySelectorAll(".pop-imap-input,.nntp-input")) {
+        setupAutocompleteInput(input, this.highlightNonMatches);
+      }
+    }
+
+    /**
+     * Create a new recipient row container with the input autocomplete.
+     *
+     * @param {Array} recipient - The unique identifier of the email header.
+     * @returns {XULElement} - The newly created recipient row.
+     */
+    buildRecipientRows(recipient) {
+      let row = document.createXULElement("hbox");
+      row.setAttribute("id", recipient.row);
+      row.classList.add("addressingWidgetItem", "address-row");
+
+      let firstCol = document.createXULElement("hbox");
+      firstCol.classList.add("aw-firstColBox");
+
+      row.classList.add("hidden");
+
+      let firstLabel = document.createXULElement("label");
+      let labelId =
+        recipient.type == "addr_other" ? recipient.labelId : recipient.type;
+      firstLabel.addEventListener("click", () => {
+        hideAddressRow(firstLabel, labelId);
+      });
+      firstLabel.addEventListener("keypress", event => {
+        if (event.key == "Enter") {
+          hideAddressRow(firstLabel, labelId);
+        }
+      });
+      // Necessary to allow focus via TAB key.
+      firstLabel.setAttribute("tabindex", 0);
+
+      let closeImage = document.createXULElement("image");
+      closeImage.classList.add("close-icon");
+
+      firstLabel.appendChild(closeImage);
+      firstCol.appendChild(firstLabel);
+      row.appendChild(firstCol);
+
+      let labelContainer = document.createXULElement("hbox");
+      labelContainer.setAttribute("align", "top");
+      labelContainer.setAttribute("pack", "end");
+      labelContainer.setAttribute("flex", 1);
+      labelContainer.classList.add("address-label-container");
+      labelContainer.setAttribute(
+        "style",
+        getComposeBundle().getString("headersSpaceStyle")
+      );
+
+      let label = document.createXULElement("label");
+      label.setAttribute("id", recipient.label);
+      label.setAttribute("value", recipient.labelId);
+      label.setAttribute("control", recipient.id);
+      label.setAttribute("flex", 1);
+      label.setAttribute("crop", "end");
+      labelContainer.appendChild(label);
+      row.appendChild(labelContainer);
+
+      let inputContainer = document.createXULElement("hbox");
+      inputContainer.setAttribute("id", recipient.container);
+      inputContainer.setAttribute("flex", 1);
+      inputContainer.setAttribute("align", "center");
+      inputContainer.classList.add(
+        "input-container",
+        "wrap-container",
+        "address-container"
+      );
+      inputContainer.addEventListener("click", focusAddressInput);
+
+      let input = document.createElement("input", {
+        is: "autocomplete-input",
+      });
+      input.setAttribute("id", recipient.id);
+
+      input.setAttribute("type", "text");
+      input.classList.add("plain", "address-input", recipient.class);
+      input.setAttribute("disableonsend", true);
+      input.setAttribute("autocompletesearch", "mydomain addrbook ldap news");
+      input.setAttribute("autocompletesearchparam", "{}");
+      input.setAttribute("timeout", 300);
+      input.setAttribute("maxrows", 6);
+      input.setAttribute("completedefaultindex", true);
+      input.setAttribute("forcecomplete", true);
+      input.setAttribute("completeselectedindex", true);
+      input.setAttribute("minresultsforpopup", 2);
+      input.setAttribute("ignoreblurwhilesearching", true);
+
+      input.addEventListener("focus", () => {
+        highlightAddressContainer(input);
+      });
+      input.addEventListener("blur", () => {
+        resetAddressContainer(input);
+      });
+      input.addEventListener("keypress", event => {
+        recipientKeyPress(event, input);
+      });
+
+      input.setAttribute("recipienttype", recipient.type);
+      input.setAttribute("size", 1);
+
+      setupAutocompleteInput(input, this.highlightNonMatches);
+
+      inputContainer.appendChild(input);
+
+      row.appendChild(inputContainer);
+
+      return row;
+    }
+
+    /**
+     * Create a new recipient pill.
+     *
+     * @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
+      );
+      let isMailingList =
+        listNames.length > 0 &&
+        MailServices.ab.mailListNameExists(listNames[0].name);
+      let isNewsgroup = element.classList.contains("nntp-input");
+
+      pill.classList.toggle(
+        "error",
+        !isValidAddress(address.email) && !isMailingList && !isNewsgroup
+      );
+
+      let emailCard = DisplayNameUtils.getCardForEmail(address.email);
+      pill.classList.toggle(
+        "warning",
+        isValidAddress(address.email) &&
+          !emailCard.card &&
+          !isMailingList &&
+          !isNewsgroup
+      );
+
+      pill.addEventListener("click", event => {
+        this.checkSelected(pill, event);
+      });
+      pill.addEventListener("dblclick", event => {
+        this.startEditing(pill, event);
+      });
+      pill.addEventListener("keypress", event => {
+        this.handleKeyPress(pill, event);
+      });
+
+      element.closest(".address-container").insertBefore(pill, element);
+    }
+
+    /**
+     * 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.
+     */
+    handleKeyPress(pill, event) {
+      if (pill.isEditing) {
+        return;
+      }
+
+      switch (event.key) {
+        case "Enter":
+        case "F2": // For Windows users
+          this.startEditing(pill, event);
+          break;
+
+        case "Delete":
+        case "Backspace":
+          this.removePills(pill);
+          break;
+
+        case "ArrowLeft":
+          if (pill.previousElementSibling) {
+            pill.previousElementSibling.focus();
+            this.checkKeyboardSelected(event, pill.previousElementSibling);
+          }
+          break;
+
+        case "ArrowRight":
+          if (pill.nextElementSibling.hasAttribute("hidden")) {
+            pill.nextElementSibling.removeAttribute("hidden");
+            pill.nextElementSibling.focus();
+            break;
+          }
+          pill.nextElementSibling.focus();
+          this.checkKeyboardSelected(event, pill.nextElementSibling);
+          break;
+
+        case " ":
+          this.checkSelected(pill, event);
+          break;
+
+        case "Home":
+          pill.removeAttribute("selected");
+          this.setFocusOnFirstPill(pill);
+          break;
+
+        case "End":
+          pill.originalInput.focus();
+          break;
+
+        case "Tab":
+          for (let item of this.getSiblingPills(pill)) {
+            item.removeAttribute("selected");
+          }
+          break;
+
+        case "a":
+          if (event.ctrlKey || event.metaKey) {
+            this.selectPills(pill);
+          }
+          break;
+
+        case "c":
+          if (event.ctrlKey || event.metaKey) {
+            copyEmailNewsAddress(pill);
+          }
+          break;
+
+        case "x":
+          if (event.ctrlKey || event.metaKey) {
+            copyEmailNewsAddress(pill);
+            deleteAddressPill(pill);
+          }
+          break;
+      }
+    }
+
+    /**
+     * Handle the selection and focus of the pill elements on mouse events.
+     *
+     * @param {XULElement} pill - The mail-address-pill element.
+     * @param {Event} event - The DOM Event.
+     */
+    checkSelected(pill, event) {
+      if (
+        pill.isEditing ||
+        (pill.hasAttribute("selected") && event.which == 3)
+      ) {
+        return;
+      }
+
+      if (!event.ctrlKey && !event.metaKey && event.key != " ") {
+        this.clearSelected();
+      }
+
+      pill.toggleAttribute("selected");
+      if (!pill.hasAttribute("selected") && event.key != " ") {
+        pill.blur();
+      } else {
+        pill.focus();
+      }
+    }
+
+    /**
+     * Handle the selection and focus of the pill elements on keyboard
+     * navigation.
+     *
+     * @param {Event} event - The DOM Event.
+     * @param {XULElement} element - The mail-address-pill element.
+     */
+    checkKeyboardSelected(event, element) {
+      if (event.shiftKey) {
+        if (this.hasAttribute("selected") && element.hasAttribute("selected")) {
+          this.removeAttribute("selected");
+          return;
+        }
+
+        this.setAttribute("selected", "selected");
+        element.setAttribute("selected", "selected");
+      } else if (!event.ctrlKey) {
+        this.clearSelected();
+      }
+    }
+
+    clearSelected() {
+      for (let pill of this.getAllPills()) {
+        pill.removeAttribute("selected");
+      }
+    }
+
+    /**
+     * Trigger the pill.startEditing() method.
+     *
+     * @param {XULElement} pill - The mail-address-pill element.
+     * @param {Event} event - The DOM Event.
+     */
+    startEditing(pill, event) {
+      if (pill.isEditing) {
+        event.stopPropagation();
+        return;
+      }
+
+      pill.startEditing();
+    }
+
+    /**
+     * 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) {
+      for (let item of this.getAllSelectedPills()) {
+        item.remove();
+      }
+
+      pill.originalInput.focus();
+      pill.remove();
+
+      onRecipientsChanged();
+    }
+
+    /**
+     * Move the focus on the first pill from the same .address-container.
+     *
+     * @param {XULElement} pill - The mail-address-pill element.
+     */
+    setFocusOnFirstPill(pill) {
+      pill.closest(".address-container").firstElementChild.focus();
+    }
+
+    /**
+     * Select all the pills from the same .address-container.
+     *
+     * @param {XULElement} pill - The mail-address-pill element.
+     */
+    selectPills(pill) {
+      for (let item of this.getSiblingPills(pill)) {
+        item.setAttribute("selected", "selected");
+      }
+    }
+
+    /**
+     * Return all the pills from the same .address-container.
+     *
+     * @param {XULElement} pill - The mail-address-pill element.
+     * @return {Array} Array of mail-address-pill elements.
+     */
+    getSiblingPills(pill) {
+      return pill
+        .closest(".address-container")
+        .querySelectorAll("mail-address-pill");
+    }
+
+    /**
+     * Return all the pills currently available in the address area.
+     *
+     * @return {Array} Array of mail-address-pill elements.
+     */
+    getAllPills() {
+      return this.querySelectorAll("mail-address-pill");
+    }
+
+    /**
+     * Return all the selected pills currently available in the address area.
+     *
+     * @return {Array} Array of selected mail-address-pill elements.
+     */
+    getAllSelectedPills() {
+      return this.querySelectorAll("mail-address-pill[selected]");
+    }
+  }
+
+  customElements.define("mail-recipients-area", MailRecipientsArea);
 }
--- a/mail/components/compose/content/MsgComposeCommands.js
+++ b/mail/components/compose/content/MsgComposeCommands.js
@@ -1,13 +1,13 @@
 /* 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/. */
 
-/* global MozElements getSiblingPills */
+/* global MozElements */
 
 /* import-globals-from ../../../../mailnews/addrbook/content/abDragDrop.js */
 /* import-globals-from ../../../base/content/mailCore.js */
 /* import-globals-from ../../../base/content/utilityOverlay.js */
 /* import-globals-from addressingWidgetOverlay.js */
 /* import-globals-from ComposerCommands.js */
 /* import-globals-from editor.js */
 /* import-globals-from editorUtilities.js */
@@ -3397,33 +3397,43 @@ function WizCallback(state) {
 function ComposeLoad() {
   let otherHeaders = Services.prefs.getCharPref(
     "mail.compose.other.header",
     ""
   );
 
   AddMessageComposeOfflineQuitObserver();
 
-  setupAutocomplete();
-
   try {
     SetupCommandUpdateHandlers();
     // This will do migration, or create a new account if we need to.
     // We also want to open the account wizard if no identities are found
     let state = verifyAccounts(WizCallback, true);
 
     if (otherHeaders) {
       let addressingWidgetLabels = document.getElementById(
         "addressingWidgetLabels"
       );
       let recipientsContainer = document.getElementById("recipientsContainer");
 
       for (let header of otherHeaders.split(",")) {
+        let recipient = {
+          id: `${header}AddrInput`,
+          row: `addressRow${header}`,
+          label: `${header}AddrLabel`,
+          labelId: header,
+          container: `${header}AddrContainer`,
+          class: "nntp-input",
+          type: "addr_other",
+        };
+
         addressingWidgetLabels.appendChild(createRecipientLabel(header));
-        recipientsContainer.appendChild(createRecipientRow(header));
+        recipientsContainer.appendChild(
+          recipientsContainer.buildRecipientRows(recipient)
+        );
       }
     }
     if (state) {
       ComposeStartup(null);
     }
   } catch (ex) {
     Cu.reportError(ex);
     Services.prompt.alert(
@@ -3532,122 +3542,16 @@ function createRecipientLabel(labelID) {
   let newImage = document.createXULElement("image");
   newImage.classList.add("new-icon");
   label.prepend(newImage);
 
   return label;
 }
 
 /**
- * Create a new recipient row container with the input autocomplete.
- *
- * @param {string} labelID - The unique identifier of the email header.
- * @returns {XULElement} - The newly created recipient row.
- */
-function createRecipientRow(labelID) {
-  let row = document.createXULElement("hbox");
-  row.setAttribute("id", `addressRow${labelID}`);
-  row.classList.add("addressingWidgetItem", "address-row", "hidden");
-
-  let firstCol = document.createXULElement("hbox");
-  firstCol.setAttribute("align", "start");
-  firstCol.classList.add("aw-firstColBox");
-
-  let firstLabel = document.createXULElement("label");
-  firstLabel.addEventListener("click", () => {
-    hideAddressRow(firstLabel, labelID);
-  });
-  firstLabel.addEventListener("keypress", event => {
-    if (event.key == "Enter") {
-      hideAddressRow(firstLabel, labelID);
-    }
-  });
-  // Necessary to allow focus via TAB key.
-  firstLabel.setAttribute("tabindex", 0);
-
-  let closeImage = document.createXULElement("image");
-  closeImage.classList.add("close-icon");
-
-  firstLabel.appendChild(closeImage);
-  firstCol.appendChild(firstLabel);
-
-  let secondCol = document.createXULElement("hbox");
-  secondCol.setAttribute("align", "start");
-  secondCol.setAttribute("pack", "end");
-  secondCol.setAttribute(
-    "style",
-    getComposeBundle().getString("headersSpaceStyle")
-  );
-  secondCol.classList.add("address-label-container");
-
-  let secondLabel = document.createXULElement("label");
-  secondLabel.setAttribute("id", `${labelID}AddrLabel`);
-  secondLabel.value = labelID;
-  secondLabel.setAttribute("control", `${labelID}AddrInput`);
-
-  secondCol.appendChild(secondLabel);
-
-  let container = document.createXULElement("hbox");
-  container.setAttribute("id", `${labelID}AddrContainer`);
-  container.setAttribute("flex", "1");
-  container.setAttribute("align", "center");
-  container.classList.add(
-    "input-container",
-    "wrap-container",
-    "address-container"
-  );
-  container.addEventListener("click", focusAddressInput);
-
-  let input = document.createElement("input", {
-    is: "autocomplete-input",
-  });
-  input.setAttribute("id", `${labelID}AddrInput`);
-
-  input.setAttribute("type", "text");
-  input.classList.add("plain", "address-input", "nntp-input");
-  input.setAttribute("disableonsend", true);
-  input.setAttribute("autocompletesearch", "mydomain addrbook ldap news");
-  input.setAttribute("autocompletesearchparam", "{}");
-  input.setAttribute("timeout", 300);
-  input.setAttribute("maxrows", 6);
-  input.setAttribute("completedefaultindex", true);
-  input.setAttribute("forcecomplete", true);
-  input.setAttribute("completeselectedindex", true);
-  input.setAttribute("minresultsforpopup", 2);
-  input.setAttribute("ignoreblurwhilesearching", true);
-
-  input.addEventListener("focus", () => {
-    highlightAddressContainer(input);
-  });
-  input.addEventListener("blur", () => {
-    resetAddressContainer(input);
-  });
-  input.addEventListener("keypress", event => {
-    recipientKeyPress(event, input);
-  });
-
-  input.setAttribute("recipienttype", "addr_other");
-  input.setAttribute("size", 1);
-
-  let highlightNonMatches = Services.prefs.getBoolPref(
-    "mail.autoComplete.highlightNonMatches"
-  );
-
-  setupAutocompleteInput(input, highlightNonMatches);
-
-  container.appendChild(input);
-
-  row.appendChild(firstCol);
-  row.appendChild(secondCol);
-  row.appendChild(container);
-
-  return row;
-}
-
-/**
  * Return the full display string for any non-default text encoding of the
  * current composition (friendly name plus official character set name).
  * For the default text encoding, return empty string (""), to reduce
  * ux-complexity, e.g. for the default Status Bar display.
  * Note: The default is retrieved from mailnews.send_default_charset.
  *
  * @return string representation of non-default charset, otherwise "".
  */
@@ -6560,26 +6464,16 @@ function MakeFromFieldEditable(ignoreWar
   identityElement.value = identityElement.selectedItem.value;
   identityElement.select();
   identityElement.placeholder = bundle.getFormattedString(
     "msgIdentityPlaceholder",
     [identityElement.selectedItem.value]
   );
 }
 
-function setupAutocomplete() {
-  let highlightNonMatches = Services.prefs.getBoolPref(
-    "mail.autoComplete.highlightNonMatches"
-  );
-
-  for (let input of document.querySelectorAll(".pop-imap-input,.nntp-input")) {
-    setupAutocompleteInput(input, highlightNonMatches);
-  }
-}
-
 function setupAutocompleteInput(input, highlightNonMatches) {
   let params = JSON.parse(input.getAttribute("autocompletesearchparam"));
   params.type = input.getAttribute("recipienttype");
   input.setAttribute("autocompletesearchparam", JSON.stringify(params));
 
   // This method overrides the autocomplete binding's openPopup (essentially
   // duplicating the logic from the autocomplete popup binding's
   // openAutocompletePopup method), modifying it so that the popup is aligned
@@ -6592,31 +6486,16 @@ function setupAutocompleteInput(input, h
       );
     }
   };
 
   // Request that input that isn't matched be highlighted.
   input.highlightNonMatches = highlightNonMatches;
 }
 
-/**
- * Select all the pills in the same recipient container if they exist.
- *
- * @param {HTMLElement} input - The autocomplete input field.
- */
-function selectRecipientPills(input) {
-  let previous = input.previousElementSibling;
-  if (previous && previous.tagName == "mail-address-pill") {
-    for (let pill of getSiblingPills(input)) {
-      pill.setAttribute("selected", "selected");
-    }
-    previous.focus();
-  }
-}
-
 function fromKeyPress(event) {
   if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
     document.getElementById("toAddrInput").focus();
   }
 }
 
 function subjectKeyPress(event) {
   gSubjectChanged = true;
--- a/mail/components/compose/content/addressingWidgetOverlay.js
+++ b/mail/components/compose/content/addressingWidgetOverlay.js
@@ -259,19 +259,19 @@ function awAddRecipientsArray(aRecipient
     aAddressArray
   );
   let element = document.getElementById(label.getAttribute("control"));
 
   if (label && element.closest(".address-row").classList.contains("hidden")) {
     label.click();
   }
 
+  let recipientArea = document.getElementById("recipientsContainer");
   for (let address of addresses) {
-    let pill = createRecipientPill(element, address);
-    element.closest(".address-container").insertBefore(pill, element);
+    recipientArea.createRecipientPill(element, address);
   }
 
   if (element.id != "replyAddrInput") {
     onRecipientsChanged();
   }
 
   // Add the recipients to our spell check ignore list.
   addRecipientsToIgnoreList(aAddressArray.join(", "));
@@ -383,17 +383,21 @@ function getLoadContext() {
  * @param {Event} event - The DOM keypress event.
  * @param {HTMLElement} element - The element that triggered the keypress event.
  */
 function recipientKeyPress(event, element) {
   switch (event.key) {
     case "a":
       // Select all the pills if the input is empty.
       if ((event.ctrlKey || event.metaKey) && !element.value.trim()) {
-        selectRecipientPills(element);
+        let previous = element.previousElementSibling;
+        if (previous && previous.tagName == "mail-address-pill") {
+          document.getElementById("recipientsContainer").selectPills(previous);
+          previous.focus();
+        }
       }
       break;
     case ",":
       event.preventDefault();
       element.handleEnter(event);
       break;
     case "Home":
     case "End":
@@ -401,23 +405,27 @@ function recipientKeyPress(event, elemen
     case "Backspace":
       if (!element.value.trim() && !event.repeat) {
         let pills = element
           .closest(".address-container")
           .querySelectorAll("mail-address-pill");
         if (pills.length) {
           let key = event.key == "Home" ? 0 : pills.length - 1;
           pills[key].focus();
-          pills[key].checkKeyboardSelected(event, pills[key]);
+          document
+            .getElementById("recipientsContainer")
+            .checkKeyboardSelected(event, pills[key]);
         }
       }
       break;
     case "Enter":
       // No address entered, move focus to Subject field.
       if (!element.value.trim()) {
+        event.stopPropagation();
+        event.preventDefault();
         document.getElementById("msgSubject").focus();
         return;
       }
       break;
     case "Tab":
       // Trigger the autocomplete controller only if we have a value
       // to prevent interfering with the natural change of focus on Tab.
       if (element.value.trim()) {
@@ -449,26 +457,23 @@ function recipientKeyPress(event, elemen
  *   was invoked programmatically and should not be considered a change of
  *   message content.
  */
 function recipientAddPill(element, automatic = false) {
   if (!element.value.trim()) {
     return;
   }
 
-  let parent = document.getElementById(
-    element.closest(".address-container").id
-  );
   let addresses = MailServices.headerParser.makeFromDisplayAddress(
     element.value
   );
+  let recipientArea = document.getElementById("recipientsContainer");
 
   for (let address of addresses) {
-    let pill = createRecipientPill(element, address);
-    parent.insertBefore(pill, element);
+    recipientArea.createRecipientPill(element, address);
 
     // Be sure to add the user add recipient to our ignore list
     // when the user hits enter in an autocomplete widget...
     addRecipientsToIgnoreList(element.value);
   }
 
   // Reset the input element.
   element.removeAttribute("nomatch");
@@ -484,64 +489,20 @@ function recipientAddPill(element, autom
   // Attach it again to enable autocomplete.
   element.attachController();
 
   onRecipientsChanged(automatic);
   calculateHeaderHeight();
 }
 
 /**
- * Create a new recipient pill.
- *
- * @param {HTMLElement} element - The original autocomplete input that generated
- *   the pill.
- * @param {Array} address - The array containing the recipient's info.
- * @returns {XULElement} The newly created pill element.
- */
-function 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
-  );
-  let isMailingList =
-    listNames.length > 0 &&
-    MailServices.ab.mailListNameExists(listNames[0].name);
-  let isNewsgroup = element.classList.contains("nntp-input");
-
-  pill.classList.toggle(
-    "error",
-    !isValidAddress(address.email) && !isMailingList && !isNewsgroup
-  );
-
-  let emailCard = DisplayNameUtils.getCardForEmail(address.email);
-  pill.classList.toggle(
-    "warning",
-    isValidAddress(address.email) &&
-      !emailCard.card &&
-      !isMailingList &&
-      !isNewsgroup
-  );
-
-  return pill;
-}
-
-/**
  * Force a focused styling on the recipient container of the currently
  * selected input element.
  *
- * @param {HTMLElement} element - The element receving focus.
+ * @param {HTMLElement} element - The element receiving focus.
  */
 function highlightAddressContainer(element) {
   element.closest(".address-container").setAttribute("focused", "true");
   deselectAllPills();
 }
 
 /**
  * Deselect any previously selected pills.
@@ -587,28 +548,32 @@ function resetAddressContainer(element) 
 /**
  * Trigger the startEditing() method of the mail-address-pill element.
  *
  * @param {XULlement} element - The element from which the context menu was
  *   opened.
  * @param {Event} event - The DOM event.
  */
 function editAddressPill(element, event) {
-  element.closest("mail-address-pill").startEditing(event);
+  document
+    .getElementById("recipientsContainer")
+    .startEditing(element.closest("mail-address-pill"), event);
 }
 
 /**
  * Copy the selected pills email address.
  *
  * @param {XULElement} element - The element from which the context menu was
  *   opened.
  */
 function copyEmailNewsAddress(element) {
   let allAddresses = [];
-  for (let pill of getAllSelectedPills(element.closest("mail-address-pill"))) {
+  for (let pill of document
+    .getElementById("recipientsContainer")
+    .getAllSelectedPills()) {
     allAddresses.push(pill.fullAddress);
   }
 
   let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
     Ci.nsIClipboardHelper
   );
   clipboard.copyString(allAddresses.join(", "));
 }
@@ -626,24 +591,24 @@ function cutEmailNewsAddress(element) {
 
 /**
  * Delete the selected pill/pills.
  *
  * @param {XULElement} element - The element from which the context menu was
  *   opened.
  */
 function deleteAddressPill(element) {
-  let firstPill = element.closest("mail-address-pill");
-
   // We need to store the input location before removing the pills.
-  let input = firstPill
+  let input = element
     .closest(".address-container")
     .querySelector(`input[is="autocomplete-input"][recipienttype]`);
 
-  for (let pill of getAllSelectedPills(firstPill)) {
+  for (let pill of document
+    .getElementById("recipientsContainer")
+    .getAllSelectedPills()) {
     pill.remove();
   }
 
   input.focus();
   onRecipientsChanged();
 }
 
 /**
@@ -658,17 +623,17 @@ function showAddressRowKeyPress(event, l
   if (event.key == "Enter") {
     showAddressRow(label, rowID);
   }
 }
 
 /**
  * Show the container row of an hidden recipient (Cc, Bcc, etc.).
  *
- * @param {XULelement} label - The clicked label to hide.
+ * @param {XULElement} label - The clicked label to hide.
  * @param {string} rowID - The ID of the container to reveal.
  */
 function showAddressRow(label, rowID) {
   let container = document.getElementById(rowID);
   let input = container.querySelector(`input[is="autocomplete-input"]`);
 
   container.classList.remove("hidden");
   label.setAttribute("collapsed", "true");
@@ -731,53 +696,8 @@ function calculateHeaderHeight() {
     document.getElementById("recipientsContainer").classList.add("overflow");
 
     let header = document.getElementById("headers-box");
     if (!header.hasAttribute("height")) {
       header.setAttribute("height", 300);
     }
   }
 }
-
-/**
- * Move the focus on the first pill from the same .address-container.
- *
- * @param {XULElement} pill - The mail-address-pill element.
- */
-function setFocusOnFirstPill(pill) {
-  pill.closest(".address-container").firstElementChild.focus();
-}
-
-// #TODO: The getSiblingPills(), getAllPills(), and getAllSelectedPills()
-// methods are not a good way to handle these scenarios, and they should be
-// moved into their own CE. See Bug 1601740
-
-/**
- * Return all the pills from the same .address-container.
- *
- * @param {XULElement} pill - The mail-address-pill element.
- * @return {Array} Array of mail-address-pill elements.
- */
-function getSiblingPills(pill) {
-  return pill
-    .closest(".address-container")
-    .querySelectorAll("mail-address-pill");
-}
-
-/**
- * Return all the pills currently available in the document.
- *
- * @return {Array} Array of mail-address-pill elements.
- */
-function getAllPills() {
-  return document.querySelectorAll("mail-address-pill");
-}
-
-/**
- * Return all the selected pills currently available in the document.
- *
- * @return {Array} Array of selected mail-address-pill elements.
- */
-function getAllSelectedPills() {
-  return document.querySelectorAll(`mail-address-pill[selected]`);
-}
-
-// #END TODO
--- a/mail/components/compose/content/messengercompose.xhtml
+++ b/mail/components/compose/content/messengercompose.xhtml
@@ -2004,21 +2004,22 @@
             <menulist is="menulist-editable" id="msgIdentity"
                       type="description" flex="1"
                       disableautoselect="true" onkeypress="fromKeyPress(event);"
                       oncommand="LoadIdentity(false);" disableonsend="true">
               <menupopup id="msgIdentityPopup"/>
             </menulist>
           </hbox>
 
-          <vbox id="recipientsContainer" class="recipients-container" flex="1">
+          <mail-recipients-area id="recipientsContainer" orient="vertical"
+                                class="recipients-container" flex="1">
             <hbox id="addressRowTo" class="addressingWidgetItem address-row">
               <hbox class="aw-firstColBox"/>
-              <hbox class="address-label-container" align="top"
-                    pack="end" style="&headersSpace2.style;">
+              <hbox class="address-label-container" align="top" pack="end"
+                    style="&headersSpace2.style;">
                 <label id="toAddrLabel" value="&toAddr2.label;"
                        control="toAddrInput"/>
               </hbox>
               <hbox id="toAddrContainer" flex="1" align="center"
                     class="input-container wrap-container address-container"
                     onclick="focusAddressInput(event);">
                 <html:input is="autocomplete-input" id="toAddrInput"
                             type="text"
@@ -2044,18 +2045,18 @@
 
             <hbox id="addressRowCc" class="addressingWidgetItem address-row hidden">
               <hbox class="aw-firstColBox" align="top">
                 <label onclick="hideAddressRow(this, 'addr_cc');"
                        onkeypress="if (event.key == 'Enter') { hideAddressRow(this, 'addr_cc'); }">
                   <image class="close-icon"/>
                 </label>
               </hbox>
-              <hbox class="address-label-container" align="top"
-                    pack="end" style="&headersSpace2.style;">
+              <hbox class="address-label-container" align="top" pack="end"
+                    style="&headersSpace2.style;">
                 <label id="ccAddrLabel" value="&ccAddr2.label;"
                        control="ccAddrInput"/>
               </hbox>
               <hbox id="ccAddrContainer" flex="1" align="center"
                     class="input-container wrap-container address-container"
                     onclick="focusAddressInput(event);">
                 <html:input is="autocomplete-input" id="ccAddrInput"
                             type="text"
@@ -2081,18 +2082,18 @@
 
             <hbox id="addressRowBcc" class="addressingWidgetItem address-row hidden">
               <hbox class="aw-firstColBox" align="top">
                 <label onclick="hideAddressRow(this, 'addr_bcc');"
                        onkeypress="if (event.key == 'Enter') { hideAddressRow(this, 'addr_bcc'); }">
                   <image class="close-icon"/>
                 </label>
               </hbox>
-              <hbox class="address-label-container" align="top"
-                    pack="end" style="&headersSpace2.style;">
+              <hbox class="address-label-container" align="top" pack="end"
+                    style="&headersSpace2.style;">
                 <label id="bccAddrLabel" value="&bccAddr2.label;"
                        control="bccAddrInput"/>
               </hbox>
               <hbox id="bccAddrContainer" flex="1" align="center"
                     class="input-container wrap-container address-container"
                     onclick="focusAddressInput(event);">
                 <html:input is="autocomplete-input" id="bccAddrInput"
                             type="text"
@@ -2118,18 +2119,18 @@
 
             <hbox id="addressRowReply" class="addressingWidgetItem address-row hidden">
               <hbox class="aw-firstColBox" align="top">
                 <label onclick="hideAddressRow(this, 'addr_reply');"
                        onkeypress="if (event.key == 'Enter') { hideAddressRow(this, 'addr_reply'); }">
                   <image class="close-icon"/>
                 </label>
               </hbox>
-              <hbox class="address-label-container" align="top"
-                    pack="end" style="&headersSpace2.style;">
+              <hbox class="address-label-container" align="top" pack="end"
+                    style="&headersSpace2.style;">
                 <label id="replyAddrLabel" value="&replyAddr2.label;"
                        control="replyAddrInput"/>
               </hbox>
               <hbox id="replyAddrContainer" flex="1" align="center"
                     class="input-container wrap-container address-container"
                     onclick="focusAddressInput(event);">
                 <html:input is="autocomplete-input" id="replyAddrInput"
                             type="text"
@@ -2155,18 +2156,18 @@
 
             <hbox id="addressRowNewsgroups" class="addressingWidgetItem address-row hidden">
               <hbox class="aw-firstColBox" align="top">
                 <label onclick="hideAddressRow(this, 'addr_newsgroups');"
                        onkeypress="if (event.key == 'Enter') { hideAddressRow(this, 'addr_newsgroups'); }">
                   <image class="close-icon"/>
                 </label>
               </hbox>
-              <hbox class="address-label-container" align="top"
-                    pack="end" style="&headersSpace2.style;">
+              <hbox class="address-label-container" align="top" pack="end"
+                    style="&headersSpace2.style;">
                 <label id="newsgroupsAddrLabel" value="&newsgroupsAddr2.label;"
                        control="newsgroupsAddrInput"/>
               </hbox>
               <hbox id="newsgroupsAddrContainer" flex="1" align="center"
                     class="input-container wrap-container address-container"
                     onclick="focusAddressInput(event);">
                 <html:input is="autocomplete-input" id="newsgroupsAddrInput"
                             type="text"
@@ -2192,18 +2193,18 @@
 
             <hbox id="addressRowFollowup" class="addressingWidgetItem address-row hidden">
               <hbox class="aw-firstColBox" align="top">
                 <label onclick="hideAddressRow(this, 'addr_followup');"
                        onkeypress="if (event.key == 'Enter') { hideAddressRow(this, 'addr_followup'); }">
                   <image class="close-icon"/>
                 </label>
               </hbox>
-              <hbox class="address-label-container" align="top"
-                    pack="end" style="&headersSpace2.style;">
+              <hbox class="address-label-container" align="top" pack="end"
+                    style="&headersSpace2.style;">
                 <label id="followupAddrLabel" value="&followupAddr2.label;"
                        control="followupAddrInput"/>
               </hbox>
               <hbox id="followupAddrContainer" flex="1" align="center"
                     class="input-container wrap-container address-container"
                     onclick="focusAddressInput(event);">
                 <html:input is="autocomplete-input" id="followupAddrInput"
                             type="text"
@@ -2221,17 +2222,17 @@
                             ignoreblurwhilesearching="true"
                             onfocus="highlightAddressContainer(this);"
                             onblur="resetAddressContainer(this);"
                             onkeypress="recipientKeyPress(event, this);"
                             recipienttype="addr_followup"
                             size="1"/>
               </hbox>
             </hbox>
-          </vbox>
+          </mail-recipients-area>
 
           <hbox class="addressingWidgetItem">
             <hbox class="aw-firstColBox"/>
             <hbox class="address-label-container" style="&headersSpace2.style;"/>
             <hbox id="addressingWidgetLabels" class="address-extra-recipients"
                   flex="1" align="center">
               <label id="addr_to" control="toAddrInput" hidden="true"/>
               <label id="addr_cc" onclick="showAddressRow(this, 'addressRowCc')"
--- a/mail/themes/shared/mail/messengercompose.css
+++ b/mail/themes/shared/mail/messengercompose.css
@@ -454,17 +454,18 @@
 #signing-status,
 #encryption-status {
   display: flex;
   align-items: center;
 }
 
 #identityLabel,
 .address-label-container label {
-  margin-inline-end: 6px
+  margin-inline-end: 6px;
+  text-align: right;
 }
 
 #msgIdentity {
   -moz-appearance: none;
   -moz-box-align: center;
   font: inherit;
   margin-inline-end: 8px;
   border: 1px solid var(--toolbarbutton-hover-bordercolor);
@@ -540,45 +541,31 @@
 
 .address-input {
   color: inherit;
 }
 
 .address-pill {
   display: flex;
   align-items: center;
-  border-radius: 9999px;
-  margin-inline-end: 2px;
+  border-radius: 4px;
+  margin-inline-end: 3px;
   margin-block: 2px;
   background-color: rgba(0,0,0,0.1);
   transition: color .2s ease, background-color .2s ease, box-shadow .2s ease;
   -moz-user-focus: normal;
   cursor: default;
   box-shadow: inset 0 0 0 2px transparent;
 }
 
-.address-pill label,
-.address-pill .delete-pill-icon {
+.address-pill label {
   -moz-user-focus: none;
   cursor: default;
 }
 
-.delete-pill-icon {
-  width: 1.25em;
-  height: 1.25em;
-  margin-inline-end: 2px;
-  padding: 3px;
-  border-radius: 50%;
-  background-color: rgba(0,0,0,0.1);
-  list-style-image: url("chrome://messenger/skin/icons/stop.svg");
-  -moz-context-properties: fill;
-  fill: currentColor;
-  transition: color .2s ease, background-color .2s ease;
-}
-
 .address-pill:hover:not(.editing),
 .address-pill:focus:not(.editing) {
   box-shadow: inset 0 0 0 2px rgba(0,0,0,0.3);
 }
 
 .address-pill.editing {
   flex: 1;
   background-color: transparent;
@@ -641,26 +628,16 @@
 .address-pill.warning[selected]:not(.editing),
 #MsgHeadersToolbar[brighttext] .address-pill.warning[selected]:not(.editing),
 .address-pill.error[selected]:not(.editing),
 #MsgHeadersToolbar[brighttext] .address-pill.error[selected]:not(.editing) {
   color: HighlightText;
   background-color: Highlight;
 }
 
-#MsgHeadersToolbar[brighttext] .delete-pill-icon {
-  background-color: rgba(255, 255, 255, 0.1);
-}
-
-.delete-pill-icon:hover,
-#MsgHeadersToolbar[brighttext] .delete-pill-icon:hover {
-  background-color: HighlightText;
-  color: Highlight;
-}
-
 .address-extra-recipients {
   margin-inline-end: 8px;
 }
 
 .address-extra-recipients label {
   margin-top: 0;
   margin-bottom: 6px;
   transition: color 0.2s;