Skip to content

Commit 6864de8

Browse files
nperez0111Ovgoddclaude
committed
fix(a11y): use figure/figcaption for media block captions
Closes #2055. Supersedes #2056. When a file/image/video/audio block has a caption, render the wrapper as <figure> with a <figcaption> instead of a <div>+<p>. This matches the WCAG-recommended semantic for caption-content association and removes the need for ad-hoc ARIA fallbacks. Image alt text logic is also tightened: - caption present -> alt="" (the figcaption is the accessible name; this avoids screen readers double-announcing the caption) - no caption, name present -> alt={name} - neither -> alt="" (decorative; aria-hidden was dropped because it would have removed unintentionally-unlabeled images from the accessibility tree entirely) Co-Authored-By: Cyril G <c.gromoff@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a77c887 commit 6864de8

37 files changed

Lines changed: 125 additions & 100 deletions

File tree

packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ export const createFileBlockWrapper = (
2626
element?: { dom: HTMLElement; destroy?: () => void },
2727
buttonIcon?: HTMLElement,
2828
) => {
29-
const wrapper = document.createElement("div");
29+
// Use a <figure>/<figcaption> when the block has a caption, so the caption
30+
// is semantically associated with its content for assistive tech. Falls back
31+
// to a plain <div> when there is no caption (or the file has not been
32+
// uploaded yet, since the upload UI never shows the caption).
33+
const useFigure = block.props.url !== "" && !!block.props.caption;
34+
const wrapper = document.createElement(useFigure ? "figure" : "div");
3035
wrapper.className = "bn-file-block-content-wrapper";
3136

3237
// Show the add file button if the file has not been uploaded yet. Change to
@@ -73,7 +78,7 @@ export const createFileBlockWrapper = (
7378

7479
// Show the caption if there is one.
7580
if (block.props.caption) {
76-
const caption = document.createElement("p");
81+
const caption = document.createElement("figcaption");
7782
caption.className = "bn-file-caption";
7883
caption.textContent = block.props.caption;
7984
wrapper.appendChild(caption);

packages/core/src/blocks/Image/block.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,11 @@ export const imageRender =
116116
image.src = block.props.url;
117117
}
118118

119-
image.alt = block.props.name || block.props.caption || "BlockNote image";
119+
// When a caption is set, the wrapper renders a <figcaption> that serves as
120+
// the image's accessible name, so an empty alt avoids double-announcement.
121+
// Otherwise prefer the file name; fall back to "" (decorative) when neither
122+
// is provided.
123+
image.alt = block.props.caption ? "" : block.props.name || "";
120124
image.contentEditable = "false";
121125
image.draggable = false;
122126
imageWrapper.appendChild(image);
@@ -153,7 +157,10 @@ export const imageToExternalHTML =
153157
if (block.props.showPreview) {
154158
image = document.createElement("img");
155159
image.src = block.props.url;
156-
image.alt = block.props.name || block.props.caption || "BlockNote image";
160+
// When a caption is set, createFigureWithCaption wraps the image in a
161+
// <figure>/<figcaption> below, which serves as the accessible name.
162+
// An empty alt avoids screen readers announcing the caption twice.
163+
image.alt = block.props.caption ? "" : block.props.name || "";
157164
if (block.props.previewWidth) {
158165
image.width = block.props.previewWidth;
159166
}

packages/react/src/blocks/File/helpers/render/FileBlockWrapper.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,14 @@ export const FileBlockWrapper = (
2929
) => {
3030
const showLoader = useUploadLoading(props.block.id);
3131

32+
// Use a <figure>/<figcaption> when the block has a caption, so the caption
33+
// is semantically associated with its content for assistive tech.
34+
const useFigure =
35+
props.block.props.url !== "" && !!props.block.props.caption && !showLoader;
36+
const Wrapper = useFigure ? "figure" : "div";
37+
3238
return (
33-
<div
39+
<Wrapper
3440
className={"bn-file-block-content-wrapper"}
3541
onMouseEnter={props.onMouseEnter}
3642
onMouseLeave={props.onMouseLeave}
@@ -54,10 +60,12 @@ export const FileBlockWrapper = (
5460
)}
5561
{props.block.props.caption && (
5662
// Show the caption if there is one.
57-
<p className={"bn-file-caption"}>{props.block.props.caption}</p>
63+
<figcaption className={"bn-file-caption"}>
64+
{props.block.props.caption}
65+
</figcaption>
5866
)}
5967
</>
6068
)}
61-
</div>
69+
</Wrapper>
6270
);
6371
};

packages/react/src/blocks/Image/block.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export const ImagePreview = (
1818
) => {
1919
const resolved = useResolveUrl(props.block.props.url!);
2020

21+
// When a caption is set, FileBlockWrapper renders a <figcaption> that serves
22+
// as the image's accessible name; an empty alt avoids double-announcement.
23+
const alt = props.block.props.caption ? "" : props.block.props.name || "";
24+
2125
return (
2226
<img
2327
className={"bn-visual-media"}
@@ -26,7 +30,7 @@ export const ImagePreview = (
2630
? props.block.props.url
2731
: resolved.downloadUrl
2832
}
29-
alt={props.block.props.caption || "BlockNote image"}
33+
alt={alt}
3034
contentEditable={false}
3135
draggable={false}
3236
/>
@@ -43,12 +47,13 @@ export const ImageToExternalHTML = (
4347
return <p>Add image</p>;
4448
}
4549

50+
// When a caption is set, the image is wrapped in <figure>/<figcaption>
51+
// below; an empty alt avoids double-announcement of the caption text.
52+
const alt = props.block.props.caption ? "" : props.block.props.name || "";
4653
const image = props.block.props.showPreview ? (
4754
<img
4855
src={props.block.props.url}
49-
alt={
50-
props.block.props.name || props.block.props.caption || "BlockNote image"
51-
}
56+
alt={alt}
5257
width={props.block.props.previewWidth}
5358
/>
5459
) : (

tests/src/unit/core/clipboard/copy/__snapshots__/text/html/basicBlocksWithProps.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ <h2 data-level="2">Heading 1</h2>
5454
>
5555
<img
5656
src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Placeholder_view_vector.svg/1280px-Placeholder_view_vector.svg.png"
57-
alt="1280px-Placeholder_view_vector.svg.png"
57+
alt=""
5858
width="256"
5959
/>
6060
<figcaption>Placeholder</figcaption>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<img
22
src="https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg"
3-
alt="BlockNote image"
3+
alt=""
44
data-url="https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg"
55
/>

tests/src/unit/core/clipboard/copy/__snapshots__/text/html/nestedImage.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<p>Paragraph 1</p>
22
<img
33
src="https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg"
4-
alt="BlockNote image"
4+
alt=""
55
data-url="https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg"
66
data-nesting-level="1"
77
/>

tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/basic.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
data-caption="Caption"
1010
data-file-block=""
1111
>
12-
<div class="bn-file-block-content-wrapper">
12+
<figure class="bn-file-block-content-wrapper">
1313
<div class="bn-file-name-with-icon">
1414
<div class="bn-file-icon">
1515
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
@@ -20,8 +20,8 @@
2020
</div>
2121
<p class="bn-file-name">example</p>
2222
</div>
23-
<p class="bn-file-caption">Caption</p>
24-
</div>
23+
<figcaption class="bn-file-caption">Caption</figcaption>
24+
</figure>
2525
</div>
2626
</div>
2727
</div>

tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/nested.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
data-caption="Caption"
1010
data-file-block=""
1111
>
12-
<div class="bn-file-block-content-wrapper">
12+
<figure class="bn-file-block-content-wrapper">
1313
<div class="bn-file-name-with-icon">
1414
<div class="bn-file-icon">
1515
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
@@ -20,8 +20,8 @@
2020
</div>
2121
<p class="bn-file-name">example</p>
2222
</div>
23-
<p class="bn-file-caption">Caption</p>
24-
</div>
23+
<figcaption class="bn-file-caption">Caption</figcaption>
24+
</figure>
2525
</div>
2626
<div class="bn-block-group" data-node-type="blockGroup">
2727
<div class="bn-block-outer" data-node-type="blockOuter" data-id="2">
@@ -34,7 +34,7 @@
3434
data-caption="Caption"
3535
data-file-block=""
3636
>
37-
<div class="bn-file-block-content-wrapper">
37+
<figure class="bn-file-block-content-wrapper">
3838
<div class="bn-file-name-with-icon">
3939
<div class="bn-file-icon">
4040
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
@@ -45,8 +45,8 @@
4545
</div>
4646
<p class="bn-file-name">example</p>
4747
</div>
48-
<p class="bn-file-caption">Caption</p>
49-
</div>
48+
<figcaption class="bn-file-caption">Caption</figcaption>
49+
</figure>
5050
</div>
5151
</div>
5252
</div>

tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/noName.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
data-caption="Caption"
99
data-file-block=""
1010
>
11-
<div class="bn-file-block-content-wrapper">
11+
<figure class="bn-file-block-content-wrapper">
1212
<div class="bn-file-name-with-icon">
1313
<div class="bn-file-icon">
1414
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
@@ -19,8 +19,8 @@
1919
</div>
2020
<p class="bn-file-name"></p>
2121
</div>
22-
<p class="bn-file-caption">Caption</p>
23-
</div>
22+
<figcaption class="bn-file-caption">Caption</figcaption>
23+
</figure>
2424
</div>
2525
</div>
2626
</div>

0 commit comments

Comments
 (0)