diff --git a/browser/components/payments/res/containers/address-form.css b/browser/components/payments/res/containers/address-form.css index d9148bb38fb5..202c6dd2dc9d 100644 --- a/browser/components/payments/res/containers/address-form.css +++ b/browser/components/payments/res/containers/address-form.css @@ -2,26 +2,22 @@ * 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/. */ -.error-text:not(:empty) { +.error-text { color: #fff; background-color: #d70022; border-radius: 2px; - /* The padding-top and padding-bottom are referenced by address-form.js */ + margin: 5px 3px 0 3px; + /* The padding-top and padding-bottom are referenced by address-form.js */ /* TODO */ padding: 5px 12px; position: absolute; z-index: 1; pointer-events: none; + top: 100%; + visibility: hidden; } -body[dir="ltr"] .error-text { - left: 3px; -} - -body[dir="rtl"] .error-text { - right: 3px; -} - -:-moz-any(input, textarea, select):focus ~ .error-text:not(:empty)::before { +/* ::before is the error on the error text panel */ +:-moz-any(input, textarea, select) ~ .error-text::before { background-color: #d70022; top: -7px; content: '.'; @@ -34,17 +30,17 @@ body[dir="rtl"] .error-text { z-index: -1 } -body[dir=ltr] .error-text::before { +/* Position the arrow */ +.error-text:dir(ltr)::before { left: 12px } -body[dir=rtl] .error-text::before { +.error-text:dir(rtl)::before { right: 12px } -:-moz-any(input, textarea, select):not(:focus) ~ .error-text, -:-moz-any(input, textarea, select):valid ~ .error-text { - display: none; +:-moz-any(input, textarea, select):-moz-ui-invalid:focus ~ .error-text { + visibility: visible; } address-form > footer > .cancel-button { diff --git a/browser/components/payments/res/containers/address-form.js b/browser/components/payments/res/containers/address-form.js index bd4781e5804b..a9a31f46f9e8 100644 --- a/browser/components/payments/res/containers/address-form.js +++ b/browser/components/payments/res/containers/address-form.js @@ -4,6 +4,7 @@ /* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/ import LabelledCheckbox from "../components/labelled-checkbox.js"; +import PaymentDialog from "./payment-dialog.js"; import PaymentRequestPage from "../components/payment-request-page.js"; import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js"; import paymentRequest from "../paymentRequest.js"; @@ -97,6 +98,12 @@ export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequ this.form.addEventListener("invalid", this); this.form.addEventListener("change", this); + // The "invalid" event does not bubble and needs to be listened for on each + // form element. + for (let field of this.form.elements) { + field.addEventListener("invalid", this); + } + this.body.appendChild(this.persistCheckbox); this.body.appendChild(this.genericErrorText); @@ -176,40 +183,11 @@ export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequ 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); - } + let span = PaymentDialog.maybeCreateFieldErrorElement(container); span.textContent = errorText; } - // Position the error messages all at once so layout flushes only once. - let formRect = this.form.getBoundingClientRect(); - let errorSpanData = [...this.form.querySelectorAll(".error-text:not(:empty)")].map(span => { - let relatedInput = span.parentNode.querySelector("input, textarea, select"); - let relatedRect = relatedInput.getBoundingClientRect(); - return { - span, - top: relatedRect.height, - left: relatedRect.left - formRect.left, - right: formRect.right - relatedRect.right, - }; - }); - let isRTL = this.form.matches(":dir(rtl)"); - for (let data of errorSpanData) { - // Add 10px for the padding-top and padding-bottom. - data.span.style.top = (data.top + 10) + "px"; - if (isRTL) { - data.span.style.right = data.right + "px"; - } else { - data.span.style.left = data.left + "px"; - } - } - this.updateSaveButtonState(); } @@ -228,7 +206,12 @@ export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequ break; } case "invalid": { - this.onInvalid(event); + if (event.target instanceof HTMLFormElement) { + this.onInvalidForm(event); + break; + } + + this.onInvalidField(event); break; } } @@ -269,20 +252,22 @@ export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequ } onInput(event) { - let container = event.target.closest(`#${event.target.id}-container`); - if (container) { - container.classList.remove("error"); - event.target.setCustomValidity(""); - - let span = container.querySelector(".error-text"); - if (span) { - span.textContent = ""; - } - } + event.target.setCustomValidity(""); this.updateSaveButtonState(); } - onInvalid(event) { + /** + * @param {Event} event - "invalid" event + * Note: Keep this in-sync with the equivalent version in basic-card-form.js + */ + onInvalidField(event) { + let field = event.target; + let container = field.closest(`#${field.id}-container`); + let errorTextSpan = PaymentDialog.maybeCreateFieldErrorElement(container); + errorTextSpan.textContent = field.validationMessage; + } + + onInvalidForm() { this.saveButton.disabled = true; } diff --git a/browser/components/payments/res/containers/basic-card-form.js b/browser/components/payments/res/containers/basic-card-form.js index 52f240ee2ccb..8009a44b6288 100644 --- a/browser/components/payments/res/containers/basic-card-form.js +++ b/browser/components/payments/res/containers/basic-card-form.js @@ -5,6 +5,7 @@ /* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/ import AcceptedCards from "../components/accepted-cards.js"; import LabelledCheckbox from "../components/labelled-checkbox.js"; +import PaymentDialog from "./payment-dialog.js"; import PaymentRequestPage from "../components/payment-request-page.js"; import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js"; import paymentRequest from "../paymentRequest.js"; @@ -97,6 +98,12 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe form.addEventListener("input", this); form.addEventListener("invalid", this); + // The "invalid" event does not bubble and needs to be listened for on each + // form element. + for (let field of this.form.elements) { + field.addEventListener("invalid", this); + } + let fragment = document.createDocumentFragment(); fragment.append(this.addressAddLink); fragment.append(" "); @@ -222,7 +229,12 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe break; } case "invalid": { - this.onInvalid(event); + if (event.target instanceof HTMLFormElement) { + this.onInvalidForm(event); + break; + } + + this.onInvalidField(event); break; } } @@ -319,10 +331,22 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe } onInput(event) { + event.target.setCustomValidity(""); this.updateSaveButtonState(); } - onInvalid(event) { + /** + * @param {Event} event - "invalid" event + * Note: Keep this in-sync with the equivalent version in address-form.js + */ + onInvalidField(event) { + let field = event.target; + let container = field.closest(`#${field.id}-container`); + let errorTextSpan = PaymentDialog.maybeCreateFieldErrorElement(container); + errorTextSpan.textContent = field.validationMessage; + } + + onInvalidForm() { this.saveButton.disabled = true; } diff --git a/browser/components/payments/res/containers/payment-dialog.js b/browser/components/payments/res/containers/payment-dialog.js index e98757b0fd99..efdfdcc2506a 100644 --- a/browser/components/payments/res/containers/payment-dialog.js +++ b/browser/components/payments/res/containers/payment-dialog.js @@ -382,6 +382,16 @@ export default class PaymentDialog extends PaymentStateSubscriberMixin(HTMLEleme this.setAttribute("complete-status", request.completeStatus); this._disabledOverlay.hidden = !state.changesPrevented; } + + static maybeCreateFieldErrorElement(container) { + let span = container.querySelector(".error-text"); + if (!span) { + span = document.createElement("span"); + span.className = "error-text"; + container.appendChild(span); + } + return span; + } } customElements.define("payment-dialog", PaymentDialog); diff --git a/browser/components/payments/res/containers/payment-method-picker.js b/browser/components/payments/res/containers/payment-method-picker.js index d1acbaa035d8..a04fbc697a20 100644 --- a/browser/components/payments/res/containers/payment-method-picker.js +++ b/browser/components/payments/res/containers/payment-method-picker.js @@ -20,6 +20,9 @@ export default class PaymentMethodPicker extends RichPicker { this.securityCodeInput.autocomplete = "off"; this.securityCodeInput.placeholder = this.dataset.cvvPlaceholder; this.securityCodeInput.size = 3; + this.securityCodeInput.required = true; + // 3 or more digits + this.securityCodeInput.pattern = "[0-9]{3,}"; this.securityCodeInput.classList.add("security-code"); this.securityCodeInput.addEventListener("change", this); } diff --git a/browser/components/payments/res/containers/rich-picker.css b/browser/components/payments/res/containers/rich-picker.css index 5a099706dfe7..41309ff8eb21 100644 --- a/browser/components/payments/res/containers/rich-picker.css +++ b/browser/components/payments/res/containers/rich-picker.css @@ -28,7 +28,7 @@ .rich-picker > .edit-link { grid-area: edit; - border-right: 1px solid #0C0C0D33; + border-inline-end: 1px solid #0C0C0D33; } .rich-picker > rich-select { @@ -60,8 +60,10 @@ payment-method-picker.rich-picker { payment-method-picker > input { border: 1px solid #0C0C0D33; - border-left: none; + border-inline-start: none; grid-area: cvv; margin: 14px 0; /* Has to be same as rich-select */ padding: 8px; + /* So the error outline appears above the adjacent dropdown */ + z-index: 1; } diff --git a/browser/components/payments/res/debugging.html b/browser/components/payments/res/debugging.html index 3b33b97b9765..18b62b5f0a5c 100644 --- a/browser/components/payments/res/debugging.html +++ b/browser/components/payments/res/debugging.html @@ -16,6 +16,7 @@ +

Requests

diff --git a/browser/components/payments/res/debugging.js b/browser/components/payments/res/debugging.js index 2fcdebeb2cdf..f00a9e6789a8 100644 --- a/browser/components/payments/res/debugging.js +++ b/browser/components/payments/res/debugging.js @@ -514,6 +514,11 @@ let buttonActions = { request: Object.assign({}, request, { completeStatus }), }); }, + + toggleDirectionality() { + let body = paymentDialog.ownerDocument.body; + body.dir = body.dir == "rtl" ? "ltr" : "rtl"; + }, }; window.addEventListener("click", function onButtonClick(evt) { diff --git a/browser/components/payments/res/paymentRequest.css b/browser/components/payments/res/paymentRequest.css index 991927260b9d..9a257a21905e 100644 --- a/browser/components/payments/res/paymentRequest.css +++ b/browser/components/payments/res/paymentRequest.css @@ -145,7 +145,7 @@ payment-dialog #pay::before { content: url(chrome://browser/skin/connection-secure.svg); fill: currentColor; height: 16px; - margin-right: 0.5em; + margin-inline-end: 0.5em; vertical-align: text-bottom; width: 16px; } diff --git a/browser/components/payments/test/browser/browser_card_edit.js b/browser/components/payments/test/browser/browser_card_edit.js index 0943966094b0..8b71b1b39937 100644 --- a/browser/components/payments/test/browser/browser_card_edit.js +++ b/browser/components/payments/test/browser/browser_card_edit.js @@ -2,6 +2,8 @@ "use strict"; +requestLongerTimeout(2); + async function setup(addresses = [], cards = []) { await setupFormAutofillStorage(); await cleanupFormAutofillStorage(); diff --git a/browser/components/payments/test/mochitest/mochitest.ini b/browser/components/payments/test/mochitest/mochitest.ini index 9f940851d673..55fa35736279 100644 --- a/browser/components/payments/test/mochitest/mochitest.ini +++ b/browser/components/payments/test/mochitest/mochitest.ini @@ -14,6 +14,7 @@ skip-if = !e10s [test_accepted_cards.html] [test_address_form.html] [test_address_option.html] +skip-if = os == "linux" # Bug 1490077 comment 7 [test_address_picker.html] [test_basic_card_form.html] [test_basic_card_option.html] diff --git a/browser/components/payments/test/mochitest/test_address_form.html b/browser/components/payments/test/mochitest/test_address_form.html index 062625f64e16..8c2933b50ce5 100644 --- a/browser/components/payments/test/mochitest/test_address_form.html +++ b/browser/components/payments/test/mochitest/test_address_form.html @@ -518,6 +518,47 @@ add_task(async function test_field_validation() { form.remove(); }); + +add_task(async function test_field_validation_dom_errors() { + let form = new AddressForm(); + await form.promiseReady; + const state = { + page: { + id: "address-page", + }, + "address-page": { + title: "Sample page title", + }, + }; + await form.requestStore.setState(state); + display.appendChild(form); + await asyncElementRendered(); + + const BAD_POSTAL_CODE = "hi mom"; + let postalCode = document.getElementById("postal-code"); + postalCode.focus(); + sendString(BAD_POSTAL_CODE, window); + postalCode.blur(); + let errorTextSpan = postalCode.parentNode.querySelector(".error-text"); + is(errorTextSpan.textContent, "Please match the requested format.", + "DOM validation messages should be reflected in the error-text #1"); + + postalCode.focus(); + while (postalCode.value) { + sendKey("BACK_SPACE", window); + } + postalCode.blur(); + is(errorTextSpan.textContent, "Please fill out this field.", + "DOM validation messages should be reflected in the error-text #2"); + + postalCode.focus(); + sendString("12345", window); + is(errorTextSpan.innerText, "", "DOM validation message should be removed when no error"); + postalCode.blur(); + + form.remove(); +}); + diff --git a/browser/components/payments/test/mochitest/test_basic_card_form.html b/browser/components/payments/test/mochitest/test_basic_card_form.html index 874c4152d690..4eaa026b5d3e 100644 --- a/browser/components/payments/test/mochitest/test_basic_card_form.html +++ b/browser/components/payments/test/mochitest/test_basic_card_form.html @@ -20,7 +20,9 @@ Test the basic-card-form element -

+

+