[Accessibility] Inform screen reader users when mini cart updates (#48295)

* Create add_to_cart_success_message method in WC_Product_Simple class

* Add data-success_message attribute to remove item button in cart

* Add data-success_message to Add to cart button

* Make screen readers announce mini cart updates

* Add changelog file related to #37597 #37598

* Add translator comment to remove button data-success_message attribute

* Add docblock to woocommerce_product_add_to_cart_success_message filter

* Bump mini-cart template version

* Refresh mini cart live region by removing it from the DOM

* Add mini cart live region to DOM beforehand

* Remove aria-relevant attribute before adding new content to live region

* Add a delay before updating live region

* Bump mini-cart template version from 9.1.0 to 9.2.0

Co-authored-by: Paulo Arromba <17236129+wavvves@users.noreply.github.com>

* Bump version of woocommerce_product_add_to_cart_success_message filter

Co-authored-by: Paulo Arromba <17236129+wavvves@users.noreply.github.com>

* Add description to the $text param on woocommerce_product_add_to_cart_success_message filter

* Refactor logic in the add_to_cart_success_message function

---------

Co-authored-by: Paulo Arromba <17236129+wavvves@users.noreply.github.com>
This commit is contained in:
Gabriel Manussakis 2024-07-25 10:22:40 -03:00 committed by GitHub
parent c2fc236341
commit 2840c2f3d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 103 additions and 9 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
Inform screen reader users when mini cart updates

View File

@ -12,13 +12,14 @@ jQuery( function( $ ) {
this.requests = []; this.requests = [];
this.addRequest = this.addRequest.bind( this ); this.addRequest = this.addRequest.bind( this );
this.run = this.run.bind( this ); this.run = this.run.bind( this );
this.$liveRegion = this.createLiveRegion();
$( document.body ) $( document.body )
.on( 'click', '.add_to_cart_button:not(.wc-interactive)', { addToCartHandler: this }, this.onAddToCart ) .on( 'click', '.add_to_cart_button:not(.wc-interactive)', { addToCartHandler: this }, this.onAddToCart )
.on( 'click', '.remove_from_cart_button', { addToCartHandler: this }, this.onRemoveFromCart ) .on( 'click', '.remove_from_cart_button', { addToCartHandler: this }, this.onRemoveFromCart )
.on( 'added_to_cart', this.updateButton ) .on( 'added_to_cart', { addToCartHandler: this }, this.onAddedToCart )
.on( 'ajax_request_not_sent.adding_to_cart', this.updateButton ) .on( 'removed_from_cart', { addToCartHandler: this }, this.onRemovedFromCart )
.on( 'added_to_cart removed_from_cart', { addToCartHandler: this }, this.updateFragments ); .on( 'ajax_request_not_sent.adding_to_cart', this.updateButton );
}; };
/** /**
@ -65,6 +66,12 @@ jQuery( function( $ ) {
return true; return true;
} }
// Clean existing text in mini cart live region and update aria-relevant attribute
// so screen readers can identify the next update if it's the same as the previous one.
e.data.addToCartHandler.$liveRegion
.text( '' )
.removeAttr( 'aria-relevant' );
e.preventDefault(); e.preventDefault();
$thisbutton.removeClass( 'added' ); $thisbutton.removeClass( 'added' );
@ -127,6 +134,10 @@ jQuery( function( $ ) {
var $thisbutton = $( this ), var $thisbutton = $( this ),
$row = $thisbutton.closest( '.woocommerce-mini-cart-item' ); $row = $thisbutton.closest( '.woocommerce-mini-cart-item' );
e.data.addToCartHandler.$liveRegion
.text( '' )
.removeAttr( 'aria-relevant' );
e.preventDefault(); e.preventDefault();
$row.block({ $row.block({
@ -207,6 +218,55 @@ jQuery( function( $ ) {
} }
}; };
/**
* Update cart live region message after add/remove cart events.
*/
AddToCartHandler.prototype.alertCartUpdated = function( e, fragments, cart_hash, $button ) {
var message = $button.data( 'success_message' );
if ( !message ) {
return;
}
// If the response after adding/removing an item to/from the cart is really fast,
// screen readers may not have time to identify the changes in the live region element.
// So, we add a delay to ensure an interval between messages.
e.data.addToCartHandler.$liveRegion
.delay(1000)
.text( message )
.attr( 'aria-relevant', 'all' );
};
/**
* Add live region into the body element.
*/
AddToCartHandler.prototype.createLiveRegion = function() {
var existingLiveRegion = $( '.widget_shopping_cart_live_region' );
if ( existingLiveRegion.length ) {
return existingLiveRegion;
}
return $( '<div class="widget_shopping_cart_live_region screen-reader-text" role="status"></div>' ).appendTo( 'body' );
};
/**
* Callbacks after added to cart event.
*/
AddToCartHandler.prototype.onAddedToCart = function( e, fragments, cart_hash, $button ) {
e.data.addToCartHandler.updateButton( e, fragments, cart_hash, $button );
e.data.addToCartHandler.updateFragments( e, fragments );
e.data.addToCartHandler.alertCartUpdated( e, fragments, cart_hash, $button );
};
/**
* Callbacks after removed from cart event.
*/
AddToCartHandler.prototype.onRemovedFromCart = function( e, fragments, cart_hash, $button ) {
e.data.addToCartHandler.updateFragments( e, fragments );
e.data.addToCartHandler.alertCartUpdated( e, fragments, cart_hash, $button );
};
/** /**
* Init AddToCartHandler. * Init AddToCartHandler.
*/ */

View File

@ -74,4 +74,28 @@ class WC_Product_Simple extends WC_Product {
return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( $text, $this->get_name() ), $this ); return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( $text, $this->get_name() ), $this );
} }
/**
* Get the add to cart button success message - used to update the mini cart live region.
*
* @return string
*/
public function add_to_cart_success_message() {
$text = '';
if ( $this->is_purchasable() && $this->is_in_stock() ) {
/* translators: %s: Product title */
$text = __( '&ldquo;%s&rdquo; has been added to your cart', 'woocommerce' );
$text = sprintf( $text, $this->get_name() );
}
/**
* Filter product add to cart success message.
*
* @since 9.2.0
* @param string $text The success message when a product is added to the cart.
* @param WC_Product_Simple $this Reference to the current WC_Product_Simple instance.
*/
return apply_filters( 'woocommerce_product_add_to_cart_success_message', $text, $this );
}
} }

View File

@ -1393,6 +1393,10 @@ if ( ! function_exists( 'woocommerce_template_loop_add_to_cart' ) ) {
), ),
); );
if ( is_a( $product, 'WC_Product_Simple' ) ) {
$defaults['attributes']['data-success_message'] = $product->add_to_cart_success_message();
}
$args = apply_filters( 'woocommerce_loop_add_to_cart_args', wp_parse_args( $args, $defaults ), $product ); $args = apply_filters( 'woocommerce_loop_add_to_cart_args', wp_parse_args( $args, $defaults ), $product );
if ( ! empty( $args['attributes']['aria-describedby'] ) ) { if ( ! empty( $args['attributes']['aria-describedby'] ) ) {

View File

@ -14,7 +14,7 @@
* *
* @see https://woocommerce.com/document/template-structure/ * @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates * @package WooCommerce\Templates
* @version 7.9.0 * @version 9.2.0
*/ */
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
@ -47,13 +47,15 @@ do_action( 'woocommerce_before_mini_cart' ); ?>
echo apply_filters( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo apply_filters( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'woocommerce_cart_item_remove_link', 'woocommerce_cart_item_remove_link',
sprintf( sprintf(
'<a href="%s" class="remove remove_from_cart_button" aria-label="%s" data-product_id="%s" data-cart_item_key="%s" data-product_sku="%s">&times;</a>', '<a href="%s" class="remove remove_from_cart_button" aria-label="%s" data-product_id="%s" data-cart_item_key="%s" data-product_sku="%s" data-success_message="%s">&times;</a>',
esc_url( wc_get_cart_remove_url( $cart_item_key ) ), esc_url( wc_get_cart_remove_url( $cart_item_key ) ),
/* translators: %s is the product name */ /* translators: %s is the product name */
esc_attr( sprintf( __( 'Remove %s from cart', 'woocommerce' ), wp_strip_all_tags( $product_name ) ) ), esc_attr( sprintf( __( 'Remove %s from cart', 'woocommerce' ), wp_strip_all_tags( $product_name ) ) ),
esc_attr( $product_id ), esc_attr( $product_id ),
esc_attr( $cart_item_key ), esc_attr( $cart_item_key ),
esc_attr( $_product->get_sku() ) esc_attr( $_product->get_sku() ),
/* translators: %s is the product name */
esc_attr( sprintf( __( '&ldquo;%s&rdquo; has been removed from your cart', 'woocommerce' ), wp_strip_all_tags( $product_name ) ) )
), ),
$cart_item_key $cart_item_key
); );