diff --git a/packages/block-library/src/gallery/test/__snapshots__/index.native.js.snap b/packages/block-library/src/gallery/test/__snapshots__/index.native.js.snap
index ee156d806d4ba6..42c0c29e11d586 100644
--- a/packages/block-library/src/gallery/test/__snapshots__/index.native.js.snap
+++ b/packages/block-library/src/gallery/test/__snapshots__/index.native.js.snap
@@ -121,7 +121,7 @@ exports[`Gallery block sets caption to gallery 1`] = `
exports[`Gallery block sets caption to gallery items 1`] = `
"
"
`;
diff --git a/packages/block-library/src/gallery/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/gallery/test/__snapshots__/transforms.native.js.snap
index 5c9c04e137dea4..7ba041de8a89ce 100644
--- a/packages/block-library/src/gallery/test/__snapshots__/transforms.native.js.snap
+++ b/packages/block-library/src/gallery/test/__snapshots__/transforms.native.js.snap
@@ -5,15 +5,15 @@ exports[`Gallery block transformations to Columns block 1`] = `
-Paragraph
+Paragraph
-Heading
+Heading
-Subheading
+Subheading
@@ -24,15 +24,15 @@ exports[`Gallery block transformations to Group block 1`] = `
"
-Paragraph
+Paragraph
-Heading
+Heading
-Subheading
+Subheading
"
@@ -40,14 +40,14 @@ exports[`Gallery block transformations to Group block 1`] = `
exports[`Gallery block transformations to Image block 1`] = `
"
-Paragraph
+Paragraph
-Heading
+Heading
-Subheading
+Subheading
"
`;
diff --git a/packages/block-library/src/image/deprecated.js b/packages/block-library/src/image/deprecated.js
index 9b7a41cab188de..bf3ae6a64ce96f 100644
--- a/packages/block-library/src/image/deprecated.js
+++ b/packages/block-library/src/image/deprecated.js
@@ -6,7 +6,12 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { RichText, useBlockProps } from '@wordpress/block-editor';
+import {
+ RichText,
+ useBlockProps,
+ __experimentalGetElementClassName,
+ __experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles,
+} from '@wordpress/block-editor';
const blockAttributes = {
align: {
@@ -93,6 +98,183 @@ const blockSupports = {
};
const deprecated = [
+ // Deprecate the version that does not use the caption to describe the image
+ // when the image has a caption but no alt text.
+ {
+ attributes: {
+ align: {
+ type: 'string',
+ },
+ url: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'src',
+ __experimentalRole: 'content',
+ },
+ alt: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'alt',
+ default: '',
+ __experimentalRole: 'content',
+ },
+ caption: {
+ type: 'string',
+ source: 'html',
+ selector: 'figcaption',
+ __experimentalRole: 'content',
+ },
+ title: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'title',
+ __experimentalRole: 'content',
+ },
+ href: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'href',
+ __experimentalRole: 'content',
+ },
+ rel: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'rel',
+ },
+ linkClass: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'class',
+ },
+ id: {
+ type: 'number',
+ __experimentalRole: 'content',
+ },
+ width: {
+ type: 'number',
+ },
+ height: {
+ type: 'number',
+ },
+ sizeSlug: {
+ type: 'string',
+ },
+ linkDestination: {
+ type: 'string',
+ },
+ linkTarget: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'target',
+ },
+ },
+ supports: {
+ anchor: true,
+ color: {
+ text: false,
+ background: false,
+ },
+ filter: {
+ duotone: true,
+ },
+ __experimentalBorder: {
+ color: true,
+ radius: true,
+ width: true,
+ __experimentalSelector: 'img, .wp-block-image__crop-area',
+ __experimentalSkipSerialization: true,
+ __experimentalDefaultControls: {
+ color: true,
+ radius: true,
+ width: true,
+ },
+ },
+ },
+ save( { attributes } ) {
+ const {
+ url,
+ alt,
+ caption,
+ align,
+ href,
+ rel,
+ linkClass,
+ width,
+ height,
+ id,
+ linkTarget,
+ sizeSlug,
+ title,
+ } = attributes;
+
+ const newRel = ! rel ? undefined : rel;
+ const borderProps = getBorderClassesAndStyles( attributes );
+
+ const classes = classnames( {
+ [ `align${ align }` ]: align,
+ [ `size-${ sizeSlug }` ]: sizeSlug,
+ 'is-resized': width || height,
+ 'has-custom-border':
+ !! borderProps.className ||
+ ( borderProps.style &&
+ Object.keys( borderProps.style ).length > 0 ),
+ } );
+
+ const imageClasses = classnames( borderProps.className, {
+ [ `wp-image-${ id }` ]: !! id,
+ } );
+
+ const image = (
+
+ );
+
+ const figure = (
+ <>
+ { href ? (
+
+ { image }
+
+ ) : (
+ image
+ ) }
+ { ! RichText.isEmpty( caption ) && (
+
+ ) }
+ >
+ );
+ return (
+
+ { figure }
+
+ );
+ },
+ },
// The following deprecation moves existing border radius styles onto the
// inner img element where new border block support styles must be applied.
// It will also add a new `.has-custom-border` class for existing blocks
diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js
index 90850e4253b357..d632e4b04e3c9a 100644
--- a/packages/block-library/src/image/image.js
+++ b/packages/block-library/src/image/image.js
@@ -325,6 +325,19 @@ export default function Image( {
);
}
+ let describedById = 'wp-image-caption';
+ if ( url ) {
+ // Use the hashed url to create an ID to use with aria-describedby.
+ const hashString = ( str ) =>
+ str
+ .split( '' )
+ .map( ( c ) =>
+ c.charCodeAt( 0 ).toString( 32 ).padStart( 2, '0' )
+ )
+ .join( '' );
+ describedById = `wp-image-caption-${ hashString( url ) }`;
+ }
+
const controls = (
<>
@@ -487,6 +500,11 @@ export default function Image( {
onImageError() }
onLoad={ ( event ) => {
setLoadedNaturalSize( {
@@ -637,6 +655,11 @@ export default function Image( {
aria-label={ __( 'Image caption text' ) }
placeholder={ __( 'Add caption' ) }
value={ caption }
+ id={
+ ! alt && ! RichText.isEmpty( caption )
+ ? describedById
+ : undefined
+ }
onChange={ ( value ) =>
setAttributes( { caption: value } )
}
diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php
index 828211f4f135d6..532ecb3379f945 100644
--- a/packages/block-library/src/image/index.php
+++ b/packages/block-library/src/image/index.php
@@ -11,7 +11,7 @@
*
* @param array $attributes The block attributes.
* @param string $content The block content.
- * @return string Returns the block content with the data-id attribute added.
+ * @return string Returns the block content with the data-id attribute and aria-describedby added.
*/
function render_block_core_image( $attributes, $content ) {
@@ -22,12 +22,43 @@ function render_block_core_image( $attributes, $content ) {
return '';
}
+ // Check if the image block has an alternative text.
+ $find_alt_attribute = new WP_HTML_Tag_Processor( $content );
+ $find_alt_attribute->next_tag( array( 'tag_name' => 'img' ) );
+ $alt = $find_alt_attribute->get_attribute( 'alt' ) ? $find_alt_attribute->get_attribute( 'alt' ) : '';
+
+ // If an image block has no alternative text but has a caption,
+ // and aria-describedby is not set, add aria-describedby to the image or image link.
+ if ( empty( $alt ) &&
+ str_contains( $content, 'wp-element-caption' ) &&
+ ! str_contains( $content, 'aria-describedby' )
+ ) {
+ $unique_id = wp_unique_id( 'wp-image-caption-' );
+ $processed_content = new WP_HTML_Tag_Processor( $content );
+ if ( str_contains( $content, 'href' ) ) {
+ $processed_content->next_tag( array( 'tag_name' => 'a' ) );
+ $processed_content->set_attribute( 'aria-describedby', $unique_id );
+ } else {
+ $processed_content->next_tag( array( 'tag_name' => 'img' ) );
+ $processed_content->set_attribute( 'aria-describedby', $unique_id );
+ }
+ $processed_content->next_tag(
+ array(
+ 'tag_name' => 'figcaption',
+ 'class_name' => 'wp-element-caption',
+ )
+ );
+ $processed_content->set_attribute( 'id', $unique_id );
+ $content = $processed_content->get_updated_html();
+ }
+
if ( isset( $attributes['data-id'] ) ) {
// Add the data-id="$id" attribute to the img element
// to provide backwards compatibility for the Gallery Block,
// which now wraps Image Blocks within innerBlocks.
// The data-id attribute is added in a core/gallery `render_block_data` hook.
$processor->set_attribute( 'data-id', $attributes['data-id'] );
+ $content = $processor->get_updated_html();
}
$link_destination = isset( $attributes['linkDestination'] ) ? $attributes['linkDestination'] : 'none';
@@ -102,7 +133,7 @@ function render_block_core_image( $attributes, $content ) {
HTML;
}
- return $processor->get_updated_html();
+ return $content;
}
/**
diff --git a/packages/block-library/src/image/save.js b/packages/block-library/src/image/save.js
index d0fd5ef3d6f98b..5c18997b469b99 100644
--- a/packages/block-library/src/image/save.js
+++ b/packages/block-library/src/image/save.js
@@ -47,10 +47,28 @@ export default function save( { attributes } ) {
[ `wp-image-${ id }` ]: !! id,
} );
+ let describedById = 'wp-image-caption';
+ if ( url ) {
+ // Use the hashed url to create an ID to use with aria-describedby.
+ const hashString = ( str ) =>
+ str
+ .split( '' )
+ .map( ( c ) =>
+ c.charCodeAt( 0 ).toString( 32 ).padStart( 2, '0' )
+ )
+ .join( '' );
+ describedById = `wp-image-caption-${ hashString( url ) }`;
+ }
+
const image = (
{ image }
@@ -78,6 +101,11 @@ export default function save( { attributes } ) {
className={ __experimentalGetElementClassName( 'caption' ) }
tagName="figcaption"
value={ caption }
+ id={
+ ! alt && ! RichText.isEmpty( caption )
+ ? describedById
+ : undefined
+ }
/>
) }
>
diff --git a/packages/block-library/src/image/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/image/test/__snapshots__/transforms.native.js.snap
index 56a5fe3c259120..4c846766549edf 100644
--- a/packages/block-library/src/image/test/__snapshots__/transforms.native.js.snap
+++ b/packages/block-library/src/image/test/__snapshots__/transforms.native.js.snap
@@ -4,7 +4,7 @@ exports[`Image block transformations to Columns block 1`] = `
"
-
Mountain
+
Mountain
"
@@ -27,7 +27,7 @@ exports[`Image block transformations to File block 1`] = `
exports[`Image block transformations to Gallery block 1`] = `
"
-Mountain
+Mountain
"
`;
@@ -35,7 +35,7 @@ exports[`Image block transformations to Gallery block 1`] = `
exports[`Image block transformations to Group block 1`] = `
"
-
Mountain
+
Mountain
"
`;
diff --git a/packages/block-library/src/image/test/edit.native.js b/packages/block-library/src/image/test/edit.native.js
index 1f1bdd6ae4eda5..5807e1a6c1c72a 100644
--- a/packages/block-library/src/image/test/edit.native.js
+++ b/packages/block-library/src/image/test/edit.native.js
@@ -94,10 +94,10 @@ describe( 'Image Block', () => {
const initialHtml = `
-
+
- Mountain
+ Mountain
`;
const screen = await initializeEditor( { initialHtml } );
// Check that image is fetched via `getMedia`
@@ -114,7 +114,7 @@ describe( 'Image Block', () => {
fireEvent.press( screen.getByText( 'None' ) );
const expectedHtml = `
-Mountain
+Mountain
`;
expect( getEditorHtml() ).toBe( expectedHtml );
} );
@@ -123,8 +123,8 @@ describe( 'Image Block', () => {
const initialHtml = `
-
- Mountain
+
+ Mountain
`;
const screen = await initializeEditor( { initialHtml } );
// Check that image is fetched via `getMedia`
@@ -141,7 +141,7 @@ describe( 'Image Block', () => {
fireEvent.press( screen.getByText( 'Media File' ) );
const expectedHtml = `
-Mountain
+Mountain
`;
expect( getEditorHtml() ).toBe( expectedHtml );
} );
@@ -150,8 +150,8 @@ describe( 'Image Block', () => {
const initialHtml = `
-
- Mountain
+
+ Mountain
`;
const screen = await initializeEditor( { initialHtml } );
// Check that image is fetched via `getMedia`
@@ -178,7 +178,7 @@ describe( 'Image Block', () => {
);
const expectedHtml = `
-Mountain
+Mountain
`;
expect( getEditorHtml() ).toBe( expectedHtml );
} );
@@ -187,8 +187,8 @@ describe( 'Image Block', () => {
const initialHtml = `
-
- Mountain
+
+ Mountain
`;
const screen = await initializeEditor( { initialHtml } );
// Check that image is fetched via `getMedia`
@@ -218,7 +218,7 @@ describe( 'Image Block', () => {
fireEvent.press( screen.getByText( 'Media File' ) );
const expectedHtml = `
-Mountain
+Mountain
`;
expect( getEditorHtml() ).toBe( expectedHtml );
} );
@@ -227,10 +227,10 @@ describe( 'Image Block', () => {
const initialHtml = `
-
+
- Mountain
+ Mountain
`;
const screen = await initializeEditor( { initialHtml } );
// Check that image is not fetched via `getMedia` due to the presence of query parameters in the URL.
@@ -252,10 +252,10 @@ describe( 'Image Block', () => {
const initialHtml = `
-
+
- Mountain
+ Mountain
`;
const screen = await initializeEditor( { initialHtml } );
// Check that image is fetched via `getMedia`
@@ -273,7 +273,7 @@ describe( 'Image Block', () => {
fireEvent.press( linkTargetButton );
const expectedHtml = `
-Mountain
+Mountain
`;
expect( getEditorHtml() ).toBe( expectedHtml );
} );
@@ -282,10 +282,10 @@ describe( 'Image Block', () => {
const initialHtml = `
-
+
- Mountain
+ Mountain
`;
const screen = await initializeEditor( { initialHtml } );
@@ -304,7 +304,7 @@ describe( 'Image Block', () => {
fireEvent.press( linkTargetButton );
const expectedHtml = `
-Mountain
+Mountain
`;
expect( getEditorHtml() ).toBe( expectedHtml );
} );
diff --git a/packages/react-native-editor/src/initial-html.js b/packages/react-native-editor/src/initial-html.js
index 9cf963fa7bee88..09afccd03063dd 100644
--- a/packages/react-native-editor/src/initial-html.js
+++ b/packages/react-native-editor/src/initial-html.js
@@ -59,7 +59,7 @@ export const mediaBlocks = `
-Mountain
+Mountain
@@ -80,67 +80,67 @@ export const mediaBlocks = `
-Paragraph
+Paragraph
-Heading
+Heading
-Subheading
+Subheading
-Quote
+Quote
-Image
+Image
-Gallery
+Gallery
-Cover Image
+Cover Image
-Video
+Video
-Audio
+Audio
-Columns
+Columns
-File
+File
-Code
+Code
-List
+List
-Button
+Button
-Embeds
+Embeds
-More
+More
diff --git a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap
index 7ae0dc9b886893..fbd6c44394a875 100644
--- a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap
+++ b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap
@@ -157,13 +157,13 @@ exports[`rawHandler should convert HTML post to blocks with minimal content chan
exports[`rawHandler should convert a caption shortcode 1`] = `
"
-test
+test
"
`;
exports[`rawHandler should convert a caption shortcode with caption 1`] = `
"
-test
+test
"
`;
diff --git a/test/integration/fixtures/blocks/core__image__center-caption.html b/test/integration/fixtures/blocks/core__image__center-caption.html
index 1a221343202d06..00c711f25500d1 100644
--- a/test/integration/fixtures/blocks/core__image__center-caption.html
+++ b/test/integration/fixtures/blocks/core__image__center-caption.html
@@ -1,3 +1,3 @@
-
-Give it a try. Press the "really wide" button on the image toolbar.
-
+
+Give it a try. Press the "really wide" button on the image toolbar.
+
\ No newline at end of file
diff --git a/test/integration/fixtures/blocks/core__image__center-caption.parsed.json b/test/integration/fixtures/blocks/core__image__center-caption.parsed.json
index 1875086370f42f..74db36c143cf50 100644
--- a/test/integration/fixtures/blocks/core__image__center-caption.parsed.json
+++ b/test/integration/fixtures/blocks/core__image__center-caption.parsed.json
@@ -5,9 +5,9 @@
"align": "center"
},
"innerBlocks": [],
- "innerHTML": "\nGive it a try. Press the "really wide" button on the image toolbar.\n",
+ "innerHTML": "\nGive it a try. Press the "really wide" button on the image toolbar.\n",
"innerContent": [
- "\nGive it a try. Press the "really wide" button on the image toolbar.\n"
+ "\nGive it a try. Press the "really wide" button on the image toolbar.\n"
]
}
]
diff --git a/test/integration/fixtures/blocks/core__image__center-caption.serialized.html b/test/integration/fixtures/blocks/core__image__center-caption.serialized.html
index 75f992e1c441b9..b41600511448cc 100644
--- a/test/integration/fixtures/blocks/core__image__center-caption.serialized.html
+++ b/test/integration/fixtures/blocks/core__image__center-caption.serialized.html
@@ -1,3 +1,3 @@
-Give it a try. Press the "really wide" button on the image toolbar.
+Give it a try. Press the "really wide" button on the image toolbar.
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.html b/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.html
new file mode 100644
index 00000000000000..1a221343202d06
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.html
@@ -0,0 +1,3 @@
+
+Give it a try. Press the "really wide" button on the image toolbar.
+
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.json b/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.json
new file mode 100644
index 00000000000000..a369e433b4028e
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.json
@@ -0,0 +1,13 @@
+[
+ {
+ "name": "core/image",
+ "isValid": true,
+ "attributes": {
+ "align": "center",
+ "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==",
+ "alt": "",
+ "caption": "Give it a try. Press the \"really wide\" button on the image toolbar."
+ },
+ "innerBlocks": []
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.parsed.json b/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.parsed.json
new file mode 100644
index 00000000000000..1875086370f42f
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.parsed.json
@@ -0,0 +1,13 @@
+[
+ {
+ "blockName": "core/image",
+ "attrs": {
+ "align": "center"
+ },
+ "innerBlocks": [],
+ "innerHTML": "\nGive it a try. Press the "really wide" button on the image toolbar.\n",
+ "innerContent": [
+ "\nGive it a try. Press the "really wide" button on the image toolbar.\n"
+ ]
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.serialized.html b/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.serialized.html
new file mode 100644
index 00000000000000..b41600511448cc
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__image__deprecated-center-caption-6.serialized.html
@@ -0,0 +1,3 @@
+
+Give it a try. Press the "really wide" button on the image toolbar.
+
diff --git a/test/integration/fixtures/blocks/core__image__with-alt-text.html b/test/integration/fixtures/blocks/core__image__with-alt-text.html
new file mode 100644
index 00000000000000..0313a1f0102db3
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__image__with-alt-text.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/test/integration/fixtures/blocks/core__image__with-alt-text.json b/test/integration/fixtures/blocks/core__image__with-alt-text.json
new file mode 100644
index 00000000000000..2b115384860777
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__image__with-alt-text.json
@@ -0,0 +1,12 @@
+[
+ {
+ "name": "core/image",
+ "isValid": true,
+ "attributes": {
+ "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==",
+ "alt": "Sample alt text",
+ "caption": ""
+ },
+ "innerBlocks": []
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__image__with-alt-text.parsed.json b/test/integration/fixtures/blocks/core__image__with-alt-text.parsed.json
new file mode 100644
index 00000000000000..67cf5fbd5d557d
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__image__with-alt-text.parsed.json
@@ -0,0 +1,11 @@
+[
+ {
+ "blockName": "core/image",
+ "attrs": {},
+ "innerBlocks": [],
+ "innerHTML": "\n\n",
+ "innerContent": [
+ "\n\n"
+ ]
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__image__with-alt-text.serialized.html b/test/integration/fixtures/blocks/core__image__with-alt-text.serialized.html
new file mode 100644
index 00000000000000..99b38547382373
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__image__with-alt-text.serialized.html
@@ -0,0 +1,3 @@
+
+
+