Bug 1776214 - Unify the type selection in the edit contact. r=darktrojan draft
authorNicolai Kasper <nicolai@thunderbird.net>
Mon, 27 Jun 2022 09:05:15 +0000
changeset 119333 91683740f55d3af8b8ce405434d7ca797e3665cc
parent 119328 40b247165baa05a2bb80f4624d8f9c9ef4878bc3
child 119334 2523b1259978b75c7ebe7397d6a1e9469943cba2
push id16219
push usernicolai@thunderbird.net
push dateMon, 04 Jul 2022 13:28:23 +0000
treeherdertry-comm-central@2523b1259978 [default view] [failures only]
reviewersdarktrojan
bugs1776214
Bug 1776214 - Unify the type selection in the edit contact. r=darktrojan Differential Revision: https://phabricator.services.mozilla.com/D150260
mail/components/addrbook/content/vcard-edit/adr.js
mail/components/addrbook/content/vcard-edit/edit.js
mail/components/addrbook/content/vcard-edit/email.js
mail/components/addrbook/content/vcard-edit/tel.js
mail/components/addrbook/content/vcard-edit/url.js
mail/components/addrbook/content/vcard-edit/vCardTemplates.inc.xhtml
mail/components/addrbook/test/browser/browser_edit_card.js
--- a/mail/components/addrbook/content/vcard-edit/adr.js
+++ b/mail/components/addrbook/content/vcard-edit/adr.js
@@ -13,19 +13,16 @@ ChromeUtils.defineModuleGetter(
 /**
  * @implements {VCardPropertyEntryView}
  * @see RFC6350 ADR
  */
 class VCardAdrComponent extends HTMLElement {
   /** @type {VCardPropertyEntry} */
   vCardPropertyEntry;
 
-  /** @type {HTMLSelectElement} */
-  selectEl;
-
   static newVCardPropertyEntry() {
     return new VCardPropertyEntry("adr", {}, "text", [
       "",
       "",
       "",
       "",
       "",
       "",
@@ -68,28 +65,28 @@ class VCardAdrComponent extends HTMLElem
       this.assignIds(this.regionEl, this.querySelector('label[for="code"]'));
 
       this.countryEl = this.querySelector('input[name="country"]');
       this.assignIds(
         this.countryEl,
         this.querySelector('label[for="country"]')
       );
 
-      this.selectEl = this.querySelector("select");
-      let selectId = vCardIdGen.next().value;
-      this.selectEl.id = selectId;
-      this.querySelector('label[for="select"]').htmlFor = selectId;
+      // Adjust the adr type selection.
+      this.vCardType = this.querySelector("vcard-type");
+      this.vCardType.vCardPropertyEntry = this.vCardPropertyEntry;
+      this.vCardType.createLabel = true;
 
       this.fromVCardPropertyEntryToUI();
     }
   }
 
   disconnectedCallback() {
     if (!this.isConnected) {
-      this.selectEl = null;
+      this.vCardType = null;
       this.vCardPropertyEntry = null;
       this.poboxEl = null;
       this.extEl = null;
       this.streetEl = null;
       this.localityEl = null;
       this.regionEl = null;
       this.codeEl = null;
       this.countryEl = null;
@@ -106,37 +103,19 @@ class VCardAdrComponent extends HTMLElem
     } else {
       this.streetEl.value = this.vCardPropertyEntry.value[2] || "";
     }
     this.resizeStreetEl();
     this.localityEl.value = this.vCardPropertyEntry.value[3] || "";
     this.regionEl.value = this.vCardPropertyEntry.value[4] || "";
     this.codeEl.value = this.vCardPropertyEntry.value[5] || "";
     this.countryEl.value = this.vCardPropertyEntry.value[6] || "";
-
-    /**
-     * @TODO
-     * Create an element for type selection of home, work, ...
-     */
-    let paramsType = this.vCardPropertyEntry.params.type;
-    if (paramsType && !Array.isArray(paramsType)) {
-      this.selectEl.value = this.vCardPropertyEntry.params.type;
-    }
   }
 
   fromUIToVCardPropertyEntry() {
-    /**
-     * @TODO
-     * Create an element for type selection of home, work, ...
-     */
-    let paramsType = this.selectEl.value;
-    if (paramsType) {
-      this.vCardPropertyEntry.params.type = paramsType;
-    }
-
     let streetValue = this.streetEl.value || "";
     streetValue = streetValue.trim();
     if (streetValue.includes("\n")) {
       streetValue = streetValue.replaceAll("\r", "");
       streetValue = streetValue.split("\n");
     }
 
     this.vCardPropertyEntry.value = [
--- a/mail/components/addrbook/content/vcard-edit/edit.js
+++ b/mail/components/addrbook/content/vcard-edit/edit.js
@@ -754,16 +754,94 @@ function* vCardHtmlIdGen() {
   let internalId = 0;
   while (true) {
     yield `vcard-id-${internalId++}`;
   }
 }
 
 let vCardIdGen = vCardHtmlIdGen();
 
+class VCardTypeSelection extends HTMLElement {
+  /** @type {HTMLSelectElement} */
+  selectEl;
+
+  constructor() {
+    super();
+  }
+
+  connectedCallback() {
+    if (this.isConnected) {
+      if (!this.vCardPropertyEntry) {
+        return;
+      }
+
+      let template;
+      let types;
+      if (this.tel) {
+        types = ["work", "home", "cell", "fax", "pager"];
+        template = document.getElementById("template-vcard-edit-type-tel");
+      } else {
+        types = ["work", "home"];
+        template = document.getElementById("template-vcard-edit-type");
+      }
+      let clonedTemplate = template.content.cloneNode(true);
+      this.appendChild(clonedTemplate);
+
+      this.selectEl = this.querySelector("select");
+      let selectId = vCardIdGen.next().value;
+      this.selectEl.id = selectId;
+
+      // Just abandon any values we don't have UI for. We don't have any way to
+      // know whether to keep them or not, and they're very rarely used.
+      // let types = ["work", "home", "cell", "fax", "pager"];
+      let paramsType = this.vCardPropertyEntry.params.type;
+      if (paramsType && Array.isArray(paramsType)) {
+        this.selectEl.value = paramsType.find(t => types.includes(t)) || "";
+      } else if (types.includes(paramsType)) {
+        this.selectEl.value = this.vCardPropertyEntry.params.type;
+      }
+
+      // Change the value on the vCardPropertyEntry.
+      this.selectEl.addEventListener("change", e => {
+        if (this.selectEl.value) {
+          this.vCardPropertyEntry.params.type = this.selectEl.value;
+        } else {
+          delete this.vCardPropertyEntry.params.type;
+        }
+      });
+
+      // Set an aria-labelledyby on the select.
+      if (this.setAriaLabelledBy) {
+        this.querySelector("select").setAttribute(
+          "aria-labelledby",
+          this.setAriaLabelledBy
+        );
+      }
+
+      // Create a label element for the select.
+      if (this.createLabel) {
+        let labelEl = document.createElement("label");
+        labelEl.htmlFor = selectId;
+        labelEl.setAttribute("data-l10n-id", "vcard-entry-type-label");
+        labelEl.classList.add("screen-reader-only");
+        this.appendChild(labelEl);
+      }
+    }
+  }
+
+  disconnectedCallback() {
+    if (!this.isConnected) {
+      this.selectEl = null;
+      this.vCardPropertyEntry = null;
+    }
+  }
+}
+
+customElements.define("vcard-type", VCardTypeSelection);
+
 /**
  * Interface for vCard Fields in the edit view.
  *
  * @interface VCardPropertyEntryView
  */
 
 /**
  * Getter/Setter for rich data do not use HTMLAttributes for this.
--- a/mail/components/addrbook/content/vcard-edit/email.js
+++ b/mail/components/addrbook/content/vcard-edit/email.js
@@ -13,18 +13,16 @@ ChromeUtils.defineModuleGetter(
 /**
  * @implements {VCardPropertyEntryView}
  * @see RFC6350 EMAIL
  */
 class VCardEmailComponent extends HTMLTableRowElement {
   /** @type {VCardPropertyEntry} */
   vCardPropertyEntry;
 
-  /** @type {HTMLSelectElement} */
-  selectEl;
   /** @type {HTMLInputElement} */
   emailEl;
   /** @type {HTMLInputElement} */
   checkboxEl;
 
   static newVCardPropertyEntry() {
     return new VCardPropertyEntry("email", {}, "text", "");
   }
@@ -52,61 +50,46 @@ class VCardEmailComponent extends HTMLTa
 
       // Uncheck the checkbox of other VCardEmailComponents if this one is
       // checked.
       this.checkboxEl.addEventListener("change", event => {
         if (event.target.checked === true) {
           this.dispatchEvent(VCardEmailComponent.CheckboxEvent());
         }
       });
+
+      // Adjust the email type selection.
+      this.vCardType = this.querySelector("vcard-type");
+      this.vCardType.vCardPropertyEntry = this.vCardPropertyEntry;
+      this.vCardType.setAriaLabelledBy = "addr-book-edit-email-type";
+
       this.fromVCardPropertyEntryToUI();
     }
   }
 
   disconnectedCallback() {
     if (!this.isConnected) {
       this.checkboxEl = null;
       this.emailEl = null;
-      this.selectEl = null;
+      this.vCardType = null;
       this.vCardPropertyEntry = null;
     }
   }
 
   fromVCardPropertyEntryToUI() {
     this.emailEl.value = this.vCardPropertyEntry.value;
-    /**
-     * @TODO
-     * Create an element for type selection of home, work, ...
-     */
-    let paramsType = this.vCardPropertyEntry.params.type;
-    if (paramsType && !Array.isArray(paramsType)) {
-      this.selectEl.value = this.vCardPropertyEntry.params.type;
-    }
+
     let pref = this.vCardPropertyEntry.params.pref;
     if (pref === "1") {
       this.checkboxEl.checked = true;
     }
   }
 
   fromUIToVCardPropertyEntry() {
     this.vCardPropertyEntry.value = this.emailEl.value;
-    /**
-     * @TODO
-     * Create an element for type selection of home, work, ...
-     */
-    let paramsType = this.selectEl.value;
-    if (paramsType && paramsType !== "") {
-      this.vCardPropertyEntry.params.type = this.selectEl.value;
-    } else if (paramsType && !Array.isArray(paramsType)) {
-      /**
-       * @TODO params.type is string | Array<string> | falsy.
-       * Right now the case is only handled for string.
-       */
-      delete this.vCardPropertyEntry.params.type;
-    }
 
     if (this.checkboxEl.checked) {
       this.vCardPropertyEntry.params.pref = "1";
     } else if (
       this.vCardPropertyEntry.params.pref &&
       this.vCardPropertyEntry.params.pref === "1"
     ) {
       // Only delete the pref if a pref of 1 is set and the checkbox is not
--- a/mail/components/addrbook/content/vcard-edit/tel.js
+++ b/mail/components/addrbook/content/vcard-edit/tel.js
@@ -16,18 +16,16 @@ ChromeUtils.defineModuleGetter(
  *
  * @TODO missing type-param-tel support.
  * "text, voice, fax, cell, video, pager, textphone"
  */
 class VCardTelComponent extends HTMLElement {
   /** @type {VCardPropertyEntry} */
   vCardPropertyEntry;
 
-  /** @type {HTMLSelectElement} */
-  selectEl;
   /** @type {HTMLInputElement} */
   inputElement;
 
   static newVCardPropertyEntry() {
     return new VCardPropertyEntry("tel", {}, "text", "");
   }
 
   constructor() {
@@ -42,55 +40,40 @@ class VCardTelComponent extends HTMLElem
       this.inputElement = this.querySelector('input[type="text"]');
       let urlId = vCardIdGen.next().value;
       this.inputElement.id = urlId;
       let urlLabel = this.querySelector('label[for="text"]');
       urlLabel.htmlFor = urlId;
       document.l10n.setAttributes(urlLabel, "vcard-tel-label");
       this.inputElement.type = "tel";
 
-      this.selectEl = this.querySelector("select");
-      let selectId = vCardIdGen.next().value;
-      this.selectEl.id = selectId;
-      this.querySelector('label[for="select"]').htmlFor = selectId;
+      // Adjust the tel type selection.
+      this.vCardType = this.querySelector("vcard-type");
+      this.vCardType.vCardPropertyEntry = this.vCardPropertyEntry;
+      this.vCardType.createLabel = true;
+      this.vCardType.tel = true;
 
       this.fromVCardPropertyEntryToUI();
     }
   }
 
   disconnectedCallback() {
     if (!this.isConnected) {
       this.inputElement = null;
-      this.selectEl = null;
+      this.vCardType = null;
       this.vCardPropertyEntry = null;
     }
   }
 
   fromVCardPropertyEntryToUI() {
     this.inputElement.value = this.vCardPropertyEntry.value;
-
-    // Just abandon any values we don't have UI for. We don't have any way to
-    // know whether to keep them or not, and they're very rarely used.
-    let types = ["work", "home", "cell", "fax", "pager"];
-    let paramsType = this.vCardPropertyEntry.params.type;
-    if (paramsType && Array.isArray(paramsType)) {
-      this.selectEl.value = paramsType.find(t => types.includes(t)) || "";
-    } else if (types.includes(paramsType)) {
-      this.selectEl.value = this.vCardPropertyEntry.params.type;
-    }
   }
 
   fromUIToVCardPropertyEntry() {
     this.vCardPropertyEntry.value = this.inputElement.value;
-
-    if (this.selectEl.value) {
-      this.vCardPropertyEntry.params.type = this.selectEl.value;
-    } else {
-      delete this.vCardPropertyEntry.params.type;
-    }
   }
 
   valueIsEmpty() {
     return this.vCardPropertyEntry.value === "";
   }
 }
 
 customElements.define("vcard-tel", VCardTelComponent);
--- a/mail/components/addrbook/content/vcard-edit/url.js
+++ b/mail/components/addrbook/content/vcard-edit/url.js
@@ -13,18 +13,16 @@ ChromeUtils.defineModuleGetter(
 /**
  * @implements {VCardPropertyEntryView}
  * @see RFC6350 URL
  */
 class VCardURLComponent extends HTMLElement {
   /** @type {VCardPropertyEntry} */
   vCardPropertyEntry;
 
-  /** @type {HTMLSelectElement} */
-  selectEl;
   /** @type {HTMLInputElement} */
   urlEl;
 
   static newVCardPropertyEntry() {
     return new VCardPropertyEntry("url", {}, "uri", "");
   }
 
   constructor() {
@@ -49,67 +47,45 @@ class VCardURLComponent extends HTMLElem
         if (
           this.urlEl.value.length > "https://".length &&
           !/^https?:\/\//.test(this.urlEl.value)
         ) {
           this.urlEl.value = "https://" + this.urlEl.value;
         }
       });
 
-      this.selectEl = this.querySelector("select");
-      let selectId = vCardIdGen.next().value;
-      this.selectEl.id = selectId;
-      this.querySelector('label[for="select"]').htmlFor = selectId;
+      // Adjust the url type selection.
+      this.vCardType = this.querySelector("vcard-type");
+      this.vCardType.vCardPropertyEntry = this.vCardPropertyEntry;
+      this.vCardType.createLabel = true;
 
       this.fromVCardPropertyEntryToUI();
     }
   }
 
   disconnectedCallback() {
     if (!this.isConnected) {
       this.urlEl = null;
-      this.selectEl = null;
+      this.vCardType = null;
       this.vCardPropertyEntry = null;
     }
   }
 
   fromVCardPropertyEntryToUI() {
     let value = this.vCardPropertyEntry.value;
     if (/^https?\\:/.test(value)) {
       // Google escapes some characters in violation of RFC6350. A backslash
       // wouldn't be expected in a URL so removing them shouldn't be a problem.
       value = value.replace(/\\(.)/g, "$1");
     }
     this.urlEl.value = value;
-    /**
-     * @TODO
-     * Create an element for type selection of home, work, ...
-     */
-    let paramsType = this.vCardPropertyEntry.params.type;
-    if (paramsType && !Array.isArray(paramsType)) {
-      this.selectEl.value = this.vCardPropertyEntry.params.type;
-    }
   }
 
   fromUIToVCardPropertyEntry() {
     this.vCardPropertyEntry.value = this.urlEl.value;
-    /**
-     * @TODO
-     * Create an element for type selection of home, work, ...
-     */
-    let paramsType = this.selectEl.value;
-    if (paramsType && !Array.isArray(paramsType) && paramsType !== "") {
-      this.vCardPropertyEntry.params.type = this.selectEl.value;
-    } else if (paramsType && !Array.isArray(paramsType)) {
-      /**
-       * @TODO params.type is string | Array<string> | falsy.
-       * Right now the case is only handled for string.
-       */
-      delete this.vCardPropertyEntry.params.type;
-    }
   }
 
   valueIsEmpty() {
     return this.vCardPropertyEntry.value === "";
   }
 }
 
 customElements.define("vcard-url", VCardURLComponent);
--- a/mail/components/addrbook/content/vcard-edit/vCardTemplates.inc.xhtml
+++ b/mail/components/addrbook/content/vcard-edit/vCardTemplates.inc.xhtml
@@ -196,65 +196,38 @@
   <!-- FIXME: Replace this DTD string with a fluent ID after 102. -->
   <label for="vCardNickName">&NickName.label;</label>
   <input id="vCardNickName" type="text"/>
 </template>
 
 <!-- Email -->
 <template id="template-vcard-edit-email">
   <td>
-    <!-- The type selection is repeated boilerplate and will be reduced -->
-    <select class="vcard-type-selection"
-            aria-labelledby="addr-book-edit-email-type">
-      <option value="work" data-l10n-id="vcard-entry-type-work" />
-      <option value="home" data-l10n-id="vcard-entry-type-home" />
-      <option value="" data-l10n-id="vcard-entry-type-none" selected="selected" />
-    </select>
+    <vcard-type></vcard-type>
   </td>
   <td class="email-column">
     <input type="email"
            aria-labelledby="addr-book-edit-email-label" />
   </td>
   <td class="default-column">
     <input type="checkbox"
            aria-labelledby="addr-book-edit-email-default" />
   </td>
 </template>
 
 <!-- Phone -->
 <template id="template-vcard-edit-tel">
-  <label class="screen-reader-only"
-         for="select"
-         data-l10n-id="vcard-entry-type-label"/>
-  <!-- The type selection is repeated boilerplate and will be reduced -->
-  <select class="vcard-type-selection">
-    <option value="work" data-l10n-id="vcard-entry-type-work"/>
-    <option value="home" data-l10n-id="vcard-entry-type-home"/>
-    <!-- FIXME: Replace these strings and remove aboutAddressBook.ftl from
-         AccountManager.xhtml. -->
-    <option value="cell" data-l10n-id="about-addressbook-entry-type-cell"/>
-    <option value="fax" data-l10n-id="about-addressbook-entry-type-fax"/>
-    <option value="pager" data-l10n-id="about-addressbook-entry-type-pager"/>
-    <option value="" data-l10n-id="vcard-entry-type-none" selected="selected"/>
-  </select>
+  <vcard-type></vcard-type>
   <label class="screen-reader-only" for="text"/>
   <input type="text"/>
 </template>
 
 <!-- Field with type and text -->
 <template id="template-vcard-edit-type-text">
-  <label class="screen-reader-only"
-         for="select"
-         data-l10n-id="vcard-entry-type-label"/>
-  <!-- The type selection is repeated boilerplate and will be reduced -->
-  <select class="vcard-type-selection">
-    <option value="work" data-l10n-id="vcard-entry-type-work"/>
-    <option value="home" data-l10n-id="vcard-entry-type-home"/>
-    <option value="" data-l10n-id="vcard-entry-type-none" selected="selected"/>
-  </select>
+  <vcard-type></vcard-type>
   <label class="screen-reader-only" for="text"/>
   <input type="text"/>
 </template>
 
 <!-- Time Zone -->
 <template id="template-vcard-edit-tz">
   <select>
     <option value=""></option>
@@ -289,25 +262,17 @@
     </div>
   </fieldset>
 </template>
 
 <!-- Address -->
 <template id="template-vcard-edit-adr">
   <fieldset class="fieldset-grid fieldset-reset">
     <legend class="screen-reader-only" data-l10n-id="vcard-adr-label"/>
-    <label class="screen-reader-only"
-           for="select"
-           data-l10n-id="vcard-entry-type-label"/>
-    <!-- The type selection is repeated boilerplate and will be reduced -->
-    <select class="vcard-type-selection">
-      <option value="work" data-l10n-id="vcard-entry-type-work"/>
-      <option value="home" data-l10n-id="vcard-entry-type-home"/>
-      <option value="" data-l10n-id="vcard-entry-type-none" selected="selected"/>
-    </select>
+    <vcard-type></vcard-type>
     <div class="vcard-adr-inputs">
       <label for="pobox" data-l10n-id="vcard-adr-pobox"/>
       <input type="text" name="pobox"/>
     </div>
     <div class="vcard-adr-inputs">
       <label for="ext" data-l10n-id="vcard-adr-ext"/>
       <input type="text" name="ext"/>
     </div>
@@ -353,8 +318,29 @@
   </div>
 </template>
 <template id="template-vcard-edit-org">
   <div class="vcard-adr-inputs">
     <label for="org" data-l10n-id="vcard-org-org"/>
     <textarea name="org"></textarea>
   </div>
 </template>
+
+<template id="template-vcard-edit-type">
+  <select class="vcard-type-selection">
+    <option value="work" data-l10n-id="vcard-entry-type-work"/>
+    <option value="home" data-l10n-id="vcard-entry-type-home"/>
+    <option value="" data-l10n-id="vcard-entry-type-none" selected="selected"/>
+  </select>
+</template>
+
+<template id="template-vcard-edit-type-tel">
+  <select class="vcard-type-selection">
+    <option value="work" data-l10n-id="vcard-entry-type-work"/>
+    <option value="home" data-l10n-id="vcard-entry-type-home"/>
+    <!-- FIXME: Replace these strings and remove aboutAddressBook.ftl from
+         AccountManager.xhtml. -->
+    <option value="cell" data-l10n-id="about-addressbook-entry-type-cell"/>
+    <option value="fax" data-l10n-id="about-addressbook-entry-type-fax"/>
+    <option value="pager" data-l10n-id="about-addressbook-entry-type-pager"/>
+    <option value="" data-l10n-id="vcard-entry-type-none" selected="selected"/>
+  </select>
+</template>
--- a/mail/components/addrbook/test/browser/browser_edit_card.js
+++ b/mail/components/addrbook/test/browser/browser_edit_card.js
@@ -1,15 +1,17 @@
 /* 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/. */
 
 // TODO: Fix the UI so that we don't have to do this.
 window.maximize();
 
+requestLongerTimeout(2);
+
 async function inEditingMode() {
   let abWindow = getAddressBookWindow();
   let abDocument = abWindow.document;
 
   await TestUtils.waitForCondition(
     () => abWindow.detailsPane.isEditing,
     "entering editing mode"
   );
@@ -242,28 +244,28 @@ function checkVCardInputValues(expected)
 
     for (let [index, field] of fields.entries()) {
       let expectedEntry = expectedEntries[index];
       let valueField;
       let typeField;
       switch (key) {
         case "email":
           valueField = field.emailEl;
-          typeField = field.selectEl;
+          typeField = field.vCardType.selectEl;
           break;
         case "impp":
           valueField = field.imppEl;
           break;
         case "url":
           valueField = field.urlEl;
-          typeField = field.selectEl;
+          typeField = field.vCardType.selectEl;
           break;
         case "tel":
           valueField = field.inputElement;
-          typeField = field.selectEl;
+          typeField = field.vCardType.selectEl;
           break;
         case "note":
           valueField = field.textAreaEl;
           break;
       }
 
       // Check the input value of the field.
       Assert.equal(
@@ -364,66 +366,101 @@ function setInputValues(changes) {
       } else {
         EventUtils.synthesizeKey("VK_BACK_SPACE", {}, abWindow);
       }
     }
   }
   EventUtils.synthesizeKey("VK_TAB", {}, abWindow);
 }
 
-function setVCardInputValues(changes) {
+/**
+ * Uses EventUtils.synthesizeMouseAtCenter and XULPopup.activateItem to
+ * activate optionValue from the select element typeField.
+ *
+ * @param {HTMLSelectElement} typeField Select element.
+ * @param {string} optionValue The value attribute of the option element from
+ * typeField.
+ */
+async function activateTypeSelect(typeField, optionValue) {
+  let abWindow = getAddressBookWindow();
+  // Ensure that the select field is inside the viewport.
+  typeField.scrollIntoView();
+  EventUtils.synthesizeMouseAtCenter(typeField, {}, abWindow);
+  let selectPopup = await BrowserTestUtils.waitForSelectPopupShown(window);
+
+  // Get the index of the optionValue from typeField
+  let index = Array.from(typeField.children).findIndex(
+    child => child.value === optionValue
+  );
+  Assert.ok(index >= 0, "Type in select field found");
+
+  // No change event is fired if the same option is activated.
+  if (index === typeField.selectedIndex) {
+    let popupHidden = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
+    selectPopup.hidePopup();
+    await popupHidden;
+    return;
+  }
+
+  // The change event saves the vCard value.
+  let changeEvent = BrowserTestUtils.waitForEvent(typeField, "change");
+  selectPopup.activateItem(selectPopup.children[index]);
+  await changeEvent;
+}
+
+async function setVCardInputValues(changes) {
   let abWindow = getAddressBookWindow();
 
   for (let [key, entries] of Object.entries(changes)) {
     let fields = getFields(key, true, entries.length);
     for (let [index, field] of fields.entries()) {
       let changeEntry = entries[index];
       let valueField;
       let typeField;
       switch (key) {
         case "email":
           valueField = field.emailEl;
-          typeField = field.selectEl;
+          typeField = field.vCardType.selectEl;
 
           if (
             (field.checkboxEl.checked && changeEntry && !changeEntry.pref) ||
             (!field.checkboxEl.checked &&
               changeEntry &&
               changeEntry.pref == "1")
           ) {
             EventUtils.synthesizeMouseAtCenter(field.checkboxEl, {}, abWindow);
           }
           break;
         case "impp":
           valueField = field.imppEl;
           break;
         case "url":
           valueField = field.urlEl;
-          typeField = field.selectEl;
+          typeField = field.vCardType.selectEl;
           break;
         case "tel":
           valueField = field.inputElement;
-          typeField = field.selectEl;
+          typeField = field.vCardType.selectEl;
           break;
         case "note":
           valueField = field.textAreaEl;
           break;
       }
 
       valueField.select();
       if (changeEntry && changeEntry.value) {
         EventUtils.sendString(changeEntry.value);
       } else {
         EventUtils.synthesizeKey("VK_BACK_SPACE", {}, abWindow);
       }
 
       if (typeField && changeEntry && changeEntry.type) {
-        field.selectEl.value = changeEntry.type;
+        await activateTypeSelect(typeField, changeEntry.type);
       } else if (typeField) {
-        field.selectEl.value = "";
+        await activateTypeSelect(typeField, "");
       }
     }
   }
   EventUtils.synthesizeKey("VK_TAB", {}, abWindow);
 }
 
 /**
  * Open the contact at the given index in the #cards element.
@@ -1781,17 +1818,17 @@ add_task(async function test_email_field
 
   checkVCardValues(book.childCards[0], {
     email: [{ value: "contact1.lastname1@invalid", pref: "1" }],
   });
 
   // Edit contact1 set type.
   await editContactAtIndex(0, { useMouse: true, useActivate: true });
 
-  setVCardInputValues({
+  await setVCardInputValues({
     email: [{ value: "contact1.lastname1@invalid", type: "work" }],
   });
 
   await checkDefaultEmailChoice(false, 0);
 
   EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
   await notInEditingMode(cardsList);
 
@@ -1825,17 +1862,17 @@ add_task(async function test_email_field
   await editContactAtIndex(0, { useMouse: true });
 
   checkVCardInputValues({
     email: [{ value: "contact1.lastname1@invalid", type: "work" }],
   });
 
   await checkDefaultEmailChoice(false, 0);
 
-  setVCardInputValues({
+  await setVCardInputValues({
     email: [
       { value: "contact1.lastname1@invalid", pref: "1", type: "work" },
       { value: "another.contact1@invalid", type: "home" },
     ],
   });
 
   await checkDefaultEmailChoice(true, 0);
 
@@ -1860,17 +1897,17 @@ add_task(async function test_email_field
     email: [
       { value: "contact1.lastname1@invalid", type: "work" },
       { value: "another.contact1@invalid", type: "home" },
     ],
   });
 
   await checkDefaultEmailChoice(true, 0);
 
-  setVCardInputValues({
+  await setVCardInputValues({
     email: [
       { value: "contact1.lastname1@invalid", type: "work" },
       { value: "another.contact1@invalid", type: "home", pref: "1" },
     ],
   });
 
   await checkDefaultEmailChoice(true, 1);
 
@@ -1895,17 +1932,17 @@ add_task(async function test_email_field
     email: [
       { value: "contact1.lastname1@invalid", type: "work" },
       { value: "another.contact1@invalid", type: "home" },
     ],
   });
 
   await checkDefaultEmailChoice(true, 1);
 
-  setVCardInputValues({
+  await setVCardInputValues({
     email: [{}, { value: "another.contact1@invalid", type: "home", pref: "1" }],
   });
 
   // The default email choosing is still visible until the contact is saved.
   await checkDefaultEmailChoice(true, 1);
 
   EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
   await notInEditingMode(editButton);
@@ -1924,36 +1961,36 @@ add_task(async function test_email_field
   await editContactAtIndex(1, {});
 
   checkVCardInputValues({
     email: [{ value: "contact2.lastname2@invalid" }],
   });
 
   await checkDefaultEmailChoice(false, 0);
 
-  setVCardInputValues({
+  await setVCardInputValues({
     email: [
       { value: "home.contact2@invalid", type: "home", pref: "1" },
       { value: "work.contact2@invalid", type: "work", pref: "1" },
     ],
   });
 
   await checkDefaultEmailChoice(true, 1);
 
-  setVCardInputValues({
+  await setVCardInputValues({
     email: [
       { value: "home.contact2@invalid", type: "home", pref: "1" },
       { value: "work.contact2@invalid", type: "work", pref: "1" },
       { value: "some.contact2@invalid" },
     ],
   });
 
   await checkDefaultEmailChoice(true, 1);
 
-  setVCardInputValues({
+  await setVCardInputValues({
     email: [
       { value: "home.contact2@invalid", type: "home", pref: "1" },
       { value: "work.contact2@invalid", type: "work", pref: "1" },
       { value: "some.contact2@invalid", pref: "1" },
       { value: "default.email.contact2@invalid", type: "home", pref: "1" },
     ],
   });
 
@@ -1984,17 +2021,17 @@ add_task(async function test_email_field
       { value: "work.contact2@invalid", type: "work" },
       { value: "some.contact2@invalid" },
       { value: "default.email.contact2@invalid", type: "home" },
     ],
   });
 
   await checkDefaultEmailChoice(true, 3);
 
-  setVCardInputValues({
+  await setVCardInputValues({
     email: [{ value: "home.contact2@invalid", type: "home" }],
   });
 
   // The default email choosing is still visible until the contact is saved.
   // For this case the default email is left on an empty field which will be
   // removed.
   await checkDefaultEmailChoice(true, 3);
 
@@ -2014,17 +2051,17 @@ add_task(async function test_email_field
   await editContactAtIndex(1, { useActivate: true });
 
   checkVCardInputValues({
     email: [{ value: "home.contact2@invalid", type: "home" }],
   });
 
   await checkDefaultEmailChoice(false, 0);
 
-  setVCardInputValues({
+  await setVCardInputValues({
     email: [
       { value: "home.contact2@invalid", type: "home", pref: "1" },
       { value: "work.contact2@invalid", type: "work", pref: "1" },
       { value: "some.contact2@invalid", pref: "1" },
       { value: "default.email.contact2@invalid", type: "home", pref: "1" },
     ],
   });
 
@@ -2115,17 +2152,17 @@ add_task(async function test_vCard_field
 
   // Cancel the new contact creation.
   EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
   await notInEditingMode(searchInput);
 
   // Set values for contact1 with one entry for each field.
   await editContactAtIndex(0, { useMouse: true, useActivate: true });
 
-  setVCardInputValues({
+  await setVCardInputValues({
     impp: [{ value: "matrix:u/contact1:example.com" }],
     url: [{ value: "http://www.example.com" }],
     tel: [{ value: "+123456 789" }],
     note: [{ value: "A note to this contact" }],
   });
 
   EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
   await notInEditingMode(cardsList);
@@ -2150,17 +2187,17 @@ add_task(async function test_vCard_field
 
   checkVCardInputValues({
     impp: [{ value: "matrix:u/contact1:example.com" }],
     url: [{ value: "http://www.example.com" }],
     tel: [{ value: "+123456 789" }],
     note: [{ value: "A note to this contact" }],
   });
 
-  setVCardInputValues({
+  await setVCardInputValues({
     impp: [
       { value: "matrix:u/contact1:example.com" },
       { value: "irc:irc.example.com/contact1,isuser" },
       { value: "xmpp:test@example.com" },
     ],
     url: [
       { value: "http://example.com" },
       { value: "https://hello", type: "home" },
@@ -2204,17 +2241,17 @@ add_task(async function test_vCard_field
   });
 
   // Switch from contact1 to contact2 and set some entries.
   // Ensure that no fields from contact1 are leaked.
   await editContactAtIndex(1, { useMouse: true });
 
   checkVCardInputValues({ impp: [], url: [], tel: [], note: [] });
 
-  setVCardInputValues({
+  await setVCardInputValues({
     impp: [{ value: "invalid:example.com" }],
     url: [{ value: "http://www.thunderbird.net" }],
     tel: [{ value: "650-903-0800" }],
     note: [{ value: "Another note\nfor contact 2" }],
   });
 
   EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
   await notInEditingMode(editButton);
@@ -2263,17 +2300,17 @@ add_task(async function test_vCard_field
     tel: [
       { value: "+123456 789", type: "home" },
       { value: "809 77 666 8" },
       { value: "+1113456789", type: "work" },
     ],
     note: [{ value: "Another note contact1\n\n\n" }],
   });
 
-  setVCardInputValues({
+  await setVCardInputValues({
     impp: [{ value: "" }, { value: "" }, { value: "" }],
     url: [{ value: "" }, { value: "" }, { value: "" }],
     tel: [{ value: "" }, { value: "" }, { value: "" }],
     note: [{ value: "" }],
   });
 
   EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
   await notInEditingMode(editButton);
@@ -2297,17 +2334,17 @@ add_task(async function test_vCard_field
 
   checkVCardInputValues({
     impp: [{ value: "invalid:example.com" }],
     url: [{ value: "http://www.thunderbird.net" }],
     tel: [{ value: "650-903-0800" }],
     note: [{ value: "Another note\nfor contact 2" }],
   });
 
-  setVCardInputValues({
+  await setVCardInputValues({
     impp: [{ value: "" }],
     url: [
       { value: "http://www.thunderbird.net" },
       { value: "www.another.url", type: "work" },
     ],
     tel: [{ value: "650-903-0800" }, { value: "+123 456 789", type: "home" }],
     note: [],
   });
@@ -2362,8 +2399,147 @@ add_task(async function test_vCard_field
   // Check that no values from contact2 are leaked to contact1 when cancelling.
   await editContactAtIndex(0, {});
 
   checkVCardInputValues({ impp: [], url: [], tel: [], note: [] });
 
   await closeAddressBookWindow();
   await promiseDirectoryRemoved(book.URI);
 });
+
+/**
+ * Switches to different types to verify that all works accordingly.
+ */
+add_task(async function test_type_selection() {
+  let abWindow = await openAddressBookWindow();
+  let abDocument = abWindow.document;
+  let book = createAddressBook("Test Book Type Selection");
+
+  let contact1 = createContact("contact1", "lastname");
+  book.addCard(contact1);
+
+  openDirectory(book);
+
+  let editButton = abDocument.getElementById("editButton");
+  let saveEditButton = abDocument.getElementById("saveEditButton");
+
+  await editContactAtIndex(0, {});
+
+  await setVCardInputValues({
+    email: [
+      { value: "contact1@invalid" },
+      { value: "home.contact1@invalid", type: "home" },
+      { value: "work.contact1@invalid", type: "work" },
+    ],
+    url: [
+      { value: "http://none.example.com" },
+      { value: "https://home.example.com", type: "home" },
+      { value: "https://work.example.com", type: "work" },
+    ],
+    tel: [
+      { value: "+123456 789" },
+      { value: "809 HOME 77 666 8", type: "home" },
+      { value: "+111 WORK 3456789", type: "work" },
+      { value: "+123 CELL 456 789", type: "cell" },
+      { value: "809 FAX 77 666 8", type: "fax" },
+      { value: "+111 PAGER 3456789", type: "pager" },
+    ],
+  });
+
+  EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+  await notInEditingMode(editButton);
+
+  checkVCardValues(book.childCards[0], {
+    email: [
+      { value: "contact1@invalid", pref: "1" },
+      { value: "home.contact1@invalid", type: "home" },
+      { value: "work.contact1@invalid", type: "work" },
+    ],
+    url: [
+      { value: "http://none.example.com" },
+      { value: "https://home.example.com", type: "home" },
+      { value: "https://work.example.com", type: "work" },
+    ],
+    tel: [
+      { value: "+123456 789" },
+      { value: "809 HOME 77 666 8", type: "home" },
+      { value: "+111 WORK 3456789", type: "work" },
+      { value: "+123 CELL 456 789", type: "cell" },
+      { value: "809 FAX 77 666 8", type: "fax" },
+      { value: "+111 PAGER 3456789", type: "pager" },
+    ],
+  });
+
+  EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+  await inEditingMode();
+
+  checkVCardInputValues({
+    email: [
+      { value: "contact1@invalid", pref: "1" },
+      { value: "home.contact1@invalid", type: "home" },
+      { value: "work.contact1@invalid", type: "work" },
+    ],
+    url: [
+      { value: "http://none.example.com" },
+      { value: "https://home.example.com", type: "home" },
+      { value: "https://work.example.com", type: "work" },
+    ],
+    tel: [
+      { value: "+123456 789" },
+      { value: "809 HOME 77 666 8", type: "home" },
+      { value: "+111 WORK 3456789", type: "work" },
+      { value: "+123 CELL 456 789", type: "cell" },
+      { value: "809 FAX 77 666 8", type: "fax" },
+      { value: "+111 PAGER 3456789", type: "pager" },
+    ],
+  });
+
+  await setVCardInputValues({
+    email: [
+      { value: "contact1@invalid", type: "work" },
+      { value: "home.contact1@invalid" },
+      { value: "work.contact1@invalid", type: "home" },
+    ],
+    url: [
+      { value: "http://none.example.com", type: "work" },
+      { value: "https://home.example.com" },
+      { value: "https://work.example.com", type: "home" },
+    ],
+    tel: [
+      { value: "+123456 789", type: "pager" },
+      { value: "809 HOME 77 666 8" },
+      { value: "+111 WORK 3456789", type: "home" },
+      { value: "+123 CELL 456 789" },
+      { value: "809 FAX 77 666 8", type: "fax" },
+      { value: "+111 PAGER 3456789", type: "cell" },
+    ],
+  });
+
+  EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+  await notInEditingMode(editButton);
+
+  checkVCardValues(book.childCards[0], {
+    email: [
+      { value: "contact1@invalid", type: "work", pref: "1" },
+      { value: "home.contact1@invalid" },
+      { value: "work.contact1@invalid", type: "home" },
+    ],
+    url: [
+      { value: "http://none.example.com", type: "work" },
+      { value: "https://home.example.com" },
+      { value: "https://work.example.com", type: "home" },
+    ],
+    tel: [
+      { value: "+123456 789", type: "pager" },
+      { value: "809 HOME 77 666 8" },
+      { value: "+111 WORK 3456789", type: "home" },
+      { value: "+123 CELL 456 789" },
+      { value: "809 FAX 77 666 8", type: "fax" },
+      { value: "+111 PAGER 3456789", type: "cell" },
+    ],
+  });
+
+  EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+  await notInEditingMode(editButton);
+
+  await closeAddressBookWindow();
+  await promiseDirectoryRemoved(book.URI);
+});