Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const BulletItems = ({ buttonCount }) => {
type="button"
className="glide__bullet"
data-glide-dir={`=${i}`}
aria-label={`Slide view ${i + 1}`}
aria-label={`Go to slide ${i+1} of ${buttonCount}`}
/>
</GaEventWrapper>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
};
}

Expand Down Expand Up @@ -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.
Expand All @@ -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");
}
}

Expand Down Expand Up @@ -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");

@davidornelas11 davidornelas11 Jul 31, 2026

Copy link
Copy Markdown
Contributor

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.

}


if (header) {
let allTexts = []
getText(slides[i], allTexts)
gliderElement.setAttribute("aria-labelledby", allTexts[0]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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}`);

@davidornelas11 davidornelas11 Jul 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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");

@davidornelas11 davidornelas11 Jul 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up, for the image gallery bullets .glide__bullet matches the <img>, not the <button role="option"> wrapping it, so disabled/aria-current won't actually do anything there.

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,18 @@ const BaseCarousel = ({

return (
<div
role={role}
aria-labelledby={ariaLabelledBy}
// role={role}
// role="group"
role="region"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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} />
Expand Down