Skip to content

Commit

Permalink
Adds Block Appender as placeholder to empty InnerBlocks (#14241)
Browse files Browse the repository at this point in the history
* Adds Block Appender as placeholder to empty InnerBlocks

Previously when inserting a Block that makes use of InnerBlocks (eg: Columns) it wasn’t clear that the Block had been inserted and by default an empty paragraph Block was inserted.

This changes checks for any innerBlocks and if available disables the default Block insertion (ie: paragraph) and displays the Block Appender as a placeholder. Once a Block is inserted then the placeholder is removed.

* Updates to allow InnerBlocks to control it’s placeholder when empty

Previously all Blocks making use of `InnerBlocks` were defaulted to showing the `BlockListAppender` as the placeholder. This update allows `InnerBlocks` to opt-in to this feature. It does however, require some prop drilling under the hood.

Addresses: #14241 (comment)

* Updates Column Block to opt-in to using Block Appender as placeholder

* Fix edge case where block can be null value

Previously test for presence of child blocks assumed that the block being interogated would always be present. In some circumstances this isn’t true resulting in an error. Fixed to include check for presence of block as if it isn’t there then it cannot have any children (obviously).

* Simplifies wording on Block Appender

* Adds background color to Block Appender

Addresses #14241 (comment)

* Adds support for dark themes

Resolves #14241 (comment)

* Adds keyboard focus support for visual cues

* Fixes comment to approved version

Resolves #14241 (review)

* Adjusts background to use lighter colour variant

Resolves #14241 (comment)

* Adds consistent spacing around appender

This update creates a consistent visual spacing around the appender itself. To handle the edge case of Blocks acting as pass through Blocks (which have negative margins) we have to double the margin on these Blocks.

* Removes unecessary Fragment usage

Resolves #14241 (comment)

* Adjusts font weight and space between icon and “Add Block” text

Addresses part of #14241 (comment)

* Adjusts appender padding to match supplied visual

Addresses visual design as provided in #14241 (comment)

* Aligns appender with sibling Blocks

* Revert "Aligns appender with sibling Blocks"

This reverts commit 49b03efe22f79dd45564def2a22d0693ae2530e7.

* Updates to use explict and more extenable prop for choosing the placeholder

Previously it was only possible to enable/disable the “Add Block” placeholder. In view of requests to be able to enable yet more states (ie: no Block appender _at all_) the API has been updated to a form which will make it easier to add different placeholders in the future.

* Reverts Column to use default placeholder behaviour

As discuseed, removing this from Column to facilitate merge.

* Removes “is passthrough” edge case for Columns

This reverts commit dc5c3526b1ba10e50a0934d934240307657c174a whilst retaining the consistent spacing around the appender which is still required.

This was reverted because it has been decided that Block List Appender should obey SRP and avoid adding styles to cater of edge case Blocks. Such Block variations should either captured within an API or rather should add overides themselves on an case by case basis.

* Fixes Columns layout edge case for Block Appender

Whilst “block-appender” mode will not be enabled for Columns by default we should cater for this scenario.

* Updates colors for hover and active states

Resolves #14241 (comment)

* Fix docs to show correct variable type

* Updates to appender rendering to utilise render prop

Previously we were constraining control over the type of appenders that could be created. Be utilising an optional render prop we provide developers with the ability to fully customise the render. However, by creating sensible default enum constants within InnerBlocks we preserve the “simple defaults”.

* Adds default appender as explicit constant

Updates to add default “auto insert” appender behaviour as a constant. This way if it is ever removed as the default we have an explicit constant by which to reference this behaviour by which isn’t tied to the term “default”.

* Adds option to hide appender entirely for InnerBlocks if children present

Creates separate prop for InnerBlocks to optionally hide the appender if child Blocks are present. The lower level components have been adjusted with `hideAppender` prop to selectively enable/disable rendering of the appender.

* Updates documentation for `renderAppender` and `hideAppenderWhenChildren`

* Updates to remove superflous usage of `appender` suffix

* Fixes Markdown usage on README heading

* Fix to ensure default appender render when default block is not active

Previously render was returning nothing when `canInsertDefaultBlock` was false (eg: when paragraph Block is disabled but `auto-insert` is still set as the default placeholder when a container Block is inserted. Fixes e2e test failures.

* Exposes placeholder defaults as components on InnerBlocks

Addresses ideas expressed #14241 (review)

Also an `HideWhenChildren` component to control rendering in scenario when children are present. Note that control over this functionality is now soley controlled by the render prop and cannot be activated via a simple prop.

* Adds mixins to handle visually hiding content but preserving for screenreaders

For approach see https://a11yproject.com/posts/how-to-hide-content/

* Removes the “Add Block” text from the button appender

Hides text visually but retains it for assistive technologies as we cannot rely on an icon to convey meaning.

Addresses #14241 (comment)

* Adds Button Block Appender component to DRY up component usage

Previously we had duplicates of the Button appender both within `InnerBlocks` and again within the `BlockListAppender` component. Creates a new standalone  `ButtonBlockAppender` component which is used across both components.

Also removes option to provide enum to `InnerBlocks` `renderAppender` prop. Now only render functions are valid. We still expose x2 default “appenders” on `InnerBlocks` to enable easy usage.

* Removes superfluous enum handling on renderAppender prop

* Minor - updates comments to end in full stops

Addresses #14241 (comment)

* Extracts `hideWhenChildBlocks` component for use on `InnerBlocks`

* Updates docs with correct props and usage examples

* Add docs for `ButtonBlockAppender` component

* Adds ability to pass custom CSS className to ButtonBlockAppender component

* Fixes e2e test by ensuring correct className is restored on appender when used inside BlockListAppender

* Revert "Adds mixins to handle visually hiding content but preserving for screenreaders"

This reverts commit 1a9a2f80ad780709fc781532352585772eb68257.

* Utilise existing WP Core screen reader text class

* Removes dependency on compose

* Improves naming of aliased component

In order to avoid inline component functions within `compose` components have been moved to their own functional components and then enhanced. However this causes a naming clash between the base component and the one being created and then enhanced within this file. As a result, all duplicates are aliased with the `Base` prefix to allow a component of the same name to be reused within the same file.

* Capitalise component name

* Correct spelling

* Simplifies check for prop

* Remove `utils` directory and flatten

* Simplify the use of if statements in BlockListAppender

* Use classnames utility to concat classes

* Rename class to block-editor-button-block-appender

* Switch to either arrow functions assigned to a const or exported function declarations

* revert unintentional formatting changes

* Remove HideWhenChildBlocks. This will be added on a separate branch

* Tidy up docs
  • Loading branch information
getdave authored and talldan committed Apr 12, 2019
1 parent c82cdcf commit db7decd
Show file tree
Hide file tree
Showing 15 changed files with 289 additions and 37 deletions.
4 changes: 4 additions & 0 deletions packages/block-editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ Undocumented declaration.

Undocumented declaration.

<a name="ButtonBlockerAppender" href="#ButtonBlockerAppender">#</a> **ButtonBlockerAppender**

Undocumented declaration.

<a name="ColorPalette" href="#ColorPalette">#</a> **ColorPalette**

Undocumented declaration.
Expand Down
47 changes: 25 additions & 22 deletions packages/block-editor/src/components/block-list-appender/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,56 @@ import { last } from 'lodash';
*/
import { withSelect } from '@wordpress/data';
import { getDefaultBlockName } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { IconButton } from '@wordpress/components';

/**
* Internal dependencies
*/
import IgnoreNestedEvents from '../ignore-nested-events';
import DefaultBlockAppender from '../default-block-appender';
import Inserter from '../inserter';
import ButtonBlockAppender from '../button-block-appender';

function BlockListAppender( {
blockClientIds,
rootClientId,
canInsertDefaultBlock,
isLocked,
renderAppender,
} ) {
if ( isLocked ) {
return null;
}

// A render prop has been provided, use it to render the appender.
if ( renderAppender ) {
return (
<div className="block-list-appender">
{ renderAppender() }
</div>
);
}

// Render the default block appender when renderAppender has not been
// provided and the context supports use of the default appender.
if ( canInsertDefaultBlock ) {
return (
<IgnoreNestedEvents childHandledEvents={ [ 'onFocus', 'onClick', 'onKeyDown' ] }>
<DefaultBlockAppender
rootClientId={ rootClientId }
lastBlockClientId={ last( blockClientIds ) }
/>
</IgnoreNestedEvents>
<div className="block-list-appender">
<IgnoreNestedEvents childHandledEvents={ [ 'onFocus', 'onClick', 'onKeyDown' ] }>
<DefaultBlockAppender
rootClientId={ rootClientId }
lastBlockClientId={ last( blockClientIds ) }
/>
</IgnoreNestedEvents>
</div>
);
}

// Fallback in the case no renderAppender has been provided and the
// default block can't be inserted.
return (
<div className="block-list-appender">
<Inserter
<ButtonBlockAppender
rootClientId={ rootClientId }
renderToggle={ ( { onToggle, disabled, isOpen } ) => (
<IconButton
label={ __( 'Add block' ) }
icon="insert"
onClick={ onToggle }
className="block-list-appender__toggle"
aria-haspopup="true"
aria-expanded={ isOpen }
disabled={ disabled }
/>
) }
isAppender
className="block-list-appender__toggle"
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
.block-list-appender > .block-editor-inserter {
display: block;
.block-list-appender {
margin: $block-padding;
}

.block-list-appender__toggle {
display: flex;
align-items: center;
justify-content: center;
padding: $grid-size-large;
outline: $border-width dashed $dark-gray-150;
width: 100%;
color: $dark-gray-500;

&:hover {
outline: $border-width dashed $dark-gray-500;
}
.block-list-appender > .block-editor-inserter {
display: block;
}
8 changes: 7 additions & 1 deletion packages/block-editor/src/components/block-list/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ class BlockList extends Component {
selectedBlockClientId,
multiSelectedBlockClientIds,
hasMultiSelection,
renderAppender,
} = this.props;

return (
Expand All @@ -222,7 +223,11 @@ class BlockList extends Component {
</AsyncModeProvider>
);
} ) }
<BlockListAppender rootClientId={ rootClientId } />

<BlockListAppender
rootClientId={ rootClientId }
renderAppender={ renderAppender }
/>
</div>
);
}
Expand All @@ -244,6 +249,7 @@ export default compose( [
getMultiSelectedBlockClientIds,
hasMultiSelection,
} = select( 'core/block-editor' );

const { rootClientId } = ownProps;

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
ButtonBlockAppender
=============================

`ButtonBlockAppender` provides button with a `+` (plus) icon which when clicked will trigger the default Block `Inserter` UI to allow a Block to be inserted.

This is typically used as an alternative to the `<DefaultBlockAppender />` component to determine the initial placeholder behaviour for a Block when displayed in the editor UI.

## Usage

In a block's `edit` implementation, render a `<ButtonBlockAppender />` component passing in the `rootClientId`.


```jsx
function render( { clientId }) {
return (
<div>
<p>Some rendered content here</p>
<ButtonBlockAppender rootClientId={ clientId } />
</div>
);
}
```

_Note:_

## Props

### `rootClientId`
* **Type:** `String`
* **Required** `true`
* **Default:** `undefined`

The `clientId` of the Block from who's root new Blocks should be inserted. This prop is required by the block `Inserter` component. Typically this is the `clientID` of the Block where the prop is being rendered.

### `className`
* **Type:** `String`
* **Default:** `""`

A CSS `class` to be _prepended_ to the default class of `"button-block-appender"`.

## Examples

The [`<InnerBlocks>` component](packages/block-editor/src/components/inner-blocks/) exposes an enhanced version of `ButtonBlockAppender` to allow consumers to choose it as an alternative to the standard behaviour of auto-inserting the default Block (typically `core/paragraph`).
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { Button, Icon } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import Inserter from '../inserter';

export default function ButtonBlockAppender( { rootClientId, className } ) {
return (
<Inserter
rootClientId={ rootClientId }
renderToggle={ ( { onToggle, disabled, isOpen } ) => (
<Button
className={ classnames( className, 'block-editor-button-block-appender' ) }
onClick={ onToggle }
aria-expanded={ isOpen }
disabled={ disabled }
>
<span className="screen-reader-text">{ __( 'Add Block' ) }</span>
<Icon icon="insert" />
</Button>
) }
isAppender
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.block-editor-button-block-appender {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $block-padding*1.5;
outline: $border-width dashed $dark-gray-150;
width: 100%;
color: $dark-gray-500;
background: $dark-opacity-light-100;

&:hover,
&:focus {
outline: $border-width dashed $dark-gray-500;
color: $dark-gray-900;
}

&:active {
outline: $border-width dashed $dark-gray-900;
color: $dark-gray-900;
}

// Use opacity to work in various editor styles
.is-dark-theme & {
background: $light-opacity-light-100;
color: $light-gray-100;

&:hover,
&:focus {
outline: $border-width dashed $white;
}
}
}
1 change: 1 addition & 0 deletions packages/block-editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { default as BlockFormatControls } from './block-format-controls';
export { default as BlockNavigationDropdown } from './block-navigation/dropdown';
export { default as BlockIcon } from './block-icon';
export { default as BlockVerticalAlignmentToolbar } from './block-vertical-alignment-toolbar';
export { default as ButtonBlockerAppender } from './button-block-appender';
export { default as ColorPalette } from './color-palette';
export { default as withColorContext } from './color-palette/with-color-context';
export * from './colors';
Expand Down
34 changes: 34 additions & 0 deletions packages/block-editor/src/components/inner-blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,37 @@ Template locking allows locking the `InnerBlocks` area for the current template.
If locking is not set in an `InnerBlocks` area: the locking of the parent `InnerBlocks` area is used.

If the block is a top level block: the locking of the Custom Post Type is used.

### `renderAppender`
* **Type:** `Function`
* **Default:** - `undefined`. When `renderAppender` is not specific the `<DefaultBlockAppender>` component is as a default. It automatically inserts whichever block is configured as the default block via `wp.blocks.setDefaultBlockName` (typically `paragraph`).

A 'render prop' function that can be used to customize the block's appender.

#### Notes
* For convenience two predefined appender components are exposed on `InnerBlocks` which can be consumed within the render function:
- `<InnerBlocks.ButtonBlockAppender />` - display a `+` (plus) icon button that, when clicked, displays the block picker menu. No default Block is inserted.
- `<InnerBlocks.DefaultBlockAppender />` - display the default block appender as set by `wp.blocks.setDefaultBlockName`. Typically this is the `paragraph` block.
* Consumers are also free to pass any valid render function. This provides the full flexibility to define a bespoke block appender.

#### Example usage

```jsx
// Utilise a predefined component
<InnerBlocks
renderAppender={ () => (
<InnerBlocks.ButtonBlockAppender />
) }
/>

// Fully custom
<InnerBlocks
renderAppender={ () => (
<button className="bespoke-appender" type="button">Some Special Appender</button>
) }
/>
```




Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Internal dependencies
*/
import BaseButtonBlockAppender from '../button-block-appender';
import withClientId from './with-client-id';

export const ButtonBlockAppender = ( { clientId } ) => {
return (
<BaseButtonBlockAppender rootClientId={ clientId } />
);
};

export default withClientId( ButtonBlockAppender );
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* External dependencies
*/
import { last } from 'lodash';

/**
* WordPress dependencies
*/
import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import IgnoreNestedEvents from '../ignore-nested-events';
import BaseDefaultBlockAppender from '../default-block-appender';
import withClientId from './with-client-id';

export const DefaultBlockAppender = ( { clientId, lastBlockClientId } ) => {
return (
<IgnoreNestedEvents childHandledEvents={ [ 'onFocus', 'onClick', 'onKeyDown' ] }>
<BaseDefaultBlockAppender
rootClientId={ clientId }
lastBlockClientId={ lastBlockClientId }
/>
</IgnoreNestedEvents>
);
};

export default compose( [
withClientId,
withSelect( ( select, { clientId } ) => {
const {
getBlockOrder,
} = select( 'core/block-editor' );

const blockClientIds = getBlockOrder( clientId );

return {
lastBlockClientId: last( blockClientIds ),
};
} ),
] )( DefaultBlockAppender );
Loading

0 comments on commit db7decd

Please sign in to comment.