-
Notifications
You must be signed in to change notification settings - Fork 13
Uds 2143 - Implement: MVP Accessibility Carousels #1726
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
17449ce
465d088
1f4ee8b
b6ca33a
e7a0561
b122540
57ce6ab
320fb73
0308ab7
76d0934
b9fa088
7d02710
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,35 +77,35 @@ function buildConfig(perView = 1, isFullWidth, hasPeek = true, isDraggable) { | |
| breakpoints: isFullWidth | ||
| ? null | ||
| : { | ||
| 576: { | ||
| // BS4 sm | ||
| perView: perViewSm, | ||
| peek: smallPeek, | ||
| }, | ||
| 768: { | ||
| // BS4 md | ||
| perView: perViewMd, | ||
| peek: smallPeek, | ||
| }, | ||
| 992: { | ||
| // BS4 lg | ||
| perView: perViewLg, | ||
| peek: smallPeek, | ||
| }, | ||
| 1260: { | ||
| // BS4 xl | ||
| perView: perViewLg, | ||
| peek: smallPeek, | ||
| }, | ||
| 1400: { | ||
| perView: perViewLg, | ||
| peek: largePeek, | ||
| }, | ||
| 1920: { | ||
| perView: perViewLg, | ||
| peek: largePeek, | ||
| }, | ||
| 576: { | ||
| // BS4 sm | ||
| perView: perViewSm, | ||
| peek: smallPeek, | ||
| }, | ||
| 768: { | ||
| // BS4 md | ||
| perView: perViewMd, | ||
| peek: smallPeek, | ||
| }, | ||
| 992: { | ||
| // BS4 lg | ||
| perView: perViewLg, | ||
| peek: smallPeek, | ||
| }, | ||
| 1260: { | ||
| // BS4 xl | ||
| perView: perViewLg, | ||
| peek: smallPeek, | ||
| }, | ||
| 1400: { | ||
| perView: perViewLg, | ||
| peek: largePeek, | ||
| }, | ||
| 1920: { | ||
| perView: perViewLg, | ||
| peek: largePeek, | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -142,19 +142,19 @@ function setNavButtonGradient(gliderElement, currentIndex, buttonCount) { | |
| imageGalleryNavigation?.classList.add("slider-start"); | ||
| // Enable/disable prev/next styles. Glide takes care of actual disable. | ||
| arrowPrev?.classList.add(cssDisabledClass); | ||
| arrowPrev?.setAttribute("aria-disabled", "true"); | ||
| arrowPrev?.setAttribute("disabled", "true"); | ||
| arrowNext?.classList.remove(cssDisabledClass); | ||
| arrowNext?.setAttribute("aria-disabled", "false"); | ||
| arrowNext?.removeAttribute("disabled"); | ||
| } else if (currentIndex >= buttonCount - 1) { | ||
| // LAST SLIDE. | ||
| // Gradient for end. | ||
| gliderTrack?.classList.add("slider-end"); | ||
| imageGalleryNavigation?.classList.add("slider-end"); | ||
| // Enable/disable prev/next styles. Glide takes care of actual disable. | ||
| arrowPrev?.classList.remove(cssDisabledClass); | ||
| arrowPrev?.setAttribute("aria-disabled", "false"); | ||
| arrowPrev?.removeAttribute("disabled"); | ||
| arrowNext?.classList.add(cssDisabledClass); | ||
| arrowNext?.setAttribute("aria-disabled", "true"); | ||
| arrowNext?.setAttribute("disabled", "true"); | ||
| } else { | ||
| // MIDDLE SLIDES. | ||
| // Gradient for middle. | ||
|
|
@@ -163,8 +163,8 @@ function setNavButtonGradient(gliderElement, currentIndex, buttonCount) { | |
| // Enable/disable prev/next styles. Glide takes care of actual disable. | ||
| arrowPrev?.classList.remove(cssDisabledClass); | ||
| arrowNext?.classList.remove(cssDisabledClass); | ||
| arrowPrev?.setAttribute("aria-disabled", "false"); | ||
| arrowNext?.setAttribute("aria-disabled", "false"); | ||
| arrowPrev?.removeAttribute("disabled"); | ||
| arrowNext?.removeAttribute("disabled"); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -293,20 +293,81 @@ function setupCaroarousel({ | |
| imageNavLeft; | ||
|
|
||
| if (currentIndex === 0 || currentSlideLeft <= 0 + viewPadding) { | ||
| imageGalleryNav.style.left = `${ | ||
| currentLeft - currentSlideLeft + viewPadding | ||
| }px`; | ||
| imageGalleryNav.style.left = `${currentLeft - currentSlideLeft + viewPadding | ||
| }px`; | ||
| } else if (currentSlideRight >= fullNavWidth - viewPadding) { | ||
| const outsideAmount = currentSlideRight - fullNavWidth; | ||
| imageGalleryNav.style.left = `${ | ||
| currentLeft - outsideAmount - viewPadding | ||
| }px`; | ||
| imageGalleryNav.style.left = `${currentLeft - outsideAmount - viewPadding | ||
| }px`; | ||
| } | ||
| } | ||
|
|
||
| // We use event listeners to clear and set class names to show/hide | ||
| // gradients when at the start, middle or end of a slider. | ||
| setNavButtonGradient(gliderElement, currentIndex, buttonCount); | ||
|
|
||
|
|
||
| // Set the main container with aria-labelledby with the header of the active card | ||
| const slides = gliderElement.querySelectorAll(".slider"); | ||
|
|
||
| // Getting the text from the main header tag | ||
| //source: https://stackoverflow.com/questions/67134998/javascript-recursion-to-get-innertext | ||
| function getText(node, accumulator) { | ||
| if (node.nodeType === 3) { // 3 == text node | ||
| accumulator.push(node.nodeValue) | ||
| } else { | ||
| for (let child of node.childNodes) | ||
| getText(child, accumulator) | ||
| } | ||
| } | ||
|
|
||
| for (let i = 0; i < slides.length; i++) { | ||
| if (i === currentIndex) { | ||
|
|
||
| // Find the main h tag in the card if one exists | ||
| let header; | ||
| for (let j = 1; j < 9; j++) { | ||
| if (!header) { | ||
| header = slides[i].querySelector(`h${j}`); | ||
| } | ||
| } | ||
|
|
||
| let cardDiv = slides[i].querySelectorAll(`.card`) | ||
| if (cardDiv && cardDiv[0]) { | ||
| cardDiv[0].setAttribute("aria-live", "polite"); | ||
| cardDiv[0].setAttribute("role", "alert"); | ||
| } | ||
|
|
||
|
|
||
| if (header) { | ||
| let allTexts = [] | ||
| getText(slides[i], allTexts) | ||
| gliderElement.setAttribute("aria-labelledby", allTexts[0]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aria-labelledby needs to reference an element id, not raw text - allTexts[0] is just the heading's text content, so this won't resolve to anything for a screen reader. Also getText is called on the whole slide instead of just header, so it could grab text that comes before the heading. |
||
| gliderElement.removeAttribute("aria-label"); | ||
| } else { | ||
| gliderElement.setAttribute("aria-label", `Card ${i + 1}`); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This fallback runs for any carousel without a heading in the slide, not just cards, an image/gallery slide with no caption would get announced as "Card 1". Might be worth making this generic ("Slide 1") instead. |
||
| gliderElement.removeAttribute("aria-labelledby"); | ||
| } | ||
|
|
||
| // slides[i].focus(); | ||
|
|
||
| } | ||
| } | ||
|
|
||
|
|
||
| // Update bullet accessibility | ||
| const bullets = gliderElement.querySelectorAll(".glide__bullet"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Heads up, for the image gallery |
||
| for (let i = 0; i < bullets.length; i++) { | ||
| if (i === currentIndex) { | ||
| bullets[i].setAttribute("disabled", ""); | ||
| bullets[i].setAttribute("aria-current", "true"); | ||
| } else { | ||
| bullets[i].removeAttribute("disabled"); | ||
| bullets[i].setAttribute("aria-current", "false"); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| // set the current index | ||
| gliderElement.setAttribute("data-current-index", currentIndex); | ||
| onItemClick && onItemClick(currentIndex); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,14 +79,18 @@ const BaseCarousel = ({ | |
|
|
||
| return ( | ||
| <div | ||
| role={role} | ||
| aria-labelledby={ariaLabelledBy} | ||
| // role={role} | ||
| // role="group" | ||
| role="region" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. role is still a prop here but it's dead now that role is hardcoded to "region" - ImageGalleryCarousel passes role="figure" and that'll get silently overridden. Also the commented-out lines above can probably go. |
||
| aria-label="Carousel pagination" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This aria-label should be on the bullets group container, not here - that's the exact gap Andrew flagged on UDS-2150 (pagination group missing its label). BaseBulletItemContainer still only has role="group" with nothing on it. |
||
| className={`glide ${cssClass}`} | ||
| id={instanceName} | ||
| style={{ width, maxWidth }} | ||
| data-remove-side-background={removeSideBackground} | ||
| data-image-auto-size={imageAutoSize} | ||
| data-has-shadow={hasShadow} | ||
| aria-roledescription="carousel" | ||
| > | ||
| <div className="glide__track" data-glide-el="track"> | ||
| <SliderItems carouselItems={carouselItems} /> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
role="alert" plus an explicit aria-live="polite" on the same element conflicts (alert already implies assertive), and this announces the whole card body on every slide change instead of something short. Looks like #1763 already dropped this exact block for a cleaner aria-hidden/gliderTrack approach, might be easiest to just pull that over instead of patching this in place.