Bug 1383687 - Order edit dialog fields based on country selected. r=steveck,lchang draft
authorScott Wu <scottcwwu@gmail.com>
Fri, 03 Nov 2017 16:24:30 +0800
changeset 707727 d602fcdc7a7b9e467def6d536fda7dfeb1336465
parent 707249 f2cf6d1473808039be5ecd8727cc3791d5d7d2d4
child 743014 0b95cedc986fe5bdd8a3aaf308e1036d32f8d185
push id92194
push userbmo:scwwu@mozilla.com
push dateTue, 05 Dec 2017 17:44:03 +0000
reviewerssteveck, lchang
bugs1383687
milestone59.0a1
Bug 1383687 - Order edit dialog fields based on country selected. r=steveck,lchang MozReview-Commit-ID: 1qPxvHhNGtK
browser/extensions/formautofill/FormAutofillUtils.jsm
browser/extensions/formautofill/content/editAddress.xhtml
browser/extensions/formautofill/content/editDialog.js
browser/extensions/formautofill/skin/shared/editAddress.css
browser/extensions/formautofill/skin/shared/editDialog.css
browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
browser/extensions/formautofill/test/browser/head.js
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -652,23 +652,25 @@ this.FormAutofillUtils = {
 
   /**
    * Get formatting information of a given country
    * @param   {string} country
    * @returns {object}
    *         {
    *           {string} addressLevel1Label
    *           {string} postalCodeLabel
+   *           {object} fieldsOrder
    *         }
    */
   getFormFormat(country) {
     const dataset = this.getCountryAddressData(country);
     return {
       "addressLevel1Label": dataset.state_name_type || "province",
       "postalCodeLabel": dataset.zip_name_type || "postalCode",
+      "fieldsOrder": this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C, %S %Z"),
     };
   },
 
   /**
    * Localize elements with "data-localization" attribute
    * @param   {string} bundleURI
    * @param   {DOMElement} root
    */
--- a/browser/extensions/formautofill/content/editAddress.xhtml
+++ b/browser/extensions/formautofill/content/editAddress.xhtml
@@ -8,48 +8,52 @@
   <title data-localization="addNewAddressTitle"/>
   <link rel="stylesheet" href="chrome://formautofill-shared/skin/editDialog.css"/>
   <link rel="stylesheet" href="chrome://formautofill-shared/skin/editAddress.css"/>
   <link rel="stylesheet" href="chrome://formautofill/skin/editDialog.css"/>
   <script src="chrome://formautofill/content/editDialog.js"></script>
 </head>
 <body>
   <form id="form" autocomplete="off">
-    <label id="given-name-container">
-      <span data-localization="givenName"/>
-      <input id="given-name" type="text"/>
-    </label>
-    <label id="additional-name-container">
-      <span data-localization="additionalName"/>
-      <input id="additional-name" type="text"/>
-    </label>
-    <label id="family-name-container">
-      <span data-localization="familyName"/>
-      <input id="family-name" type="text"/>
-    </label>
-    <label id="organization-container">
-      <span data-localization="organization2"/>
-      <input id="organization" type="text"/>
-    </label>
-    <label id="street-address-container">
-      <span data-localization="streetAddress"/>
-      <textarea id="street-address" rows="3"/>
-    </label>
-    <label id="address-level2-container">
-      <span data-localization="city"/>
-      <input id="address-level2" type="text"/>
-    </label>
-    <label id="address-level1-container">
-      <span/>
-      <input id="address-level1" type="text"/>
-    </label>
-    <label id="postal-code-container">
-      <span/>
-      <input id="postal-code" type="text"/>
-    </label>
+    <div>
+      <div id="name-container">
+        <label id="given-name-container">
+          <span data-localization="givenName"/>
+          <input id="given-name" type="text"/>
+        </label>
+        <label id="additional-name-container">
+          <span data-localization="additionalName"/>
+          <input id="additional-name" type="text"/>
+        </label>
+        <label id="family-name-container">
+          <span data-localization="familyName"/>
+          <input id="family-name" type="text"/>
+        </label>
+      </div>
+      <label id="organization-container">
+        <span data-localization="organization2"/>
+        <input id="organization" type="text"/>
+      </label>
+      <label id="street-address-container">
+        <span data-localization="streetAddress"/>
+        <textarea id="street-address" rows="3"/>
+      </label>
+      <label id="address-level2-container">
+        <span data-localization="city"/>
+        <input id="address-level2" type="text"/>
+      </label>
+      <label id="address-level1-container">
+        <span/>
+        <input id="address-level1" type="text"/>
+      </label>
+      <label id="postal-code-container">
+        <span/>
+        <input id="postal-code" type="text"/>
+      </label>
+    </div>
     <label id="country-container">
       <span data-localization="country"/>
       <select id="country">
         <option/>
       </select>
     </label>
     <p id="country-warning-message" data-localization="countryWarningMessage2"/>
     <label id="email-container">
--- a/browser/extensions/formautofill/content/editDialog.js
+++ b/browser/extensions/formautofill/content/editDialog.js
@@ -115,16 +115,20 @@ class EditDialog {
       case "input": {
         this.handleInput(event);
         break;
       }
       case "keypress": {
         this.handleKeyPress(event);
         break;
       }
+      case "change": {
+        this.handleChange(event);
+        break;
+      }
     }
   }
 
   /**
    * Handle click events
    *
    * @param  {DOMEvent} event
    */
@@ -175,35 +179,75 @@ class EditDialog {
   /**
    * Remove event listener
    */
   detachEventListeners() {
     window.removeEventListener("keypress", this);
     this._elements.controlsContainer.removeEventListener("click", this);
     document.removeEventListener("input", this);
   }
+
+  // An interface to be inherited.
+  localizeDocument() {}
+
+  // An interface to be inherited.
+  handleSubmit(event) {}
+
+  // An interface to be inherited.
+  handleChange(event) {}
 }
 
 class EditAddress extends EditDialog {
   constructor(elements, record) {
     super("addresses", elements, record);
     this.formatForm(record && record.country);
   }
 
   /**
    * Format the form based on country. The address-level1 and postal-code labels
    * should be specific to the given country.
    * @param  {string} country
    */
   formatForm(country) {
-    // TODO: Use fmt to show/hide and order fields (Bug 1383687)
-    const {addressLevel1Label, postalCodeLabel} = FormAutofillUtils.getFormFormat(country);
+    const {addressLevel1Label, postalCodeLabel, fieldsOrder} = FormAutofillUtils.getFormFormat(country);
     this._elements.addressLevel1Label.dataset.localization = addressLevel1Label;
     this._elements.postalCodeLabel.dataset.localization = postalCodeLabel;
     FormAutofillUtils.localizeMarkup(AUTOFILL_BUNDLE_URI, document);
+    this.arrangeFields(fieldsOrder);
+  }
+
+  arrangeFields(fieldsOrder) {
+    let fields = [
+      "name",
+      "organization",
+      "street-address",
+      "address-level2",
+      "address-level1",
+      "postal-code",
+    ];
+    let inputs = [];
+    for (let i = 0; i < fieldsOrder.length; i++) {
+      let {fieldId, newLine} = fieldsOrder[i];
+      let container = document.getElementById(`${fieldId}-container`);
+      inputs.push(...container.querySelectorAll("input, textarea, select"));
+      container.style.display = "flex";
+      container.style.order = i;
+      container.style.pageBreakAfter = newLine ? "always" : "auto";
+      // Remove the field from the list of fields
+      fields.splice(fields.indexOf(fieldId), 1);
+    }
+    for (let i = 0; i < inputs.length; i++) {
+      // Assign tabIndex starting from 1
+      inputs[i].tabIndex = i + 1;
+    }
+    // Hide the remaining fields
+    for (let field of fields) {
+      let container = document.getElementById(`${field}-container`);
+      container.style.display = "none";
+    }
   }
 
   localizeDocument() {
     if (this._record) {
       this._elements.title.dataset.localization = "editAddressTitle";
     }
     let fragment = document.createDocumentFragment();
     for (let country of FormAutofillUtils.supportedCountries) {
@@ -215,16 +259,30 @@ class EditAddress extends EditDialog {
     this._elements.country.appendChild(fragment);
     FormAutofillUtils.localizeMarkup(REGIONS_BUNDLE_URI, this._elements.country);
   }
 
   async handleSubmit() {
     await this.saveRecord(this.buildFormObject(), this._record ? this._record.guid : null);
     window.close();
   }
+
+  handleChange(event) {
+    this.formatForm(event.target.value);
+  }
+
+  attachEventListeners() {
+    this._elements.country.addEventListener("change", this);
+    super.attachEventListeners();
+  }
+
+  detachEventListeners() {
+    this._elements.country.removeEventListener("change", this);
+    super.detachEventListeners();
+  }
 }
 
 class EditCreditCard extends EditDialog {
   constructor(elements, record) {
     super("creditCards", elements, record);
     this.generateYears();
   }
 
--- a/browser/extensions/formautofill/skin/shared/editAddress.css
+++ b/browser/extensions/formautofill/skin/shared/editAddress.css
@@ -21,23 +21,21 @@ select {
 #country-warning-message,
 #family-name-container,
 #organization-container,
 #address-level2-container,
 #tel-container {
   flex: 0 1 50%;
 }
 
-#family-name-container,
-#organization-container,
-#address-level2-container,
 #tel-container {
   padding-inline-end: 50%;
 }
 
+#name-container,
 #street-address-container,
 #email-container {
   flex: 0 1 100%;
 }
 
 #street-address,
 #email {
   flex: 1 0 auto;
--- a/browser/extensions/formautofill/skin/shared/editDialog.css
+++ b/browser/extensions/formautofill/skin/shared/editDialog.css
@@ -4,23 +4,27 @@
 
 form,
 label,
 div,
 p {
   display: flex;
 }
 
+form,
+div {
+  flex-wrap: wrap;
+}
+
 form {
-  flex-wrap: wrap;
   /* Add extra space to ensure invalid input box is displayed properly */
   padding: 2px;
 }
 
-form > label,
+form label,
 form > p {
   margin: 0 0 0.5em !important;
 }
 
 label > span,
 div > span {
   box-sizing: border-box;
   padding-inline-end: 0.7em;
--- a/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
@@ -29,42 +29,51 @@ add_task(async function test_cancelEditA
 add_task(async function test_saveAddress() {
   await new Promise(resolve => {
     let win = window.openDialog(EDIT_ADDRESS_DIALOG_URL);
     win.addEventListener("load", () => {
       win.addEventListener("unload", () => {
         ok(true, "Edit address dialog is closed");
         resolve();
       }, {once: true});
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey(TEST_ADDRESS_1["given-name"], {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey(TEST_ADDRESS_1["additional-name"], {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey(TEST_ADDRESS_1["family-name"], {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey(TEST_ADDRESS_1.organization, {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey(TEST_ADDRESS_1["street-address"], {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level2"], {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level1"], {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey(TEST_ADDRESS_1["postal-code"], {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey(TEST_ADDRESS_1.country, {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey(TEST_ADDRESS_1.email, {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey(TEST_ADDRESS_1.tel, {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      EventUtils.synthesizeKey("VK_TAB", {}, win);
-      info("saving address");
-      EventUtils.synthesizeKey("VK_RETURN", {}, win);
+      let doc = win.document;
+      // Verify labels
+      is(doc.querySelector("#address-level1-container > span").textContent, "State",
+                           "US address-level1 label should be 'State'");
+      is(doc.querySelector("#postal-code-container > span").textContent, "Zip Code",
+                           "US postal-code label should be 'Zip Code'");
+      // Input address info and verify move through form with tab keys
+      const keyInputs = [
+        "VK_TAB",
+        TEST_ADDRESS_1["given-name"],
+        "VK_TAB",
+        TEST_ADDRESS_1["additional-name"],
+        "VK_TAB",
+        TEST_ADDRESS_1["family-name"],
+        "VK_TAB",
+        TEST_ADDRESS_1.organization,
+        "VK_TAB",
+        TEST_ADDRESS_1["street-address"],
+        "VK_TAB",
+        TEST_ADDRESS_1["address-level2"],
+        "VK_TAB",
+        TEST_ADDRESS_1["address-level1"],
+        "VK_TAB",
+        TEST_ADDRESS_1["postal-code"],
+        "VK_TAB",
+        TEST_ADDRESS_1.country,
+        "VK_TAB",
+        TEST_ADDRESS_1.email,
+        "VK_TAB",
+        TEST_ADDRESS_1.tel,
+        "VK_TAB",
+        "VK_TAB",
+        "VK_RETURN",
+      ];
+      keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win));
     }, {once: true});
   });
   let addresses = await getAddresses();
 
   is(addresses.length, 1, "only one address is in storage");
   is(Object.keys(TEST_ADDRESS_1).length, 11, "Sanity check number of properties");
   for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_1)) {
     is(addresses[0][fieldName], fieldValue, "check " + fieldName);
@@ -90,8 +99,114 @@ add_task(async function test_editAddress
 
   is(addresses.length, 1, "only one address is in storage");
   is(addresses[0]["given-name"], TEST_ADDRESS_1["given-name"] + "test", "given-name changed");
   await removeAddresses([addresses[0].guid]);
 
   addresses = await getAddresses();
   is(addresses.length, 0, "Address storage is empty");
 });
+
+add_task(async function test_saveAddressCA() {
+  await new Promise(resolve => {
+    let win = window.openDialog(EDIT_ADDRESS_DIALOG_URL);
+    win.addEventListener("load", () => {
+      win.addEventListener("unload", () => {
+        ok(true, "Edit address dialog is closed");
+        resolve();
+      }, {once: true});
+      let doc = win.document;
+      // Change country to verify labels
+      doc.querySelector("#country").focus();
+      EventUtils.synthesizeKey("Canada", {}, win);
+      is(doc.querySelector("#address-level1-container > span").textContent, "Province",
+                           "CA address-level1 label should be 'Province'");
+      is(doc.querySelector("#postal-code-container > span").textContent, "Postal Code",
+                           "CA postal-code label should be 'Postal Code'");
+      // Input address info and verify move through form with tab keys
+      doc.querySelector("#given-name").focus();
+      const keyInputs = [
+        TEST_ADDRESS_CA_1["given-name"],
+        "VK_TAB",
+        TEST_ADDRESS_CA_1["additional-name"],
+        "VK_TAB",
+        TEST_ADDRESS_CA_1["family-name"],
+        "VK_TAB",
+        TEST_ADDRESS_CA_1.organization,
+        "VK_TAB",
+        TEST_ADDRESS_CA_1["street-address"],
+        "VK_TAB",
+        TEST_ADDRESS_CA_1["address-level2"],
+        "VK_TAB",
+        TEST_ADDRESS_CA_1["address-level1"],
+        "VK_TAB",
+        TEST_ADDRESS_CA_1["postal-code"],
+        "VK_TAB",
+        TEST_ADDRESS_CA_1.country,
+        "VK_TAB",
+        TEST_ADDRESS_CA_1.email,
+        "VK_TAB",
+        TEST_ADDRESS_CA_1.tel,
+        "VK_TAB",
+        "VK_TAB",
+        "VK_RETURN",
+      ];
+      keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win));
+    }, {once: true});
+  });
+  let addresses = await getAddresses();
+  for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_CA_1)) {
+    is(addresses[0][fieldName], fieldValue, "check " + fieldName);
+  }
+  await removeAllRecords();
+});
+
+add_task(async function test_saveAddressDE() {
+  await new Promise(resolve => {
+    let win = window.openDialog(EDIT_ADDRESS_DIALOG_URL);
+    win.addEventListener("load", () => {
+      win.addEventListener("unload", () => {
+        ok(true, "Edit address dialog is closed");
+        resolve();
+      }, {once: true});
+      let doc = win.document;
+      // Change country to verify labels
+      doc.querySelector("#country").focus();
+      EventUtils.synthesizeKey("Germany", {}, win);
+      is(doc.querySelector("#postal-code-container > span").textContent, "Postal Code",
+                           "DE postal-code label should be 'Postal Code'");
+      is(doc.querySelector("#address-level1-container").style.display, "none",
+                           "DE address-level1 should be hidden");
+      // Input address info and verify move through form with tab keys
+      doc.querySelector("#given-name").focus();
+      const keyInputs = [
+        TEST_ADDRESS_DE_1["given-name"],
+        "VK_TAB",
+        TEST_ADDRESS_DE_1["additional-name"],
+        "VK_TAB",
+        TEST_ADDRESS_DE_1["family-name"],
+        "VK_TAB",
+        TEST_ADDRESS_DE_1.organization,
+        "VK_TAB",
+        TEST_ADDRESS_DE_1["street-address"],
+        "VK_TAB",
+        TEST_ADDRESS_DE_1["postal-code"],
+        "VK_TAB",
+        TEST_ADDRESS_DE_1["address-level2"],
+        "VK_TAB",
+        TEST_ADDRESS_DE_1.country,
+        "VK_TAB",
+        TEST_ADDRESS_DE_1.email,
+        "VK_TAB",
+        TEST_ADDRESS_DE_1.tel,
+        "VK_TAB",
+        "VK_TAB",
+        "VK_RETURN",
+      ];
+      keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win));
+    }, {once: true});
+  });
+  let addresses = await getAddresses();
+  for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_DE_1)) {
+    is(addresses[0][fieldName], fieldValue, "check " + fieldName);
+  }
+  await removeAllRecords();
+});
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -1,10 +1,10 @@
 /* exported MANAGE_ADDRESSES_DIALOG_URL, MANAGE_CREDIT_CARDS_DIALOG_URL, EDIT_ADDRESS_DIALOG_URL, EDIT_CREDIT_CARD_DIALOG_URL,
-            BASE_URL, TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3, TEST_ADDRESS_4, TEST_ADDRESS_5,
+            BASE_URL, TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3, TEST_ADDRESS_4, TEST_ADDRESS_5, TEST_ADDRESS_CA_1, TEST_ADDRESS_DE_1,
             TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2, TEST_CREDIT_CARD_3, FORM_URL, CREDITCARD_FORM_URL,
             FTU_PREF, ENABLED_AUTOFILL_ADDRESSES_PREF, AUTOFILL_CREDITCARDS_AVAILABLE_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF,
             SYNC_USERNAME_PREF, SYNC_ADDRESSES_PREF, SYNC_CREDITCARDS_PREF, SYNC_CREDITCARDS_AVAILABLE_PREF, CREDITCARDS_USED_STATUS_PREF,
             sleep, expectPopupOpen, openPopupOn, expectPopupClose, closePopup, clickDoorhangerButton,
             getAddresses, saveAddress, removeAddresses, saveCreditCard,
             getDisplayedPopupItems, getDoorhangerCheckbox, waitForMasterPasswordDialog,
             getNotification, getDoorhangerButton, removeAllRecords */
 
@@ -62,16 +62,43 @@ const TEST_ADDRESS_4 = {
   country: "US",
   email: "timbl@w3.org",
 };
 
 const TEST_ADDRESS_5 = {
   tel: "+16172535702",
 };
 
+const TEST_ADDRESS_CA_1 = {
+  "given-name": "John",
+  "additional-name": "R.",
+  "family-name": "Smith",
+  organization: "Mozilla",
+  "street-address": "163 W Hastings\nSuite 209",
+  "address-level2": "Vancouver",
+  "address-level1": "BC",
+  "postal-code": "V6B 1H5",
+  country: "CA",
+  tel: "+17787851540",
+  email: "timbl@w3.org",
+};
+
+const TEST_ADDRESS_DE_1 = {
+  "given-name": "John",
+  "additional-name": "R.",
+  "family-name": "Smith",
+  organization: "Mozilla",
+  "street-address": "Geb\u00E4ude 3, 4. Obergeschoss\nSchlesische Stra\u00DFe 27",
+  "address-level2": "Berlin",
+  "postal-code": "10997",
+  country: "DE",
+  tel: "+4930983333000",
+  email: "timbl@w3.org",
+};
+
 const TEST_CREDIT_CARD_1 = {
   "cc-name": "John Doe",
   "cc-number": "1234567812345678",
   "cc-exp-month": 4,
   "cc-exp-year": 2017,
 };
 
 const TEST_CREDIT_CARD_2 = {