Bug 1427950 - Implement a multi-line PaymentRequest shipping address picker. r?MattN draft
authorJared Wein <jwein@mozilla.com>
Fri, 19 Jan 2018 13:30:03 -0500
changeset 747771 eaece8f7296fd868ab7cb4edd8db351764f448a1
parent 747168 59960ae69d7e675cfcfbf0ead6125cc8d3719f1f
push id97001
push userbmo:jaws@mozilla.com
push dateFri, 26 Jan 2018 19:48:40 +0000
reviewersMattN
bugs1427950
milestone60.0a1
Bug 1427950 - Implement a multi-line PaymentRequest shipping address picker. r?MattN MozReview-Commit-ID: 1CfKosjulNV
toolkit/components/payments/res/components/address-option.js
toolkit/components/payments/res/components/rich-option.js
toolkit/components/payments/res/components/rich-select.js
toolkit/components/payments/res/containers/address-picker.js
toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
toolkit/components/payments/res/paymentRequest.xhtml
toolkit/components/payments/test/mochitest/test_address_picker.html
--- a/toolkit/components/payments/res/components/address-option.js
+++ b/toolkit/components/payments/res/components/address-option.js
@@ -31,19 +31,17 @@ class AddressOption extends ObservedProp
       "name",
       "postal-code",
       "street-address",
       "tel",
     ]);
   }
 
   connectedCallback() {
-    for (let child of this.children) {
-      child.remove();
-    }
+    this.innerHTML = "";
 
     let fragment = document.createDocumentFragment();
     RichOption._createElement(fragment, "name");
     RichOption._createElement(fragment, "street-address");
     RichOption._createElement(fragment, "email");
     RichOption._createElement(fragment, "tel");
     this.appendChild(fragment);
 
--- a/toolkit/components/payments/res/components/rich-option.js
+++ b/toolkit/components/payments/res/components/rich-option.js
@@ -57,28 +57,36 @@ class RichOption extends ObservedPropert
     }
   }
 
   get selected() {
     return this.hasAttribute("selected");
   }
 
   set selected(value) {
+    let changed = this.selected != value;
+
+    let select = this.closest("rich-select");
     if (value) {
-      let oldSelectedOptions = this.parentNode.querySelectorAll("[selected]");
-      for (let option of oldSelectedOptions) {
-        option.removeAttribute("selected");
+      if (select) {
+        let oldSelectedOptions = select.querySelectorAll(`[selected]:not([guid="${this.guid}"])`);
+        changed = changed || !!oldSelectedOptions.length;
+        for (let option of oldSelectedOptions) {
+          option.removeAttribute("selected");
+        }
       }
       this.setAttribute("selected", value);
     } else {
       this.removeAttribute("selected");
     }
-    let richSelect = this.closest("rich-select");
-    if (richSelect && richSelect.render) {
-      richSelect.render();
+
+    if (changed && select) {
+      let event = document.createEvent("UIEvent");
+      event.initEvent("change", true, true);
+      select.dispatchEvent(event);
     }
     return value;
   }
 
   static _createElement(fragment, className) {
     let element = document.createElement("span");
     element.classList.add(className);
     fragment.appendChild(element);
--- a/toolkit/components/payments/res/components/rich-select.js
+++ b/toolkit/components/payments/res/components/rich-select.js
@@ -43,16 +43,24 @@ class RichSelect extends ObservedPropert
   get popupBox() {
     return this.querySelector(":scope > .rich-select-popup-box");
   }
 
   get selectedOption() {
     return this.popupBox.querySelector(":scope > [selected]");
   }
 
+  get selectedStateKey() {
+    return this.getAttribute("selected-state-key");
+  }
+
+  set selectedStateKey(val) {
+    this.setAttribute("selected-state-key", val);
+  }
+
   namedItem(name) {
     return this.popupBox.querySelector(`:scope > [name="${CSS.escape(name)}"]`);
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "blur": {
         this.onBlur(event);
@@ -139,16 +147,20 @@ class RichSelect extends ObservedPropert
     /* eslint-enable max-len */
     for (let option of options) {
       popupBox.appendChild(option);
     }
 
     let selectedChild;
     for (let child of popupBox.children) {
       if (child.selected) {
+        // Only allow one child to be selected at a time.
+        if (selectedChild) {
+          selectedChild.selected = false;
+        }
         selectedChild = child;
       }
     }
     if (!selectedChild && popupBox.children.length) {
       selectedChild = popupBox.children[0];
       selectedChild.selected = true;
     }
 
--- a/toolkit/components/payments/res/containers/address-picker.js
+++ b/toolkit/components/payments/res/containers/address-picker.js
@@ -11,39 +11,61 @@
  * Container around <rich-select> (eventually providing add/edit links) with
  * <address-option> listening to savedAddresses.
  */
 
 class AddressPicker extends PaymentStateSubscriberMixin(HTMLElement) {
   constructor() {
     super();
     this.dropdown = document.createElement("rich-select");
+    this.dropdown.picker = this;
+    this.dropdown.selectedStateKey = this.selectedStateKey;
+    this.addEventListener("change", this);
   }
 
   connectedCallback() {
     this.appendChild(this.dropdown);
     super.connectedCallback();
   }
 
+  handleEvent(event) {
+    if (event.type == "change") {
+      let select = event.target;
+      let selectedKey = select.selectedStateKey;
+      if (selectedKey) {
+        let state = {};
+        state[selectedKey] = select.querySelector("[selected]").guid;
+        this.requestStore.setState(state);
+      }
+    }
+  }
+
   render(state) {
     let {savedAddresses} = state;
     let desiredOptions = [];
     for (let [guid, address] of Object.entries(savedAddresses)) {
       let optionEl = this.dropdown.namedItem(guid);
       if (!optionEl) {
         optionEl = document.createElement("address-option");
         optionEl.name = guid;
         optionEl.guid = guid;
       }
       for (let [key, val] of Object.entries(address)) {
         optionEl.setAttribute(key, val);
       }
+      if (guid == state[this.selectedStateKey]) {
+        optionEl.setAttribute("selected", "true");
+      }
       desiredOptions.push(optionEl);
     }
     let el = null;
     while ((el = this.dropdown.popupBox.querySelector(":scope > address-option"))) {
       el.remove();
     }
     this.dropdown.popupBox.append(...desiredOptions);
   }
+
+  get selectedStateKey() {
+    return this.getAttribute("selected-state-key");
+  }
 }
 
 customElements.define("address-picker", AddressPicker);
--- a/toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
+++ b/toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -30,16 +30,17 @@ let requestStore = new PaymentsStore({
     paymentOptions: {
       requestPayerName: false,
       requestPayerEmail: false,
       requestPayerPhone: false,
       requestShipping: false,
       shippingType: "shipping",
     },
   },
+  selectedShippingAddress: null,
   savedAddresses: {},
   savedBasicCards: {},
 });
 
 
 /* exported PaymentStateSubscriberMixin */
 
 /**
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -31,17 +31,17 @@
     <div id="host-name"></div>
 
     <div id="total">
       <h2 class="label"></h2>
       <currency-amount></currency-amount>
     </div>
 
     <div><label>Shipping Address</label></div>
-    <address-picker>
+    <address-picker selected-state-key="selectedShippingAddress">
     </address-picker>
 
     <div id="controls-container">
       <button id="cancel">Cancel</button>
       <button id="pay">Pay</button>
     </div>
   </template>
 </head>
--- a/toolkit/components/payments/test/mochitest/test_address_picker.html
+++ b/toolkit/components/payments/test/mochitest/test_address_picker.html
@@ -19,17 +19,18 @@ Test the address-picker component
   <script src="rich-option.js"></script>
   <script src="address-option.js"></script>
   <link rel="stylesheet" type="text/css" href="rich-select.css"/>
   <link rel="stylesheet" type="text/css" href="address-option.css"/>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
-    <address-picker id="picker1"></address-picker>
+    <address-picker id="picker1"
+                    selected-state-key="selectedShippingAddress"></address-picker>
   </p>
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 <script type="application/javascript">
 /** Test the address-picker component **/
@@ -109,16 +110,32 @@ add_task(async function test_update() {
   is(options.length, 2, "Check dropdown still has both addresses");
   ok(options[0].textContent.includes("MI-edit"), "Check updated first address-level1");
   ok(options[0].textContent.includes("Some City-edit"), "Check updated first address-level2");
   ok(options[0].textContent.includes("new-edit"), "Check updated first address");
 
   ok(options[1].textContent.includes("P.O. Box 123"), "Check second address is the same");
 });
 
+add_task(async function test_change_selected_address() {
+  let options = picker1.dropdown.popupBox.children;
+  let selectedOption = picker1.dropdown.popupBox.querySelector("[selected]");
+  is(selectedOption, options[0], "Selected option should default to the first option");
+  let {selectedShippingAddress} = picker1.requestStore.getState();
+  is(selectedShippingAddress, selectedOption.guid, "should should have first option selected");
+
+  options[1].selected = true;
+  await asyncElementRendered();
+
+  selectedOption = picker1.dropdown.popupBox.querySelector("[selected]");
+  is(selectedOption, options[1], "Selected option should now be the second option");
+  selectedShippingAddress = picker1.requestStore.getState().selectedShippingAddress;
+  is(selectedShippingAddress, selectedOption.guid, "store should have second option selected");
+});
+
 add_task(async function test_delete() {
   picker1.requestStore.setState({
     savedAddresses: {
       // 48bnds6854t was deleted
       "68gjdh354j": {
         "address-level1": "CA",
         "address-level2": "Mountain View",
         "country": "US",