From 645de90342f942bfef26c978570a7ffbef740aa5 Mon Sep 17 00:00:00 2001
From: Martin Leblanc <m26lebla@uwaterloo.ca>
Date: Thu, 27 Feb 2025 13:15:07 -0500
Subject: [PATCH] ISTWCMS-5650: Add tab indexing to image gallery carousel nav
 and dots

---
 .../image-gallery/_image-gallery.twig         |   4 +-
 .../image-gallery/image-gallery.js            | 152 +++++++++++-------
 2 files changed, 95 insertions(+), 61 deletions(-)

diff --git a/src/patterns/04-components/image-gallery/_image-gallery.twig b/src/patterns/04-components/image-gallery/_image-gallery.twig
index c36acebd..10312314 100644
--- a/src/patterns/04-components/image-gallery/_image-gallery.twig
+++ b/src/patterns/04-components/image-gallery/_image-gallery.twig
@@ -146,8 +146,8 @@
   </div>
   {% if type == NULL or type == 'slider' %}
     <div class="uw-button--wrap">
-      <button class="uw-ig-button--previous"> < Prev</button>
-      <button class="uw-ig-button--next">Next ></button>
+      <button tabindex="0" class="uw-ig-button--previous"> < Prev</button>
+      <button tabindex="0" class="uw-ig-button--next">Next ></button>
     </div>
   {% endif %}
 </div>
diff --git a/src/patterns/04-components/image-gallery/image-gallery.js b/src/patterns/04-components/image-gallery/image-gallery.js
index 6b4cbb0c..3b53eb6d 100644
--- a/src/patterns/04-components/image-gallery/image-gallery.js
+++ b/src/patterns/04-components/image-gallery/image-gallery.js
@@ -1,76 +1,117 @@
 /**
  * @file
+ * Provides behavior for the image gallery using Flickity.
  */
 
-(function ($, Drupal) {
+(function ($, Drupal, window) {
   'use strict';
+
   Drupal.behaviors.imagegallery = {
     attach: function () {
 
+      // Ensure code runs after the DOM is fully loaded.
       $(document).ready(function () {
 
-        // Step through each FF on the page.
+        // Iterate over each image gallery component.
         $('.uw-ig').each(function () {
 
-          // Get the id to reference the individual FF.
-          // Need this to ensure that if more than one FF on the page,
-          // that all FFs get the carousel added.
+          // Get the unique ID for each image gallery instance.
           var id = '#uw-ig-' + $(this).data('id');
+
+          // Retrieve configuration options from data attributes.
           var imagesNum = $(this).data('images-num') || 1;
           var navStyle = $(this).data('nav') || 'both';
-
-          // Create carousel config first.
-          var flickityOptions = {
-            cellAlign: 'left',
-            contain: true,
-            wrapAround: true,
-            draggable: false,
-            groupCells: function () {
-              var width = $(window).width();
-              return width <= 600 ? 1 : imagesNum;
-            },
-            prevNextButtons: false,
-            pageDots: navStyle === 'pagination' || navStyle === 'both'
-          };
-
           var $carousel = $(id + ' .carousel');
 
+          // Initialize Flickity only if the carousel exists.
           if ($carousel.length) {
-            $carousel.flickity(flickityOptions);
+            $carousel.flickity({
+              cellAlign: 'left',
+              contain: true,
+              wrapAround: true,
+              draggable: false,
+              groupCells: function () {
+                // Adjust the number of visible images based on screen width.
+                var width = $(window).width();
+                if (width <= 600) {
+                  return 1;
+                }
+                else if (width <= 1024) {
+                  return Math.min(2, imagesNum);
+                }
+                else {
+                  return imagesNum;
+                }
+              },
+              prevNextButtons: false,
+              pageDots: navStyle === 'pagination' || navStyle === 'both',
+            });
+
+            // Get the Flickity instance from the element.
+            var flkty = $carousel.data('flickity');
+
+            // Handle keyboard navigation for Flickity pagination dots.
+            var dots = document.querySelectorAll('.uw-ig .flickity-page-dots .dot');
+
+            if (!dots.length) {
+              return;
+            }
+
+            dots.forEach(function (dot) {
+              dot.setAttribute('tabindex', '0'); // Make dots focusable.
+
+              dot.addEventListener('keydown', function (event) {
+                if (event.key === 'Enter' || event.keyCode === 13) {
+                  var label = dot.getAttribute('aria-label');
+
+                  // Extract the slide index from the dot label.
+                  var match = label.match(/\d+/);
+                  if (!match) {
+                    return;
+                  }
+
+                  var targetIndex = parseInt(match[0], 10) - 1;
+
+                  if (flkty) {
+                    flkty.select(targetIndex); // Move to the selected slide.
+                  }
+                }
+              });
+            });
           }
-          // previous button
-          $('.uw-ig-button--previous').on( 'click', function() {
+
+          // Previous button event listener.
+          $('.uw-ig-button--previous').on('click', function () {
             $carousel.flickity('previous');
           });
-          // next
-          $('.uw-ig-button--next').on( 'click', function() {
+
+          // Next button event listener.
+          $('.uw-ig-button--next').on('click', function () {
             $carousel.flickity('next');
           });
 
+          // Set lightbox open button to be unfocusable by default.
+          $('.uw-lightbox__open').attr('tabindex', -1);
 
+          // Activate navigation buttons if required.
           if (navStyle === 'navigation' || navStyle === 'both') {
             $('.uw-button--wrap').addClass('active');
           }
 
-          // Lightbox enchancements
+          // Lightbox open event.
           $('.uw-lightbox__open').on('click', function () {
             $(id + ' .uw-lightbox').addClass('openLightBox');
             $('html').addClass('no-scroll');
-
           });
-          // Lightbox close
+
+          // Lightbox close event.
           $(id + ' .uw-lightbox__close').on('click', function () {
             $('.uw-lightbox').removeClass('openLightBox');
             $('html').removeClass('no-scroll');
           });
-          // If next is clicked
-          $(id + ' .uw-lightbox__next').on('click', function () {
-            if (!$(id + ' .uw-lightbox').hasClass('openLightBox')) {
-              $(id + ' .uw-lightbox').addClass('openLightBox');
-            }
-          });
-          // If prev is clicked
-          $(id + ' .uw-lightbox__prev').on('click', function () {
+
+          // Ensure the lightbox opens when navigating within it.
+          $(id + ' .uw-lightbox__next, ' + id + ' .uw-lightbox__prev').on('click', function () {
             if (!$(id + ' .uw-lightbox').hasClass('openLightBox')) {
               $(id + ' .uw-lightbox').addClass('openLightBox');
             }
@@ -82,46 +123,39 @@
            * @returns {boolean} clicked.
            */
           function fakeClick() {
-            //use url to build the fake anchor id
             var url = window.location.href;
-            //Regex to replace the text
-            // "lightbox" with "ig" a
-            // and trim last "-###".
+
+            // Construct a fake anchor ID to match the gallery.
             var galleryAnchor = url
               .substring(url.lastIndexOf('/') + 1)
-              .replace( /(?:^|\W)lightbox(?:$|\W)/, '-ig-')
+              .replace(/(?:^|\W)lightbox(?:$|\W)/, '-ig-')
               .replace(/-\d+$/, '');
-            // Create the fake element
-            var escFake = document.createElement('a');
 
-            var linkText = document.createTextNode('fake click');
-
-            escFake.appendChild(linkText);
-            escFake.title = 'my title text';
+            // Create a temporary link element.
+            var escFake = document.createElement('a');
             escFake.href = galleryAnchor;
-            escFake.classList = 'uw-lightbox__close off-screen';
+            escFake.classList.add('uw-lightbox__close', 'off-screen');
 
-            // Append the fake button
+            // Append and trigger the click.
             document.body.appendChild(escFake);
-            //Click the button
             escFake.click();
-            // Remove no scroll
+
+            // Cleanup after closing the lightbox.
             $('html').removeClass('no-scroll');
-            // Remove open class
             $('.uw-lightbox').removeClass('openLightBox');
-            // Remove the fake button
             document.body.removeChild(escFake);
-
           }
-          // Attach the keyup event to Escape tp close
+
+          // Close lightbox when pressing Escape.
           $(document).on('keyup', function (evt) {
             if (evt.keyCode === 27) {
               fakeClick();
             }
           });
-          // If click in outside lightbox div then close
-          $(document).click( function (evt) {
-            if ($(evt.target).is( $('.uw-lightbox.openLightBox'))) {
+
+          // Close lightbox if clicking outside of the content.
+          $(document).click(function (evt) {
+            if ($(evt.target).is($('.uw-lightbox.openLightBox'))) {
               fakeClick();
             }
           });
@@ -129,4 +163,4 @@
       });
     }
   };
-})(jQuery, Drupal);
+})(jQuery, Drupal, window);
-- 
GitLab