[Accessibility] Improve checkout error messages (#49094)
* Improve required info for screen readers on checkout fields * Add style to inline error message on checkout fields * Add id attribute to terms error * Improve accessibility of checkout errors messages * Prevent screen readers from reading the asterisk on checkout fields * Revert spacing change * Revert spacing change * Add changelog file * Decrease line length * Lowercase the required term * Fix query methods in checkout tests * Add space before required text for screen readers * Fix query of shipping fields on legacy checkout tests * Remove asterisk from field name on legacy checkout test * Remove invalid character in Phone and Email fields on legacy checkout test * Add asterisk to get phone and email by label on legacy checkout test * Fix field labels for legacy checkout tests * Fix php lint errors * Add required tem to the Government ID input on tests * Revert changes on required fields label * Create checkout-inline-error-message mixin * Replace SCSS variable with a CSS one * Add checkout inline error message to T17 * Add checkout inline error message to T19 * Add checkout inline error message to TT * Add checkout inline error message to TT1 * Add checkout inline error message to TT2 * Add checkout inline error message to TT3 * Include notice banner block in the notice selectors * Add inline documentation to role attribute removal --------- Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>
This commit is contained in:
parent
864f5448d8
commit
3c076b5187
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: enhancement
|
||||
|
||||
Improve the accessibility of form errors on the legacy checkout page
|
|
@ -319,3 +319,11 @@
|
|||
font-size: 0.75em;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@mixin checkout-inline-error-message() {
|
||||
color: var(--wc-red);
|
||||
font-size: 0.75em;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
|
|
@ -1372,3 +1372,16 @@ form.checkout_coupon {
|
|||
border-color: var(--wc-red);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout error message
|
||||
*/
|
||||
.woocommerce-checkout {
|
||||
form .form-row.woocommerce-invalid input.input-text {
|
||||
border-color: var(--wc-red);
|
||||
}
|
||||
|
||||
.checkout-inline-error-message {
|
||||
@include checkout-inline-error-message();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1276,3 +1276,16 @@ form.checkout_coupon {
|
|||
border-color: var(--wc-red);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout error message
|
||||
*/
|
||||
.woocommerce-checkout {
|
||||
form .form-row.woocommerce-invalid input.input-text {
|
||||
border-color: var(--wc-red);
|
||||
}
|
||||
|
||||
.checkout-inline-error-message {
|
||||
@include checkout-inline-error-message();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -204,19 +204,10 @@ a.button {
|
|||
list-style: none;
|
||||
overflow: hidden;
|
||||
|
||||
a {
|
||||
a.button {
|
||||
background: #111;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.button {
|
||||
background: #111;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.woocommerce-store-notice__dismiss-link {
|
||||
|
@ -1669,7 +1660,7 @@ button.reset_variations {
|
|||
.form-row.woocommerce-invalid {
|
||||
|
||||
input.input-text {
|
||||
border: 2px solid $highlights-color;
|
||||
border: 2px solid var(--wc-red);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3104,3 +3095,12 @@ form.checkout_coupon {
|
|||
border-color: var(--wc-red);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout error message
|
||||
*/
|
||||
.checkout {
|
||||
.checkout-inline-error-message {
|
||||
@include checkout-inline-error-message();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1171,3 +1171,16 @@ form.checkout_coupon {
|
|||
border-color: var(--wc-red);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout error message
|
||||
*/
|
||||
.woocommerce-checkout {
|
||||
form .form-row.woocommerce-invalid input.input-text {
|
||||
border-color: var(--wc-red);
|
||||
}
|
||||
|
||||
.checkout-inline-error-message {
|
||||
@include checkout-inline-error-message();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1216,3 +1216,16 @@ form.checkout_coupon {
|
|||
border-color: var(--wc-red);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout error message
|
||||
*/
|
||||
.woocommerce-checkout {
|
||||
form .form-row.woocommerce-invalid input.input-text {
|
||||
border-color: var(--wc-red);
|
||||
}
|
||||
|
||||
.checkout-inline-error-message {
|
||||
@include checkout-inline-error-message();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2336,16 +2336,8 @@ button.reset_variations {
|
|||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.button {
|
||||
background: #000000;
|
||||
}
|
||||
a.button {
|
||||
background: #000000;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2431,3 +2423,12 @@ form.checkout_coupon {
|
|||
border-color: var(--wc-red);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout error message
|
||||
*/
|
||||
.checkout {
|
||||
.checkout-inline-error-message {
|
||||
@include checkout-inline-error-message();
|
||||
}
|
||||
}
|
|
@ -2131,6 +2131,12 @@ p.demo_store,
|
|||
.shipping_address {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.checkout-inline-error-message {
|
||||
color: var(--wc-red);
|
||||
font-size: 0.75em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#payment {
|
||||
|
|
|
@ -35,7 +35,7 @@ jQuery( function( $ ) {
|
|||
this.$checkout_form.on( 'submit', this.submit );
|
||||
|
||||
// Inline validation
|
||||
this.$checkout_form.on( 'input validate change', '.input-text, select, input:checkbox', this.validate_field );
|
||||
this.$checkout_form.on( 'input validate change focusout', '.input-text, select, input:checkbox', this.validate_field );
|
||||
|
||||
// Manual trigger
|
||||
this.$checkout_form.on( 'update', this.trigger_update_checkout );
|
||||
|
@ -209,16 +209,16 @@ jQuery( function( $ ) {
|
|||
event_type = e.type;
|
||||
|
||||
if ( 'input' === event_type ) {
|
||||
$this.removeAttr( 'aria-invalid' ).removeAttr( 'aria-describedby' );
|
||||
$parent.find( '.checkout-inline-error-message' ).remove();
|
||||
$parent.removeClass( 'woocommerce-invalid woocommerce-invalid-required-field woocommerce-invalid-email woocommerce-invalid-phone woocommerce-validated' ); // eslint-disable-line max-len
|
||||
}
|
||||
|
||||
if ( 'validate' === event_type || 'change' === event_type ) {
|
||||
if ( 'validate' === event_type || 'change' === event_type || 'focusout' === event_type ) {
|
||||
|
||||
if ( validate_required ) {
|
||||
if ( 'checkbox' === $this.attr( 'type' ) && ! $this.is( ':checked' ) ) {
|
||||
$parent.removeClass( 'woocommerce-validated' ).addClass( 'woocommerce-invalid woocommerce-invalid-required-field' );
|
||||
validated = false;
|
||||
} else if ( $this.val() === '' ) {
|
||||
if ( ( 'checkbox' === $this.attr( 'type' ) && ! $this.is( ':checked' ) ) || $this.val() === '' ) {
|
||||
$this.attr( 'aria-invalid', 'true' );
|
||||
$parent.removeClass( 'woocommerce-validated' ).addClass( 'woocommerce-invalid woocommerce-invalid-required-field' );
|
||||
validated = false;
|
||||
}
|
||||
|
@ -230,6 +230,7 @@ jQuery( function( $ ) {
|
|||
pattern = new RegExp( /^([a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+(\.[a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)*|"((([ \t]*\r\n)?[ \t]+)?([\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\\[\x01-\x09\x0b\x0c\x0d-\x7f\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))*(([ \t]*\r\n)?[ \t]+)?")@(([a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.)+([a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[0-9a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.?$/i ); // eslint-disable-line max-len
|
||||
|
||||
if ( ! pattern.test( $this.val() ) ) {
|
||||
$this.attr( 'aria-invalid', 'true' );
|
||||
$parent.removeClass( 'woocommerce-validated' ).addClass( 'woocommerce-invalid woocommerce-invalid-email woocommerce-invalid-phone' ); // eslint-disable-line max-len
|
||||
validated = false;
|
||||
}
|
||||
|
@ -240,12 +241,15 @@ jQuery( function( $ ) {
|
|||
pattern = new RegExp( /[\s\#0-9_\-\+\/\(\)\.]/g );
|
||||
|
||||
if ( 0 < $this.val().replace( pattern, '' ).length ) {
|
||||
$this.attr( 'aria-invalid', 'true' );
|
||||
$parent.removeClass( 'woocommerce-validated' ).addClass( 'woocommerce-invalid woocommerce-invalid-phone' );
|
||||
validated = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( validated ) {
|
||||
$this.removeAttr( 'aria-invalid' ).removeAttr( 'aria-describedby' );
|
||||
$parent.find( '.checkout-inline-error-message' ).remove();
|
||||
$parent.removeClass( 'woocommerce-invalid woocommerce-invalid-required-field woocommerce-invalid-email woocommerce-invalid-phone' ).addClass( 'woocommerce-validated' ); // eslint-disable-line max-len
|
||||
}
|
||||
}
|
||||
|
@ -533,6 +537,8 @@ jQuery( function( $ ) {
|
|||
success: function( result ) {
|
||||
// Detach the unload handler that prevents a reload / redirect
|
||||
wc_checkout_form.detachUnloadEventsOnSubmit();
|
||||
|
||||
$( '.checkout-inline-error-message' ).remove();
|
||||
|
||||
try {
|
||||
if ( 'success' === result.result &&
|
||||
|
@ -561,7 +567,17 @@ jQuery( function( $ ) {
|
|||
|
||||
// Add new errors
|
||||
if ( result.messages ) {
|
||||
wc_checkout_form.submit_error( result.messages );
|
||||
var $msgs = $( result.messages )
|
||||
// The error notice template (plugins/woocommerce/templates/notices/error.php)
|
||||
// adds the role="alert" to a list HTML element. This becomes a problem in this context
|
||||
// because screen readers won't read the list content correctly if its role is not "list".
|
||||
.removeAttr( 'role' )
|
||||
.attr( 'tabindex', '-1' );
|
||||
var $msgsWithLink = wc_checkout_form.wrapMessagesInsideLink( $msgs );
|
||||
var $msgsWrapper = $( '<div role="alert"></div>' ).append( $msgsWithLink );
|
||||
|
||||
wc_checkout_form.submit_error( $msgsWrapper.prop( 'outerHTML' ) );
|
||||
wc_checkout_form.show_inline_errors( $msgs );
|
||||
} else {
|
||||
wc_checkout_form.submit_error( '<div class="woocommerce-error">' + wc_checkout_params.i18n_checkout_error + '</div>' ); // eslint-disable-line max-len
|
||||
}
|
||||
|
@ -599,10 +615,40 @@ jQuery( function( $ ) {
|
|||
wc_checkout_form.$checkout_form.removeClass( 'processing' ).unblock();
|
||||
wc_checkout_form.$checkout_form.find( '.input-text, select, input:checkbox' ).trigger( 'validate' ).trigger( 'blur' );
|
||||
wc_checkout_form.scroll_to_notices();
|
||||
wc_checkout_form.$checkout_form.find(
|
||||
'.woocommerce-error[tabindex="-1"], .wc-block-components-notice-banner.is-error[tabindex="-1"]' )
|
||||
.focus();
|
||||
$( document.body ).trigger( 'checkout_error' , [ error_message ] );
|
||||
},
|
||||
wrapMessagesInsideLink: function( $msgs ) {
|
||||
$( 'li[data-id]', $msgs ).each( function() {
|
||||
var $this = $( this );
|
||||
|
||||
$this.wrapInner( '<a href="#' + $this.attr( 'data-id' ) + '"></a>' );
|
||||
} );
|
||||
|
||||
return $msgs;
|
||||
},
|
||||
show_inline_errors: function( $messages ) {
|
||||
$messages.find( 'li[data-id]' ).each( function() {
|
||||
var $this = $( this );
|
||||
var dataId = $this.attr( 'data-id' );
|
||||
var $field = $( '#' + dataId );
|
||||
|
||||
if ( $field.length === 1 ) {
|
||||
var descriptionId = dataId + '_description';
|
||||
var msg = $this.text().trim();
|
||||
var $formRow = $field.closest( '.form-row' );
|
||||
|
||||
$formRow.append( '<p id="' + descriptionId + '" class="checkout-inline-error-message">' + msg + '</p>' );
|
||||
$field
|
||||
.attr( 'aria-describedby', descriptionId )
|
||||
.attr( 'aria-invalid', 'true' );
|
||||
}
|
||||
} );
|
||||
},
|
||||
scroll_to_notices: function() {
|
||||
var scrollElement = $( '.woocommerce-NoticeGroup-updateOrderReview, .woocommerce-NoticeGroup-checkout' );
|
||||
var scrollElement = $( '.woocommerce-NoticeGroup-updateOrderReview, .woocommerce-NoticeGroup-checkout' );
|
||||
|
||||
if ( ! scrollElement.length ) {
|
||||
scrollElement = $( 'form.checkout' );
|
||||
|
@ -678,7 +724,7 @@ jQuery( function( $ ) {
|
|||
url: wc_checkout_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'apply_coupon' ),
|
||||
data: data,
|
||||
success: function( response ) {
|
||||
$( '.woocommerce-error, .woocommerce-message, .is-error, .is-success' ).remove();
|
||||
$( '.woocommerce-error, .woocommerce-message, .is-error, .is-success, .checkout-inline-error-message' ).remove();
|
||||
$form.removeClass( 'processing' ).unblock();
|
||||
|
||||
if ( response ) {
|
||||
|
|
|
@ -934,7 +934,7 @@ class WC_Checkout {
|
|||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
if ( empty( $data['woocommerce_checkout_update_totals'] ) && empty( $data['terms'] ) && ! empty( $data['terms-field'] ) ) {
|
||||
$errors->add( 'terms', __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ) );
|
||||
$errors->add( 'terms', __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ), array( 'id' => 'terms' ) );
|
||||
}
|
||||
|
||||
if ( WC()->cart->needs_shipping() ) {
|
||||
|
|
|
@ -222,25 +222,39 @@ test.describe(
|
|||
page.locator( 'form[name="checkout"]' ).getByRole( 'alert' )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Billing First name is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Billing First name is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Billing Last name is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Billing Last name is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Billing Street address is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Billing Street address is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Billing Town / City is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Billing Town / City is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Billing ZIP Code is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Billing ZIP Code is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Billing Phone is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Billing Phone is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Billing Email address is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Billing Email address is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
|
||||
// toggle ship to different address, fill out billing info and confirm error shown
|
||||
|
@ -259,19 +273,29 @@ test.describe(
|
|||
await page.getByRole( 'button', { name: 'Place order' } ).click();
|
||||
|
||||
await expect(
|
||||
page.getByText( 'Shipping First name is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Shipping First name is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Shipping Last name is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Shipping Last name is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Shipping Street address is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Shipping Street address is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Shipping Town / City is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Shipping Town / City is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Shipping ZIP Code is a required field.' )
|
||||
page.getByRole( 'link', {
|
||||
name: 'Shipping ZIP Code is a required field.',
|
||||
} )
|
||||
).toBeVisible();
|
||||
} );
|
||||
|
||||
|
|
Loading…
Reference in New Issue