Skip to content

Commit

Permalink
Improve child blocks API's and UI. (#7003)
Browse files Browse the repository at this point in the history
This PR extends the blocks API with two new selectors getChildBlockNames and hasChildBlocks.
A class is added to inserter items with children, and the UI is changed using the new class. Now we have a special design in blocks with children.
A new area that appears when we are inserting blocks inside blocks with children was created.
  • Loading branch information
jorgefilipecosta authored May 31, 2018
1 parent 02c9edb commit c93b778
Show file tree
Hide file tree
Showing 13 changed files with 333 additions and 27 deletions.
2 changes: 2 additions & 0 deletions blocks/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export {
getBlockSupport,
hasBlockSupport,
isSharedBlock,
getChildBlockNames,
hasChildBlocks,
} from './registration';
export {
isUnmodifiedDefaultBlock,
Expand Down
22 changes: 22 additions & 0 deletions blocks/api/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,25 @@ export function hasBlockSupport( nameOrType, feature, defaultSupports ) {
export function isSharedBlock( blockOrType ) {
return blockOrType.name === 'core/block';
}

/**
* Returns an array with the child blocks of a given block.
*
* @param {string} blockName Block type name.
*
* @return {Array} Array of child block names.
*/
export const getChildBlockNames = ( blockName ) => {
return select( 'core/blocks' ).getChildBlockNames( blockName );
};

/**
* Returns a boolean indicating if a block has child blocks or not.
*
* @param {string} blockName Block type name.
*
* @return {boolean} True if a block contains child blocks and false otherwise.
*/
export const hasChildBlocks = ( blockName ) => {
return select( 'core/blocks' ).hasChildBlocks( blockName );
};
35 changes: 35 additions & 0 deletions blocks/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import createSelector from 'rememo';
import { filter, includes, map } from 'lodash';

/**
* Returns all the available block types.
Expand Down Expand Up @@ -61,3 +62,37 @@ export function getDefaultBlockName( state ) {
export function getFallbackBlockName( state ) {
return state.fallbackBlockName;
}

/**
* Returns an array with the child blocks of a given block.
*
* @param {Object} state Data state.
* @param {string} blockName Block type name.
*
* @return {Array} Array of child block names.
*/
export const getChildBlockNames = createSelector(
( state, blockName ) => {
return map(
filter( state.blockTypes, ( blockType ) => {
return includes( blockType.parent, blockName );
} ),
( { name } ) => name
);
},
( state ) => [
state.blockTypes,
]
);

/**
* Returns a boolean indicating if a block has child blocks or not.
*
* @param {Object} state Data state.
* @param {string} blockName Block type name.
*
* @return {boolean} True if a block contains child blocks and false otherwise.
*/
export const hasChildBlocks = ( state, blockName ) => {
return getChildBlockNames( state, blockName ).length > 0;
};
137 changes: 137 additions & 0 deletions blocks/store/test/selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Internal dependencies
*/
import { getChildBlockNames } from '../selectors';

describe( 'selectors', () => {
describe( 'getChildBlockNames', () => {
it( 'should return an empty array if state is empty', () => {
const state = {};

expect( getChildBlockNames( state, 'parent1' ) ).toHaveLength( 0 );
} );

it( 'should return an empty array if no children exist', () => {
const state = {
blockTypes: [
{
name: 'child1',
parent: [ 'parent1' ],
},
{
name: 'child2',
parent: [ 'parent2' ],
},
{
name: 'parent3',
},
],
};

expect( getChildBlockNames( state, 'parent3' ) ).toHaveLength( 0 );
} );

it( 'should return an empty array if the parent block is not found', () => {
const state = {
blockTypes: [
{
name: 'child1',
parent: [ 'parent1' ],
},
{
name: 'parent1',
},
],
};

expect( getChildBlockNames( state, 'parent3' ) ).toHaveLength( 0 );
} );

it( 'should return an array with the child block names', () => {
const state = {
blockTypes: [
{
name: 'child1',
parent: [ 'parent1' ],
},
{
name: 'child2',
parent: [ 'parent2' ],
},
{
name: 'child3',
parent: [ 'parent1' ],
},
{
name: 'child4',
},
{
name: 'parent1',
},
{
name: 'parent2',
},
],
};

expect( getChildBlockNames( state, 'parent1' ) ).toEqual( [ 'child1', 'child3' ] );
} );

it( 'should return an array with the child block names even if only one child exists', () => {
const state = {
blockTypes: [
{
name: 'child1',
parent: [ 'parent1' ],
},
{
name: 'child2',
parent: [ 'parent2' ],
},
{
name: 'child4',
},
{
name: 'parent1',
},
{
name: 'parent2',
},
],
};

expect( getChildBlockNames( state, 'parent1' ) ).toEqual( [ 'child1' ] );
} );

it( 'should return an array with the child block names even if children have multiple parents', () => {
const state = {
blockTypes: [
{
name: 'child1',
parent: [ 'parent1' ],
},
{
name: 'child2',
parent: [ 'parent1', 'parent2' ],
},
{
name: 'child3',
parent: [ 'parent1' ],
},
{
name: 'child4',
},
{
name: 'parent1',
},
{
name: 'parent2',
},
],
};

expect( getChildBlockNames( state, 'parent1' ) ).toEqual( [ 'child1', 'child2', 'child3' ] );
expect( getChildBlockNames( state, 'parent2' ) ).toEqual( [ 'child2' ] );
} );
} );
} );
2 changes: 1 addition & 1 deletion components/notice/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import Notice from './';
* @param {Array} $0.notices Array of notices to render.
* @param {Function} $0.onRemove Function called when a notice should be removed / dismissed.
* @param {Object} $0.className Name of the class used by the component.
* @param {Object} $0.children Array of childs to be rendered inside the notice list.
* @param {Object} $0.children Array of children to be rendered inside the notice list.
* @return {Object} The rendered notices list.
*/
function NoticeList( { notices, onRemove = noop, className = 'components-notice-list', children } ) {
Expand Down
2 changes: 1 addition & 1 deletion edit-post/assets/stylesheets/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ $admin-sidebar-width-big: 190px;
$admin-sidebar-width-collapsed: 36px;

// Visuals
$shadow-popover: 0 3px 20px rgba( $dark-gray-900, .1 ), 0 1px 3px rgba( $dark-gray-900, .1 );
$shadow-popover: 0 3px 30px rgba( $dark-gray-900, .1 );
$shadow-toolbar: 0 2px 10px rgba( $dark-gray-900, .1 ), 0 0 2px rgba( $dark-gray-900, .1 );
$shadow-below-only: 0 5px 10px rgba( $dark-gray-900, .1 ), 0 2px 2px rgba( $dark-gray-900, .1 );

Expand Down
49 changes: 49 additions & 0 deletions editor/components/inserter/child-blocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* WordPress dependencies
*/
import { compose } from '@wordpress/element';
import { withSelect } from '@wordpress/data';
import { ifCondition } from '@wordpress/components';

/**
* Internal dependencies
*/
import './style.scss';
import ItemList from './item-list';
import BlockIcon from '../block-icon';

function ChildBlocks( { rootBlockIcon, rootBlockTitle, items, ...props } ) {
return (
<div className="editor-inserter__child-blocks">
{ ( rootBlockIcon || rootBlockTitle ) && (
<div className="editor-inserter__parent-block-header">
{ rootBlockIcon && (
<div className="editor-inserter__parent-block-icon">
<BlockIcon icon={ rootBlockIcon } />
</div>
) }
{ rootBlockTitle && <h2>{ rootBlockTitle }</h2> }
</div>
) }
<ItemList items={ items } { ...props } />
</div>
);
}

export default compose(
ifCondition( ( { items } ) => items && items.length > 0 ),
withSelect( ( select, { rootUID } ) => {
const {
getBlockType,
} = select( 'core/blocks' );
const {
getBlockName,
} = select( 'core/editor' );
const rootBlockName = getBlockName( rootUID );
const rootBlockType = getBlockType( rootBlockName );
return {
rootBlockTitle: rootBlockType && rootBlockType.title,
rootBlockIcon: rootBlockType && rootBlockType.icon,
};
} ),
)( ChildBlocks );
4 changes: 3 additions & 1 deletion editor/components/inserter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class Inserter extends Component {
title,
children,
onInsertBlock,
rootUID,
} = this.props;

if ( items.length === 0 ) {
Expand Down Expand Up @@ -74,7 +75,7 @@ class Inserter extends Component {
onClose();
};

return <InserterMenu items={ items } onSelect={ onSelect } />;
return <InserterMenu items={ items } onSelect={ onSelect } rootUID={ rootUID } />;
} }
/>
);
Expand All @@ -96,6 +97,7 @@ export default compose( [
insertionPoint,
selectedBlock: getSelectedBlock(),
items: getInserterItems( rootUID ),
rootUID,
};
} ),
withDispatch( ( dispatch, ownProps ) => ( {
Expand Down
10 changes: 9 additions & 1 deletion editor/components/inserter/item-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,15 @@ class ItemList extends Component {
<button
role="menuitem"
key={ item.id }
className={ classnames( 'editor-inserter__item', getBlockMenuDefaultClassName( item.id ) ) }
className={
classnames(
'editor-inserter__item',
getBlockMenuDefaultClassName( item.id ),
{
'editor-inserter__item-has-children': item.hasChildBlocks,
}
)
}
onClick={ () => onSelect( item ) }
tabIndex={ isCurrent || item.isDisabled ? null : '-1' }
disabled={ item.isDisabled }
Expand Down
Loading

0 comments on commit c93b778

Please sign in to comment.