diff --git a/packages/unity-bootstrap-theme/src/js/card-bodies.js b/packages/unity-bootstrap-theme/src/js/card-bodies.js index 685379c5db..55058a7c6f 100644 --- a/packages/unity-bootstrap-theme/src/js/card-bodies.js +++ b/packages/unity-bootstrap-theme/src/js/card-bodies.js @@ -25,7 +25,7 @@ function initCardBodies() { cardBodies.forEach((cardBody, index) => { const paragraph = cardBody.querySelector("div p"); - const originalText = paragraph.textContent; + const originalText = paragraph?.textContent; const style = window.getComputedStyle(cardBody); // Get the number of lines allowed (line clamp), usually set via CSS like -webkit-line-clamp @@ -44,7 +44,7 @@ function initCardBodies() { const maxHeight = lineClamp * actualLineHeight; // Check if the paragraph exceeds the maximum allowed height - if (paragraph.offsetHeight >= maxHeight) { + if (paragraph?.offsetHeight >= maxHeight) { let visibleText = ""; const words = originalText.split(" "); let visibleWordCount = 0; diff --git a/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/components/BulletItems.jsx b/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/components/BulletItems.jsx index 83216dce94..0689796952 100644 --- a/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/components/BulletItems.jsx +++ b/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/components/BulletItems.jsx @@ -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}`} /> ); @@ -99,7 +99,7 @@ const ImageBulletItems = ({ imageItems, onItemClick = () => null }) => { decoding="async" // @ts-ignore - fetchpriority="low" + fetchPriority="low" /> )); diff --git a/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/components/SliderItems.jsx b/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/components/SliderItems.jsx index f4d5093923..47cedbaf0c 100644 --- a/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/components/SliderItems.jsx +++ b/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/components/SliderItems.jsx @@ -12,8 +12,12 @@ import React from "react"; */ const SliderItems = ({ carouselItems }) => { // Setup carousel items from the carouselItems prop. - const listItems = carouselItems.map(sliderItem => ( -
  • + const listItems = carouselItems.map((sliderItem, index) => ( +
  • +
    + {`Slide ${index + 1}`} + {/* {`Slide ${index + 1} of ${carouselItems.length}`} length already announced */} +
    {sliderItem.item}
  • )); diff --git a/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/glide/glide.setup.js b/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/glide/glide.setup.js index fb24390023..a18cc9e8ca 100644 --- a/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/glide/glide.setup.js +++ b/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/glide/glide.setup.js @@ -59,7 +59,7 @@ function buildConfig(perView = 1, isFullWidth, hasPeek = true, isDraggable) { return { type: "slider", // No wrap-around. focusAt: 0, - bound: true, // Only if type slider with focusAt 0 + bound: false, // Only if type slider with focusAt 0 // Set to false for accessibility (all cards should be accessible) rewind: false, // Only if type slider gap, // Space between slides... may be impacted by viewport size. // `keyboard` Left/Right arrow key support for slides - true is default. @@ -142,9 +142,9 @@ 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. @@ -152,9 +152,9 @@ function setNavButtonGradient(gliderElement, currentIndex, buttonCount) { 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"); } } @@ -307,6 +307,108 @@ function setupCaroarousel({ // 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); + } + } + + let firstActiveCard = 1; + let finalActiveCard = 1; + let totalActiveCards = 0; + let totalCards = slides.length; + let getActiveState = 0; // get first active card + + for (let i = 0; i < slides.length; i++) { + const ariaHidden = slides[i].getAttribute("aria-hidden"); + if (getActiveState === 1) { + if (i === slides.length - 1) { + if (ariaHidden === "false") { + finalActiveCard = i + 1; + totalActiveCards = finalActiveCard - firstActiveCard + 1; + getActiveState = 2; // done + break; + } + } + if (ariaHidden === "true") { + finalActiveCard = i; + totalActiveCards = finalActiveCard - firstActiveCard + 1; + getActiveState = 2; // done + break; + } + } + if (getActiveState === 0) { + if (ariaHidden === "false") { + firstActiveCard = i + 1; + getActiveState = 1; // get active card count + if (i === slides.length - 1) { + finalActiveCard = firstActiveCard; + totalActiveCards = 1; + getActiveState = 2; // done + } + } + } + + 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}`); + } + } + + if (header) { + let allTexts = []; + getText(slides[i], allTexts); + gliderElement.setAttribute("aria-labelledby", allTexts[0]); + gliderElement.removeAttribute("aria-label"); + } else { + gliderElement.setAttribute("aria-label", `Card ${i + 1}`); + gliderElement.removeAttribute("aria-labelledby"); + } + + } + } + + let gliderTrack = gliderElement.querySelector(`.glide__track`); + gliderTrack?.setAttribute("tabIndex", "0"); + + if (firstActiveCard === finalActiveCard) { + gliderTrack?.setAttribute( + "aria-label", + `Carousel, showing item ${firstActiveCard} of ${totalCards}` + ); + } else { + gliderTrack?.setAttribute( + "aria-label", + `Carousel, showing items ${firstActiveCard} to ${finalActiveCard} of ${totalCards}` + // `${totalActiveCards} item carousel, showing items ${firstActiveCard} to ${finalActiveCard} of ${totalCards}` + ); + } + gliderTrack?.setAttribute("aria-live", "assertive"); + + // Update bullet accessibility + const bullets = gliderElement.querySelectorAll(".glide__bullet"); + 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); diff --git a/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/helper/width-calculator.js b/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/helper/width-calculator.js index b8f13ede76..282446b62b 100644 --- a/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/helper/width-calculator.js +++ b/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/helper/width-calculator.js @@ -26,12 +26,13 @@ function calcualteViewItems(itemCount, perView) { buttonCount = itemCount; if (vw >= 992) { // Value for lg breakpoint - if (perView >= 2) { - buttonCount = itemCount - 1; - } - if (perView >= 3) { - buttonCount = itemCount - 2; - } + // Removed for accessibility (arrow keys should not skip cards) + // if (perView >= 2) { + // buttonCount = itemCount - 1; + // } + // if (perView >= 3) { + // buttonCount = itemCount - 2; + // } } return buttonCount; diff --git a/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/index.jsx b/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/index.jsx index f03afcb3e1..73fbf4fd3b 100644 --- a/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/index.jsx +++ b/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/index.jsx @@ -79,14 +79,18 @@ const BaseCarousel = ({ return (
    diff --git a/packages/unity-react-core/src/components/Image/Image.jsx b/packages/unity-react-core/src/components/Image/Image.jsx index 485094821c..9b65834cae 100644 --- a/packages/unity-react-core/src/components/Image/Image.jsx +++ b/packages/unity-react-core/src/components/Image/Image.jsx @@ -40,7 +40,7 @@ export const Image = ({ alt, loading, decoding, - fetchpriority: fetchPriority, // React attribute bug workaround + fetchPriority: fetchPriority, // React attribute bug workaround ...(cssClasses?.length > 0 && { className: spreadClasses(cssClasses) }), ...(dataTestId && { "data-testid": dataTestId }), ...(width && { width }),