WIP(5514): Short-term Slick carousel accessibility patch (theme JS)#5528
WIP(5514): Short-term Slick carousel accessibility patch (theme JS)#5528accesswatch wants to merge 1 commit intomainfrom
Conversation
…nal theme library
There was a problem hiding this comment.
Pull request overview
Adds an optional AZ Barrio theme JS “runtime patch” to mitigate Slick carousel accessibility issues reported in #5514 by modifying Slick’s generated ARIA/focus markup during initialization.
Changes:
- Introduces
slick-a11y-patch.jsto adjust ARIA roles/labels and focus handling for Slick carousels at runtime. - Adds a new theme library entry intended to ship/enable the patch JS.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
themes/custom/az_barrio/js/slick-a11y-patch.js |
Runtime DOM patching on Slick init/reInit to reduce axe-core ARIA violations and adjust focusability. |
themes/custom/az_barrio/az_barrio.libraries.yml |
Adds a theme library to include the new patch JS (currently duplicated). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| (function ($, Drupal) { | ||
| 'use strict'; | ||
| $(document).once('slick-a11y-patch').each(function () { | ||
| // When slick initializes, apply corrections | ||
| $(document).on('init reInit', '.slick-slider', function (event, slick) { | ||
| var $slider = $(this); | ||
| // ensure track has accessible name | ||
| var $track = $slider.find('.slick-track'); | ||
| if ($track.length && !$track.attr('aria-label')) { | ||
| $track.attr('aria-label', $slider.data('a11y-label') || 'Carousel slides'); | ||
| } | ||
| // fix dots role | ||
| var $dots = $slider.find('.slick-dots'); | ||
| if ($dots.length) { | ||
| $dots.attr('role', 'list'); | ||
| $dots.find('li').each(function (i) { | ||
| $(this).removeAttr('role aria-selected aria-controls'); | ||
| // ensure internal button has accessible label | ||
| var $btn = $(this).find('button'); | ||
| if ($btn.length && !$btn.attr('aria-label')) { | ||
| $btn.attr('aria-label', 'Go to slide ' + (i+1)); | ||
| } | ||
| }); | ||
| } | ||
| // hide focusable children of aria-hidden slides | ||
| $slider.find('.slick-slide[aria-hidden="true"]').each(function () { | ||
| $(this).find('a, button, input, select, textarea').attr('tabindex', '-1'); | ||
| }); | ||
| // remove problematic role on slide wrapper | ||
| $slider.find('.slick-slide').removeAttr('role tabindex'); | ||
| }); | ||
| }); | ||
| })(jQuery, Drupal); |
There was a problem hiding this comment.
$(document).once(...) relies on the old jQuery Once plugin, which isn’t available by default in Drupal core 11 (core provides the once() utility via core/once). As written, this will likely throw $(...).once is not a function at runtime. Update this to use once() (and/or a Drupal behavior) and ensure the library depends on core/once.
| (function ($, Drupal) { | |
| 'use strict'; | |
| $(document).once('slick-a11y-patch').each(function () { | |
| // When slick initializes, apply corrections | |
| $(document).on('init reInit', '.slick-slider', function (event, slick) { | |
| var $slider = $(this); | |
| // ensure track has accessible name | |
| var $track = $slider.find('.slick-track'); | |
| if ($track.length && !$track.attr('aria-label')) { | |
| $track.attr('aria-label', $slider.data('a11y-label') || 'Carousel slides'); | |
| } | |
| // fix dots role | |
| var $dots = $slider.find('.slick-dots'); | |
| if ($dots.length) { | |
| $dots.attr('role', 'list'); | |
| $dots.find('li').each(function (i) { | |
| $(this).removeAttr('role aria-selected aria-controls'); | |
| // ensure internal button has accessible label | |
| var $btn = $(this).find('button'); | |
| if ($btn.length && !$btn.attr('aria-label')) { | |
| $btn.attr('aria-label', 'Go to slide ' + (i+1)); | |
| } | |
| }); | |
| } | |
| // hide focusable children of aria-hidden slides | |
| $slider.find('.slick-slide[aria-hidden="true"]').each(function () { | |
| $(this).find('a, button, input, select, textarea').attr('tabindex', '-1'); | |
| }); | |
| // remove problematic role on slide wrapper | |
| $slider.find('.slick-slide').removeAttr('role tabindex'); | |
| }); | |
| }); | |
| })(jQuery, Drupal); | |
| (function ($, Drupal, once) { | |
| 'use strict'; | |
| Drupal.behaviors.slickA11yPatch = { | |
| attach: function (context) { | |
| once('slick-a11y-patch', document).forEach(function () { | |
| // When slick initializes, apply corrections | |
| $(document).on('init reInit', '.slick-slider', function (event, slick) { | |
| var $slider = $(this); | |
| // ensure track has accessible name | |
| var $track = $slider.find('.slick-track'); | |
| if ($track.length && !$track.attr('aria-label')) { | |
| $track.attr('aria-label', $slider.data('a11y-label') || 'Carousel slides'); | |
| } | |
| // fix dots role | |
| var $dots = $slider.find('.slick-dots'); | |
| if ($dots.length) { | |
| $dots.attr('role', 'list'); | |
| $dots.find('li').each(function (i) { | |
| $(this).removeAttr('role aria-selected aria-controls'); | |
| // ensure internal button has accessible label | |
| var $btn = $(this).find('button'); | |
| if ($btn.length && !$btn.attr('aria-label')) { | |
| $btn.attr('aria-label', 'Go to slide ' + (i + 1)); | |
| } | |
| }); | |
| } | |
| // hide focusable children of aria-hidden slides | |
| $slider.find('.slick-slide[aria-hidden="true"]').each(function () { | |
| $(this).find('a, button, input, select, textarea').attr('tabindex', '-1'); | |
| }); | |
| // remove problematic role on slide wrapper | |
| $slider.find('.slick-slide').removeAttr('role tabindex'); | |
| }); | |
| }); | |
| } | |
| }; | |
| })(jQuery, Drupal, once); |
| // hide focusable children of aria-hidden slides | ||
| $slider.find('.slick-slide[aria-hidden="true"]').each(function () { | ||
| $(this).find('a, button, input, select, textarea').attr('tabindex', '-1'); | ||
| }); |
There was a problem hiding this comment.
The patch sets tabindex="-1" on focusable elements in aria-hidden="true" slides, but it never restores the original tabindex when a slide becomes visible again. After the first slide change, previously-hidden slides can remain unfocusable even when active. Consider also handling Slick’s slide-change events (e.g., afterChange) and restoring tabindex (ideally only removing/undoing tabindex values that this patch added).
| // fix dots role | ||
| var $dots = $slider.find('.slick-dots'); | ||
| if ($dots.length) { | ||
| $dots.attr('role', 'list'); | ||
| $dots.find('li').each(function (i) { | ||
| $(this).removeAttr('role aria-selected aria-controls'); | ||
| // ensure internal button has accessible label | ||
| var $btn = $(this).find('button'); | ||
| if ($btn.length && !$btn.attr('aria-label')) { | ||
| $btn.attr('aria-label', 'Go to slide ' + (i+1)); | ||
| } | ||
| }); |
There was a problem hiding this comment.
The dot-nav li elements commonly have aria-hidden set by Slick (and contain focusable <button> children), which is one of the core aria-hidden-focus violations described in #5514. This patch removes role/aria-selected/aria-controls from the li, but it leaves aria-hidden in place and doesn’t adjust the nested button focusability, so the aria-hidden-focus violation may still remain for dots. Consider removing aria-hidden from dot li elements (or otherwise ensuring their buttons are not focusable when hidden).
|
|
||
| az-slick-a11y-patch: | ||
| js: | ||
| js/slick-a11y-patch.js: {} | ||
| dependencies: | ||
| - core/jquery | ||
| - core/drupal |
There was a problem hiding this comment.
az-slick-a11y-patch is defined twice in this YAML. Duplicate keys are invalid YAML and can lead to parsing errors or unexpected overrides (only the last definition may win). Remove the duplicate entry, and (since the JS uses once() semantics) add the appropriate dependency (likely core/once) to the remaining library definition.
| az-slick-a11y-patch: | |
| js: | |
| js/slick-a11y-patch.js: {} | |
| dependencies: | |
| - core/jquery | |
| - core/drupal | |
| - core/once |
Addresses #5514 (short-term mitigation) — Why: the Slick carousel library generates runtime ARIA markup that triggers several critical axe-core violations (invalid roles/attributes, focusable elements hidden with
aria-hidden, missing accessible names).What this change does:
aria-labelto the slides track when missing..slick-dotstorole="list"and removes invalid ARIA attributes from dot<li>items.aria-labeltext (e.g., "Go to slide N").role/tabindexfrom slide wrappers and prevents focusable children insidearia-hiddenslides.Impact:
How to test:
az-slick-a11y-patchon a site usingaz_carousel(or ensure the theme loads the patch JS).Notes: