diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index cc1ee80f19420c..ea03f049f27c81 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -81,6 +81,10 @@ _Related_ - +# **Block** + +Undocumented declaration. + # **BlockAlignmentToolbar** Undocumented declaration. diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 0d674919bc4dfb..7796d870819cbc 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -8,7 +8,7 @@ import { animated } from 'react-spring/web.cjs'; /** * WordPress dependencies */ -import { useRef, useEffect, useLayoutEffect, useState, useContext } from '@wordpress/element'; +import { useRef, useEffect, useLayoutEffect, useState, useContext, forwardRef, createContext, useMemo } from '@wordpress/element'; import { focus, isTextField, @@ -21,6 +21,7 @@ import { isReusableBlock, isUnmodifiedDefaultBlock, getUnregisteredTypeHandlerName, + hasBlockSupport, } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; @@ -44,39 +45,18 @@ import { isInsideRootBlock } from '../../utils/dom'; import useMovingAnimation from './moving-animation'; import { Context, BlockNodes } from './root-container'; -function BlockListBlock( { - mode, - isFocusMode, - isLocked, - clientId, - isSelected, - isMultiSelected, - isPartOfMultiSelection, - isFirstMultiSelected, - isLastMultiSelected, - isTypingWithinBlock, - isEmptyDefaultBlock, - isAncestorOfSelectedBlock, - isSelectionEnabled, - className, - name, - isValid, - attributes, - initialPosition, - wrapperProps, - setAttributes, - onReplace, - onInsertBlocksAfter, - onMerge, - onRemove, - onInsertDefaultBlockAfter, - toggleSelection, - animateOnChange, - enableAnimation, - isNavigationMode, - isMultiSelecting, - hasSelectedUI = true, -} ) { +const BlockContext = createContext(); + +export const BlockComponent = forwardRef( ( { + children, + tagName = 'div', + __unstableIsHtml, + ...props +}, ref ) => { + const fallbackRef = useRef(); + + ref = ref || fallbackRef; + const onSelectionStart = useContext( Context ); const [ , setBlockNodes ] = useContext( BlockNodes ); // In addition to withSelect, we should favor using useSelect in this component going forward @@ -87,8 +67,33 @@ function BlockListBlock( { }; }, [] ); - // Reference of the wrapper - const wrapper = useRef( null ); + const { + clientId, + initialPosition, + isSelected, + isFirstMultiSelected, + isLastMultiSelected, + isMultiSelecting, + isNavigationMode, + isPartOfMultiSelection, + enableAnimation, + animateOnChange, + onInsertDefaultBlockAfter, + onRemove, + isFocusMode, + isTypingWithinBlock, + hasSelectedUI, + isValid, + hasError, + isEmptyDefaultBlock, + isMultiSelected, + isAncestorOfSelectedBlock, + className, + isLocked, + name, + wrapperProps, + mode, + } = useContext( BlockContext ); // Provide the selected node, or the first and last nodes of a multi- // selection, so it can be used to position the contextual block toolbar. @@ -96,7 +101,7 @@ function BlockListBlock( { // are no longer selected. useLayoutEffect( () => { if ( isSelected || isFirstMultiSelected || isLastMultiSelected ) { - const node = wrapper.current; + const node = ref.current; setBlockNodes( ( nodes ) => ( { ...nodes, [ clientId ]: node } ) ); return () => { setBlockNodes( ( nodes ) => omit( nodes, clientId ) ); @@ -104,10 +109,6 @@ function BlockListBlock( { } }, [ isSelected, isFirstMultiSelected, isLastMultiSelected ] ); - // Handling the error state - const [ hasError, setErrorState ] = useState( false ); - const onBlockError = () => setErrorState( true ); - const blockType = getBlockType( name ); // translators: %s: Type of block (i.e. Text, Image etc) const blockLabel = sprintf( __( 'Block: %s' ), blockType.title ); @@ -124,16 +125,16 @@ function BlockListBlock( { // should only consider tabbables within editable display, since it // may be the wrapper itself or a side control which triggered the // focus event, don't unnecessary transition to an inner tabbable. - if ( wrapper.current.contains( document.activeElement ) ) { + if ( ref.current.contains( document.activeElement ) ) { return; } // Find all tabbables within node. const textInputs = focus.tabbable - .find( wrapper.current ) + .find( ref.current ) .filter( isTextField ) // Exclude inner blocks - .filter( ( node ) => ! ignoreInnerBlocks || isInsideRootBlock( wrapper.current, node ) ); + .filter( ( node ) => ! ignoreInnerBlocks || isInsideRootBlock( ref.current, node ) ); // If reversed (e.g. merge via backspace), use the last in the set of // tabbables. @@ -141,7 +142,7 @@ function BlockListBlock( { const target = ( isReverse ? last : first )( textInputs ); if ( ! target ) { - wrapper.current.focus(); + ref.current.focus(); return; } @@ -164,7 +165,7 @@ function BlockListBlock( { ] ); // Block Reordering animation - const animationStyle = useMovingAnimation( wrapper, isSelected || isPartOfMultiSelection, isSelected || isFirstMultiSelected, enableAnimation, animateOnChange ); + const animationStyle = useMovingAnimation( ref, isSelected || isPartOfMultiSelection, isSelected || isFirstMultiSelected, enableAnimation, animateOnChange ); // Other event handlers @@ -179,9 +180,14 @@ function BlockListBlock( { const onKeyDown = ( event ) => { const { keyCode, target } = event; + if ( props.onKeyDown ) { + props.onKeyDown( event ); + return; + } + switch ( keyCode ) { case ENTER: - if ( target === wrapper.current ) { + if ( target === ref.current ) { // Insert default block after current block if enter and event // not already handled by descendant. onInsertDefaultBlockAfter(); @@ -190,7 +196,7 @@ function BlockListBlock( { break; case BACKSPACE: case DELETE: - if ( target === wrapper.current ) { + if ( target === ref.current ) { // Remove block on backspace. onRemove( clientId ); event.preventDefault(); @@ -222,15 +228,6 @@ function BlockListBlock( { ! isTypingWithinBlock; const isDragging = isDraggingBlocks && ( isSelected || isPartOfMultiSelection ); - - // Determine whether the block has props to apply to the wrapper. - if ( blockType.getEditWrapperProps ) { - wrapperProps = { - ...wrapperProps, - ...blockType.getEditWrapperProps( attributes ), - }; - } - const isAligned = wrapperProps && wrapperProps[ 'data-align' ]; // The wp-block className is important for editor styles. @@ -254,7 +251,85 @@ function BlockListBlock( { className ); - const blockElementId = `block-${ clientId }`; + const htmlSuffix = mode === 'html' && ! __unstableIsHtml ? '-visual' : ''; + const blockElementId = `block-${ clientId }${ htmlSuffix }`; + const Animated = animated[ tagName ]; + + return ( + + { children } + + ); +} ); + +const elements = [ 'p', 'div' ]; + +const ExtendedBlockComponent = elements.reduce( ( acc, element ) => { + acc[ element ] = forwardRef( ( props, ref ) => { + return ; + } ); + return acc; +}, BlockComponent ); + +export const Block = ExtendedBlockComponent; + +function BlockListBlock( { + mode, + isFocusMode, + isLocked, + clientId, + isSelected, + isMultiSelected, + isPartOfMultiSelection, + isFirstMultiSelected, + isLastMultiSelected, + isTypingWithinBlock, + isEmptyDefaultBlock, + isAncestorOfSelectedBlock, + isSelectionEnabled, + className, + name, + isValid, + attributes, + initialPosition, + wrapperProps, + setAttributes, + onReplace, + onInsertBlocksAfter, + onMerge, + onRemove, + onInsertDefaultBlockAfter, + toggleSelection, + animateOnChange, + enableAnimation, + isNavigationMode, + isMultiSelecting, + hasSelectedUI = true, +} ) { + // Handling the error state + const [ hasError, setErrorState ] = useState( false ); + const onBlockError = () => setErrorState( true ); // We wrap the BlockEdit component in a div that hides it when editing in // HTML mode. This allows us to render all of the ancillary pieces @@ -275,6 +350,49 @@ function BlockListBlock( { /> ); + const blockType = getBlockType( name ); + const lightBlockWrapper = hasBlockSupport( blockType, 'lightBlockWrapper', false ); + const value = { + clientId, + initialPosition, + isSelected, + isFirstMultiSelected, + isLastMultiSelected, + isMultiSelecting, + isNavigationMode, + isPartOfMultiSelection, + enableAnimation, + animateOnChange, + onInsertDefaultBlockAfter, + onRemove, + isFocusMode, + isTypingWithinBlock, + hasSelectedUI, + isValid, + hasError, + isEmptyDefaultBlock, + isMultiSelected, + isAncestorOfSelectedBlock, + className, + isLocked, + name, + mode, + }; + + // Determine whether the block has props to apply to the wrapper. + if ( ! lightBlockWrapper ) { + if ( blockType.getEditWrapperProps ) { + wrapperProps = { + ...wrapperProps, + ...blockType.getEditWrapperProps( attributes ), + }; + } + + value.wrapperProps = wrapperProps; + } + + const isAligned = wrapperProps && wrapperProps[ 'data-align' ]; + // For aligned blocks, provide a wrapper element so the block can be // positioned relative to the block column. This is enabled with the // .is-block-content className. @@ -286,47 +404,42 @@ function BlockListBlock( { blockEdit =
{ blockEdit }
; } + const memoizedValue = useMemo( () => value, Object.values( value ) ); + return ( - + - { isValid && blockEdit } - { isValid && mode === 'html' && ( - + { isValid && lightBlockWrapper && ( + <> + { blockEdit } + { mode === 'html' && ( + + + + ) } + + ) } + { isValid && ! lightBlockWrapper && ( + + { blockEdit } + { mode === 'html' && ( + + ) } + + ) } + { ! isValid && ( + + +
{ getSaveElement( blockType, attributes ) }
+
) } - { ! isValid && [ - , -
- { getSaveElement( blockType, attributes ) } -
, - ] }
- { !! hasError && } -
+ { !! hasError && ( + + + + ) } + ); } diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 48e8559d7990b9..d6ba818d56e4ba 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -66,6 +66,7 @@ export { } from './page-template-picker'; export { default as BlockInspector } from './block-inspector'; export { default as BlockList } from './block-list'; +export { Block } from './block-list/block'; export { default as BlockMover } from './block-mover'; export { default as BlockPreview } from './block-preview'; export { default as BlockSelectionClearer } from './block-selection-clearer'; diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index 551b0dded6dd00..4fcbe1de661853 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -20,6 +20,7 @@ import { RichText, withFontSizes, __experimentalUseColors, + Block, } from '@wordpress/block-editor'; import { createBlock } from '@wordpress/blocks'; import { compose } from '@wordpress/compose'; @@ -147,7 +148,7 @@ function ParagraphBlock( { { it( 'Can insert the block', async () => { await insertBlock( blockTitle ); expect( - await getInnerHTML( `[data-type="${ blockName }"] [data-type="core/paragraph"] p` ) + await getInnerHTML( `[data-type="${ blockName }"] [data-type="core/paragraph"]` ) ).toEqual( blockTitle ); } ); diff --git a/packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js b/packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js index 92689539fb5e84..68febbcfa22951 100644 --- a/packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js +++ b/packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js @@ -62,7 +62,7 @@ describe( 'cpt locking', () => { } ); it( 'should not error when deleting the cotents of a paragraph', async () => { - await page.click( '.block-editor-block-list__block[data-type="core/paragraph"] p' ); + await page.click( '.block-editor-block-list__block[data-type="core/paragraph"]' ); const textToType = 'Paragraph'; await page.keyboard.type( 'Paragraph' ); await pressKeyTimes( 'Backspace', textToType.length + 1 ); diff --git a/packages/e2e-tests/specs/editor/plugins/hooks-api.test.js b/packages/e2e-tests/specs/editor/plugins/hooks-api.test.js index 4641c534493b3f..e0cc1e39773ce9 100644 --- a/packages/e2e-tests/specs/editor/plugins/hooks-api.test.js +++ b/packages/e2e-tests/specs/editor/plugins/hooks-api.test.js @@ -31,7 +31,7 @@ describe( 'Using Hooks API', () => { it( 'Pressing reset block button resets the block', async () => { await clickBlockAppender(); await page.keyboard.type( 'First paragraph' ); - const paragraphContent = await page.$eval( 'div[data-type="core/paragraph"] p', ( element ) => element.textContent ); + const paragraphContent = await page.$eval( 'div[data-type="core/paragraph"]', ( element ) => element.textContent ); expect( paragraphContent ).toEqual( 'First paragraph' ); await page.click( '.edit-post-sidebar .e2e-reset-block-button' ); expect( await getEditedPostContent() ).toMatchSnapshot(); diff --git a/packages/e2e-tests/specs/editor/various/editor-modes.test.js b/packages/e2e-tests/specs/editor/various/editor-modes.test.js index a1a3f27249ea33..e84e6f1f7e5db5 100644 --- a/packages/e2e-tests/specs/editor/various/editor-modes.test.js +++ b/packages/e2e-tests/specs/editor/various/editor-modes.test.js @@ -17,7 +17,7 @@ describe( 'Editing modes (visual/HTML)', () => { it( 'should switch between visual and HTML modes', async () => { // This block should be in "visual" mode by default. - let visualBlock = await page.$$( '.block-editor-block-list__layout .block-editor-block-list__block .rich-text' ); + let visualBlock = await page.$$( '.block-editor-block-list__layout .block-editor-block-list__block.rich-text' ); expect( visualBlock ).toHaveLength( 1 ); // Move the mouse to show the block toolbar @@ -43,7 +43,7 @@ describe( 'Editing modes (visual/HTML)', () => { await changeModeButton.click(); // This block should be in "visual" mode by default. - visualBlock = await page.$$( '.block-editor-block-list__layout .block-editor-block-list__block .rich-text' ); + visualBlock = await page.$$( '.block-editor-block-list__layout .block-editor-block-list__block.rich-text' ); expect( visualBlock ).toHaveLength( 1 ); } ); diff --git a/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js b/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js index aeb9b47fc35c37..43d2e1984f5704 100644 --- a/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js +++ b/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js @@ -31,9 +31,6 @@ const tabThroughParagraphBlock = async ( paragraphText ) => { await tabThroughBlockMoverControl(); await tabThroughBlockToolbar(); - await page.keyboard.press( 'Tab' ); - await expect( await getActiveLabel() ).toBe( 'Block: Paragraph' ); - await page.keyboard.press( 'Tab' ); await expect( await getActiveLabel() ).toBe( 'Paragraph block' ); await expect( await page.evaluate( () => diff --git a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js index 7f2e8a4b6ea03b..c720457d6b9fc6 100644 --- a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js +++ b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js @@ -169,7 +169,7 @@ describe( 'Reusable blocks', () => { // Check that its content is up to date const text = await page.$eval( - '.block-editor-block-list__block[data-type="core/paragraph"] p', + '.block-editor-block-list__block[data-type="core/paragraph"]', ( element ) => element.innerText ); expect( text ).toMatch( 'Oh! Hello there!' );