diff --git a/.changeset/giant-donkeys-wink.md b/.changeset/giant-donkeys-wink.md
new file mode 100644
index 0000000000..c8864127ee
--- /dev/null
+++ b/.changeset/giant-donkeys-wink.md
@@ -0,0 +1,5 @@
+---
+"@khanacademy/perseus": patch
+---
+
+[Image] | (a11y) | Add aria-describedby to Explore Image modal
diff --git a/packages/perseus/src/widgets/image/components/explore-image-modal-content.tsx b/packages/perseus/src/widgets/image/components/explore-image-modal-content.tsx
index 9a7004b542..7011f793a2 100644
--- a/packages/perseus/src/widgets/image/components/explore-image-modal-content.tsx
+++ b/packages/perseus/src/widgets/image/components/explore-image-modal-content.tsx
@@ -16,7 +16,12 @@ import type {CommonImageProps, ZoomProps, GifProps} from "./image-info-area";
const MODAL_HEIGHT = 568;
-type Props = CommonImageProps & ZoomProps & GifProps;
+type Props = CommonImageProps &
+ ZoomProps &
+ GifProps & {
+ captionId: string;
+ longDescId: string;
+ };
export default function ExploreImageModalContent({
backgroundImage,
@@ -30,6 +35,8 @@ export default function ExploreImageModalContent({
labels,
range,
zoomSize,
+ captionId,
+ longDescId,
}: Props) {
const [isGifPlaying, setIsGifPlaying] = React.useState(false);
const context = React.useContext(PerseusI18nContext);
@@ -146,7 +153,10 @@ export default function ExploreImageModalContent({
)}
{caption && (
-
+
{/* Use Renderer so that the caption can support markdown and TeX. */}
{/* Use Renderer so that the description can support markdown and TeX. */}
-
+
+
+
);
diff --git a/packages/perseus/src/widgets/image/components/explore-image-modal.test.tsx b/packages/perseus/src/widgets/image/components/explore-image-modal.test.tsx
index 78c4d06d98..0b1c2b4b74 100644
--- a/packages/perseus/src/widgets/image/components/explore-image-modal.test.tsx
+++ b/packages/perseus/src/widgets/image/components/explore-image-modal.test.tsx
@@ -234,6 +234,38 @@ describe("ExploreImageModal", () => {
expect(screen.getByText("widget long description")).toBeInTheDocument();
});
+ it("sets the describedby to short and long description IDs if there is a caption", () => {
+ // Arrange, Act
+ renderModal({
+ ...defaultProps,
+ backgroundImage: earthMoonImage,
+ caption: "widget caption",
+ });
+
+ // Assert
+ const dialog = screen.getByRole("dialog");
+ // Regex for "uniqueId-caption uniqueId-long-desc"
+ expect(dialog.getAttribute("aria-describedby")).toMatch(
+ /^:r\w+:-caption :r\w+:-long-desc$/,
+ );
+ });
+
+ it("sets the describedby to long description ID if there is no caption", () => {
+ // Arrange, Act
+ renderModal({
+ ...defaultProps,
+ backgroundImage: earthMoonImage,
+ longDescription: "widget long description",
+ });
+
+ // Assert
+ const dialog = screen.getByRole("dialog");
+ const ariaDescribedBy = dialog.getAttribute("aria-describedby");
+ // Regex for "uniqueId-long-desc"
+ expect(ariaDescribedBy).toMatch(/^:r\w+:-long-desc$/);
+ expect(ariaDescribedBy).not.toMatch(/caption/);
+ });
+
describe("gif controls", () => {
it("should render gif controls if the image is a gif", () => {
// Arrange, Act
diff --git a/packages/perseus/src/widgets/image/components/explore-image-modal.tsx b/packages/perseus/src/widgets/image/components/explore-image-modal.tsx
index 2fc6948af0..5858e66a6b 100644
--- a/packages/perseus/src/widgets/image/components/explore-image-modal.tsx
+++ b/packages/perseus/src/widgets/image/components/explore-image-modal.tsx
@@ -14,6 +14,9 @@ type Props = CommonImageProps & ZoomProps & GifProps;
export const ExploreImageModal = (props: Props) => {
const context = React.useContext(PerseusI18nContext);
+ const uniqueId = React.useId();
+ const captionId = `${uniqueId}-caption`;
+ const longDescId = `${uniqueId}-long-desc`;
const titleText = props.title || context.strings.imageAlternativeTitle;
const title = (
@@ -39,7 +42,16 @@ export const ExploreImageModal = (props: Props) => {
>
}
+ content={
+
+ }
+ aria-describedby={
+ props.caption ? `${captionId} ${longDescId}` : longDescId
+ }
styles={{
root: wbStyles.root,
}}