Bug 1469464 - Consistent PaymentRequest footer positioning with <payment-request-page>. r=sfoster

MozReview-Commit-ID: Oq06q6xF0e

--HG--
extra : rebase_source : 76ee5c4a8e23db6a24e755e09eff05cdf0ae9f52
This commit is contained in:
Matthew Noorenberghe 2018-07-12 09:52:30 -07:00
Родитель 6bab329a7b
Коммит 798c0466a9
12 изменённых файлов: 135 добавлений и 76 удалений

Просмотреть файл

@ -14,7 +14,7 @@ Debugging/Development
=====================
Must Have Electrolysis
-------
----------------------
Web Payments `does not work without e10s <https://bugzilla.mozilla.org/show_bug.cgi?id=1365964>`_!
@ -69,3 +69,19 @@ Instead, all communication across the privileged/unprivileged boundary is done v
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.

Просмотреть файл

@ -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);

Просмотреть файл

@ -4,6 +4,7 @@
/* 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 */
@ -11,15 +12,18 @@ import paymentRequest from "../paymentRequest.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");
@ -73,8 +77,7 @@ export default class AddressForm extends PaymentStateSubscriberMixin(HTMLElement
connectedCallback() {
this.promiseReady.then(form => {
this.appendChild(this.pageTitle);
this.appendChild(form);
this.body.appendChild(form);
let record = {};
this.formHandler = new EditAddress({
@ -85,11 +88,12 @@ export default class AddressForm extends PaymentStateSubscriberMixin(HTMLElement
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();
@ -123,7 +127,7 @@ export default class AddressForm extends PaymentStateSubscriberMixin(HTMLElement
this.removeAttribute("address-fields");
}
this.pageTitle.textContent = addressPage.title;
this.pageTitleHeading.textContent = addressPage.title;
this.genericErrorText.textContent = page.error;
let editing = !!addressPage.guid;
@ -161,8 +165,8 @@ export default class AddressForm extends PaymentStateSubscriberMixin(HTMLElement
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);

Просмотреть файл

@ -4,6 +4,7 @@
/* 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";
@ -16,17 +17,12 @@ import paymentRequest from "../paymentRequest.js";
* 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)";
@ -36,6 +32,14 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
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);
@ -44,8 +48,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
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";
@ -70,8 +73,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
connectedCallback() {
this.promiseReady.then(form => {
this.appendChild(this.pageTitle);
this.appendChild(form);
this.body.appendChild(form);
let record = {};
let addresses = [];
@ -89,11 +91,8 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
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();
@ -135,7 +134,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
// 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);
@ -143,7 +142,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
// 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) {

Просмотреть файл

@ -8,6 +8,7 @@ import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.j
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";

Просмотреть файл

@ -3,9 +3,6 @@
* 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 */

Просмотреть файл

@ -3,28 +3,14 @@
* 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] {
@ -35,8 +21,7 @@ body {
/* 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;
@ -45,7 +30,9 @@ body {
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;
@ -57,14 +44,28 @@ payment-dialog > header {
#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 {
@ -72,6 +73,7 @@ payment-dialog > header {
}
#order-details-overlay {
background-color: var(--in-content-page-background);
overflow: auto;
position: absolute;
top: 0;
@ -81,11 +83,6 @@ payment-dialog > header {
z-index: 1;
}
payment-dialog > footer {
align-items: baseline;
display: flex;
}
#total {
flex: 1 1 auto;
margin: 5px;
@ -115,6 +112,7 @@ payment-dialog[changes-prevented][completion-state="success"] #pay {
#disabled-overlay {
background: white;
grid-area: disabled-overlay;
opacity: 0.6;
width: 100%;
height: 100%;

Просмотреть файл

@ -92,8 +92,8 @@
</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"
@ -122,9 +122,9 @@
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"
@ -134,14 +134,13 @@
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;"
@ -156,7 +155,6 @@
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;"
@ -182,8 +180,8 @@
</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;"

Просмотреть файл

@ -94,7 +94,7 @@ add_task(async function test_backButton() {
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();

Просмотреть файл

@ -78,7 +78,7 @@ add_task(async function test_backButton() {
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, {});

Просмотреть файл

@ -94,7 +94,7 @@ add_task(async function test_viewAllButtonVisibility() {
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);
@ -182,6 +182,19 @@ add_task(async function test_completionStateChangesPrevented() {
}
});
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();

Просмотреть файл

@ -139,7 +139,7 @@ class EditAddress extends EditAutofillForm {
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);
@ -155,7 +155,7 @@ class EditAddress extends EditAutofillForm {
}
// 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;
@ -164,7 +164,7 @@ class EditAddress extends EditAutofillForm {
}
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 {