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 ee156d806d4ba..42c0c29e11d58 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 5c9c04e137dea..7ba041de8a89c 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`] = `
@@ -24,15 +24,15 @@ exports[`Gallery block transformations to Group block 1`] = ` "
" @@ -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 9b7a41cab188d..bf3ae6a64ce96 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 90850e4253b35..d632e4b04e3c9 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 828211f4f135d..532ecb3379f94 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 d0fd5ef3d6f98..5c18997b469b9 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 56a5fe3c25912..4c846766549ed 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`] = ` " " `; @@ -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 1f1bdd6ae4eda..5807e1a6c1c72 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 9cf963fa7bee8..09afccd03063d 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 = ` diff --git a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap index 7ae0dc9b88689..fbd6c44394a87 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 1a221343202d0..00c711f25500d 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 1875086370f42..74db36c143cf5 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": "\n
\"\"
Give it a try. Press the "really wide" button on the image toolbar.
\n", + "innerHTML": "\n
\"\"
Give it a try. Press the "really wide" button on the image toolbar.
\n", "innerContent": [ - "\n
\"\"
Give it a try. Press the "really wide" button on the image toolbar.
\n" + "\n
\"\"
Give 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 75f992e1c441b..b41600511448c 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 0000000000000..1a221343202d0 --- /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 0000000000000..a369e433b4028 --- /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 0000000000000..1875086370f42 --- /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": "\n
\"\"
Give it a try. Press the "really wide" button on the image toolbar.
\n", + "innerContent": [ + "\n
\"\"
Give 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 0000000000000..b41600511448c --- /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 0000000000000..0313a1f0102db --- /dev/null +++ b/test/integration/fixtures/blocks/core__image__with-alt-text.html @@ -0,0 +1,3 @@ + +
Sample alt text
+ 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 0000000000000..2b11538486077 --- /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 0000000000000..67cf5fbd5d557 --- /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
\"Sample
\n", + "innerContent": [ + "\n
\"Sample
\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 0000000000000..99b3854738237 --- /dev/null +++ b/test/integration/fixtures/blocks/core__image__with-alt-text.serialized.html @@ -0,0 +1,3 @@ + +
Sample alt text
+