PageSourceSearch

https://www.justborn.com/wp-content/themes/studioswift/assets/js/frontend/slick-accessibility.js

js justborn.com collected 2026-10-02 08:00:55 UTC 4,655 bytes, 138 lines download raw bytes

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.