From 0f1634d6dadb6b397ea8e808e1af993d57fd1a37 Mon Sep 17 00:00:00 2001 From: Gabriel Manussakis <9420947+Manussakis@users.noreply.github.com> Date: Fri, 30 Aug 2024 05:14:22 -0300 Subject: [PATCH] [Accessibility] Trap focus inside the product gallery modal (#50730) * Add i18n_open_product_gallery prop to wc_single_product_params * Trap focus inside the product gallery modal * Ensure the product gallery controls are always visible * Add changelog file * Add space before parameter * Early return if there is no elements enough to trap focus * Revert changes on the gallery trigger link * Remove unnecessary white sopace * Fix trap focus when the arrow buttons are hidden * Remove usage of the Photoswipe destroy event --- plugins/woocommerce/changelog/fix-43605 | 4 ++ .../legacy/js/frontend/single-product.js | 69 ++++++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-43605 diff --git a/plugins/woocommerce/changelog/fix-43605 b/plugins/woocommerce/changelog/fix-43605 new file mode 100644 index 00000000000..ad1d926bd91 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-43605 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Trap focus inside the product gallery modal. diff --git a/plugins/woocommerce/client/legacy/js/frontend/single-product.js b/plugins/woocommerce/client/legacy/js/frontend/single-product.js index 6b5c99cd8e8..e8adcc662fc 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/single-product.js +++ b/plugins/woocommerce/client/legacy/js/frontend/single-product.js @@ -127,6 +127,8 @@ jQuery( function( $ ) { this.onResetSlidePosition = this.onResetSlidePosition.bind( this ); this.getGalleryItems = this.getGalleryItems.bind( this ); this.openPhotoswipe = this.openPhotoswipe.bind( this ); + this.trapFocusPhotoswipe = this.trapFocusPhotoswipe.bind( this ); + this.handlePswpTrapFocus = this.handlePswpTrapFocus.bind( this ); if ( this.flexslider_enabled ) { this.initFlexslider( args.flexslider ); @@ -307,8 +309,10 @@ jQuery( function( $ ) { e.preventDefault(); var pswpElement = $( '.pswp' )[0], - items = this.getGalleryItems(), - eventTarget = $( e.target ), + items = this.getGalleryItems(), + eventTarget = $( e.target ), + currentTarget = e.currentTarget, + self = this, clicked; if ( 0 < eventTarget.closest( '.woocommerce-product-gallery__trigger' ).length ) { @@ -326,14 +330,73 @@ jQuery( function( $ ) { } captionEl.children[0].textContent = item.title; return true; - } + }, + timeToIdle: 0, // Ensure the gallery controls are always visible to avoid keyboard navigation issues. }, wc_single_product_params.photoswipe_options ); // Initializes and opens PhotoSwipe. var photoswipe = new PhotoSwipe( pswpElement, PhotoSwipeUI_Default, items, options ); + + photoswipe.listen( 'afterInit', function() { + self.trapFocusPhotoswipe( true ); + }); + + photoswipe.listen( 'close', function() { + self.trapFocusPhotoswipe( false ); + currentTarget.focus(); + }); + photoswipe.init(); }; + /** + * Control focus in photoswipe modal. + * + * @param {boolean} trapFocus - Whether to trap focus or not. + */ + ProductGallery.prototype.trapFocusPhotoswipe = function( trapFocus ) { + var pswp = document.querySelector( '.pswp' ); + + if ( ! pswp ) { + return; + } + + if ( trapFocus ) { + pswp.addEventListener( 'keydown', this.handlePswpTrapFocus ); + } else { + pswp.removeEventListener( 'keydown', this.handlePswpTrapFocus ); + } + }; + + /** + * Handle keydown event in photoswipe modal. + */ + ProductGallery.prototype.handlePswpTrapFocus = function( e ) { + var allFocusablesEls = e.currentTarget.querySelectorAll( 'button:not([disabled])' ); + var filteredFocusablesEls = Array.from( allFocusablesEls ).filter( function( btn ) { + return btn.style.display !== 'none' && window.getComputedStyle( btn ).display !== 'none'; + } ); + + if ( 1 >= filteredFocusablesEls.length ) { + return; + } + + var firstTabStop = filteredFocusablesEls[0]; + var lastTabStop = filteredFocusablesEls[filteredFocusablesEls.length - 1]; + + if ( e.key === 'Tab' ) { + if ( e.shiftKey ) { + if ( document.activeElement === firstTabStop ) { + e.preventDefault(); + lastTabStop.focus(); + } + } else if ( document.activeElement === lastTabStop ) { + e.preventDefault(); + firstTabStop.focus(); + } + } + }; + /** * Function to call wc_product_gallery on jquery selector. */