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