[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:
Gabriel Manussakis 2024-10-18 13:00:46 -03:00 committed by GitHub
parent 864f5448d8
commit 3c076b5187
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 185 additions and 44 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: enhancement
Improve the accessibility of form errors on the legacy checkout page

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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() ) {

View File

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