diff --git a/packages/block-library/src/image/editor.scss b/packages/block-library/src/image/editor.scss index f20f0afee5f06c..3e4bb7dd6392f0 100644 --- a/packages/block-library/src/image/editor.scss +++ b/packages/block-library/src/image/editor.scss @@ -66,7 +66,10 @@ figure.wp-block-image:not(.wp-block) { // This is necessary for the editor resize handles to accurately work on a non-floated, non-resized, small image. .wp-block-image .components-resizable-box__container { - display: inline-block; + // Using "display: table" because: + // - it visually hides empty white space in between elements + // - it allows the element to be as wide as its contents (instead of 100% width, as it would be with `display: block`) + display: table; img { display: block; width: inherit; diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index ff0b0dddde561f..12e7f6dd96d2f1 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -40,7 +40,12 @@ import { getDefaultBlockName, switchToBlockType, } from '@wordpress/blocks'; -import { crop, overlayText, upload } from '@wordpress/icons'; +import { + crop, + overlayText, + upload, + caption as captionIcon, +} from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; @@ -89,7 +94,8 @@ export default function Image( { } = attributes; const imageRef = useRef(); const captionRef = useRef(); - const prevUrl = usePrevious( url ); + const prevCaption = usePrevious( caption ); + const [ showCaption, setShowCaption ] = useState( !! caption ); const { allowResize = true } = context; const { getBlock } = useSelect( blockEditorStore ); @@ -180,15 +186,20 @@ export default function Image( { .catch( () => {} ); }, [ id, url, isSelected, externalBlob ] ); - // Focus the caption after inserting an image from the placeholder. This is - // done to preserve the behaviour of focussing the first tabbable element - // when a block is mounted. Previously, the image block would remount when - // the placeholder is removed. Maybe this behaviour could be removed. + // We need to show the caption when changes come from + // history navigation(undo/redo). + useEffect( () => { + if ( caption && ! prevCaption ) { + setShowCaption( true ); + } + }, [ caption, prevCaption ] ); + + // Focus the caption when we click to add one. useEffect( () => { - if ( url && ! prevUrl && isSelected ) { - captionRef.current.focus(); + if ( showCaption && ! caption ) { + captionRef.current?.focus(); } - }, [ url, prevUrl ] ); + }, [ caption, showCaption ] ); // Get naturalWidth and naturalHeight from image ref, and fall back to loaded natural // width and height. This resolves an issue in Safari where the loaded natural @@ -297,8 +308,11 @@ export default function Image( { useEffect( () => { if ( ! isSelected ) { setIsEditingImage( false ); + if ( ! caption ) { + setShowCaption( false ); + } } - }, [ isSelected ] ); + }, [ isSelected, caption ] ); const canEditImage = id && naturalWidth && naturalHeight && imageEditing; const allowCrop = ! multiImageSelection && canEditImage && ! isEditingImage; @@ -319,6 +333,19 @@ export default function Image( { onChange={ updateAlignment } /> ) } + { ! isContentLocked && ( + { + setShowCaption( ! showCaption ); + if ( showCaption && caption ) { + setAttributes( { caption: undefined } ); + } + } } + icon={ captionIcon } + isPressed={ showCaption } + label={ __( 'Caption' ) } + /> + ) } { ! multiImageSelection && ! isEditingImage && ( - setAttributes( { caption: value } ) - } - inlineToolbar - __unstableOnSplitAtEnd={ () => - insertBlocksAfter( - createBlock( getDefaultBlockName() ) - ) - } - /> - ) } + { showCaption && + ( ! RichText.isEmpty( caption ) || isSelected ) && ( + + setAttributes( { caption: value } ) + } + inlineToolbar + __unstableOnSplitAtEnd={ () => + insertBlocksAfter( + createBlock( getDefaultBlockName() ) + ) + } + /> + ) } ); } diff --git a/packages/e2e-tests/specs/editor/blocks/gallery.test.js b/packages/e2e-tests/specs/editor/blocks/gallery.test.js index 8ffe087f2668a9..068975ba56409d 100644 --- a/packages/e2e-tests/specs/editor/blocks/gallery.test.js +++ b/packages/e2e-tests/specs/editor/blocks/gallery.test.js @@ -16,6 +16,7 @@ import { clickButton, openListView, getListViewBlocks, + clickBlockToolbarButton, } from '@wordpress/e2e-test-utils'; async function upload( selector ) { @@ -110,7 +111,7 @@ describe( 'Gallery', () => { const imageListLink = ( await getListViewBlocks( 'Image' ) )[ 0 ]; await imageListLink.click(); - + await clickBlockToolbarButton( 'Caption' ); const captionElement = await figureElement.$( '.block-editor-rich-text__editable' ); diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index bfc32a594d33c5..f5071f80d1a1e3 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -27,6 +27,7 @@ export { default as button } from './library/button'; export { default as buttons } from './library/buttons'; export { default as calendar } from './library/calendar'; export { default as cancelCircleFilled } from './library/cancel-circle-filled'; +export { default as caption } from './library/caption'; export { default as capturePhoto } from './library/capture-photo'; export { default as captureVideo } from './library/capture-video'; export { default as category } from './library/category'; diff --git a/packages/icons/src/library/caption.js b/packages/icons/src/library/caption.js new file mode 100644 index 00000000000000..b6fd950c31b51a --- /dev/null +++ b/packages/icons/src/library/caption.js @@ -0,0 +1,16 @@ +/** + * WordPress dependencies + */ +import { Path, SVG } from '@wordpress/primitives'; + +const caption = ( + + + +); + +export default caption; diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index a86f9c1d20443b..0413aa3e33e012 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -141,7 +141,7 @@ test.describe( 'Image', () => { } } ); - test( 'should place caret at end of caption after merging empty paragraph', async ( { + test( 'should place caret on caption when clicking to add one', async ( { editor, page, imageBlockUtils, @@ -157,7 +157,7 @@ test.describe( 'Image', () => { imageBlock.locator( 'data-testid=form-file-upload-input' ) ); await expect( image ).toHaveAttribute( 'src', new RegExp( filename ) ); - + await editor.clickBlockToolbarButton( 'Caption' ); await page.keyboard.type( '1' ); await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'Backspace' ); @@ -186,7 +186,7 @@ test.describe( 'Image', () => { await expect( image ).toBeVisible(); await expect( image ).toHaveAttribute( 'src', new RegExp( fileName ) ); - + await editor.clickBlockToolbarButton( 'Caption' ); await page.keyboard.type( '12' ); await page.keyboard.press( 'ArrowLeft' ); await page.keyboard.press( 'Enter' ); @@ -216,7 +216,8 @@ test.describe( 'Image', () => { await expect( image ).toBeVisible(); await expect( image ).toHaveAttribute( 'src', new RegExp( fileName ) ); - // Navigate to inline toolbar, + // Add caption and navigate to inline toolbar. + await editor.clickBlockToolbarButton( 'Caption' ); await pageUtils.pressKeyWithModifier( 'shift', 'Tab' ); await expect( await page.evaluate( () => @@ -516,7 +517,7 @@ test.describe( 'Image', () => { ); await expect( image ).toHaveAttribute( 'src', new RegExp( filename ) ); - + await page.focus( '.wp-block-image' ); await pageUtils.pressKeyWithModifier( 'primary', 'z' ); // Expect an empty image block (placeholder) rather than one with a