1/** 2 * Slick Carousel accessibility fixes. 3 * 4 * Slick is loaded from a CDN and not vendored in this repo, so these patch known gaps 5 * in its own built-in accessibility handling (initADA()) after the fact rather than 6 * editing the library. 7 * 8 * @package StudioSwift 9 */ 10( function ( $ ) { 11 'use strict'; 12 13 if ( typeof $ === 'undefined' ) { 14 return; 15 } 16 17 /** 18 * When a Slick instance has `dots: true`, Slick's own initADA() feature sets 19 * `aria-describedby` (not `aria-labelledby`) on each `role="tabpanel"` slide. Per the 20 * ARIA spec, `aria-describedby` does not contribute to an element's accessible name, 21 * so every one of those tabpanels is reported as having no programmatically 22 * associated name. This mirrors that same reference into `aria-labelledby`. 23 */ 24 function fixTabpanelNames( $slider ) { 25 $slider.find( '[role="tabpanel"]' ).each( function () { 26 var $panel = $( this ); 27 if ( $panel.attr( 'aria-label' ) || $panel.attr( 'aria-labelledby' ) ) { 28 return; 29 } 30 var describedBy = $panel.attr( 'aria-describedby' ); 31 if ( describedBy ) { 32 $panel.attr( 'aria-labelledby', describedBy ); 33 } 34 } ); 35 } 36 37 /** 38 * Slick can leave an off-screen/inactive slide with `aria-hidden="true"` while it 39 * (or a focusable element inside it, e.g. a button) still carries a tabindex that 40 * keeps it in the keyboard tab order â an aria-hidden element must never be 41 * focusable, nor contain focusable descendants. Forces tabindex="-1" on anything 42 * hidden this way, and restores what it changed once the slide becomes visible 43 * again (e.g. real content like a "view more" button inside a slide). 44 */ 45 function fixHiddenFocusable( $slider ) { 46 $slider.find( '.slick-slide' ).each( function () { 47 var $slide = $( this ); 48 var isHidden = 'true' === $slide.attr( 'aria-hidden' ); 49 50 if ( isHidden ) { 51 if ( '-1' !== $slide.attr( 'tabindex' ) ) { 52 $slide.attr( 'tabindex', '-1' ).data('slickA11yForcedTabindex', true); 53 } 54 } else if ( $slide.data( 'slickA11yForcedTabindex' ) ) { 55 $slide.removeAttr( 'tabindex' ).removeData( 'slickA11yForcedTabindex' ); 56 } 57 58 var $focusables = $slide.find( 'a[href], button, input, select, textarea, [tabindex]' ); 59 60 if ( isHidden ) { 61 $focusables.each( function () { 62 var $el = $( this ); 63 if ( '-1' !== $el.attr( 'tabindex' ) ) { 64 $el.attr( 'tabindex', '-1' ).data( 'slickA11yWasFocusable', true ); 65 } 66 } ); 67 } else { 68 $focusables.each( function () { 69 var $el = $( this ); 70 if ( $el.data( 'slickA11yWasFocusable' ) ) { 71 $el.removeAttr( 'tabindex' ).removeData( 'slickA11yWasFocusable' ); 72 } 73 } ); 74 } 75 } ); 76 } 77 78 function runFixes( $slider ) { 79 fixTabpanelNames( $slider ); 80 fixHiddenFocusable( $slider ); 81 } 82 83 $( document ).on( 'init reInit afterChange', '.slick-slider', function () { 84 runFixes( $( this ) ); 85 } ); 86 87 // Belt-and-suspenders: also sweep anything already initialized before this script 88 // ran (e.g. if a block's own script happened to execute first). 89 $( function () { 90 $( '.slick-slider' ).each( function () { 91 runFixes( $( this ) ); 92 } ); 93 } ); 94 95 /** 96 * Some Slick transition modes (fade: true in particular) can touch 97 * aria-hidden/tabindex again slightly after firing their own init/afterChange 98 * events, which can re-introduce the aria-hidden+tabindex conflict after our 99 * event-based fix above already ran. A MutationObserver reacts to the actual 100 * DOM state instead of guessing every internal Slick timing path, so it 101 * self-corrects regardless of when or why Slick changes these attributes. 102 */ 103 if ( typeof MutationObserver !== 'undefined' ) { 104 // Debounce flag lives per-slider (not shared) so concurrent mutations on 105 // different carousel instances can't cause one of them to be dropped. 106 function scheduleRun( $slider ) { 107 if ( $slider.data( 'slickA11yRunScheduled' ) ) { 108 return; 109 } 110 $slider.data( 'slickA11yRunScheduled', true ); 111 window.setTimeout( function () { 112 $slider.removeData( 'slickA11yRunScheduled' ); 113 runFixes( $slider ); 114 }, 0 ); 115 } 116 117 $( function () { 118 $( '.slick-slider' ).each( function () { 119 var slider = this; 120 var $slider = $( slider ); 121 var observer = new MutationObserver( function ( mutations ) { 122 for ( var i = 0; i < mutations.length; i++ ) { 123 var attr = mutations[ i ].attributeName; 124 if ( 'aria-hidden' === attr || 'tabindex' === attr ) { 125 scheduleRun( $slider ); 126 return; 127 } 128 } 129 } ); 130 observer.observe( slider, { 131 attributes: true, 132 attributeFilter: [ 'aria-hidden', 'tabindex' ], 133 subtree: true, 134 } ); 135 } ); 136 } ); 137 } 138} )( window.jQuery );
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.