Bug 1469464 - Consistent PaymentRequest footer positioning with <payment-request-page>. r=sfoster
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Thu, 12 Jul 2018 09:52:30 -0700
changeset 426413 e6f0e3fb69d6de8265b5bdbdf886f59504c37709
parent 426412 d6174a81ff894c42c63a900c47a9179be68e49b0
child 426414 8da83fe09309320af068b5cebb97b876af2d427e
push id34272
push userebalazs@mozilla.com
push dateFri, 13 Jul 2018 08:51:04 +0000
treeherdermozilla-central@254564563107 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfoster
bugs1469464
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1469464 - Consistent PaymentRequest footer positioning with <payment-request-page>. r=sfoster MozReview-Commit-ID: Oq06q6xF0e
browser/components/payments/docs/index.rst
browser/components/payments/res/components/payment-request-page.js
browser/components/payments/res/containers/address-form.js
browser/components/payments/res/containers/basic-card-form.js
browser/components/payments/res/containers/payment-dialog.js
browser/components/payments/res/debugging.css
browser/components/payments/res/paymentRequest.css
browser/components/payments/res/paymentRequest.xhtml
browser/components/payments/test/mochitest/test_address_form.html
browser/components/payments/test/mochitest/test_basic_card_form.html
browser/components/payments/test/mochitest/test_payment_dialog.html
browser/extensions/formautofill/content/autofillEditForms.js
--- a/browser/components/payments/docs/index.rst
+++ b/browser/components/payments/docs/index.rst
@@ -9,17 +9,17 @@ JSDoc style comments are used within the
 .. toctree::
    :maxdepth: 5
 
 
 Debugging/Development
 =====================
 
 Must Have Electrolysis
--------
+----------------------
 
 Web Payments `does not work without e10s <https://bugzilla.mozilla.org/show_bug.cgi?id=1365964>`_!
 
 Logging
 -------
 
 Set the pref ``dom.payments.loglevel`` to "Debug" to increase the verbosity of console messages.
 
@@ -64,8 +64,24 @@ In order to communicate across the proce
 This is because the unprivileged document cannot access message managers.
 Instead, all communication across the privileged/unprivileged boundary is done via custom DOM events:
 
 * A ``paymentContentToChrome`` event is dispatched when the dialog contents want to communicate with the privileged dialog wrapper.
 * A ``paymentChromeToContent`` event is dispatched on the ``window`` with the ``detail`` property populated when the privileged dialog wrapper communicates with the unprivileged dialog.
 
 These events are converted to/from message manager messages of the same name to communicate to the other process.
 The purpose of `paymentDialogFrameScript.js` is to simply convert unprivileged DOM events to/from messages from the other process.
+
+Custom Elements
+---------------
+
+The Payment Request UI uses Custom Elements for the UI components.
+
+Some guidelines:
+* If you're overriding a lifecycle callback, don't forget to call that method on
+  ``super`` from the implementation to ensure that mixins and ancestor classes
+  work properly.
+* From within a custom element, don't use ``document.getElementById`` or
+  ``document.querySelector*`` because they can return elements that are outside
+  of the component, thus breaking the modularization. It can also cause problems
+  if the elements you're looking for aren't attached to the document yet. Use
+  ``querySelector*`` on ``this`` (the custom element) or one of its descendants
+  instead.
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/res/components/payment-request-page.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+/**
+ * <payment-request-page></payment-request-page>
+ */
+
+export default class PaymentRequestPage extends HTMLElement {
+  constructor() {
+    super();
+
+    this.classList.add("page");
+
+    this.pageTitleHeading = document.createElement("h2");
+
+    // The body and footer may be pre-defined in the template so re-use them if they exist.
+    this.body = this.querySelector(":scope > .page-body") || document.createElement("div");
+    this.body.classList.add("page-body");
+
+    this.footer = this.querySelector(":scope > footer") || document.createElement("footer");
+  }
+
+  connectedCallback() {
+    // The heading goes inside the body so it scrolls.
+    this.body.prepend(this.pageTitleHeading);
+    this.appendChild(this.body);
+
+    this.appendChild(this.footer);
+  }
+}
+
+customElements.define("payment-request-page", PaymentRequestPage);
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -1,30 +1,34 @@
 /* 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/. */
 
 /* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
 import LabelledCheckbox from "../components/labelled-checkbox.js";
+import PaymentRequestPage from "../components/payment-request-page.js";
 import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
 import paymentRequest from "../paymentRequest.js";
 /* import-globals-from ../unprivileged-fallbacks.js */
 
 /**
  * <address-form></address-form>
  *
+ * Don't use document.getElementById or document.querySelector* to access form
+ * elements, use querySelector on `this` or `this.form` instead so that elements
+ * can be found before the element is connected.
+ *
  * XXX: Bug 1446164 - This form isn't localized when used via this custom element
  * as it will be much easier to share the logic once we switch to Fluent.
  */
 
-export default class AddressForm extends PaymentStateSubscriberMixin(HTMLElement) {
+export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequestPage) {
   constructor() {
     super();
 
-    this.pageTitle = document.createElement("h2");
     this.genericErrorText = document.createElement("div");
 
     this.cancelButton = document.createElement("button");
     this.cancelButton.className = "cancel-button";
     this.cancelButton.addEventListener("click", this);
 
     this.backButton = document.createElement("button");
     this.backButton.className = "back-button";
@@ -68,33 +72,33 @@ export default class AddressForm extends
       });
       xhr.open("GET", url);
       xhr.send();
     });
   }
 
   connectedCallback() {
     this.promiseReady.then(form => {
-      this.appendChild(this.pageTitle);
-      this.appendChild(form);
+      this.body.appendChild(form);
 
       let record = {};
       this.formHandler = new EditAddress({
         form,
       }, record, {
         DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
         getFormFormat: PaymentDialogUtils.getFormFormat,
         supportedCountries: PaymentDialogUtils.supportedCountries,
       });
 
-      this.appendChild(this.persistCheckbox);
-      this.appendChild(this.genericErrorText);
-      this.appendChild(this.cancelButton);
-      this.appendChild(this.backButton);
-      this.appendChild(this.saveButton);
+      this.body.appendChild(this.persistCheckbox);
+      this.body.appendChild(this.genericErrorText);
+
+      this.footer.appendChild(this.cancelButton);
+      this.footer.appendChild(this.backButton);
+      this.footer.appendChild(this.saveButton);
       // Only call the connected super callback(s) once our markup is fully
       // connected, including the shared form fetched asynchronously.
       super.connectedCallback();
     });
   }
 
   render(state) {
     let record = {};
@@ -118,17 +122,17 @@ export default class AddressForm extends
     this.cancelButton.hidden = !page.onboardingWizard;
 
     if (addressPage.addressFields) {
       this.setAttribute("address-fields", addressPage.addressFields);
     } else {
       this.removeAttribute("address-fields");
     }
 
-    this.pageTitle.textContent = addressPage.title;
+    this.pageTitleHeading.textContent = addressPage.title;
     this.genericErrorText.textContent = page.error;
 
     let editing = !!addressPage.guid;
     let addresses = paymentRequest.getAddresses(state);
 
     // If an address is selected we want to edit it.
     if (editing) {
       record = addresses[addressPage.guid];
@@ -156,18 +160,18 @@ export default class AddressForm extends
         container.setAttribute("required", "true");
       } else {
         container.removeAttribute("required");
       }
     }
 
     let shippingAddressErrors = request.paymentDetails.shippingAddressErrors;
     for (let [errorName, errorSelector] of Object.entries(this._errorFieldMap)) {
-      let container = document.querySelector(errorSelector + "-container");
-      let field = document.querySelector(errorSelector);
+      let container = this.form.querySelector(errorSelector + "-container");
+      let field = this.form.querySelector(errorSelector);
       let errorText = (shippingAddressErrors && shippingAddressErrors[errorName]) || "";
       container.classList.toggle("error", !!errorText);
       field.setCustomValidity(errorText);
       let span = container.querySelector(".error-text");
       if (!span) {
         span = document.createElement("span");
         span.className = "error-text";
         container.appendChild(span);
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -1,56 +1,59 @@
 /* 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/. */
 
 /* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
 import LabelledCheckbox from "../components/labelled-checkbox.js";
+import PaymentRequestPage from "../components/payment-request-page.js";
 import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
 import paymentRequest from "../paymentRequest.js";
 
 /* import-globals-from ../unprivileged-fallbacks.js */
 
 /**
  * <basic-card-form></basic-card-form>
  *
  * XXX: Bug 1446164 - This form isn't localized when used via this custom element
  * as it will be much easier to share the logic once we switch to Fluent.
  */
 
-export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLElement) {
+export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRequestPage) {
   constructor() {
     super();
 
-    this.pageTitle = document.createElement("h2");
     this.genericErrorText = document.createElement("div");
 
-    this.cancelButton = document.createElement("button");
-    this.cancelButton.className = "cancel-button";
-    this.cancelButton.addEventListener("click", this);
-
     this.addressAddLink = document.createElement("a");
     this.addressAddLink.className = "add-link";
     this.addressAddLink.href = "javascript:void(0)";
     this.addressAddLink.addEventListener("click", this);
     this.addressEditLink = document.createElement("a");
     this.addressEditLink.className = "edit-link";
     this.addressEditLink.href = "javascript:void(0)";
     this.addressEditLink.addEventListener("click", this);
 
+    this.persistCheckbox = new LabelledCheckbox();
+    this.persistCheckbox.className = "persist-checkbox";
+
+    // page footer
+    this.cancelButton = document.createElement("button");
+    this.cancelButton.className = "cancel-button";
+    this.cancelButton.addEventListener("click", this);
+
     this.backButton = document.createElement("button");
     this.backButton.className = "back-button";
     this.backButton.addEventListener("click", this);
 
     this.saveButton = document.createElement("button");
     this.saveButton.className = "save-button primary";
     this.saveButton.addEventListener("click", this);
 
-    this.persistCheckbox = new LabelledCheckbox();
-    this.persistCheckbox.className = "persist-checkbox";
+    this.footer.append(this.cancelButton, this.backButton, this.saveButton);
 
     // The markup is shared with form autofill preferences.
     let url = "formautofill/editCreditCard.xhtml";
     this.promiseReady = this._fetchMarkup(url).then(doc => {
       this.form = doc.getElementById("form");
       return this.form;
     });
   }
@@ -65,18 +68,17 @@ export default class BasicCardForm exten
       });
       xhr.open("GET", url);
       xhr.send();
     });
   }
 
   connectedCallback() {
     this.promiseReady.then(form => {
-      this.appendChild(this.pageTitle);
-      this.appendChild(form);
+      this.body.appendChild(form);
 
       let record = {};
       let addresses = [];
       this.formHandler = new EditCreditCard({
         form,
       }, record, addresses, {
         isCCNumber: PaymentDialogUtils.isCCNumber,
         getAddressLabel: PaymentDialogUtils.getAddressLabel,
@@ -84,21 +86,18 @@ export default class BasicCardForm exten
 
       let fragment = document.createDocumentFragment();
       fragment.append(this.addressAddLink);
       fragment.append(" ");
       fragment.append(this.addressEditLink);
       let billingAddressRow = this.form.querySelector(".billingAddressRow");
       billingAddressRow.appendChild(fragment);
 
-      this.appendChild(this.persistCheckbox);
-      this.appendChild(this.genericErrorText);
-      this.appendChild(this.cancelButton);
-      this.appendChild(this.backButton);
-      this.appendChild(this.saveButton);
+      this.body.appendChild(this.persistCheckbox);
+      this.body.appendChild(this.genericErrorText);
       // Only call the connected super callback(s) once our markup is fully
       // connected, including the shared form fetched asynchronously.
       super.connectedCallback();
     });
   }
 
   render(state) {
     let {
@@ -130,25 +129,25 @@ export default class BasicCardForm exten
 
     this.genericErrorText.textContent = page.error;
 
     let editing = !!basicCardPage.guid;
     this.form.querySelector("#cc-number").disabled = editing;
 
     // If a card is selected we want to edit it.
     if (editing) {
-      this.pageTitle.textContent = this.dataset.editBasicCardTitle;
+      this.pageTitleHeading.textContent = this.dataset.editBasicCardTitle;
       record = basicCards[basicCardPage.guid];
       if (!record) {
         throw new Error("Trying to edit a non-existing card: " + basicCardPage.guid);
       }
       // When editing an existing record, prevent changes to persistence
       this.persistCheckbox.hidden = true;
     } else {
-      this.pageTitle.textContent = this.dataset.addBasicCardTitle;
+      this.pageTitleHeading.textContent = this.dataset.addBasicCardTitle;
       // Use a currently selected shipping address as the default billing address
       record.billingAddressGUID = basicCardPage.billingAddressGUID;
       if (!record.billingAddressGUID && selectedShippingAddress) {
         record.billingAddressGUID = selectedShippingAddress;
       }
       // Adding a new record: default persistence to checked when in a not-private session
       this.persistCheckbox.hidden = false;
       this.persistCheckbox.checked = !state.isPrivate;
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 import "../vendor/custom-elements.min.js";
 
 import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
 import paymentRequest from "../paymentRequest.js";
 
 import "../components/currency-amount.js";
+import "../components/payment-request-page.js";
 import "./address-picker.js";
 import "./address-form.js";
 import "./basic-card-form.js";
 import "./order-details.js";
 import "./payment-method-picker.js";
 import "./shipping-option-picker.js";
 
 /* import-globals-from ../unprivileged-fallbacks.js */
--- a/browser/components/payments/res/debugging.css
+++ b/browser/components/payments/res/debugging.css
@@ -1,16 +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/. */
 
 html {
-  /* Based on global.css styles for top-level XUL windows */
-  -moz-appearance: dialog;
-  background-color: -moz-Dialog;
   color: -moz-DialogText;
   font: message-box;
   /* Make sure the background ends to the bottom if there is unused space */
   height: 100%;
 }
 
 h1 {
   font-size: 1em;
--- a/browser/components/payments/res/paymentRequest.css
+++ b/browser/components/payments/res/paymentRequest.css
@@ -1,96 +1,93 @@
 /* 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/. */
 
 html {
-  /* Based on global.css styles for top-level XUL windows */
-  color: -moz-DialogText;
-  font: message-box;
   height: 100%;
 }
 
 body {
-  /* Override font-size from in-content/common.css which is too large */
-  font-size: inherit;
-}
-
-#order-details-overlay,
-html {
-  /* Based on global.css styles for top-level XUL windows */
-  -moz-appearance: dialog;
-  background-color: -moz-Dialog;
-}
-
-body {
   height: 100%;
   margin: 0;
-  overflow: hidden;
+  /* Override font-size from in-content/common.css which is too large */
+  font-size: inherit;
 }
 
 [hidden] {
   display: none !important;
 }
 
 #debugging-console {
   /* include the default borders in the max-height */
   box-sizing: border-box;
   float: right;
-  /* avoid causing the body to scroll */
-  max-height: 100vh;
+  height: 100vh;
   /* Float above the other overlays */
   position: relative;
   z-index: 99;
 }
 
 payment-dialog {
   box-sizing: border-box;
   display: grid;
-  grid-template-rows: fit-content(10%) auto;
+  grid-template: "header" auto
+                 "main"   1fr
+                 "disabled-overlay" auto;
   height: 100%;
   margin: 0 10%;
   padding: 1em;
 }
 
 payment-dialog > header {
   display: flex;
 }
 
 #main-container {
   display: flex;
+  grid-area: main;
   position: relative;
+  max-height: 100%;
 }
 
-#payment-summary {
-  display: grid;
-  flex: 1 1 auto;
-  grid-template-rows: fit-content(10%) auto fit-content(10%);
+.page {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
   position: relative;
+  width: 100%;
+}
+
+.page > .page-body {
+  /* The area above the footer should scroll, if necessary. */
+  overflow: auto;
+}
+
+.page > footer {
+  align-items: end;
+  display: flex;
+  flex-grow: 1;
 }
 
 #error-text {
   text-align: center;
 }
 
 #order-details-overlay {
+  background-color: var(--in-content-page-background);
   overflow: auto;
   position: absolute;
   top: 0;
   right: 0;
   bottom: 0;
   left: 0;
   z-index: 1;
 }
 
-payment-dialog > footer {
-  align-items: baseline;
-  display: flex;
-}
-
 #total {
   flex: 1 1 auto;
   margin: 5px;
 }
 
 #total > currency-amount > .currency-code {
   color: GrayText;
 }
@@ -110,16 +107,17 @@ payment-dialog[changes-prevented][comple
 payment-dialog[changes-prevented][completion-state="success"] #pay {
   /* Show the pay button above #disabled-overlay */
   position: relative;
   z-index: 1;
 }
 
 #disabled-overlay {
   background: white;
+  grid-area: disabled-overlay;
   opacity: 0.6;
   width: 100%;
   height: 100%;
   position: absolute;
   top: 0;
   right: 0;
   bottom: 0;
   left: 0;
--- a/browser/components/payments/res/paymentRequest.xhtml
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -87,18 +87,18 @@
         <div>&header.payTo; <span id="host-name"></span></div>
       </div>
       <div id="top-buttons" hidden="hidden">
         <button id="view-all" class="closed">&viewAllItems;</button>
       </div>
     </header>
 
     <div id="main-container">
-      <section id="payment-summary" class="page">
-        <section>
+      <payment-request-page id="payment-summary">
+        <div class="page-body">
           <div id="error-text"></div>
 
           <div class="shipping-related"
                id="shipping-type-label"
                data-shipping-address-label="&shippingAddressLabel;"
                data-delivery-address-label="&deliveryAddressLabel;"
                data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
                data-pickup-address-label="&pickupAddressLabel;"><label></label></div>
@@ -117,51 +117,49 @@
           </payment-method-picker>
 
           <div class="payer-related"><label>&payerLabel;</label></div>
           <address-picker class="payer-related"
                           data-add-link-label="&payer.addLink.label;"
                           data-edit-link-label="&payer.editLink.label;"
                           selected-state-key="selectedPayerAddress"></address-picker>
           <div id="error-text"></div>
-        </section>
+        </div>
 
-        <footer id="controls-container">
+        <footer>
           <button id="cancel">&cancelPaymentButton.label;</button>
           <button id="pay"
                   class="primary"
                   data-initial-label="&approvePaymentButton.label;"
                   data-processing-label="&processingPaymentButton.label;"
                   data-fail-label="&failPaymentButton.label;"
                   data-unknown-label="&unknownPaymentButton.label;"
                   data-success-label="&successPaymentButton.label;"></button>
         </footer>
-      </section>
+      </payment-request-page>
       <section id="order-details-overlay" hidden="hidden">
         <h2>&orderDetailsLabel;</h2>
         <order-details></order-details>
       </section>
 
       <basic-card-form id="basic-card-page"
-                       class="page"
                        data-add-basic-card-title="&basicCard.addPage.title;"
                        data-edit-basic-card-title="&basicCard.editPage.title;"
                        data-error-generic-save="&basicCardPage.error.genericSave;"
                        data-address-add-link-label="&basicCardPage.addressAddLink.label;"
                        data-address-edit-link-label="&basicCardPage.addressEditLink.label;"
                        data-billing-address-title-add="&billingAddress.addPage.title;"
                        data-billing-address-title-edit="&billingAddress.editPage.title;"
                        data-back-button-label="&basicCardPage.backButton.label;"
                        data-save-button-label="&basicCardPage.saveButton.label;"
                        data-cancel-button-label="&cancelPaymentButton.label;"
                        data-persist-checkbox-label="&basicCardPage.persistCheckbox.label;"
                        hidden="hidden"></basic-card-form>
 
       <address-form id="address-page"
-                    class="page"
                     data-error-generic-save="&addressPage.error.genericSave;"
                     data-cancel-button-label="&addressPage.cancelButton.label;"
                     data-back-button-label="&addressPage.backButton.label;"
                     data-save-button-label="&addressPage.saveButton.label;"
                     data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
                     hidden="hidden"></address-form>
     </div>
 
@@ -177,18 +175,18 @@
     <div class="details-total">
       <h2 class="label">&orderTotalLabel;</h2>
       <currency-amount></currency-amount>
     </div>
   </template>
 </head>
 <body dir="&locale.dir;">
   <iframe id="debugging-console"
-          hidden="hidden"
-          height="400"></iframe>
+          hidden="hidden">
+  </iframe>
   <payment-dialog data-shipping-address-title-add="&shippingAddress.addPage.title;"
                   data-shipping-address-title-edit="&shippingAddress.editPage.title;"
                   data-delivery-address-title-add="&deliveryAddress.addPage.title;"
                   data-delivery-address-title-edit="&deliveryAddress.editPage.title;"
                   data-pickup-address-title-add="&pickupAddress.addPage.title;"
                   data-pickup-address-title-edit="&pickupAddress.editPage.title;"
                   data-billing-address-title-add="&billingAddress.addPage.title;"
                   data-payer-title-add="&payer.addPage.title;"
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -89,17 +89,17 @@ add_task(async function test_backButton(
       title: "Sample page title",
     },
   });
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
 
   let stateChangePromise = promiseStateChange(form.requestStore);
-  is(form.pageTitle.textContent, "Sample page title", "Check label");
+  is(form.pageTitleHeading.textContent, "Sample page title", "Check label");
 
   is(form.backButton.textContent, "Back", "Check label");
   form.backButton.scrollIntoView();
   synthesizeMouseAtCenter(form.backButton, {});
 
   let {page} = await stateChangePromise;
   is(page.id, "payment-summary", "Check initial page after appending");
 
--- a/browser/components/payments/test/mochitest/test_basic_card_form.html
+++ b/browser/components/payments/test/mochitest/test_basic_card_form.html
@@ -73,17 +73,17 @@ add_task(async function test_backButton(
     "basic-card-page": {
     },
   });
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
 
   let stateChangePromise = promiseStateChange(form.requestStore);
-  is(form.pageTitle.textContent, "Sample page title 2", "Check title");
+  is(form.pageTitleHeading.textContent, "Sample page title 2", "Check title");
   is(form.backButton.textContent, "Back", "Check label");
   synthesizeMouseAtCenter(form.backButton, {});
 
   let {page} = await stateChangePromise;
   is(page.id, "payment-summary", "Check initial page after appending");
 
   form.remove();
 });
--- a/browser/components/payments/test/mochitest/test_payment_dialog.html
+++ b/browser/components/payments/test/mochitest/test_payment_dialog.html
@@ -89,17 +89,17 @@ add_task(async function test_initialStat
   is(initialState.page.id, "payment-summary", "Check initial page");
 });
 
 add_task(async function test_viewAllButtonVisibility() {
   await setup();
 
   let button = el1._viewAllButton;
   ok(button.hidden, "Button is initially hidden when there are no items to show");
-  ok(isHidden(button), "Button should be visibly hidden since bug 1469464")
+  ok(isHidden(button), "Button should be visibly hidden since bug 1469464");
 
   // Add a display item.
   let request = deepClone(el1.requestStore.getState().request);
   request.paymentDetails.displayItems = [
     {
       "label": "Triangle",
       "amount": {
         "currency": "CAD",
@@ -177,16 +177,29 @@ add_task(async function test_completionS
     ok(payButton.disabled, "Button is disabled");
     let rect = payButton.getBoundingClientRect();
     let visibleElement =
       document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2);
     ok(payButton === visibleElement, "Pay button is on top of the overlay");
   }
 });
 
+add_task(async function test_scrollPaymentRequestPage() {
+  await setup();
+  info("making the payment-dialog container small to require scrolling");
+  el1.parentElement.style.height = "100px";
+  let summaryPageBody = document.querySelector("#payment-summary .page-body");
+  is(summaryPageBody.scrollTop, 0, "Page body not scrolled initially");
+  let securityCodeInput = summaryPageBody.querySelector("payment-method-picker input");
+  securityCodeInput.focus();
+  await new Promise(resolve => SimpleTest.executeSoon(resolve));
+  ok(summaryPageBody.scrollTop > 0, "Page body scrolled after focusing the CVV field");
+  el1.parentElement.style.height = "";
+});
+
 add_task(async function test_disconnect() {
   await setup();
 
   el1.remove();
   await el1.requestStore.setState({orderDetailsShowing: true});
   await asyncElementRendered();
   ok(el1.stateChangeCallback.notCalled, "stateChangeCallback not called");
   ok(el1.render.notCalled, "render not called");
--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -134,42 +134,42 @@ class EditAddress extends EditAutofillFo
       "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`);
+      let container = this._elements.form.querySelector(`#${fieldId}-container`);
       let containerInputs = [...container.querySelectorAll("input, textarea, select")];
       containerInputs.forEach(function(input) { input.disabled = false; });
       inputs.push(...containerInputs);
       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`);
+      let container = this._elements.form.querySelector(`#${field}-container`);
       container.style.display = "none";
       for (let input of [...container.querySelectorAll("input, textarea, select")]) {
         input.disabled = true;
       }
     }
   }
 
   updatePostalCodeValidation(postalCodePattern) {
-    let postalCodeInput = document.getElementById("postal-code");
+    let postalCodeInput = this._elements.form.querySelector("#postal-code");
     if (postalCodePattern && postalCodeInput.style.display != "none") {
       postalCodeInput.setAttribute("pattern", postalCodePattern);
     } else {
       postalCodeInput.removeAttribute("pattern");
     }
   }
 
   populateCountries() {