From 03390265e5967f734b29ceecb7ea90cab5dfc143 Mon Sep 17 00:00:00 2001 From: Gabe Petersen Date: Fri, 3 Feb 2023 20:38:22 -0500 Subject: [PATCH] Add a11y attr option for hidden/visible slides This will enable a default option that will set a visible class to all elements in the current view and set aria-hidden on slides that are not in view. This will also apply a negative tab index for all focusable elements for slides that are not currently in the view. --- src/components/build.js | 28 +++++++++++++++++++++++++++- src/defaults.js | 10 +++++++++- src/utils/dom.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/components/build.js b/src/components/build.js index 9e14e417..e8e97e76 100644 --- a/src/components/build.js +++ b/src/components/build.js @@ -1,4 +1,4 @@ -import { siblings } from '../utils/dom' +import { siblings, getFocusableElements } from '../utils/dom' export default function (Glide, Components, Events) { const Build = { @@ -44,6 +44,28 @@ export default function (Glide, Components, Events) { } }, + /** + * Sets visible classes and hidden accessibility attributes on slides + * + * @return {Void} + */ + setVisibleSlideAttributes () { + Components.Html.slides.forEach((slide, index) => { + // if slide is in visible range + if (index >= Glide.index && index < (Glide.index + Glide.settings.perView)) { + // remove aria hidden and negative tabindex on focusable children + getFocusableElements(slide, true).forEach((focusableElement) => focusableElement.removeAttribute('tabindex')) + slide.removeAttribute('aria-hidden') + slide.classList.add(Glide.settings.classes.slide.visible) + } else { + // apply tabindex = -1 to all focusable elements within the slide + getFocusableElements(slide).forEach((focusableElement) => focusableElement.setAttribute('tabindex', '-1')) + slide.setAttribute('aria-hidden', 'true') + slide.classList.remove(Glide.settings.classes.slide.visible) + } + }) + }, + /** * Removes HTML classes applied at building. * @@ -84,6 +106,10 @@ export default function (Glide, Components, Events) { */ Events.on('move.after', () => { Build.activeClass() + + if (Glide.settings.setVisibleAttributes) { + Build.setVisibleSlideAttributes() + } }) return Build diff --git a/src/defaults.js b/src/defaults.js index 88063601..4874c604 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -165,6 +165,13 @@ export default { */ direction: 'ltr', + /** + * Sets class and accessibility attributes on hidden/visible slides + * + * @type {Boolean} + */ + setVisibleAttributes: true, + /** * The distance value of the next and previous viewports which * have to peek in the current view. Accepts number and @@ -216,7 +223,8 @@ export default { }, slide: { clone: 'glide__slide--clone', - active: 'glide__slide--active' + active: 'glide__slide--active', + visible: 'glide__slide--visible' }, arrow: { disabled: 'glide__arrow--disabled' diff --git a/src/utils/dom.js b/src/utils/dom.js index 3c20c8e1..3d135ded 100644 --- a/src/utils/dom.js +++ b/src/utils/dom.js @@ -45,3 +45,36 @@ export function exist (node) { export function toArray (nodeList) { return Array.prototype.slice.call(nodeList) } + +/** + * Gathers a list of focusable elements within the passed in element (including the element itself). + * Thank you to https://zellwk.com/blog/keyboard-focusable-elements/ for the inspiration + * @param {Element} element + * @param {Boolean} includeNegativeTabIndex - if true, includes elements with negative tabindex + * + * @returns {Array} + */ +export function getFocusableElements (element, includeNegativeTabIndex = false) { + if (!element) { + console.error('getFocusableElements: element does not exist') + } + const focusableElementsSelector = 'a[href], button, input, textarea, select, details, [tabindex], [contenteditable]' + const focusableElements = [] + + // if the container element itself is focusable, add it to the list first + if (element.matches(focusableElementsSelector)) { + focusableElements.push(element) + } + focusableElements.push(...element.querySelectorAll(focusableElementsSelector)) + + // filter by relevant tabindex, styling, and disabled attributes + return focusableElements.filter((element) => { + if (!includeNegativeTabIndex && element.hasAttribute('tabindex') && (element.getAttribute('tabindex') < 0)) { + return false + } + if (element.hasAttribute('disabled') || element.hasAttribute('hidden')) { + return false + } + return true + }) +}