Skip to content

Commit

Permalink
Inserter: Fix handling of child blocks (#23231)
Browse files Browse the repository at this point in the history
* Inserter: Fix handling of child blocks

When a block C specifies `parent: [ P ]`, it means that C may only be
added to P. It does NOT mean that P may *only* contain C. (Which is what
`<InnerBlocks allowedBlocks={ [ C ] }>` means.)

This fixes the Inserter so that the correct blocks are shown when
inserting into a block that is referenced by `parent`. It does so by
leaning on `getInserterItems()` which does the right thing.

* Inserter: Make ChildBlocks have a similar API to InserterPanel

* E2E Tests: Add Child Blocks tests and test plugin

* Inserter: Clarify inline comment
  • Loading branch information
noisysocks authored Jun 18, 2020
1 parent 8828d9c commit e1a6a78
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 49 deletions.
40 changes: 16 additions & 24 deletions packages/block-editor/src/components/inserter/block-list.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
/**
* External dependencies
*/
import {
map,
includes,
findIndex,
flow,
sortBy,
groupBy,
isEmpty,
} from 'lodash';
import { map, findIndex, flow, sortBy, groupBy, isEmpty } from 'lodash';

/**
* WordPress dependencies
Expand Down Expand Up @@ -48,13 +40,14 @@ export function InserterBlockList( {
rootClientId,
onInsert
);
const rootChildBlocks = useSelect(

const hasChildItems = useSelect(
( select ) => {
const { getBlockName } = select( 'core/block-editor' );
const { getChildBlockNames } = select( 'core/blocks' );
const rootBlockName = getBlockName( rootClientId );

return getChildBlockNames( rootBlockName );
return !! getChildBlockNames( rootBlockName ).length;
},
[ rootClientId ]
);
Expand All @@ -63,12 +56,6 @@ export function InserterBlockList( {
return searchBlockItems( items, categories, collections, filterValue );
}, [ filterValue, items, categories, collections ] );

const childItems = useMemo( () => {
return filteredItems.filter( ( { name } ) =>
includes( rootChildBlocks, name )
);
}, [ filteredItems, rootChildBlocks ] );

const suggestedItems = useMemo( () => {
return items.slice( 0, MAX_SUGGESTED_ITEMS );
}, [ items ] );
Expand Down Expand Up @@ -127,16 +114,21 @@ export function InserterBlockList( {
}, [ filterValue, debouncedSpeak ] );

const hasItems = ! isEmpty( filteredItems );
const hasChildItems = childItems.length > 0;

return (
<div>
<ChildBlocks
rootClientId={ rootClientId }
items={ childItems }
onSelect={ onSelectItem }
onHover={ onHover }
/>
{ hasChildItems && (
<ChildBlocks rootClientId={ rootClientId }>
<BlockTypesList
// Pass along every block, as useBlockTypesState() and
// getInserterItems() will have already filtered out
// non-child blocks.
items={ filteredItems }
onSelect={ onSelectItem }
onHover={ onHover }
/>
</ChildBlocks>
) }

{ ! hasChildItems && !! suggestedItems.length && ! filterValue && (
<InserterPanel title={ _x( 'Most used', 'blocks' ) }>
Expand Down
33 changes: 14 additions & 19 deletions packages/block-editor/src/components/inserter/child-blocks.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
/**
* WordPress dependencies
*/
import { withSelect } from '@wordpress/data';
import { ifCondition, compose } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import BlockTypesList from '../block-types-list';
import BlockIcon from '../block-icon';

function ChildBlocks( { rootBlockIcon, rootBlockTitle, items, ...props } ) {
export default function ChildBlocks( { rootClientId, children } ) {
const { rootBlockTitle, rootBlockIcon } = useSelect( ( select ) => {
const { getBlockType } = select( 'core/blocks' );
const { getBlockName } = select( 'core/block-editor' );
const rootBlockName = getBlockName( rootClientId );
const rootBlockType = getBlockType( rootBlockName );
return {
rootBlockTitle: rootBlockType && rootBlockType.title,
rootBlockIcon: rootBlockType && rootBlockType.icon,
};
} );

return (
<div className="block-editor-inserter__child-blocks">
{ ( rootBlockIcon || rootBlockTitle ) && (
Expand All @@ -19,21 +28,7 @@ function ChildBlocks( { rootBlockIcon, rootBlockTitle, items, ...props } ) {
{ rootBlockTitle && <h2>{ rootBlockTitle }</h2> }
</div>
) }
<BlockTypesList items={ items } { ...props } />
{ children }
</div>
);
}

export default compose(
ifCondition( ( { items } ) => items && items.length > 0 ),
withSelect( ( select, { rootClientId } ) => {
const { getBlockType } = select( 'core/blocks' );
const { getBlockName } = select( 'core/block-editor' );
const rootBlockName = getBlockName( rootClientId );
const rootBlockType = getBlockType( rootBlockName );
return {
rootBlockTitle: rootBlockType && rootBlockType.title,
rootBlockIcon: rootBlockType && rootBlockType.icon,
};
} )
)( ChildBlocks );
35 changes: 29 additions & 6 deletions packages/block-editor/src/components/inserter/test/block-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import { useSelect } from '@wordpress/data';
*/
import { InserterBlockList as BaseInserterBlockList } from '../block-list';
import items, { categories, collections } from './fixtures';
import useBlockTypesState from '../hooks/use-block-types-state';

jest.mock( '../hooks/use-block-types-state', () => {
// This allows us to tweak the returned value on each test
const mock = jest.fn();
return mock;
} );

jest.mock( '@wordpress/data/src/components/use-select', () => {
// This allows us to tweak the returned value on each test
Expand Down Expand Up @@ -63,20 +70,22 @@ describe( 'InserterMenu', () => {
beforeEach( () => {
debouncedSpeak.mockClear();

useSelect.mockImplementation( () => ( {
useBlockTypesState.mockImplementation( () => [
items,
categories,
collections,
items,
} ) );
] );

useSelect.mockImplementation( () => false );
} );

it( 'should show nothing if there are no items', () => {
const noItems = [];
useSelect.mockImplementation( () => ( {
useBlockTypesState.mockImplementation( () => [
noItems,
categories,
collections,
items: noItems,
} ) );
] );
const { container } = render(
<InserterBlockList filterValue="random" />
);
Expand Down Expand Up @@ -149,6 +158,20 @@ describe( 'InserterMenu', () => {
assertNoResultsMessageNotToBePresent( container );
} );

it( 'displays child blocks UI when root block has child blocks', () => {
useSelect.mockImplementation( () => true );

const { container } = render( <InserterBlockList /> );

const childBlocksContent = container.querySelector(
'.block-editor-inserter__child-blocks'
);

expect( childBlocksContent ).not.toBeNull();

assertNoResultsMessageNotToBePresent( container );
} );

it( 'should disable items with `isDisabled`', () => {
const { container } = initializeAllClosedMenuState();
const layoutTabContent = container.querySelectorAll(
Expand Down
28 changes: 28 additions & 0 deletions packages/e2e-tests/plugins/child-blocks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
/**
* Plugin Name: Gutenberg Test Child Blocks
* Plugin URI: https://github.com/WordPress/gutenberg
* Author: Gutenberg Team
*
* @package gutenberg-test-child-blocks
*/

/**
* Registers a custom script for the plugin.
*/
function enqueue_child_blocks_script() {
wp_enqueue_script(
'gutenberg-test-child-blocks',
plugins_url( 'child-blocks/index.js', __FILE__ ),
array(
'wp-blocks',
'wp-block-editor',
'wp-element',
'wp-i18n',
),
filemtime( plugin_dir_path( __FILE__ ) . 'child-blocks/index.js' ),
true
);
}

add_action( 'init', 'enqueue_child_blocks_script' );
79 changes: 79 additions & 0 deletions packages/e2e-tests/plugins/child-blocks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
( function() {
const { InnerBlocks } = wp.blockEditor;
const { createElement: el } = wp.element;
const { registerBlockType } = wp.blocks;

registerBlockType( 'test/child-blocks-unrestricted-parent', {
title: 'Child Blocks Unrestricted Parent',
icon: 'carrot',
category: 'text',

edit() {
return el(
'div',
{},
el( InnerBlocks )
);
},

save() {
return el(
'div',
{},
el( InnerBlocks.Content )
);
},
} );

registerBlockType( 'test/child-blocks-restricted-parent', {
title: 'Child Blocks Restricted Parent',
icon: 'carrot',
category: 'text',

edit() {
return el(
'div',
{},
el(
InnerBlocks,
{ allowedBlocks: [ 'core/paragraph', 'core/image' ] }
)
);
},

save() {
return el(
'div',
{},
el( InnerBlocks.Content )
);
},
} );

registerBlockType( 'test/child-blocks-child', {
title: 'Child Blocks Child',
icon: 'carrot',
category: 'text',

parent: [
'test/child-blocks-unrestricted-parent',
'test/child-blocks-restricted-parent',
],

edit() {
return el(
'div',
{},
'Child'
);
},

save() {
return el(
'div',
{},
'Child'
);
},
} );
} )();
65 changes: 65 additions & 0 deletions packages/e2e-tests/specs/editor/plugins/child-blocks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* WordPress dependencies
*/
import {
activatePlugin,
closeGlobalBlockInserter,
createNewPost,
deactivatePlugin,
getAllBlockInserterItemTitles,
insertBlock,
openGlobalBlockInserter,
} from '@wordpress/e2e-test-utils';

describe( 'Child Blocks', () => {
beforeAll( async () => {
await activatePlugin( 'gutenberg-test-child-blocks' );
} );

beforeEach( async () => {
await createNewPost();
} );

afterAll( async () => {
await deactivatePlugin( 'gutenberg-test-child-blocks' );
} );

it( 'are hidden from the global block inserter', async () => {
await openGlobalBlockInserter();
await expect( await getAllBlockInserterItemTitles() ).not.toContain(
'Child Blocks Child'
);
} );

it( 'shows up in a parent block', async () => {
await insertBlock( 'Child Blocks Unrestricted Parent' );
await closeGlobalBlockInserter();
await page.waitForSelector(
'[data-type="test/child-blocks-unrestricted-parent"] .block-editor-default-block-appender'
);
await page.click(
'[data-type="test/child-blocks-unrestricted-parent"] .block-editor-default-block-appender'
);
await openGlobalBlockInserter();
const inserterItemTitles = await getAllBlockInserterItemTitles();
expect( inserterItemTitles ).toContain( 'Child Blocks Child' );
expect( inserterItemTitles.length ).toBeGreaterThan( 20 );
} );

it( 'display in a parent block with allowedItems', async () => {
await insertBlock( 'Child Blocks Restricted Parent' );
await closeGlobalBlockInserter();
await page.waitForSelector(
'[data-type="test/child-blocks-restricted-parent"] .block-editor-default-block-appender'
);
await page.click(
'[data-type="test/child-blocks-restricted-parent"] .block-editor-default-block-appender'
);
await openGlobalBlockInserter();
expect( await getAllBlockInserterItemTitles() ).toEqual( [
'Child Blocks Child',
'Image',
'Paragraph',
] );
} );
} );

0 comments on commit e1a6a78

Please sign in to comment.