PageSourceSearch

https://cv.k12.ca.us/js/slick-a11y.js?v=20260923224145

js cv.k12.ca.us collected 2026-09-24 09:51:47 UTC 8,318 bytes, 210 lines download raw bytes

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.