Skip to content

Commit 9b441be

Browse files
nperez0111Ovgodd
andauthored
fix(a11y): use figure/figcaption for media block captions (#2717)
Co-authored-by: Cyril G <c.gromoff@gmail.com>
1 parent 377f25c commit 9b441be

33 files changed

Lines changed: 111 additions & 93 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: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ export const imageRender =
116116
image.src = block.props.url;
117117
}
118118

119-
image.alt = block.props.name || block.props.caption || "BlockNote image";
119+
// alt describes image content (per WCAG H86); figcaption (when present)
120+
// is the contextual caption. Fall back to "" so unlabelled images are
121+
// marked decorative rather than getting a noisy generic fallback.
122+
image.alt = block.props.name || "";
120123
image.contentEditable = "false";
121124
image.draggable = false;
122125
imageWrapper.appendChild(image);
@@ -150,7 +153,7 @@ export const imageToExternalHTML =
150153
if (block.props.showPreview) {
151154
image = document.createElement("img");
152155
image.src = block.props.url;
153-
image.alt = block.props.name || block.props.caption || "BlockNote image";
156+
image.alt = block.props.name || "";
154157
if (block.props.previewWidth) {
155158
image.width = block.props.previewWidth;
156159
}

packages/core/src/editor/Block.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,9 @@ NESTED BLOCKS
468468
cursor: pointer;
469469
display: flex;
470470
flex-direction: column;
471+
/* Reset default <figure> browser margins (the wrapper becomes a <figure>
472+
when the block has a caption). */
473+
margin: 0;
471474
user-select: none;
472475
}
473476

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: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ export const ImagePreview = (
1818
) => {
1919
const resolved = useResolveUrl(props.block.props.url!);
2020

21+
// alt describes image content (per WCAG H86); figcaption (when present)
22+
// is the contextual caption. Fall back to "" so unlabelled images are
23+
// marked decorative rather than getting a noisy generic fallback.
24+
const alt = props.block.props.name || "";
25+
2126
return (
2227
<img
2328
className={"bn-visual-media"}
@@ -26,7 +31,7 @@ export const ImagePreview = (
2631
? props.block.props.url
2732
: resolved.downloadUrl
2833
}
29-
alt={props.block.props.caption || "BlockNote image"}
34+
alt={alt}
3035
contentEditable={false}
3136
draggable={false}
3237
/>
@@ -43,12 +48,11 @@ export const ImageToExternalHTML = (
4348
return <p>Add image</p>;
4449
}
4550

51+
const alt = props.block.props.name || "";
4652
const image = props.block.props.showPreview ? (
4753
<img
4854
src={props.block.props.url}
49-
alt={
50-
props.block.props.name || props.block.props.caption || "BlockNote image"
51-
}
55+
alt={alt}
5256
width={props.block.props.previewWidth}
5357
/>
5458
) : (

packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`Test ServerBlockNoteEditor > converts to HTML (blocksToFullHTML) 1`] = `"<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="heading" data-background-color="blue" data-text-color="yellow" data-text-alignment="right" data-level="2"><h2 class="bn-inline-content"><strong><u>Heading </u></strong><em><s>2</s></em></h2></div><div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="2"><div class="bn-block" data-node-type="blockContainer" data-id="2"><div class="bn-block-content" data-content-type="paragraph" data-background-color="red"><p class="bn-inline-content">Paragraph</p></div></div></div><div class="bn-block-outer" data-node-type="blockOuter" data-id="3"><div class="bn-block" data-node-type="blockContainer" data-id="3"><div class="bn-block-content" data-content-type="bulletListItem"><p class="bn-inline-content">list item</p></div></div></div></div></div></div><div class="bn-block-outer" data-node-type="blockOuter" data-id="4"><div class="bn-block" data-node-type="blockContainer" data-id="4"><div class="bn-block-content" data-content-type="image" data-name="Example" data-url="exampleURL" data-caption="Caption" data-preview-width="256" data-file-block=""><div class="bn-file-block-content-wrapper" style="position: relative; width: 256px;"><div class="bn-visual-media-wrapper"><img class="bn-visual-media" src="exampleURL" alt="Example" draggable="false"><div class="bn-resize-handle" style="left: 4px; display: none;"></div><div class="bn-resize-handle" style="right: 4px; display: none;"></div></div><p class="bn-file-caption">Caption</p></div></div></div></div><div class="bn-block-outer" data-node-type="blockOuter" data-id="5"><div class="bn-block" data-node-type="blockContainer" data-id="5"><div class="bn-block-content" data-content-type="image" data-name="Example" data-url="exampleURL" data-caption="Caption" data-show-preview="false" data-preview-width="256" data-file-block=""><div class="bn-file-block-content-wrapper" style="position: relative;"><div class="bn-file-name-with-icon"><div class="bn-file-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8ZM10 4V9H5V20H19V4H10Z"></path></svg></div><p class="bn-file-name">Example</p></div><p class="bn-file-caption">Caption</p></div></div></div></div></div>"`;
3+
exports[`Test ServerBlockNoteEditor > converts to HTML (blocksToFullHTML) 1`] = `"<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="heading" data-background-color="blue" data-text-color="yellow" data-text-alignment="right" data-level="2"><h2 class="bn-inline-content"><strong><u>Heading </u></strong><em><s>2</s></em></h2></div><div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="2"><div class="bn-block" data-node-type="blockContainer" data-id="2"><div class="bn-block-content" data-content-type="paragraph" data-background-color="red"><p class="bn-inline-content">Paragraph</p></div></div></div><div class="bn-block-outer" data-node-type="blockOuter" data-id="3"><div class="bn-block" data-node-type="blockContainer" data-id="3"><div class="bn-block-content" data-content-type="bulletListItem"><p class="bn-inline-content">list item</p></div></div></div></div></div></div><div class="bn-block-outer" data-node-type="blockOuter" data-id="4"><div class="bn-block" data-node-type="blockContainer" data-id="4"><div class="bn-block-content" data-content-type="image" data-name="Example" data-url="exampleURL" data-caption="Caption" data-preview-width="256" data-file-block=""><figure class="bn-file-block-content-wrapper" style="position: relative; width: 256px;"><div class="bn-visual-media-wrapper"><img class="bn-visual-media" src="exampleURL" alt="Example" draggable="false"><div class="bn-resize-handle" style="left: 4px; display: none;"></div><div class="bn-resize-handle" style="right: 4px; display: none;"></div></div><figcaption class="bn-file-caption">Caption</figcaption></figure></div></div></div><div class="bn-block-outer" data-node-type="blockOuter" data-id="5"><div class="bn-block" data-node-type="blockContainer" data-id="5"><div class="bn-block-content" data-content-type="image" data-name="Example" data-url="exampleURL" data-caption="Caption" data-show-preview="false" data-preview-width="256" data-file-block=""><figure class="bn-file-block-content-wrapper" style="position: relative;"><div class="bn-file-name-with-icon"><div class="bn-file-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8ZM10 4V9H5V20H19V4H10Z"></path></svg></div><p class="bn-file-name">Example</p></div><figcaption class="bn-file-caption">Caption</figcaption></figure></div></div></div></div>"`;
44

55
exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 1`] = `"<h2 style="background-color: rgb(221, 235, 241); color: rgb(223, 171, 1); text-align: right;" data-background-color="blue" data-text-color="yellow" data-text-alignment="right" data-level="2"><strong><u>Heading </u></strong><em><s>2</s></em></h2><p style="background-color: rgb(251, 228, 228);" data-background-color="red" data-nesting-level="1">Paragraph</p><ul><li data-nesting-level="1"><p class="bn-inline-content">list item</p></li></ul><figure data-name="Example" data-url="exampleURL" data-caption="Caption" data-preview-width="256"><img src="exampleURL" alt="Example" width="256"><figcaption>Caption</figcaption></figure><div data-name="Example" data-url="exampleURL" data-caption="Caption" data-show-preview="false" data-preview-width="256"><a href="exampleURL">Example</a><p>Caption</p></div>"`;
66
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>

0 commit comments

Comments
 (0)