From 0100b6e081eac0e92c36d6ceacc8a4af876d26b8 Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:01:43 +0200 Subject: [PATCH 01/33] fix(a11y): identify the slide description --- dev/slides.ts | 1 + src/plugins/captions/Description.tsx | 6 ++++-- src/plugins/captions/index.ts | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dev/slides.ts b/dev/slides.ts index 15d0403..ffd3245 100644 --- a/dev/slides.ts +++ b/dev/slides.ts @@ -65,6 +65,7 @@ export const slides = [ src: imageLink(asset, width), width, height, + description: "Description of the image", srcSet: breakpoints.map((breakpoint) => ({ src: imageLink(asset, breakpoint), width: breakpoint, diff --git a/src/plugins/captions/Description.tsx b/src/plugins/captions/Description.tsx index 497c42f..5369479 100644 --- a/src/plugins/captions/Description.tsx +++ b/src/plugins/captions/Description.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { clsx, cssVar, Slide, useLightboxProps } from "../../index.js"; +import { clsx, cssVar, label as translateLabel, Slide, useLightboxProps } from "../../index.js"; import { cssPrefix } from "./utils.js"; import { defaultCaptionsProps, useCaptionsProps } from "./props.js"; import { useCaptions } from "./CaptionsContext.js"; @@ -9,7 +9,7 @@ export type DescriptionProps = Pick; export function Description({ description }: DescriptionProps) { const { descriptionTextAlign, descriptionMaxLines } = useCaptionsProps(); - const { styles } = useLightboxProps(); + const { styles, labels } = useLightboxProps(); const { visible } = useCaptions(); if (!visible) return null; @@ -32,6 +32,8 @@ export function Description({ description }: DescriptionProps) { : null), ...styles.captionsDescription, }} + role="paragraph" + aria-roledescription={translateLabel(labels, "Caption")} > {typeof description === "string" ? description.split("\n").flatMap((line, index) => [...(index > 0 ? [
] : []), line]) diff --git a/src/plugins/captions/index.ts b/src/plugins/captions/index.ts index eb39733..7758b0f 100644 --- a/src/plugins/captions/index.ts +++ b/src/plugins/captions/index.ts @@ -57,6 +57,7 @@ declare module "../../types.js" { } interface Labels { + Caption?: string; "Show captions"?: string; "Hide captions"?: string; } From 5d1e5add6e3ab3677798912835702e5804109c96 Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:16:54 +0200 Subject: [PATCH 02/33] fix(a11y): allow translation of roles descriptions --- src/modules/Carousel.tsx | 4 +++- src/modules/Controller/Controller.tsx | 7 +++++-- src/modules/Portal.tsx | 6 +++--- src/types.ts | 3 +++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/modules/Carousel.tsx b/src/modules/Carousel.tsx index c021093..7a4f36d 100644 --- a/src/modules/Carousel.tsx +++ b/src/modules/Carousel.tsx @@ -12,6 +12,7 @@ import { getSlideKey, hasSlides, isImageSlide, + label as translateLabel, makeInertWhen, parseLengthPercentage, } from "../utils.js"; @@ -43,6 +44,7 @@ function CarouselSlide({ slide, offset }: CarouselSlideProps) { carousel: { imageFit, imageProps }, on: { click: onClick }, styles: { slide: style }, + labels, } = useLightboxProps(); const { getOwnerDocument } = useDocumentContext(); @@ -91,7 +93,7 @@ function CarouselSlide({ slide, offset }: CarouselSlideProps) { {...makeInertWhen(offscreen)} style={style} role="region" - aria-roledescription="slide" + aria-roledescription={translateLabel(labels, "Slide")} > {renderSlide()} diff --git a/src/modules/Controller/Controller.tsx b/src/modules/Controller/Controller.tsx index 2239010..be6d6b8 100644 --- a/src/modules/Controller/Controller.tsx +++ b/src/modules/Controller/Controller.tsx @@ -17,6 +17,7 @@ import { computeSlideRect, cssClass, cssVar, + label as translateLabel, makeComposePrefix, makeUseContext, parseLengthPercentage, @@ -56,7 +57,7 @@ export const ControllerContext = React.createContext(); @@ -414,7 +415,9 @@ export function Controller({ children, ...props }: ComponentProps) { ...(controller.touchAction !== "none" ? { [cssVar("controller_touch_action")]: controller.touchAction } : null), ...styles.container, }} - {...(controller.aria ? { role: "region", "aria-live": "polite", "aria-roledescription": "carousel" } : null)} + {...(controller.aria + ? { role: "region", "aria-live": "polite", "aria-roledescription": translateLabel(labels, "Carousel") } + : null)} tabIndex={-1} {...registerSensors} > diff --git a/src/modules/Portal.tsx b/src/modules/Portal.tsx index f63f4e9..b4c552f 100644 --- a/src/modules/Portal.tsx +++ b/src/modules/Portal.tsx @@ -4,7 +4,7 @@ import { createPortal } from "react-dom"; import { ComponentProps } from "../types.js"; import { LightboxDefaultProps } from "../props.js"; import { createModule } from "../config.js"; -import { clsx, composePrefix, cssClass, cssVar, reflow } from "../utils.js"; +import { clsx, composePrefix, cssClass, cssVar, label as translateLabel, reflow } from "../utils.js"; import { useEventCallback, useMotionPreference } from "../hooks/index.js"; import { useEvents, useTimeouts } from "../contexts/index.js"; import { LightboxRoot } from "../components/index.js"; @@ -28,7 +28,7 @@ function setAttribute(element: Element, attribute: string, value: string) { }; } -export function Portal({ children, animation, styles, className, on, portal, close }: ComponentProps) { +export function Portal({ children, animation, styles, className, on, portal, close, labels }: ComponentProps) { const [mounted, setMounted] = React.useState(false); const [visible, setVisible] = React.useState(false); @@ -120,7 +120,7 @@ export function Portal({ children, animation, styles, className, on, portal, clo aria-modal role="dialog" aria-live="polite" - aria-roledescription="lightbox" + aria-roledescription={translateLabel(labels, "Lightbox")} style={{ ...(animation.fade !== LightboxDefaultProps.animation.fade ? { [cssVar("fade_animation_duration")]: `${animationDuration}ms` } diff --git a/src/types.ts b/src/types.ts index 941f181..f18c62b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -384,6 +384,9 @@ export interface Labels { Previous?: string; Next?: string; Close?: string; + Slide?: string; + Carousel?: string; + Lightbox?: string; } export type Label = keyof Labels; From 9d0cdd45c1a00bf6afda908f5c346f5efb092bee Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:41:59 +0200 Subject: [PATCH 03/33] fix(a11y): add slide label --- src/modules/Carousel.tsx | 20 +++++++++++++++----- src/types.ts | 1 + 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/modules/Carousel.tsx b/src/modules/Carousel.tsx index 7a4f36d..1e2fe49 100644 --- a/src/modules/Carousel.tsx +++ b/src/modules/Carousel.tsx @@ -32,12 +32,13 @@ function cssSlidePrefix(value?: string) { type CarouselSlideProps = { slide: Slide; offset: number; + index: number; }; -function CarouselSlide({ slide, offset }: CarouselSlideProps) { +function CarouselSlide({ slide, offset, index }: CarouselSlideProps) { const containerRef = React.useRef(null); - const { currentIndex } = useLightboxState(); + const { currentIndex, slides } = useLightboxState(); const { slideRect, focus } = useController(); const { render, @@ -82,6 +83,10 @@ function CarouselSlide({ slide, offset }: CarouselSlideProps) { ) : null; }; + const slideLabel = translateLabel(labels, "{{index}} / {{slidesLength}}") + .replace("{{index}}", String(index)) + .replace("{{slidesLength}}", String(slides.length)); + return (
{renderSlide()}
@@ -113,7 +119,10 @@ export function Carousel({ carousel }: ComponentProps) { const paddingValue = parseLengthPercentage(carousel.padding); const preload = calculatePreload(carousel, slides, 1); - const items: ({ key: React.Key } & ({ slide: Slide; offset: number } | { slide?: never; offset?: never }))[] = []; + const items: ({ key: React.Key } & ( + | { slide: Slide; offset: number; index: number } + | { slide?: never; offset?: never; index?: number } + ))[] = []; if (hasSlides(slides)) { for (let index = currentIndex - preload; index <= currentIndex + preload; index += 1) { @@ -126,6 +135,7 @@ export function Carousel({ carousel }: ComponentProps) { ? { key: [`${key}`, getSlideKey(slide)].filter(Boolean).join("|"), offset: index - currentIndex, + index, slide, } : { key }, @@ -145,8 +155,8 @@ export function Carousel({ carousel }: ComponentProps) { [`${cssVar(cssPrefix("padding_percent"))}`]: paddingValue.percent || 0, }} > - {items.map(({ key, slide, offset }) => - slide ? : , + {items.map(({ key, slide, offset, index }) => + slide ? : , )} ); diff --git a/src/types.ts b/src/types.ts index f18c62b..f2a90d8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -387,6 +387,7 @@ export interface Labels { Slide?: string; Carousel?: string; Lightbox?: string; + "{{index}} / {{slidesLength}}"?: string; } export type Label = keyof Labels; From 7ce34c0cf78072e32436b7a9117145938cbf2ee3 Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:49:03 +0200 Subject: [PATCH 04/33] fix(a11y): remove aria-live=polite on modal this caused the content of the modal to be announced twice because the inner carousel already has aria-live=polite --- src/modules/Portal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/Portal.tsx b/src/modules/Portal.tsx index b4c552f..ce93031 100644 --- a/src/modules/Portal.tsx +++ b/src/modules/Portal.tsx @@ -119,7 +119,6 @@ export function Portal({ children, animation, styles, className, on, portal, clo )} aria-modal role="dialog" - aria-live="polite" aria-roledescription={translateLabel(labels, "Lightbox")} style={{ ...(animation.fade !== LightboxDefaultProps.animation.fade From 11bb01a93a687024f09b807534168287edc26251 Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:09:22 +0200 Subject: [PATCH 05/33] fix(a11y): always set ARIA attributes on carousel Add aria-label to describe the content of the carousel --- src/modules/Controller/Controller.tsx | 7 ++++--- src/types.ts | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/modules/Controller/Controller.tsx b/src/modules/Controller/Controller.tsx index be6d6b8..607a6d9 100644 --- a/src/modules/Controller/Controller.tsx +++ b/src/modules/Controller/Controller.tsx @@ -415,9 +415,10 @@ export function Controller({ children, ...props }: ComponentProps) { ...(controller.touchAction !== "none" ? { [cssVar("controller_touch_action")]: controller.touchAction } : null), ...styles.container, }} - {...(controller.aria - ? { role: "region", "aria-live": "polite", "aria-roledescription": translateLabel(labels, "Carousel") } - : null)} + role="region" + aria-live="polite" + aria-roledescription={translateLabel(labels, "Carousel")} + aria-label={translateLabel(labels, "Photo gallery")} tabIndex={-1} {...registerSensors} > diff --git a/src/types.ts b/src/types.ts index f2a90d8..b8d531b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -216,7 +216,8 @@ export interface ControllerSettings { // TODO v4: remove /** @deprecated for internal use only */ touchAction: "none" | "pan-y"; - /** if `true`, set ARIA attributes on the controller div */ + // TODO v4: remove + /** @deprecated describe the carousel for AT even if it is contained in a dialog */ aria: boolean; /** if `true`, close the lightbox on pull-up gesture */ closeOnPullUp: boolean; @@ -387,6 +388,7 @@ export interface Labels { Slide?: string; Carousel?: string; Lightbox?: string; + "Photo gallery"?: string; "{{index}} / {{slidesLength}}"?: string; } From c29ca27719f0cfc8f7044e26b1f423b67d9a7340 Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:10:45 +0200 Subject: [PATCH 06/33] fix(a11y): announce the dialog role Let AT announce the dialog role, followed by a label that describes its content --- src/modules/Portal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Portal.tsx b/src/modules/Portal.tsx index ce93031..bc559ab 100644 --- a/src/modules/Portal.tsx +++ b/src/modules/Portal.tsx @@ -119,7 +119,7 @@ export function Portal({ children, animation, styles, className, on, portal, clo )} aria-modal role="dialog" - aria-roledescription={translateLabel(labels, "Lightbox")} + aria-label={translateLabel(labels, "Lightbox")} style={{ ...(animation.fade !== LightboxDefaultProps.animation.fade ? { [cssVar("fade_animation_duration")]: `${animationDuration}ms` } From e57354de2aa948b656bbdceb12eaefb33eec19c0 Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:53:22 +0200 Subject: [PATCH 07/33] fix(a11y): add focus trap on dialog Focus should be contained into the modal until ESC is pressed --- package-lock.json | 40 +++++++++++++++++++++++----- package.json | 3 +++ src/modules/Portal.tsx | 59 ++++++++++++++++++++++-------------------- 3 files changed, 68 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index b67e47d..7b0a0b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "yet-another-react-lightbox", "version": "0.0.0-semantic-release", "license": "MIT", + "dependencies": { + "focus-trap-react": "^11.0.4" + }, "devDependencies": { "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", @@ -2569,7 +2572,6 @@ "version": "19.1.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -2579,7 +2581,6 @@ "version": "19.1.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz", "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -4265,7 +4266,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -5686,6 +5686,31 @@ "dev": true, "license": "ISC" }, + "node_modules/focus-trap": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz", + "integrity": "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==", + "license": "MIT", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/focus-trap-react": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-11.0.4.tgz", + "integrity": "sha512-tC7jC/yqeAqhe4irNIzdyDf9XCtGSeECHiBSYJBO/vIN0asizbKZCt8TarB6/XqIceu42ajQ/U4lQJ9pZlWjrg==", + "license": "MIT", + "dependencies": { + "focus-trap": "^7.6.5", + "tabbable": "^6.2.0" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -9345,7 +9370,6 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9355,7 +9379,6 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.26.0" @@ -9833,7 +9856,6 @@ "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -10585,6 +10607,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", diff --git a/package.json b/package.json index 3c18a0d..3f25a51 100644 --- a/package.json +++ b/package.json @@ -186,6 +186,9 @@ "optional": true } }, + "dependencies": { + "focus-trap-react": "^11.0.4" + }, "devDependencies": { "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", diff --git a/src/modules/Portal.tsx b/src/modules/Portal.tsx index bc559ab..f1f1413 100644 --- a/src/modules/Portal.tsx +++ b/src/modules/Portal.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { createPortal } from "react-dom"; +import { FocusTrap } from "focus-trap-react"; import { ComponentProps } from "../types.js"; import { LightboxDefaultProps } from "../props.js"; @@ -109,34 +110,36 @@ export function Portal({ children, animation, styles, className, on, portal, clo return mounted ? createPortal( - { - if (!restoreFocus.current) { - restoreFocus.current = event.relatedTarget; - } - }} - > - {children} - , + + { + if (!restoreFocus.current) { + restoreFocus.current = event.relatedTarget; + } + }} + > + {children} + + , portal.root || document.body, ) : null; From dde8733650df097059920cf73432eb1c5841580d Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:28:35 +0200 Subject: [PATCH 08/33] chore: add focus-trap-react to external deps --- rollup.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollup.config.js b/rollup.config.js index da51606..e790849 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -31,7 +31,7 @@ const config = { minifyInternalExports: false, }, ], - external: ["react", "react-dom"], + external: ["react", "react-dom", "focus-trap-react"], preserveEntrySignatures: "allow-extension", treeshake: false, }; From 57ea627bd089e459e7f147ef322c69f46d8dd4be Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:28:53 +0200 Subject: [PATCH 09/33] fix: tests --- src/modules/Portal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Portal.tsx b/src/modules/Portal.tsx index f1f1413..a136ef2 100644 --- a/src/modules/Portal.tsx +++ b/src/modules/Portal.tsx @@ -110,7 +110,7 @@ export function Portal({ children, animation, styles, className, on, portal, clo return mounted ? createPortal( - + Date: Fri, 6 Jun 2025 10:26:33 +0200 Subject: [PATCH 10/33] fix(a11y): hide counter for assistive technologies --- src/plugins/counter/Counter.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plugins/counter/Counter.tsx b/src/plugins/counter/Counter.tsx index 2bc77cd..512e194 100644 --- a/src/plugins/counter/Counter.tsx +++ b/src/plugins/counter/Counter.tsx @@ -26,7 +26,12 @@ export function CounterComponent({ counter }: ComponentProps) { if (slides.length === 0) return null; return ( -
+ ); From 64b3450a9bab3cde01c37fe484cc7718e65f5dbe Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:17:09 +0200 Subject: [PATCH 11/33] fix(a11y): remove focus trap main wrapper is already set up to inert --- package-lock.json | 40 +++++----------------------- package.json | 3 --- rollup.config.js | 2 +- src/modules/Portal.tsx | 59 ++++++++++++++++++++---------------------- 4 files changed, 35 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7b0a0b8..b67e47d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "yet-another-react-lightbox", "version": "0.0.0-semantic-release", "license": "MIT", - "dependencies": { - "focus-trap-react": "^11.0.4" - }, "devDependencies": { "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", @@ -2572,6 +2569,7 @@ "version": "19.1.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -2581,6 +2579,7 @@ "version": "19.1.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz", "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -4266,6 +4265,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -5686,31 +5686,6 @@ "dev": true, "license": "ISC" }, - "node_modules/focus-trap": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz", - "integrity": "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==", - "license": "MIT", - "dependencies": { - "tabbable": "^6.2.0" - } - }, - "node_modules/focus-trap-react": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-11.0.4.tgz", - "integrity": "sha512-tC7jC/yqeAqhe4irNIzdyDf9XCtGSeECHiBSYJBO/vIN0asizbKZCt8TarB6/XqIceu42ajQ/U4lQJ9pZlWjrg==", - "license": "MIT", - "dependencies": { - "focus-trap": "^7.6.5", - "tabbable": "^6.2.0" - }, - "peerDependencies": { - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -9370,6 +9345,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9379,6 +9355,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.26.0" @@ -9856,6 +9833,7 @@ "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -10607,12 +10585,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", - "license": "MIT" - }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", diff --git a/package.json b/package.json index 3f25a51..3c18a0d 100644 --- a/package.json +++ b/package.json @@ -186,9 +186,6 @@ "optional": true } }, - "dependencies": { - "focus-trap-react": "^11.0.4" - }, "devDependencies": { "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", diff --git a/rollup.config.js b/rollup.config.js index e790849..da51606 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -31,7 +31,7 @@ const config = { minifyInternalExports: false, }, ], - external: ["react", "react-dom", "focus-trap-react"], + external: ["react", "react-dom"], preserveEntrySignatures: "allow-extension", treeshake: false, }; diff --git a/src/modules/Portal.tsx b/src/modules/Portal.tsx index a136ef2..bc559ab 100644 --- a/src/modules/Portal.tsx +++ b/src/modules/Portal.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import { createPortal } from "react-dom"; -import { FocusTrap } from "focus-trap-react"; import { ComponentProps } from "../types.js"; import { LightboxDefaultProps } from "../props.js"; @@ -110,36 +109,34 @@ export function Portal({ children, animation, styles, className, on, portal, clo return mounted ? createPortal( - - { - if (!restoreFocus.current) { - restoreFocus.current = event.relatedTarget; - } - }} - > - {children} - - , + { + if (!restoreFocus.current) { + restoreFocus.current = event.relatedTarget; + } + }} + > + {children} + , portal.root || document.body, ) : null; From 3623f4bf2d924e2e078ee77201c7ea2995d13801 Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:22:27 +0200 Subject: [PATCH 12/33] fix(a11y): announce that the button opens a dialog --- dev/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/App.tsx b/dev/App.tsx index 4b65b32..37f94b4 100644 --- a/dev/App.tsx +++ b/dev/App.tsx @@ -30,7 +30,7 @@ export default function App() { plugins={[Captions, Counter, Download, Share, Fullscreen, Slideshow, Thumbnails, Video, Zoom]} /> - From 8f7df8bdd91fe995955b9a57ff88609f05c6512a Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:28:53 +0200 Subject: [PATCH 13/33] fix(a11y): use correct slide index in ariaLabel --- src/modules/Carousel.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/Carousel.tsx b/src/modules/Carousel.tsx index 1e2fe49..2498862 100644 --- a/src/modules/Carousel.tsx +++ b/src/modules/Carousel.tsx @@ -15,6 +15,7 @@ import { label as translateLabel, makeInertWhen, parseLengthPercentage, + getSlideIndex, } from "../utils.js"; import { ImageSlide } from "../components/index.js"; import { useController } from "./Controller/index.js"; @@ -84,7 +85,7 @@ function CarouselSlide({ slide, offset, index }: CarouselSlideProps) { }; const slideLabel = translateLabel(labels, "{{index}} / {{slidesLength}}") - .replace("{{index}}", String(index)) + .replace("{{index}}", String(index + 1)) .replace("{{slidesLength}}", String(slides.length)); return ( @@ -135,7 +136,7 @@ export function Carousel({ carousel }: ComponentProps) { ? { key: [`${key}`, getSlideKey(slide)].filter(Boolean).join("|"), offset: index - currentIndex, - index, + index: getSlideIndex(index, slides.length), slide, } : { key }, From 154d66da9044db07bbfa0140d9b2086fe8c91b45 Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:01:14 +0200 Subject: [PATCH 14/33] fix(a11y): set title as heading --- src/plugins/captions/Title.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/captions/Title.tsx b/src/plugins/captions/Title.tsx index b6a03dc..b285fd0 100644 --- a/src/plugins/captions/Title.tsx +++ b/src/plugins/captions/Title.tsx @@ -15,6 +15,8 @@ export function Title({ title }: TitleProps) { return (
From ac7d92c6c529afe5fc484a044a7a8c948ce1b526 Mon Sep 17 00:00:00 2001 From: ubermanu <1533514+ubermanu@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:02:30 +0200 Subject: [PATCH 15/33] fix(a11y): update thumbnails --- src/plugins/thumbnails/Thumbnail.tsx | 23 ++++++++++++++++++++-- src/plugins/thumbnails/ThumbnailsTrack.tsx | 7 ++++++- src/plugins/thumbnails/index.ts | 1 + 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/plugins/thumbnails/Thumbnail.tsx b/src/plugins/thumbnails/Thumbnail.tsx index 9b05960..d1ee66c 100644 --- a/src/plugins/thumbnails/Thumbnail.tsx +++ b/src/plugins/thumbnails/Thumbnail.tsx @@ -9,12 +9,14 @@ import { ELEMENT_ICON, ImageSlide, isImageSlide, + label as translateLabel, makeComposePrefix, RenderThumbnailProps, Slide, useDocumentContext, useEventCallback, useLightboxProps, + useLightboxState, } from "../../index.js"; import { cssThumbnailPrefix } from "./utils.js"; import { useThumbnailsProps } from "./props.js"; @@ -75,6 +77,7 @@ export type FadeSettings = { export type ThumbnailProps = { slide: Slide | null; + index: number; onClick: () => void; active: boolean; fadeIn?: FadeSettings; @@ -83,9 +86,19 @@ export type ThumbnailProps = { onLoseFocus: () => void; }; -export function Thumbnail({ slide, onClick, active, fadeIn, fadeOut, placeholder, onLoseFocus }: ThumbnailProps) { +export function Thumbnail({ + slide, + index, + onClick, + active, + fadeIn, + fadeOut, + placeholder, + onLoseFocus, +}: ThumbnailProps) { const ref = React.useRef(null); - const { render, styles } = useLightboxProps(); + const { render, styles, labels } = useLightboxProps(); + const { slides, currentIndex } = useLightboxState(); const { getOwnerDocument } = useDocumentContext(); const { width, height, imageFit } = useThumbnailsProps(); const rect = { width, height }; @@ -98,6 +111,10 @@ export function Thumbnail({ slide, onClick, active, fadeIn, fadeOut, placeholder } }, [fadeOut, onLoseFocusCallback, getOwnerDocument]); + const thumbnailLabel = translateLabel(labels, "{{index}} / {{slidesLength}}") + .replace("{{index}}", String(index + 1)) + .replace("{{slidesLength}}", String(slides.length)); + return ( diff --git a/src/plugins/thumbnails/ThumbnailsTrack.tsx b/src/plugins/thumbnails/ThumbnailsTrack.tsx index f2ef5db..d509de1 100644 --- a/src/plugins/thumbnails/ThumbnailsTrack.tsx +++ b/src/plugins/thumbnails/ThumbnailsTrack.tsx @@ -13,6 +13,7 @@ import { getSlide, getSlideKey, hasSlides, + label as translateLabel, Slide, useAnimation, useEventCallback, @@ -22,6 +23,7 @@ import { useLightboxState, useRTL, useSensors, + getSlideIndex, } from "../../index.js"; import { cssPrefix, cssThumbnailPrefix } from "./utils.js"; import { Thumbnail } from "./Thumbnail.js"; @@ -59,7 +61,7 @@ export function ThumbnailsTrack({ visible, containerRef }: ThumbnailsTrackProps) const isRTL = useRTL(); const { publish, subscribe } = useEvents(); - const { carousel, styles } = useLightboxProps(); + const { carousel, styles, labels } = useLightboxProps(); const { slides, globalIndex, animation } = useLightboxState(); const { registerSensors, subscribeSensors } = useSensors(); @@ -159,8 +161,10 @@ export function ThumbnailsTrack({ visible, containerRef }: ThumbnailsTrackProps) >