Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ export const createFileBlockWrapper = (
element?: { dom: HTMLElement; destroy?: () => void },
buttonIcon?: HTMLElement,
) => {
const wrapper = document.createElement("div");
// Use a <figure>/<figcaption> when the block has a caption, so the caption
// is semantically associated with its content for assistive tech. Falls back
// to a plain <div> when there is no caption (or the file has not been
// uploaded yet, since the upload UI never shows the caption).
const useFigure = block.props.url !== "" && !!block.props.caption;
const wrapper = document.createElement(useFigure ? "figure" : "div");
wrapper.className = "bn-file-block-content-wrapper";

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

// Show the caption if there is one.
if (block.props.caption) {
const caption = document.createElement("p");
const caption = document.createElement("figcaption");
caption.className = "bn-file-caption";
caption.textContent = block.props.caption;
wrapper.appendChild(caption);
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/blocks/Image/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ export const imageRender =
image.src = block.props.url;
}

image.alt = block.props.name || block.props.caption || "BlockNote image";
// alt describes image content (per WCAG H86); figcaption (when present)
// is the contextual caption. Fall back to "" so unlabelled images are
// marked decorative rather than getting a noisy generic fallback.
image.alt = block.props.name || "";
Comment on lines +119 to +122
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

alt should be empty when a figcaption is present to avoid redundant AT announcements.

The comment correctly notes that a figcaption provides the contextual label, but the code doesn't act on it — image.alt is set to block.props.name regardless of whether a caption (and therefore a figcaption) is also being rendered.

When both block.props.name and block.props.caption are non-empty, screen readers will announce the alt text and the figcaption separately, producing duplicated output. The PR objectives explicitly specify "caption present → alt=""".

The same fix is needed at line 156 inside imageToExternalHTML, where the <img> is likewise assigned block.props.name || "" before being conditionally wrapped in createFigureWithCaption.

♿ Proposed fix (apply in both `imageRender` and `imageToExternalHTML`)
-    // alt describes image content (per WCAG H86); figcaption (when present)
-    // is the contextual caption. Fall back to "" so unlabelled images are
-    // marked decorative rather than getting a noisy generic fallback.
-    image.alt = block.props.name || "";
+    // When a figcaption is present it becomes the accessible name for the
+    // figure; keep img alt="" to avoid redundant AT announcements (WCAG H37).
+    // Without a caption, use the file name as the succinct image description,
+    // or "" to mark the image decorative if neither is provided.
+    image.alt = block.props.caption ? "" : block.props.name || "";
   image = document.createElement("img");
   image.src = block.props.url;
-  image.alt = block.props.name || "";
+  image.alt = block.props.caption ? "" : block.props.name || "";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// alt describes image content (per WCAG H86); figcaption (when present)
// is the contextual caption. Fall back to "" so unlabelled images are
// marked decorative rather than getting a noisy generic fallback.
image.alt = block.props.name || "";
// When a figcaption is present it becomes the accessible name for the
// figure; keep img alt="" to avoid redundant AT announcements (WCAG H37).
// Without a caption, use the file name as the succinct image description,
// or "" to mark the image decorative if neither is provided.
image.alt = block.props.caption ? "" : block.props.name || "";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/blocks/Image/block.ts` around lines 119 - 122, The alt text
is set unconditionally which causes duplicate announcements when a
caption/figcaption exists; update the assignments in imageRender (where
image.alt is set) and in imageToExternalHTML (where the <img> alt attribute is
set) to use an empty string if block.props.caption (or caption) is present —
i.e., set alt = "" when block.props.caption is non-empty, otherwise use
block.props.name || ""; keep the existing conditional wrapping with
createFigureWithCaption intact.

image.contentEditable = "false";
image.draggable = false;
imageWrapper.appendChild(image);
Expand Down Expand Up @@ -150,7 +153,7 @@ export const imageToExternalHTML =
if (block.props.showPreview) {
image = document.createElement("img");
image.src = block.props.url;
image.alt = block.props.name || block.props.caption || "BlockNote image";
image.alt = block.props.name || "";
if (block.props.previewWidth) {
image.width = block.props.previewWidth;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/editor/Block.css
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,9 @@ NESTED BLOCKS
cursor: pointer;
display: flex;
flex-direction: column;
/* Reset default <figure> browser margins (the wrapper becomes a <figure>
when the block has a caption). */
margin: 0;
user-select: none;
}

Expand Down
14 changes: 11 additions & 3 deletions packages/react/src/blocks/File/helpers/render/FileBlockWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@ export const FileBlockWrapper = (
) => {
const showLoader = useUploadLoading(props.block.id);

// Use a <figure>/<figcaption> when the block has a caption, so the caption
// is semantically associated with its content for assistive tech.
const useFigure =
props.block.props.url !== "" && !!props.block.props.caption && !showLoader;
const Wrapper = useFigure ? "figure" : "div";

return (
<div
<Wrapper
className={"bn-file-block-content-wrapper"}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
Expand All @@ -54,10 +60,12 @@ export const FileBlockWrapper = (
)}
{props.block.props.caption && (
// Show the caption if there is one.
<p className={"bn-file-caption"}>{props.block.props.caption}</p>
<figcaption className={"bn-file-caption"}>
{props.block.props.caption}
</figcaption>
)}
</>
)}
</div>
</Wrapper>
);
};
12 changes: 8 additions & 4 deletions packages/react/src/blocks/Image/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export const ImagePreview = (
) => {
const resolved = useResolveUrl(props.block.props.url!);

// alt describes image content (per WCAG H86); figcaption (when present)
// is the contextual caption. Fall back to "" so unlabelled images are
// marked decorative rather than getting a noisy generic fallback.
const alt = props.block.props.name || "";

return (
<img
className={"bn-visual-media"}
Expand All @@ -26,7 +31,7 @@ export const ImagePreview = (
? props.block.props.url
: resolved.downloadUrl
}
alt={props.block.props.caption || "BlockNote image"}
alt={alt}
contentEditable={false}
draggable={false}
/>
Expand All @@ -43,12 +48,11 @@ export const ImageToExternalHTML = (
return <p>Add image</p>;
}

const alt = props.block.props.name || "";
const image = props.block.props.showPreview ? (
<img
src={props.block.props.url}
alt={
props.block.props.name || props.block.props.caption || "BlockNote image"
}
alt={alt}
width={props.block.props.previewWidth}
/>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

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>"`;
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>"`;

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>"`;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<img
src="https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg"
alt="BlockNote image"
alt=""
data-url="https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg"
/>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<p>Paragraph 1</p>
<img
src="https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg"
alt="BlockNote image"
alt=""
data-url="https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg"
data-nesting-level="1"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
data-caption="Caption"
data-file-block=""
>
<div class="bn-file-block-content-wrapper">
<figure class="bn-file-block-content-wrapper">
<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">
Expand All @@ -20,8 +20,8 @@
</div>
<p class="bn-file-name">example</p>
</div>
<p class="bn-file-caption">Caption</p>
</div>
<figcaption class="bn-file-caption">Caption</figcaption>
</figure>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
data-caption="Caption"
data-file-block=""
>
<div class="bn-file-block-content-wrapper">
<figure class="bn-file-block-content-wrapper">
<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">
Expand All @@ -20,8 +20,8 @@
</div>
<p class="bn-file-name">example</p>
</div>
<p class="bn-file-caption">Caption</p>
</div>
<figcaption class="bn-file-caption">Caption</figcaption>
</figure>
</div>
<div class="bn-block-group" data-node-type="blockGroup">
<div class="bn-block-outer" data-node-type="blockOuter" data-id="2">
Expand All @@ -34,7 +34,7 @@
data-caption="Caption"
data-file-block=""
>
<div class="bn-file-block-content-wrapper">
<figure class="bn-file-block-content-wrapper">
<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">
Expand All @@ -45,8 +45,8 @@
</div>
<p class="bn-file-name">example</p>
</div>
<p class="bn-file-caption">Caption</p>
</div>
<figcaption class="bn-file-caption">Caption</figcaption>
</figure>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
data-caption="Caption"
data-file-block=""
>
<div class="bn-file-block-content-wrapper">
<figure class="bn-file-block-content-wrapper">
<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">
Expand All @@ -19,8 +19,8 @@
</div>
<p class="bn-file-name"></p>
</div>
<p class="bn-file-caption">Caption</p>
</div>
<figcaption class="bn-file-caption">Caption</figcaption>
</figure>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<img
class="bn-visual-media"
src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
alt="BlockNote image"
alt=""
draggable="false"
/>
<div class="bn-resize-handle" style="left: 4px; display: none;"></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
data-preview-width="256"
data-file-block=""
>
<div
<figure
class="bn-file-block-content-wrapper"
style="position: relative; width: 256px;"
>
Expand All @@ -19,8 +19,8 @@
<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>
<figcaption class="bn-file-caption">Caption</figcaption>
</figure>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@
data-preview-width="256"
data-file-block=""
>
<div
<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="Caption" draggable="false" />
<img class="bn-visual-media" src="exampleURL" alt="" 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>
<figcaption class="bn-file-caption">Caption</figcaption>
</figure>
</div>
<div class="bn-block-group" data-node-type="blockGroup">
<div class="bn-block-outer" data-node-type="blockOuter" data-id="2">
Expand All @@ -32,17 +32,17 @@
data-preview-width="256"
data-file-block=""
>
<div
<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="Caption" draggable="false" />
<img class="bn-visual-media" src="exampleURL" alt="" 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>
<figcaption class="bn-file-caption">Caption</figcaption>
</figure>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@
data-preview-width="256"
data-file-block=""
>
<div
<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="Caption" draggable="false" />
<img class="bn-visual-media" src="exampleURL" alt="" 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>
<figcaption class="bn-file-caption">Caption</figcaption>
</figure>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
data-preview-width="256"
data-file-block=""
>
<div class="bn-file-block-content-wrapper" style="position: relative;">
<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">
Expand All @@ -22,8 +22,8 @@
</div>
<p class="bn-file-name">example</p>
</div>
<p class="bn-file-caption">Caption</p>
</div>
<figcaption class="bn-file-caption">Caption</figcaption>
</figure>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@
style="position: relative; width: fit-content;"
>
<div class="bn-visual-media-wrapper">
<img
class="bn-visual-media"
src="exampleURL"
alt="BlockNote image"
draggable="false"
/>
<img class="bn-visual-media" src="exampleURL" alt="" 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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
data-caption="This is a caption"
data-file-block=""
>
<div
<figure
class="bn-file-block-content-wrapper"
style="position: relative; width: fit-content;"
>
Expand All @@ -23,8 +23,8 @@
<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">This is a caption</p>
</div>
<figcaption class="bn-file-caption">This is a caption</figcaption>
</figure>
</div>
</div>
</div>
Expand Down
Loading
Loading