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..db7adfabdd 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 @@ -21,6 +21,7 @@ const defaultGAEvent = { const BaseBulletItemContainer = ({ children }) => (
{ type="button" className="glide__bullet" data-glide-dir={`=${i}`} - aria-label={`Slide view ${i + 1}`} + aria-label={`Go to slide ${i + 1} of ${buttonCount}`} /> ); @@ -97,9 +98,7 @@ const ImageBulletItems = ({ imageItems, onItemClick = () => null }) => { className="glide__bullet bullet-image" loading="lazy" 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..0099ac1eb1 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,16 @@ 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..2fcb899d2a 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 with a keyboard) 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"); } } @@ -183,11 +183,13 @@ function updateNonVisibleSlides(gliderElement, currentIndex, perView) { const slides = gliderElement.querySelectorAll(".glide__slide"); // Get first visible slide const startVisibleIndex = currentIndex; + // Get last visible slide - const endVisibleIndex = Math.min( - slides.length - 1, - currentIndex + perView - 1 - ); + // const endVisibleIndex = Math.min( + // slides.length - 1, + // currentIndex + perView - 1 + // ); + const endVisibleIndex = currentIndex; // Don't skip to the last visible card when navigating to the right slides.forEach((slide, index) => { if (index < startVisibleIndex || index > endVisibleIndex) { @@ -307,6 +309,107 @@ 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/index.jsx b/packages/unity-react-core/src/components/ComponentCarousel/core/components/BaseCarousel/index.jsx index f03afcb3e1..dc6fe04276 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 @@ -1,12 +1,11 @@ // @ts-check import PropTypes from "prop-types"; -import React, { useEffect } from "react"; +import React, { useId, useEffect } from "react"; // Include required and custom styles for @glidejs/glide import "./styles.scss"; import { SliderItems, BulletItems, NavButtons } from "./components"; import { setupCaroarousel } from "./glide/glide.setup"; -import { calcualteViewItems } from "./helper/width-calculator"; // Requirement: We import bs4-theme css from QA site in preview-head.html. // Initially based on this approach: @@ -54,8 +53,8 @@ const BaseCarousel = ({ // Get glide instance class name. // Defaults to glide. If implementing multiple instnaces, you MUST provide // an unique instance name for all but one instance. - const instanceName = `glide-${Math.ceil(Math.random() * 10000)}`; - const buttonCount = calcualteViewItems(carouselItems.length, perView); + const instanceName = `glide-${useId()}`; + const buttonCount = carouselItems.length; useEffect(() => { setupCaroarousel({ @@ -79,14 +78,15 @@ const BaseCarousel = ({ return (
    @@ -126,4 +126,4 @@ BaseCarousel.propTypes = { hasShadow: PropTypes.bool, }; -export { BaseCarousel, calcualteViewItems }; +export { BaseCarousel }; 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 }),