diff --git a/packages/block-editor/src/components/block-list/block-wrapper.js b/packages/block-editor/src/components/block-list/block-wrapper.js index 863df1ff1d7f6c..964bef9008420a 100644 --- a/packages/block-editor/src/components/block-list/block-wrapper.js +++ b/packages/block-editor/src/components/block-list/block-wrapper.js @@ -219,7 +219,19 @@ const BlockComponent = forwardRef( } ); -const elements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul' ]; +const elements = [ + 'p', + 'div', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'ol', + 'ul', + 'figure', +]; const ExtendedBlockComponent = elements.reduce( ( acc, element ) => { acc[ element ] = forwardRef( ( props, ref ) => { diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss index 69739edde77539..97b0e902ca08bc 100644 --- a/packages/block-editor/src/components/block-list/style.scss +++ b/packages/block-editor/src/components/block-list/style.scss @@ -83,23 +83,27 @@ // Navigate mode & Focused wrapper. // We're using a pseudo element to overflow placeholder borders // and any border inside the block itself. - &:not([contenteditable]):focus::after { - position: absolute; - z-index: 1; - pointer-events: none; - content: ""; - top: 0; - bottom: 0; - left: 0; - right: 0; + &:not([contenteditable]):focus { + outline: none; - // 2px outside. - box-shadow: 0 0 0 2px $blue-medium-focus; - border-radius: $radius-block-ui; + &::after { + position: absolute; + z-index: 1; + pointer-events: none; + content: ""; + top: 0; + bottom: 0; + left: 0; + right: 0; - // Show a light color for dark themes. - .is-dark-theme & { - box-shadow: 0 0 0 2px $blue-medium-focus-dark; + // 2px outside. + box-shadow: 0 0 0 2px $blue-medium-focus; + border-radius: $radius-block-ui; + + // Show a light color for dark themes. + .is-dark-theme & { + box-shadow: 0 0 0 2px $blue-medium-focus-dark; + } } } @@ -243,6 +247,12 @@ cursor: default; } + .alignleft, + .alignright { + // Without z-index, won't be clickable as "above" adjacent content. + z-index: z-index(".block-editor-block-list__block {core/image aligned left or right}"); + } + // Alignments. &[data-align="left"], &[data-align="right"] { @@ -278,12 +288,15 @@ // Wide and full-wide. &[data-align="full"], - &[data-align="wide"] { + &[data-align="wide"], + &.alignfull, + &.alignwide { clear: both; } // Full-wide. - &[data-align="full"] { + &[data-align="full"], + &.alignfull { margin-left: -$block-padding; margin-right: -$block-padding; @@ -388,11 +401,6 @@ left: $block-padding; right: $block-padding; } - - &[data-align="full"] > .block-editor-block-list__insertion-point { - left: 0; - right: 0; - } } .block-editor-block-list__block .block-editor-block-list__block-html-textarea { diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index e844c14227e9e5..28edc6a340818a 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -29,10 +29,11 @@ import { MediaPlaceholder, MediaReplaceFlow, RichText, + __experimentalBlock as Block, __experimentalImageSizeControl as ImageSizeControl, __experimentalImageURLInputUI as ImageURLInputUI, } from '@wordpress/block-editor'; -import { Component } from '@wordpress/element'; +import { Component, Fragment } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { getPath } from '@wordpress/url'; import { withViewportMatch } from '@wordpress/viewport'; @@ -404,10 +405,13 @@ export class ImageEdit extends Component { src={ url } /> ); + const needsAlignmentWrapper = [ 'center', 'left', 'right' ].includes( + align + ); + const mediaPlaceholder = ( } - className={ className } labels={ labels } onSelect={ this.onSelectImage } onSelectURL={ this.onSelectURL } @@ -420,11 +424,25 @@ export class ImageEdit extends Component { disableMediaButtons={ url } /> ); + if ( ! url ) { return ( <> { controls } - { mediaPlaceholder } + + { needsAlignmentWrapper ? ( +
+ { mediaPlaceholder } +
+ ) : ( + mediaPlaceholder + ) } +
); } @@ -434,6 +452,7 @@ export class ImageEdit extends Component { 'is-resized': !! width || !! height, 'is-focused': isSelected, [ `size-${ sizeSlug }` ]: sizeSlug, + [ `align${ align }` ]: align, } ); const isResizable = @@ -497,188 +516,196 @@ export class ImageEdit extends Component { ); + const AlignmentWrapper = needsAlignmentWrapper ? Block.div : Fragment; + const BlockContentWrapper = needsAlignmentWrapper + ? 'figure' + : Block.figure; // Disable reason: Each block can be selected by clicking on it /* eslint-disable jsx-a11y/click-events-have-key-events */ return ( <> { controls } -
- - { ( sizes ) => { - const { - imageWidthWithinContainer, - imageHeightWithinContainer, - imageWidth, - imageHeight, - } = sizes; - - const filename = this.getFilename( url ); - let defaultedAlt; - if ( alt ) { - defaultedAlt = alt; - } else if ( filename ) { - defaultedAlt = sprintf( - __( - 'This image has an empty alt attribute; its file name is %s' - ), - filename - ); - } else { - defaultedAlt = __( - 'This image has an empty alt attribute' + + + + { ( sizes ) => { + const { + imageWidthWithinContainer, + imageHeightWithinContainer, + imageWidth, + imageHeight, + } = sizes; + + const filename = this.getFilename( url ); + let defaultedAlt; + if ( alt ) { + defaultedAlt = alt; + } else if ( filename ) { + defaultedAlt = sprintf( + __( + 'This image has an empty alt attribute; its file name is %s' + ), + filename + ); + } else { + defaultedAlt = __( + 'This image has an empty alt attribute' + ); + } + + const img = ( + // Disable reason: Image itself is not meant to be interactive, but + // should direct focus to block. + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ + <> + { + this.onImageError( url ) + } + /> + { isBlobURL( url ) && } + + /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ ); - } - const img = ( - // Disable reason: Image itself is not meant to be interactive, but - // should direct focus to block. - /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ - <> - { - this.onImageError( url ) - } - /> - { isBlobURL( url ) && } - - /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ - ); + if ( + ! isResizable || + ! imageWidthWithinContainer + ) { + return ( + <> + { getInspectorControls( + imageWidth, + imageHeight + ) } +
+ { img } +
+ + ); + } + + const currentWidth = + width || imageWidthWithinContainer; + const currentHeight = + height || imageHeightWithinContainer; + + const ratio = imageWidth / imageHeight; + const minWidth = + imageWidth < imageHeight + ? MIN_SIZE + : MIN_SIZE * ratio; + const minHeight = + imageHeight < imageWidth + ? MIN_SIZE + : MIN_SIZE / ratio; + + // With the current implementation of ResizableBox, an image needs an explicit pixel value for the max-width. + // In absence of being able to set the content-width, this max-width is currently dictated by the vanilla editor style. + // The following variable adds a buffer to this vanilla style, so 3rd party themes have some wiggleroom. + // This does, in most cases, allow you to scale the image beyond the width of the main column, though not infinitely. + // @todo It would be good to revisit this once a content-width variable becomes available. + const maxWidthBuffer = maxWidth * 2.5; + + let showRightHandle = false; + let showLeftHandle = false; + + /* eslint-disable no-lonely-if */ + // See https://github.com/WordPress/gutenberg/issues/7584. + if ( align === 'center' ) { + // When the image is centered, show both handles. + showRightHandle = true; + showLeftHandle = true; + } else if ( isRTL ) { + // In RTL mode the image is on the right by default. + // Show the right handle and hide the left handle only when it is aligned left. + // Otherwise always show the left handle. + if ( align === 'left' ) { + showRightHandle = true; + } else { + showLeftHandle = true; + } + } else { + // Show the left handle and hide the right handle only when the image is aligned right. + // Otherwise always show the right handle. + if ( align === 'right' ) { + showLeftHandle = true; + } else { + showRightHandle = true; + } + } + /* eslint-enable no-lonely-if */ - if ( - ! isResizable || - ! imageWidthWithinContainer - ) { return ( <> { getInspectorControls( imageWidth, imageHeight ) } -
+ { + onResizeStop(); + setAttributes( { + width: parseInt( + currentWidth + + delta.width, + 10 + ), + height: parseInt( + currentHeight + + delta.height, + 10 + ), + } ); + } } + > { img } -
+ ); - } - - const currentWidth = - width || imageWidthWithinContainer; - const currentHeight = - height || imageHeightWithinContainer; - - const ratio = imageWidth / imageHeight; - const minWidth = - imageWidth < imageHeight - ? MIN_SIZE - : MIN_SIZE * ratio; - const minHeight = - imageHeight < imageWidth - ? MIN_SIZE - : MIN_SIZE / ratio; - - // With the current implementation of ResizableBox, an image needs an explicit pixel value for the max-width. - // In absence of being able to set the content-width, this max-width is currently dictated by the vanilla editor style. - // The following variable adds a buffer to this vanilla style, so 3rd party themes have some wiggleroom. - // This does, in most cases, allow you to scale the image beyond the width of the main column, though not infinitely. - // @todo It would be good to revisit this once a content-width variable becomes available. - const maxWidthBuffer = maxWidth * 2.5; - - let showRightHandle = false; - let showLeftHandle = false; - - /* eslint-disable no-lonely-if */ - // See https://github.com/WordPress/gutenberg/issues/7584. - if ( align === 'center' ) { - // When the image is centered, show both handles. - showRightHandle = true; - showLeftHandle = true; - } else if ( isRTL ) { - // In RTL mode the image is on the right by default. - // Show the right handle and hide the left handle only when it is aligned left. - // Otherwise always show the left handle. - if ( align === 'left' ) { - showRightHandle = true; - } else { - showLeftHandle = true; + } } +
+ { ( ! RichText.isEmpty( caption ) || isSelected ) && ( + + setAttributes( { caption: value } ) } - } else { - // Show the left handle and hide the right handle only when the image is aligned right. - // Otherwise always show the right handle. - if ( align === 'right' ) { - showLeftHandle = true; - } else { - showRightHandle = true; - } - } - /* eslint-enable no-lonely-if */ - - return ( - <> - { getInspectorControls( - imageWidth, - imageHeight - ) } - { - onResizeStop(); - setAttributes( { - width: parseInt( - currentWidth + delta.width, - 10 - ), - height: parseInt( - currentHeight + - delta.height, - 10 - ), - } ); - } } - > - { img } - - - ); - } } -
- { ( ! RichText.isEmpty( caption ) || isSelected ) && ( - - setAttributes( { caption: value } ) - } - isSelected={ this.state.captionFocused } - inlineToolbar - /> - ) } -
- { mediaPlaceholder } + isSelected={ this.state.captionFocused } + inlineToolbar + /> + ) } + + { mediaPlaceholder } + + ); /* eslint-enable jsx-a11y/click-events-have-key-events */ diff --git a/packages/block-library/src/image/index.js b/packages/block-library/src/image/index.js index 332ab1afb062ca..4d855ca15cd107 100644 --- a/packages/block-library/src/image/index.js +++ b/packages/block-library/src/image/index.js @@ -25,6 +25,9 @@ export const settings = { 'img', // "img" is not translated as it is intended to reflect the HTML tag. __( 'photo' ), ], + supports: { + lightBlockWrapper: true, + }, example: { attributes: { sizeSlug: 'large', @@ -59,18 +62,6 @@ export const settings = { } }, transforms, - getEditWrapperProps( attributes ) { - const { align, width } = attributes; - if ( - 'left' === align || - 'center' === align || - 'right' === align || - 'wide' === align || - 'full' === align - ) { - return { 'data-align': align, 'data-resized': !! width }; - } - }, edit, save, deprecated, diff --git a/packages/block-library/src/image/test/edit.js b/packages/block-library/src/image/test/edit.js deleted file mode 100644 index a7919f15730f35..00000000000000 --- a/packages/block-library/src/image/test/edit.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * External dependencies - */ -import TestRenderer from 'react-test-renderer'; - -/** - * Internal dependencies - */ -import { ImageEdit } from '../edit'; - -describe( 'core/image/edit', () => { - describe( 'onSelectImage', () => { - test( 'should reset dimensions when changing the image and keep them on selecting the same image', () => { - const attributes = { - id: 1, - url: 'http://www.example.com/myimage.jpeg', - alt: 'alt1', - }; - const setAttributes = jest.fn( () => {} ); - const testRenderer = TestRenderer.create( - - ); - const instance = testRenderer.getInstance(); - - instance.onSelectImage( { - id: 1, - url: 'http://www.example.com/myimage.jpeg', - alt: 'alt2', - } ); - expect( setAttributes ).toHaveBeenCalledWith( { - id: 1, - url: 'http://www.example.com/myimage.jpeg', - alt: 'alt2', - } ); - instance.onSelectImage( { - id: 2, - url: 'http://www.example.com/myimage.jpeg', - alt: 'alt2', - } ); - expect( setAttributes ).toHaveBeenCalledWith( { - id: 2, - url: 'http://www.example.com/myimage.jpeg', - alt: 'alt2', - sizeSlug: 'large', - width: undefined, - height: undefined, - } ); - } ); - } ); -} ); diff --git a/packages/block-library/src/paragraph/index.js b/packages/block-library/src/paragraph/index.js index 57afe53dacaa32..7fa310086cb459 100644 --- a/packages/block-library/src/paragraph/index.js +++ b/packages/block-library/src/paragraph/index.js @@ -56,12 +56,6 @@ export const settings = { ( attributesToMerge.content || '' ), }; }, - getEditWrapperProps( attributes ) { - const { width } = attributes; - if ( [ 'wide', 'full', 'left', 'right' ].indexOf( width ) !== -1 ) { - return { 'data-align': width }; - } - }, edit, save, }; diff --git a/packages/e2e-tests/specs/editor/blocks/image.test.js b/packages/e2e-tests/specs/editor/blocks/image.test.js index 2d5a241dffe31e..51c4987857898f 100644 --- a/packages/e2e-tests/specs/editor/blocks/image.test.js +++ b/packages/e2e-tests/specs/editor/blocks/image.test.js @@ -13,8 +13,28 @@ import { insertBlock, getEditedPostContent, createNewPost, + clickButton, } from '@wordpress/e2e-test-utils'; +async function upload( selector ) { + await page.waitForSelector( selector ); + const inputElement = await page.$( selector ); + const testImagePath = path.join( + __dirname, + '..', + '..', + '..', + 'assets', + '10x10_e2e_test_image_z9T8jK.png' + ); + const filename = uuid(); + const tmpFileName = path.join( os.tmpdir(), filename + '.png' ); + fs.copyFileSync( testImagePath, tmpFileName ); + await inputElement.uploadFile( tmpFileName ); + await page.waitForSelector( '.wp-block-image img[src^="http"]' ); + return filename; +} + describe( 'Image', () => { beforeEach( async () => { await createNewPost(); @@ -22,28 +42,43 @@ describe( 'Image', () => { it( 'can be inserted', async () => { await insertBlock( 'Image' ); - await page.waitForSelector( '.wp-block-image input[type="file"]' ); - const inputElement = await page.$( - '.wp-block-image input[type="file"]' - ); - const testImagePath = path.join( - __dirname, - '..', - '..', - '..', - 'assets', - '10x10_e2e_test_image_z9T8jK.png' - ); - const filename = uuid(); - const tmpFileName = path.join( os.tmpdir(), filename + '.png' ); - fs.copyFileSync( testImagePath, tmpFileName ); - await inputElement.uploadFile( tmpFileName ); - await page.waitForSelector( '.wp-block-image img[src^="http"]' ); + const filename = await upload( '.wp-block-image input[type="file"]' ); - // Check the content. const regex = new RegExp( `\\s*
\\s*` ); expect( await getEditedPostContent() ).toMatch( regex ); } ); + + it( 'should replace, reset size, and keep selection', async () => { + await insertBlock( 'Image' ); + const filename1 = await upload( '.wp-block-image input[type="file"]' ); + + const regex1 = new RegExp( + `\\s*
\\s*` + ); + expect( await getEditedPostContent() ).toMatch( regex1 ); + + await page.click( '[aria-label="Image Size"] button' ); + + const regex2 = new RegExp( + `\\s*
<\\/figure>\\s*` + ); + + expect( await getEditedPostContent() ).toMatch( regex2 ); + + await clickButton( 'Replace' ); + const filename2 = await upload( + '.block-editor-media-replace-flow__options input[type="file"]' + ); + + const regex3 = new RegExp( + `\\s*
\\s*` + ); + expect( await getEditedPostContent() ).toMatch( regex3 ); + + await page.keyboard.press( 'Backspace' ); + + expect( await getEditedPostContent() ).toBe( '' ); + } ); } ); diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index 70bbe59a13e01c..651619f60da066 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -104,11 +104,13 @@ body.block-editor-page { .wp-block { max-width: $content-width; - &[data-align="wide"] { + &[data-align="wide"], + &.alignwide { max-width: 1100px; } - &[data-align="full"] { + &[data-align="full"], + &.alignfull { max-width: none; } } diff --git a/storybook/stories/playground/editor-styles.scss b/storybook/stories/playground/editor-styles.scss index 7337f68fe2bc70..d920ea8de5ba04 100644 --- a/storybook/stories/playground/editor-styles.scss +++ b/storybook/stories/playground/editor-styles.scss @@ -42,10 +42,12 @@ margin-left: auto; margin-right: auto; } - .wp-block[data-align="wide"] { + .wp-block[data-align="wide"], + .wp-block.alignwide { max-width: 1100px; } - .wp-block[data-align="full"] { + .wp-block[data-align="full"], + .wp-block.alignfull { max-width: none; } }