1/** 2 * Global Slick Slider Accessibility Utility 3 * 4 * Applies WAI-ARIA carousel best practices to slick slider instances: 5 * - Sets role="group" and aria-roledescription="slide" on each real slide 6 * - Sets aria-label="X of Y" on each real slide 7 * - Manages tabindex on focusable elements within active/inactive slides 8 * - Removes slick's outdated role="listbox" from the track 9 * - Disables slick's built-in arrow-key navigation (users navigate via prev/next buttons) 10 * - Marks the carousel wrapper with role="region", aria-roledescription="carousel", and a contextual aria-label 11 * - Injects a screen-reader-only instruction span as the first child of the carousel wrapper 12 * - Optionally announces the current slide via a live region for auto-rotating carousels 13 * 14 * Usage (call BEFORE .slick() so the "init" handler fires): 15 * window.slickSliderA11y($(element), $(wrapperElement)); 16 * window.slickSliderA11y($(element), $(wrapperElement), { autoRotates: true }); 17 */ 18(function setUpSlickSliderA11y() { 19 var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; 20 var SR_INSTRUCTIONS_ATTR = 'data-slick-sr'; 21 var SR_LIVE_REGION_ATTR = 'data-slick-live'; 22 23 var DEFAULT_LABELS = { 24 'post-feed': 'Post Carousel', 25 'calendar-events': 'Event Carousel', 26 'icon-matrix': 'Icon Carousel' 27 }; 28 29 function getRealSlides(component) { 30 return component.find('.slick-slide:not(.slick-cloned)'); 31 } 32 33 function applySlideAttributes(component) { 34 var slides = getRealSlides(component); 35 var totalSlides = slides.length; 36 37 // Remove slick's outdated role="listbox" from the track 38 component.find('.slick-track').removeAttr('role'); 39 40 // Set proper roles and labels on each real slide 41 slides.each(function applyRoleAndLabel(index) { 42 $(this) 43 .attr('role', 'group') 44 .attr('aria-roledescription', 'slide') 45 .attr('aria-label', (index + 1) + ' of ' + totalSlides); 46 }); 47 48 // Ensure cloned slides are fully hidden from assistive tech 49 component.find('.slick-slide.slick-cloned') 50 .attr('aria-hidden', 'true') 51 .find(FOCUSABLE_SELECTOR) 52 .attr('tabindex', '-1'); 53 } 54 55 function manageFocusableElements(component) { 56 component.find('.slick-slide:not(.slick-active)') 57 .find(FOCUSABLE_SELECTOR) 58 .attr('tabindex', '-1'); 59 60 component.find('.slick-slide.slick-active') 61 .find(FOCUSABLE_SELECTOR) 62 .attr('tabindex', '0'); 63 } 64 65 function getCarouselLabel(wrapper) { 66 var section = wrapper.closest('section[data-comp]'); 67 var headingText = section.find('.ss-component-header-title').first().text().trim(); 68 69 if (headingText) return headingText; 70 71 var compType = section.attr('data-comp') || ''; 72 return DEFAULT_LABELS[compType] || 'Carousel'; 73 } 74 75 function buildScreenReaderInstructions(wrapper, totalSlides) { 76 var hasDots = wrapper.find('.ss-im-pagination-dots, .ss-im-slick-dots').length > 0; 77 var hasPagination = wrapper.find('.pagination').length > 0; 78 var autoRotates = wrapper.closest('section[data-comp]') 79 .find('[data-auto-rotate="1"]').length > 0; 80 81 var unit = hasPagination ? 'pages' : 'slides'; 82 var instructions = 'Contains ' + totalSlides + ' ' + unit + '.'; 83 84 if (hasDots) { 85 instructions += ' Use the pagination dots to jump to a specific slide.'; 86 } else if (hasPagination) { 87 instructions += ' Use the pagination links to navigate.'; 88 } else { 89 instructions += ' Use the next and previous buttons to navigate.'; 90 } 91 92 if (autoRotates) { 93 instructions += ' Movement can be paused with the pause button.'; 94 } 95 96 return instructions; 97 } 98 99 function applyCarouselWrapper(wrapper, totalSlides) { 100 wrapper 101 .attr('role', 'region') 102 .attr('aria-roledescription', 'carousel') 103 .attr('aria-label', getCarouselLabel(wrapper)); 104 105 // Inject screen-reader-only instructions as the first child 106 var existingSr = wrapper.children('[' + SR_INSTRUCTIONS_ATTR + ']'); 107 var instructions = buildScreenReaderInstructions(wrapper, totalSlides); 108 109 if (existingSr.length) { 110 existingSr.text(instructions); 111 } else { 112 $('<span class="sr-only"></span>') 113 .attr(SR_INSTRUCTIONS_ATTR, '') 114 .text(instructions) 115 .prependTo(wrapper); 116 } 117 } 118 119 function disableArrowKeyNavigation(component, slick) { 120 component.find('.slick-list') 121 .off('keydown.slick') 122 .removeAttr('aria-live'); 123 124 if (slick) { 125 if (slick.$prevArrow) slick.$prevArrow.off('keydown.slick'); 126 if (slick.$nextArrow) slick.$nextArrow.off('keydown.slick'); 127 if (slick.$dots) slick.$dots.off('keydown.slick'); 128 } 129 } 130 131 function createLiveRegion(wrapper) { 132 var existing = wrapper.children('[' + SR_LIVE_REGION_ATTR + ']'); 133 if (existing.length) return existing; 134 135 var liveRegion = $('<span class="sr-only"></span>') 136 .attr(SR_LIVE_REGION_ATTR, '') 137 .attr('aria-live', 'polite') 138 .appendTo(wrapper); 139 140 return liveRegion; 141 } 142 143 function announceCurrentSlide(component, liveRegion) { 144 var activeSlide = component.find('.slick-slide.slick-active:not(.slick-cloned)').first(); 145 var label = activeSlide.attr('aria-label'); 146 147 if (label) { 148 liveRegion.text('Slide ' + label); 149 } 150 } 151 152 function removeCarouselWrapper(wrapper) { 153 wrapper 154 .removeAttr('role') 155 .removeAttr('aria-roledescription') 156 .removeAttr('aria-label'); 157 158 wrapper.children('[' + SR_INSTRUCTIONS_ATTR + ']').remove(); 159 wrapper.children('[' + SR_LIVE_REGION_ATTR + ']').remove(); 160 } 161 162 /** 163 * @param {jQuery} component - The slick slider container element 164 * @param {jQuery} [carouselWrapper] - The element wrapping both slides and controls 165 * @param {Object} [options] - Optional configuration 166 * @param {boolean} [options.autoRotates] - Whether the carousel auto-rotates (enables live region announcements) 167 */ 168 window.slickSliderA11y = function slickSliderA11y(component, carouselWrapper, options) { 169 var autoRotates = options && options.autoRotates; 170 var liveRegion = null; 171 172 component.on('init', function handleSlickInit(_event, slick) { 173 applySlideAttributes(component); 174 manageFocusableElements(component); 175 disableArrowKeyNavigation(component, slick); 176 if (carouselWrapper && carouselWrapper.length) { 177 applyCarouselWrapper(carouselWrapper, getRealSlides(component).length); 178 if (autoRotates) { 179 liveRegion = createLiveRegion(carouselWrapper); 180 } 181 } 182 }); 183 184 component.on('afterChange', function handleSlickAfterChange() { 185 manageFocusableElements(component); 186 if (liveRegion && liveRegion.length) { 187 announceCurrentSlide(component, liveRegion); 188 } 189 }); 190 191 component.on('reInit', function handleSlickReInit(_event, slick) { 192 disableArrowKeyNavigation(component, slick); 193 applySlideAttributes(component); 194 manageFocusableElements(component); 195 if (carouselWrapper && carouselWrapper.length) { 196 // Update instruction text in case slides were added/removed 197 var srSpan = carouselWrapper.children('[' + SR_INSTRUCTIONS_ATTR + ']'); 198 if (srSpan.length) { 199 srSpan.text(buildScreenReaderInstructions(carouselWrapper, getRealSlides(component).length)); 200 } 201 } 202 }); 203 204 component.on('destroy', function handleSlickDestroy() { 205 if (carouselWrapper && carouselWrapper.length) { 206 removeCarouselWrapper(carouselWrapper); 207 } 208 }); 209 }; 210})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.