Bug 1469464 - Consistent PaymentRequest footer positioning with <payment-request-page>. r=sfoster draft
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Thu, 12 Jul 2018 09:52:30 -0700
changeset 817532 76ee5c4a8e23db6a24e755e09eff05cdf0ae9f52
parent 817203 b5686337e8e27164b45b8326cc0bb04560e8523e
child 817533 bf7bb73ecbb0699618f7e5f7068e37facb3ca322
push id116095
push usermozilla@noorenberghe.ca
push dateThu, 12 Jul 2018 20:03:22 +0000
reviewerssfoster
bugs1469464
milestone63.0a1
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() {