From 5050289b57b00a2029e5dc95aab2e406a1e7bda3 Mon Sep 17 00:00:00 2001 From: Maxence Mauduit Date: Thu, 27 Aug 2026 14:17:50 +0900 Subject: [PATCH] feat: add dedicated alt text field to image block (#2822) --- .../__snapshots__/insertBlocks.test.ts.snap | 7 + .../__snapshots__/mergeBlocks.test.ts.snap | 5 + .../__snapshots__/moveBlocks.test.ts.snap | 24 +++ .../__snapshots__/replaceBlocks.test.ts.snap | 12 ++ .../__snapshots__/splitBlock.test.ts.snap | 6 + .../__snapshots__/updateBlock.test.ts.snap | 25 +++ packages/core/src/blocks/Image/block.ts | 13 +- .../src/blocks/Image/parseImageElement.ts | 3 +- packages/core/src/i18n/locales/ar.ts | 4 + packages/core/src/i18n/locales/de.ts | 4 + packages/core/src/i18n/locales/en.ts | 4 + packages/core/src/i18n/locales/es.ts | 4 + packages/core/src/i18n/locales/fa.ts | 4 + packages/core/src/i18n/locales/fr.ts | 4 + packages/core/src/i18n/locales/he.ts | 4 + packages/core/src/i18n/locales/hr.ts | 4 + packages/core/src/i18n/locales/is.ts | 4 + packages/core/src/i18n/locales/it.ts | 4 + packages/core/src/i18n/locales/ja.ts | 4 + packages/core/src/i18n/locales/ko.ts | 4 + packages/core/src/i18n/locales/nl.ts | 4 + packages/core/src/i18n/locales/no.ts | 4 + packages/core/src/i18n/locales/pl.ts | 4 + packages/core/src/i18n/locales/pt.ts | 4 + packages/core/src/i18n/locales/ru.ts | 4 + packages/core/src/i18n/locales/sk.ts | 4 + packages/core/src/i18n/locales/uk.ts | 4 + packages/core/src/i18n/locales/uz.ts | 4 + packages/core/src/i18n/locales/vi.ts | 4 + packages/core/src/i18n/locales/zh-tw.ts | 4 + packages/core/src/i18n/locales/zh.ts | 4 + packages/core/src/y/utils.test.ts | 2 + packages/core/src/yjs/utils.test.ts | 2 + packages/react/src/blocks/Image/block.tsx | 9 +- .../DefaultButtons/FileAltButton.tsx | 147 ++++++++++++++++++ .../FormattingToolbar/FormattingToolbar.tsx | 2 + packages/react/src/index.ts | 1 + .../src/context/ServerBlockNoteEditor.test.ts | 2 + .../ServerBlockNoteEditor.test.ts.snap | 10 +- 39 files changed, 349 insertions(+), 13 deletions(-) create mode 100644 packages/react/src/components/FormattingToolbar/DefaultButtons/FileAltButton.tsx diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap index e854849d11..657664873f 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap @@ -336,6 +336,7 @@ exports[`Test insertBlocks > Insert multiple blocks after 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -1011,6 +1012,7 @@ exports[`Test insertBlocks > Insert multiple blocks before 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -1606,6 +1608,7 @@ exports[`Test insertBlocks > Insert single basic block after 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2213,6 +2216,7 @@ exports[`Test insertBlocks > Insert single basic block before (without type) 2`] "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2808,6 +2812,7 @@ exports[`Test insertBlocks > Insert single basic block before 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -3519,6 +3524,7 @@ exports[`Test insertBlocks > Insert single complex block after 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -4230,6 +4236,7 @@ exports[`Test insertBlocks > Insert single complex block before 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", diff --git a/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap index 20c94c5ab8..44e9c46d23 100644 --- a/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap @@ -212,6 +212,7 @@ exports[`Test mergeBlocks > Basic 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -763,6 +764,7 @@ exports[`Test mergeBlocks > Blocks have different types 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -1314,6 +1316,7 @@ exports[`Test mergeBlocks > First block has children 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -1864,6 +1867,7 @@ exports[`Test mergeBlocks > Second block has children 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2432,6 +2436,7 @@ exports[`Test mergeBlocks > Second block is empty 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap index e59da045ed..03b37a8989 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap @@ -229,6 +229,7 @@ exports[`Test moveBlocksDown > Basic 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -797,6 +798,7 @@ exports[`Test moveBlocksDown > Explicit block argument moves the given block 1`] "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -1364,6 +1366,7 @@ exports[`Test moveBlocksDown > Explicit block argument with nested block 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -1932,6 +1935,7 @@ exports[`Test moveBlocksDown > Into children 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2500,6 +2504,7 @@ exports[`Test moveBlocksDown > Last block 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -3068,6 +3073,7 @@ exports[`Test moveBlocksDown > Multiple blocks 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -3636,6 +3642,7 @@ exports[`Test moveBlocksDown > Multiple blocks ending in block with children 1`] "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -4204,6 +4211,7 @@ exports[`Test moveBlocksDown > Multiple blocks ending in nested block 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -4782,6 +4790,7 @@ exports[`Test moveBlocksDown > Multiple blocks starting and ending in nested blo "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -5339,6 +5348,7 @@ exports[`Test moveBlocksDown > Multiple blocks starting in block with children 1 "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -5906,6 +5916,7 @@ exports[`Test moveBlocksDown > Multiple blocks starting in nested block 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -6474,6 +6485,7 @@ exports[`Test moveBlocksDown > Out of children 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -7041,6 +7053,7 @@ exports[`Test moveBlocksUp > Basic 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -7609,6 +7622,7 @@ exports[`Test moveBlocksUp > Explicit block argument moves the given block 1`] = "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -8177,6 +8191,7 @@ exports[`Test moveBlocksUp > Explicit block argument with nested block 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -8744,6 +8759,7 @@ exports[`Test moveBlocksUp > First block 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -9312,6 +9328,7 @@ exports[`Test moveBlocksUp > Into children 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -9880,6 +9897,7 @@ exports[`Test moveBlocksUp > Multiple blocks 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -10448,6 +10466,7 @@ exports[`Test moveBlocksUp > Multiple blocks ending in block with children 1`] = "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -11016,6 +11035,7 @@ exports[`Test moveBlocksUp > Multiple blocks ending in nested block 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -11566,6 +11586,7 @@ exports[`Test moveBlocksUp > Multiple blocks starting and ending in nested block "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -12151,6 +12172,7 @@ exports[`Test moveBlocksUp > Multiple blocks starting in block with children 1`] "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -12718,6 +12740,7 @@ exports[`Test moveBlocksUp > Multiple blocks starting in nested block 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -13286,6 +13309,7 @@ exports[`Test moveBlocksUp > Out of children 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap index d876b31175..2d29640de0 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap @@ -142,6 +142,7 @@ exports[`Test replaceBlocks > Remove multiple consecutive blocks 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -693,6 +694,7 @@ exports[`Test replaceBlocks > Remove multiple non-consecutive blocks 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -990,6 +992,7 @@ exports[`Test replaceBlocks > Remove single block 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -1522,6 +1525,7 @@ exports[`Test replaceBlocks > Replace multiple consecutive blocks with multiple "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2014,6 +2018,7 @@ exports[`Test replaceBlocks > Replace multiple consecutive blocks with single ba "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2564,6 +2569,7 @@ exports[`Test replaceBlocks > Replace multiple consecutive blocks with single co "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -3166,6 +3172,7 @@ exports[`Test replaceBlocks > Replace multiple non-consecutive blocks with multi "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -3474,6 +3481,7 @@ exports[`Test replaceBlocks > Replace multiple non-consecutive blocks with singl "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -3840,6 +3848,7 @@ exports[`Test replaceBlocks > Replace multiple non-consecutive blocks with singl "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -4188,6 +4197,7 @@ exports[`Test replaceBlocks > Replace single block with multiple 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -4750,6 +4760,7 @@ exports[`Test replaceBlocks > Replace single block with single basic 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -5370,6 +5381,7 @@ exports[`Test replaceBlocks > Replace single block with single complex 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", diff --git a/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap b/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap index 8cd297eaee..fc360f7f9d 100644 --- a/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap @@ -246,6 +246,7 @@ exports[`Test splitBlocks > Basic 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -831,6 +832,7 @@ exports[`Test splitBlocks > Block has children 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -1416,6 +1418,7 @@ exports[`Test splitBlocks > Don't keep props 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2001,6 +2004,7 @@ exports[`Test splitBlocks > Don't keep type 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2580,6 +2584,7 @@ exports[`Test splitBlocks > End of content 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -3167,6 +3172,7 @@ exports[`Test splitBlocks > Keep type 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap b/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap index f98a42cfbf..db809bd838 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap @@ -301,6 +301,7 @@ exports[`Test updateBlock > Revert all props 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -941,6 +942,7 @@ exports[`Test updateBlock > Revert single prop 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -1581,6 +1583,7 @@ exports[`Test updateBlock > Update all props 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2221,6 +2224,7 @@ exports[`Test updateBlock > Update children 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2566,6 +2570,7 @@ exports[`Test updateBlock > Update inline content to no content 1`] = ` "content": undefined, "id": "paragraph-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2584,6 +2589,7 @@ exports[`Test updateBlock > Update inline content to no content 2`] = ` "content": undefined, "id": "paragraph-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -2803,6 +2809,7 @@ exports[`Test updateBlock > Update inline content to no content 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -3387,6 +3394,7 @@ exports[`Test updateBlock > Update inline content to plain content 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -4311,6 +4319,7 @@ exports[`Test updateBlock > Update inline content to table content 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -7559,6 +7568,7 @@ exports[`Test updateBlock > Update partial (offset start + end) 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -8127,6 +8137,7 @@ exports[`Test updateBlock > Update partial (offset start) 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -8688,6 +8699,7 @@ exports[`Test updateBlock > Update partial (props + offset end) 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -9249,6 +9261,7 @@ exports[`Test updateBlock > Update partial (table cell) 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -9817,6 +9830,7 @@ exports[`Test updateBlock > Update partial (table row) 1`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -10405,6 +10419,7 @@ exports[`Test updateBlock > Update plain content to inline content 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -11045,6 +11060,7 @@ exports[`Test updateBlock > Update single prop 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -11615,6 +11631,7 @@ exports[`Test updateBlock > Update styled inline content to plain content 2`] = "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -12197,6 +12214,7 @@ exports[`Test updateBlock > Update table content to empty inline content 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -12611,6 +12629,7 @@ exports[`Test updateBlock > Update table content to inline content 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -12788,6 +12807,7 @@ exports[`Test updateBlock > Update table content to no content 1`] = ` "content": undefined, "id": "table-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -13028,6 +13048,7 @@ exports[`Test updateBlock > Update table content to no content 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -13059,6 +13080,7 @@ exports[`Test updateBlock > Update table content to no content 2`] = ` "content": undefined, "id": "table-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -13495,6 +13517,7 @@ exports[`Test updateBlock > Update type 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -14119,6 +14142,7 @@ exports[`Test updateBlock > Update with plain content 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", @@ -14745,6 +14769,7 @@ exports[`Test updateBlock > Update with styled content 2`] = ` "content": undefined, "id": "image-0", "props": { + "alt": "", "backgroundColor": "default", "caption": "", "name": "", diff --git a/packages/core/src/blocks/Image/block.ts b/packages/core/src/blocks/Image/block.ts index dc5caf270a..8ec845aef5 100644 --- a/packages/core/src/blocks/Image/block.ts +++ b/packages/core/src/blocks/Image/block.ts @@ -39,6 +39,10 @@ export const createImageBlockConfig = createBlockConfig( caption: { default: "" as const, }, + // Image alt text (accessibility label, sets the img "alt" attribute). + alt: { + default: "" as const, + }, showPreview: { default: true, @@ -117,9 +121,10 @@ export const imageRender = } // 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 || ""; + // is the contextual caption. Prefer the explicit alt text, falling back to + // the file name for backwards compatibility, then "" so unlabelled images + // are marked decorative rather than getting a noisy generic fallback. + image.alt = block.props.alt || block.props.name || ""; image.contentEditable = "false"; image.draggable = false; if (block.props.previewWidth) { @@ -156,7 +161,7 @@ export const imageToExternalHTML = if (block.props.showPreview) { image = document.createElement("img"); image.src = block.props.url; - image.alt = block.props.name || ""; + image.alt = block.props.alt || block.props.name || ""; if (block.props.previewWidth) { image.width = block.props.previewWidth; } diff --git a/packages/core/src/blocks/Image/parseImageElement.ts b/packages/core/src/blocks/Image/parseImageElement.ts index 21c0b31519..03d6117e77 100644 --- a/packages/core/src/blocks/Image/parseImageElement.ts +++ b/packages/core/src/blocks/Image/parseImageElement.ts @@ -2,6 +2,7 @@ export const parseImageElement = (imageElement: HTMLImageElement) => { const url = imageElement.src || undefined; const previewWidth = imageElement.width || undefined; const name = imageElement.alt || undefined; + const alt = imageElement.alt || undefined; - return { url, previewWidth, name }; + return { url, previewWidth, name, alt }; }; diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts index 094671d920..fb912970f4 100644 --- a/packages/core/src/i18n/locales/ar.ts +++ b/packages/core/src/i18n/locales/ar.ts @@ -263,6 +263,10 @@ export const ar: Dictionary = { tooltip: "تحرير التسمية التوضيحية", input_placeholder: "تحرير التسمية التوضيحية", }, + file_alt: { + tooltip: "تحرير النص البديل", + input_placeholder: "تحرير النص البديل", + }, file_replace: { tooltip: { image: "استبدال الصورة", diff --git a/packages/core/src/i18n/locales/de.ts b/packages/core/src/i18n/locales/de.ts index bf77a36a01..30e8a089f4 100644 --- a/packages/core/src/i18n/locales/de.ts +++ b/packages/core/src/i18n/locales/de.ts @@ -297,6 +297,10 @@ export const de: Dictionary = { tooltip: "Beschriftung bearbeiten", input_placeholder: "Beschriftung bearbeiten", }, + file_alt: { + tooltip: "Alternativtext bearbeiten", + input_placeholder: "Alternativtext bearbeiten", + }, file_replace: { tooltip: { image: "Bild ersetzen", diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts index e5386f3020..f46db0bc43 100644 --- a/packages/core/src/i18n/locales/en.ts +++ b/packages/core/src/i18n/locales/en.ts @@ -278,6 +278,10 @@ export const en = { tooltip: "Edit caption", input_placeholder: "Edit caption", }, + file_alt: { + tooltip: "Edit alt text", + input_placeholder: "Edit alt text", + }, file_replace: { tooltip: { image: "Replace image", diff --git a/packages/core/src/i18n/locales/es.ts b/packages/core/src/i18n/locales/es.ts index 743a1be05c..07412fe3fa 100644 --- a/packages/core/src/i18n/locales/es.ts +++ b/packages/core/src/i18n/locales/es.ts @@ -276,6 +276,10 @@ export const es: Dictionary = { tooltip: "Editar leyenda", input_placeholder: "Editar leyenda", }, + file_alt: { + tooltip: "Editar texto alternativo", + input_placeholder: "Editar texto alternativo", + }, file_replace: { tooltip: { image: "Reemplazar imagen", diff --git a/packages/core/src/i18n/locales/fa.ts b/packages/core/src/i18n/locales/fa.ts index 6b2783ab68..e982ac096f 100644 --- a/packages/core/src/i18n/locales/fa.ts +++ b/packages/core/src/i18n/locales/fa.ts @@ -246,6 +246,10 @@ export const fa = { tooltip: "ویرایش زیرنویس", input_placeholder: "ویرایش زیرنویس", }, + file_alt: { + tooltip: "ویرایش متن جایگزین", + input_placeholder: "ویرایش متن جایگزین", + }, file_replace: { tooltip: { image: "جایگزینی تصویر", diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index ad605db24a..d2d3b4e79f 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -324,6 +324,10 @@ export const fr: Dictionary = { tooltip: "Modifier la légende", input_placeholder: "Modifier la légende", }, + file_alt: { + tooltip: "Modifier le texte alternatif", + input_placeholder: "Modifier le texte alternatif", + }, file_replace: { tooltip: { image: "Remplacer l'image", diff --git a/packages/core/src/i18n/locales/he.ts b/packages/core/src/i18n/locales/he.ts index 4662a94202..d05bc941e3 100644 --- a/packages/core/src/i18n/locales/he.ts +++ b/packages/core/src/i18n/locales/he.ts @@ -278,6 +278,10 @@ export const he: Dictionary = { tooltip: "ערוך כיתוב", input_placeholder: "ערוך כיתוב", }, + file_alt: { + tooltip: "ערוך טקסט חלופי", + input_placeholder: "ערוך טקסט חלופי", + }, file_replace: { tooltip: { image: "החלף תמונה", diff --git a/packages/core/src/i18n/locales/hr.ts b/packages/core/src/i18n/locales/hr.ts index 03eb016eed..baa36afb38 100644 --- a/packages/core/src/i18n/locales/hr.ts +++ b/packages/core/src/i18n/locales/hr.ts @@ -291,6 +291,10 @@ export const hr: Dictionary = { tooltip: "Uredi natpis", input_placeholder: "Uredi natpis", }, + file_alt: { + tooltip: "Uredi alternativni tekst", + input_placeholder: "Uredi alternativni tekst", + }, file_replace: { tooltip: { image: "Zamijeni sliku", diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts index 913b2324b0..5f169b4263 100644 --- a/packages/core/src/i18n/locales/is.ts +++ b/packages/core/src/i18n/locales/is.ts @@ -291,6 +291,10 @@ export const is: Dictionary = { tooltip: "Breyta myndatexta", input_placeholder: "Breyta myndatexta", }, + file_alt: { + tooltip: "Breyta alt-texta", + input_placeholder: "Breyta alt-texta", + }, file_replace: { tooltip: { image: "Skipta um mynd", diff --git a/packages/core/src/i18n/locales/it.ts b/packages/core/src/i18n/locales/it.ts index 44be22c1bd..3485cb5b45 100644 --- a/packages/core/src/i18n/locales/it.ts +++ b/packages/core/src/i18n/locales/it.ts @@ -300,6 +300,10 @@ export const it: Dictionary = { tooltip: "Modifica didascalia", input_placeholder: "Modifica didascalia", }, + file_alt: { + tooltip: "Modifica testo alternativo", + input_placeholder: "Modifica testo alternativo", + }, file_replace: { tooltip: { image: "Sostituisci immagine", diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts index ead1f2fb30..1fedd163b8 100644 --- a/packages/core/src/i18n/locales/ja.ts +++ b/packages/core/src/i18n/locales/ja.ts @@ -318,6 +318,10 @@ export const ja: Dictionary = { tooltip: "キャプションを編集", input_placeholder: "キャプションを編集", }, + file_alt: { + tooltip: "代替テキストを編集", + input_placeholder: "代替テキストを編集", + }, file_replace: { tooltip: { image: "画像を置換", diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts index 2981ff1c36..825c782799 100644 --- a/packages/core/src/i18n/locales/ko.ts +++ b/packages/core/src/i18n/locales/ko.ts @@ -291,6 +291,10 @@ export const ko: Dictionary = { tooltip: "이미지 캡션 수정", input_placeholder: "이미지 캡션 수정", }, + file_alt: { + tooltip: "대체 텍스트 수정", + input_placeholder: "대체 텍스트 수정", + }, file_replace: { tooltip: { image: "이미지 교체", diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index da599e017c..fc04c885cc 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -278,6 +278,10 @@ export const nl: Dictionary = { tooltip: "Bewerk onderschrift", input_placeholder: "Bewerk onderschrift", }, + file_alt: { + tooltip: "Bewerk alt-tekst", + input_placeholder: "Bewerk alt-tekst", + }, file_replace: { tooltip: { image: "Afbeelding vervangen", diff --git a/packages/core/src/i18n/locales/no.ts b/packages/core/src/i18n/locales/no.ts index 72efc096ed..5dcf11bb5b 100644 --- a/packages/core/src/i18n/locales/no.ts +++ b/packages/core/src/i18n/locales/no.ts @@ -295,6 +295,10 @@ export const no: Dictionary = { tooltip: "Rediger bildetekst", input_placeholder: "Rediger bildetekst", }, + file_alt: { + tooltip: "Rediger alternativ tekst", + input_placeholder: "Rediger alternativ tekst", + }, file_replace: { tooltip: { image: "Bytt ut bilde", diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts index d00039633c..f3afd0fe7a 100644 --- a/packages/core/src/i18n/locales/pl.ts +++ b/packages/core/src/i18n/locales/pl.ts @@ -269,6 +269,10 @@ export const pl: Dictionary = { tooltip: "Zmień podpis", input_placeholder: "Zmień podpis", }, + file_alt: { + tooltip: "Zmień tekst alternatywny", + input_placeholder: "Zmień tekst alternatywny", + }, file_replace: { tooltip: { image: "Zmień obraz", diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts index fe719ce023..eb27000bec 100644 --- a/packages/core/src/i18n/locales/pt.ts +++ b/packages/core/src/i18n/locales/pt.ts @@ -270,6 +270,10 @@ export const pt: Dictionary = { tooltip: "Editar legenda", input_placeholder: "Editar legenda", }, + file_alt: { + tooltip: "Editar texto alternativo", + input_placeholder: "Editar texto alternativo", + }, file_replace: { tooltip: { image: "Substituir imagem", diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts index a4a7987dfc..6554a2ad52 100644 --- a/packages/core/src/i18n/locales/ru.ts +++ b/packages/core/src/i18n/locales/ru.ts @@ -321,6 +321,10 @@ export const ru: Dictionary = { tooltip: "Изменить подпись", input_placeholder: "Изменить подпись", }, + file_alt: { + tooltip: "Изменить альтернативный текст", + input_placeholder: "Изменить альтернативный текст", + }, file_replace: { tooltip: { image: "Заменить изображение", diff --git a/packages/core/src/i18n/locales/sk.ts b/packages/core/src/i18n/locales/sk.ts index 4e73dc7eca..f58f35d67a 100644 --- a/packages/core/src/i18n/locales/sk.ts +++ b/packages/core/src/i18n/locales/sk.ts @@ -276,6 +276,10 @@ export const sk = { tooltip: "Upraviť popis", input_placeholder: "Upraviť popis", }, + file_alt: { + tooltip: "Upraviť alternatívny text", + input_placeholder: "Upraviť alternatívny text", + }, file_replace: { tooltip: { image: "Nahradiť obrázok", diff --git a/packages/core/src/i18n/locales/uk.ts b/packages/core/src/i18n/locales/uk.ts index e9d379ac0b..00e188656e 100644 --- a/packages/core/src/i18n/locales/uk.ts +++ b/packages/core/src/i18n/locales/uk.ts @@ -302,6 +302,10 @@ export const uk: Dictionary = { tooltip: "Редагувати підпис", input_placeholder: "Редагувати підпис", }, + file_alt: { + tooltip: "Редагувати альтернативний текст", + input_placeholder: "Редагувати альтернативний текст", + }, file_replace: { tooltip: { image: "Замінити зображення", diff --git a/packages/core/src/i18n/locales/uz.ts b/packages/core/src/i18n/locales/uz.ts index 13aee55a73..6f14f07c3a 100644 --- a/packages/core/src/i18n/locales/uz.ts +++ b/packages/core/src/i18n/locales/uz.ts @@ -324,6 +324,10 @@ export const uz: Dictionary = { tooltip: "Izohni o‘zgartirish", input_placeholder: "Izohni o‘zgartirish", }, + file_alt: { + tooltip: "Muqobil matnni o‘zgartirish", + input_placeholder: "Muqobil matnni o‘zgartirish", + }, file_replace: { tooltip: { diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts index 8733fbf0ba..d3a07b39be 100644 --- a/packages/core/src/i18n/locales/vi.ts +++ b/packages/core/src/i18n/locales/vi.ts @@ -277,6 +277,10 @@ export const vi: Dictionary = { tooltip: "Chỉnh sửa chú thích", input_placeholder: "Chỉnh sửa chú thích", }, + file_alt: { + tooltip: "Chỉnh sửa văn bản thay thế", + input_placeholder: "Chỉnh sửa văn bản thay thế", + }, file_replace: { tooltip: { image: "Thay thế hình ảnh", diff --git a/packages/core/src/i18n/locales/zh-tw.ts b/packages/core/src/i18n/locales/zh-tw.ts index 5ac37a80c7..3ce688c077 100644 --- a/packages/core/src/i18n/locales/zh-tw.ts +++ b/packages/core/src/i18n/locales/zh-tw.ts @@ -319,6 +319,10 @@ export const zhTW: Dictionary = { tooltip: "編輯標題", input_placeholder: "編輯標題", }, + file_alt: { + tooltip: "編輯替代文字", + input_placeholder: "編輯替代文字", + }, file_replace: { tooltip: { image: "替換圖片", diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index 3f4c90bb56..9a5acea438 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -319,6 +319,10 @@ export const zh: Dictionary = { tooltip: "编辑标题", input_placeholder: "编辑标题", }, + file_alt: { + tooltip: "编辑替代文本", + input_placeholder: "编辑替代文本", + }, file_replace: { tooltip: { image: "替换图片", diff --git a/packages/core/src/y/utils.test.ts b/packages/core/src/y/utils.test.ts index edf242308e..60d8f72da9 100644 --- a/packages/core/src/y/utils.test.ts +++ b/packages/core/src/y/utils.test.ts @@ -112,6 +112,7 @@ describe("Test y (v14) utils", () => { name: "Example", url: "exampleURL", caption: "Caption", + alt: "Alt text", showPreview: true, previewWidth: 256, }, @@ -127,6 +128,7 @@ describe("Test y (v14) utils", () => { name: "Example", url: "exampleURL", caption: "Caption", + alt: "Alt text", showPreview: false, previewWidth: 256, }, diff --git a/packages/core/src/yjs/utils.test.ts b/packages/core/src/yjs/utils.test.ts index 5c388c433f..361616f9bd 100644 --- a/packages/core/src/yjs/utils.test.ts +++ b/packages/core/src/yjs/utils.test.ts @@ -112,6 +112,7 @@ describe("Test yjs utils", () => { name: "Example", url: "exampleURL", caption: "Caption", + alt: "Alt text", showPreview: true, previewWidth: 256, }, @@ -127,6 +128,7 @@ describe("Test yjs utils", () => { name: "Example", url: "exampleURL", caption: "Caption", + alt: "Alt text", showPreview: false, previewWidth: 256, }, diff --git a/packages/react/src/blocks/Image/block.tsx b/packages/react/src/blocks/Image/block.tsx index 870a16ec5c..a877088508 100644 --- a/packages/react/src/blocks/Image/block.tsx +++ b/packages/react/src/blocks/Image/block.tsx @@ -19,9 +19,10 @@ 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 || ""; + // is the contextual caption. Prefer the explicit alt text, falling back to + // the file name for backwards compatibility, then "" so unlabelled images + // are marked decorative rather than getting a noisy generic fallback. + const alt = props.block.props.alt || props.block.props.name || ""; return ( Add image

; } - const alt = props.block.props.name || ""; + const alt = props.block.props.alt || props.block.props.name || ""; const image = props.block.props.showPreview ? ( ) => ( + + + + + + +); + +export const FileAltButton = () => { + const dict = useDictionary(); + const Components = useComponentsContext()!; + + const editor = useBlockNoteEditor< + BlockSchema, + InlineContentSchema, + StyleSchema + >(); + + const block = useEditorState({ + editor, + selector: ({ editor }) => { + if (!editor.isEditable) { + return undefined; + } + + const selectedBlocks = editor.getSelection()?.blocks || [ + editor.getTextCursorPosition().block, + ]; + + if (selectedBlocks.length !== 1) { + return undefined; + } + + const block = selectedBlocks[0]; + + if ( + !blockHasType(block, editor, block.type, { + url: "string", + alt: "string", + }) + ) { + return undefined; + } + + return block; + }, + }); + + const [popoverOpen, setPopoverOpen] = useState(false); + + const handleChange = useCallback( + (event: ChangeEvent) => { + if ( + block !== undefined && + editorHasBlockWithType(editor, block.type, { + alt: "string", + }) + ) { + editor.updateBlock(block.id, { + props: { + alt: event.currentTarget.value, + }, + }); + } + }, + [block, editor], + ); + + const handleKeyDown = useCallback((event: KeyboardEvent) => { + if (event.key === "Enter" && !event.nativeEvent.isComposing) { + event.preventDefault(); + setPopoverOpen(false); + } + }, []); + + if (block === undefined) { + return null; + } + + return ( + + + } + onClick={() => setPopoverOpen((open) => !open)} + /> + + + + } + value={block.props.alt} + autoFocus={true} + placeholder={dict.formatting_toolbar.file_alt.input_placeholder} + onKeyDown={handleKeyDown} + onChange={handleChange} + /> + + + + ); +}; diff --git a/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx b/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx index afb382a563..03db3937ae 100644 --- a/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx +++ b/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx @@ -4,6 +4,7 @@ import { useComponentsContext } from "../../editor/ComponentsContext.js"; import { BasicTextStyleButton } from "./DefaultButtons/BasicTextStyleButton.js"; import { ColorStyleButton } from "./DefaultButtons/ColorStyleButton.js"; import { CreateLinkButton } from "./DefaultButtons/CreateLinkButton.js"; +import { FileAltButton } from "./DefaultButtons/FileAltButton.js"; import { FileCaptionButton } from "./DefaultButtons/FileCaptionButton.js"; import { FileDeleteButton } from "./DefaultButtons/FileDeleteButton.js"; import { FileRenameButton } from "./DefaultButtons/FileRenameButton.js"; @@ -31,6 +32,7 @@ export const getFormattingToolbarItems = ( , , , + , , , , diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 0553f8a30d..c912e4b54d 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -31,6 +31,7 @@ export * from "./components/FormattingToolbar/DefaultButtons/AddTiptapCommentBut export * from "./components/FormattingToolbar/DefaultButtons/BasicTextStyleButton.js"; export * from "./components/FormattingToolbar/DefaultButtons/ColorStyleButton.js"; export * from "./components/FormattingToolbar/DefaultButtons/CreateLinkButton.js"; +export * from "./components/FormattingToolbar/DefaultButtons/FileAltButton.js"; export * from "./components/FormattingToolbar/DefaultButtons/FileCaptionButton.js"; export * from "./components/FormattingToolbar/DefaultButtons/FileDeleteButton.js"; export * from "./components/FormattingToolbar/DefaultButtons/FileDownloadButton.js"; diff --git a/packages/server-util/src/context/ServerBlockNoteEditor.test.ts b/packages/server-util/src/context/ServerBlockNoteEditor.test.ts index fd45b2fadd..bf35ff0505 100644 --- a/packages/server-util/src/context/ServerBlockNoteEditor.test.ts +++ b/packages/server-util/src/context/ServerBlockNoteEditor.test.ts @@ -80,6 +80,7 @@ describe("Test ServerBlockNoteEditor", () => { name: "Example", url: "exampleURL", caption: "Caption", + alt: "Alt text", showPreview: true, previewWidth: 256, }, @@ -95,6 +96,7 @@ describe("Test ServerBlockNoteEditor", () => { name: "Example", url: "exampleURL", caption: "Caption", + alt: "Alt text", showPreview: false, previewWidth: 256, }, diff --git a/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap b/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap index f088373cc6..9748d6e6ec 100644 --- a/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap +++ b/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap @@ -1,8 +1,8 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Test ServerBlockNoteEditor > converts to HTML (blocksToFullHTML) 1`] = `"

Heading 2

Paragraph

list item

Example
Caption

Example

Caption
"`; +exports[`Test ServerBlockNoteEditor > converts to HTML (blocksToFullHTML) 1`] = `"

Heading 2

Paragraph

list item

Alt text
Caption

Example

Caption
"`; -exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 1`] = `"

Heading 2

Paragraph

  • list item

Example
Caption
Example

Caption

"`; +exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 1`] = `"

Heading 2

Paragraph

  • list item

Alt text
Caption
Example

Caption

"`; exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 2`] = ` [ @@ -75,6 +75,7 @@ exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLos "content": undefined, "id": "3", "props": { + "alt": "Alt text", "backgroundColor": "default", "caption": "Caption", "name": "Example", @@ -135,7 +136,7 @@ Paragraph * list item -
Example
Caption
+
Alt text
Caption
[Example](exampleURL) @@ -218,9 +219,10 @@ exports[`Test ServerBlockNoteEditor > converts to and from markdown (blocksToMar "content": undefined, "id": "3", "props": { + "alt": "Alt text", "backgroundColor": "default", "caption": "Caption", - "name": "Example", + "name": "Alt text", "showPreview": true, "textAlignment": "left", "url": "exampleURL",