Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental: allow parent Block to consume child Block's toolbar #18440

Merged
merged 36 commits into from
Dec 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a83aef2
Adds primitive hardcoded child toolbar appearing in parent toolbar area
getdave Nov 11, 2019
c54ebfe
Update to dynamically test for true block support for the “consume” t…
getdave Nov 11, 2019
e39d442
Update implementation to utilise InnerBlocks prop
getdave Nov 13, 2019
d90b837
Adds classname to signify consumption of toolbars
getdave Nov 13, 2019
5856315
Fix event bubbling via use of Slot Fill
getdave Nov 15, 2019
31b83f5
Add comments for clarity
getdave Nov 15, 2019
8eaac1a
Remove unusued isRootOfHierarchy prop
getdave Nov 15, 2019
2db6d67
Rename var to improve clarity
getdave Nov 15, 2019
49ef1c0
Update to only show toolbar on immediate parent
getdave Nov 15, 2019
e122c29
Updates to allow deeply nested toolbars to display toolbars on root a…
getdave Nov 15, 2019
3db6d1b
Update prop to better reflect usage
getdave Nov 15, 2019
df6b399
Adds optional BlockTitle to BlockToolbar
getdave Nov 15, 2019
b59e38a
Display current Block name within “captured” child Block toolbars
getdave Nov 15, 2019
bdf2e46
Commit prop name change on Nav Block
getdave Nov 15, 2019
c15e91b
Adds select to get blockListSettings for a set of Blocks (given clien…
getdave Nov 18, 2019
272cb5a
Updates to implement captureChildToolbar with new “parent only” API
getdave Nov 18, 2019
1a65b6a
Update to implement new dual API for capturing
getdave Nov 18, 2019
b0ad245
Update selector docs
getdave Nov 18, 2019
ff8d134
Documents new capture API on InnerBlock props
getdave Nov 18, 2019
6d6d089
Reinstate lost changes during rebase.
getdave Nov 29, 2019
6054b72
Updates to cache expensive selector
getdave Nov 29, 2019
e3a1d6d
Updates to move childtoolbar Slot/Fill outside of render.
getdave Nov 29, 2019
368de62
Updates to move ChildToolBar slot to separate file
getdave Nov 29, 2019
9917f68
Fix Toolbar display logic to account for forced toolbars
getdave Nov 29, 2019
e68af80
Revert "Display current Block name within “captured” child Block tool…
getdave Nov 29, 2019
8e9eafd
Update to DRY up usage of BlockContextualToolbar
getdave Nov 29, 2019
a1689e6
Fix issue with forcing toolbars and refs due to rendering x2 toolbars
getdave Dec 2, 2019
20496fb
Fix linting issues
getdave Dec 2, 2019
4a5a1bb
Simplify to single API for deeply capturing all toolbars.
getdave Dec 2, 2019
0b49632
Removes capturing toolbars on Nav Block
getdave Dec 2, 2019
f40c2f1
Fix to reuse existing var to DRY up conditionals
getdave Dec 3, 2019
a6241c7
Remove selector params from createSelector usage
getdave Dec 3, 2019
617fe50
Add classname to block to indicate that toolbar is captured
getdave Dec 13, 2019
1b9b75f
Update focus/selected visual indicator to take account of missing too…
getdave Dec 13, 2019
1f90c6f
Fix classname to indicate captured toolbars to apply without selected…
getdave Dec 13, 2019
841b4a8
Reinstate changes lost during complex rebase
getdave Dec 13, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* WordPress dependencies
*/
import { createSlotFill } from '@wordpress/components';

const { Fill, Slot } = createSlotFill( 'ChildToolbar' );

export const ChildToolbar = ( { children } ) => (
<Fill>
{ children }
</Fill>
);

// `bubblesVirtually` is required in order to avoid
// events triggered on the child toolbar from bubbling
// up to the parent Block.
export const ChildToolbarSlot = () => (
<Slot bubblesVirtually={ true } />
);
88 changes: 73 additions & 15 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
import { first, last } from 'lodash';
import { first, last, findIndex } from 'lodash';
import { animated } from 'react-spring/web.cjs';

/**
Expand Down Expand Up @@ -51,7 +51,7 @@ import InserterWithShortcuts from '../inserter-with-shortcuts';
import Inserter from '../inserter';
import { isInsideRootBlock } from '../../utils/dom';
import useMovingAnimation from './moving-animation';

import { ChildToolbar, ChildToolbarSlot } from './block-child-toolbar';
/**
* Prevents default dragging behavior within a block to allow for multi-
* selection to take effect unhampered.
Expand All @@ -77,7 +77,9 @@ function BlockListBlock( {
isTypingWithinBlock,
isCaretWithinFormattedText,
isEmptyDefaultBlock,
isParentOfSelectedBlock,
isAncestorOfSelectedBlock,
isCapturingDescendantToolbars,
hasAncestorCapturingToolbars,
isSelectionEnabled,
className,
name,
Expand Down Expand Up @@ -288,7 +290,7 @@ function BlockListBlock( {
* (via `setFocus`), typically if there is no focusable input in the block.
*/
const onFocus = () => {
if ( ! isSelected && ! isParentOfSelectedBlock && ! isPartOfMultiSelection ) {
if ( ! isSelected && ! isAncestorOfSelectedBlock && ! isPartOfMultiSelection ) {
onSelect();
}
};
Expand Down Expand Up @@ -463,9 +465,10 @@ function BlockListBlock( {
'is-reusable': isReusableBlock( blockType ),
'is-dragging': isDragging,
'is-typing': isTypingWithinBlock,
'is-focused': isFocusMode && ( isSelected || isParentOfSelectedBlock ),
'is-focused': isFocusMode && ( isSelected || isAncestorOfSelectedBlock ),
'is-focus-mode': isFocusMode,
'has-child-selected': isParentOfSelectedBlock,
'has-child-selected': isAncestorOfSelectedBlock,
'has-toolbar-captured': hasAncestorCapturingToolbars,
},
className
);
Expand Down Expand Up @@ -508,6 +511,22 @@ function BlockListBlock( {
blockEdit = <div style={ { display: 'none' } }>{ blockEdit }</div>;
}

/**
* Renders an individual `BlockContextualToolbar` component.
* This needs to be a function which generates the component
* on demand as we can only have a single toolbar for each render.
* This is because of the `isForcingContextualToolbar` logic which
* relies on a single toolbar being rendered to update the boolean
* value of the ref used to track the "force" state.
*/
const renderBlockContextualToolbar = () => (
<BlockContextualToolbar
// If the toolbar is being shown because of being forced
// it should focus the toolbar right after the mount.
focusOnMount={ isForcingContextualToolbar.current }
/>
);

return (
<IgnoreNestedEvents
id={ blockElementId }
Expand Down Expand Up @@ -564,13 +583,25 @@ function BlockListBlock( {
ref={ breadcrumb }
/>
) }
{ ( shouldShowContextualToolbar || isForcingContextualToolbar.current ) && (
<BlockContextualToolbar
// If the toolbar is being shown because of being forced
// it should focus the toolbar right after the mount.
focusOnMount={ isForcingContextualToolbar.current }
/>

{ ( isCapturingDescendantToolbars ) && (
// A slot made available on all ancestors of the selected Block
// to allow child Blocks to render their toolbars into the DOM
// of the appropriate parent.
<ChildToolbarSlot />
) }

{ ( ! ( hasAncestorCapturingToolbars ) ) && ( shouldShowContextualToolbar || isForcingContextualToolbar.current ) && renderBlockContextualToolbar() }

{ ( hasAncestorCapturingToolbars ) && ( shouldShowContextualToolbar || isForcingContextualToolbar.current ) && (
// If the parent Block is set to consume toolbars of the child Blocks
// then render the child Block's toolbar into the Slot provided
// by the parent.
<ChildToolbar>
{ renderBlockContextualToolbar() }
</ChildToolbar>
) }

{
! isNavigationMode &&
! shouldShowContextualToolbar &&
Expand Down Expand Up @@ -654,14 +685,39 @@ const applyWithSelect = withSelect(
getBlockOrder,
__unstableGetBlockWithoutInnerBlocks,
isNavigationMode,
getBlockListSettings,
__experimentalGetBlockListSettingsForBlocks,
getBlockParents,
} = select( 'core/block-editor' );

const block = __unstableGetBlockWithoutInnerBlocks( clientId );

const isSelected = isBlockSelected( clientId );
const { hasFixedToolbar, focusMode, isRTL } = getSettings();
const templateLock = getTemplateLock( rootClientId );
const isParentOfSelectedBlock = hasSelectedInnerBlock( clientId, true );
const checkDeep = true;

// "ancestor" is the more appropriate label due to "deep" check
const isAncestorOfSelectedBlock = hasSelectedInnerBlock( clientId, checkDeep );
const index = getBlockIndex( clientId, rootClientId );
const blockOrder = getBlockOrder( rootClientId );
const blockParentsClientIds = getBlockParents( clientId );
const currentBlockListSettings = getBlockListSettings( clientId );

// Get Block List Settings for all ancestors of the current Block clientId
const ancestorBlockListSettings = __experimentalGetBlockListSettingsForBlocks( blockParentsClientIds );

// Find the index of the first Block with the `captureDescendantsToolbars` prop defined
// This will be the top most ancestor because getBlockParents() returns tree from top -> bottom
const topmostAncestorWithCaptureDescendantsToolbarsIndex = findIndex( ancestorBlockListSettings, [ '__experimentalCaptureToolbars', true ] );

// Boolean to indicate whether current Block has a parent with `captureDescendantsToolbars` set
const hasAncestorCapturingToolbars = topmostAncestorWithCaptureDescendantsToolbarsIndex !== -1 ? true : false;

// Is the *current* Block the one capturing all its descendant toolbars?
// If there is no `topmostAncestorWithCaptureDescendantsToolbarsIndex` then
// we're at the top of the tree
const isCapturingDescendantToolbars = isAncestorOfSelectedBlock && ( currentBlockListSettings && currentBlockListSettings.__experimentalCaptureToolbars ) && ! hasAncestorCapturingToolbars;

// The fallback to `{}` is a temporary fix.
// This function should never be called when a block is not present in the state.
Expand All @@ -676,7 +732,7 @@ const applyWithSelect = withSelect(
// We only care about this prop when the block is selected
// Thus to avoid unnecessary rerenders we avoid updating the prop if the block is not selected.
isTypingWithinBlock:
( isSelected || isParentOfSelectedBlock ) && isTyping(),
( isSelected || isAncestorOfSelectedBlock ) && isTyping(),
isCaretWithinFormattedText: isCaretWithinFormattedText(),
mode: getBlockMode( clientId ),
isSelectionEnabled: isSelectionEnabled(),
Expand All @@ -699,7 +755,9 @@ const applyWithSelect = withSelect(
attributes,
isValid,
isSelected,
isParentOfSelectedBlock,
isAncestorOfSelectedBlock,
isCapturingDescendantToolbars,
hasAncestorCapturingToolbars,
};
}
);
Expand Down
5 changes: 5 additions & 0 deletions packages/block-editor/src/components/block-list/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@
background: transparent;
}
}

// Toolbar is captured
&.has-toolbar-captured > .block-editor-block-list__block-edit::before {
left: 0; // place "selected" indicator closer to block due to no toolbar
}
}


Expand Down
10 changes: 9 additions & 1 deletion packages/block-editor/src/components/inner-blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ 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.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.

Expand All @@ -143,6 +143,14 @@ A 'render prop' function that can be used to customize the block's appender.
/>
```

### `__experimentalCaptureToolbars`

* **Type:** `Boolean`
* **Default:** `false`

Determines whether the toolbars of _all_ child Blocks (applied deeply, recursive) should have their toolbars "captured" and shown on the Block which is consuming `InnerBlocks`.

For example, a button block, deeply nested in several levels of block `X` that utilises this property will see the button block's toolbar displayed on block `X`'s toolbar area.



5 changes: 5 additions & 0 deletions packages/block-editor/src/components/inner-blocks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@ class InnerBlocks extends Component {
updateNestedSettings,
templateLock,
parentLock,
__experimentalCaptureToolbars,
} = this.props;

const newSettings = {
allowedBlocks,
templateLock: templateLock === undefined ? parentLock : templateLock,
__experimentalCaptureToolbars: __experimentalCaptureToolbars || false,
};

if ( ! isShallowEqual( blockListSettings, newSettings ) ) {
Expand All @@ -106,11 +108,14 @@ class InnerBlocks extends Component {
hasOverlay,
renderAppender,
__experimentalMoverDirection: moverDirection,
__experimentalCaptureToolbars: captureToolbars,

} = this.props;
const { templateInProcess } = this.state;

const classes = classnames( 'block-editor-inner-blocks', {
'has-overlay': enableClickThrough && hasOverlay,
'is-capturing-toolbar': captureToolbars,
} );

return (
Expand Down
17 changes: 17 additions & 0 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1421,6 +1421,23 @@ export function isLastBlockChangePersistent( state ) {
return state.blocks.isPersistentChange;
}

/**
* Returns the Block List settings for an array of blocks, if any exist.
*
* @param {Object} state Editor state.
* @param {Array} clientIds Block client IDs.
*
* @return {Array} Block List Settings for each of the found blocks
*/
export const __experimentalGetBlockListSettingsForBlocks = createSelector(
( state, clientIds ) => {
return filter( state.blockListSettings, ( value, key ) => clientIds.includes( key ) );
},
( state ) => [
state.blockListSettings,
],
);

/**
* Returns the parsed block saved as shared block with the given ID.
*
Expand Down
55 changes: 55 additions & 0 deletions packages/block-editor/src/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const {
getTemplate,
getTemplateLock,
getBlockListSettings,
__experimentalGetBlockListSettingsForBlocks,
__experimentalGetLastBlockAttributeChanges,
INSERTER_UTILITY_HIGH,
INSERTER_UTILITY_MEDIUM,
Expand Down Expand Up @@ -2368,6 +2369,60 @@ describe( 'selectors', () => {
} );
} );

describe( '__experimentalGetBlockListSettingsForBlocks', () => {
it( 'should return the settings for a set of blocks', () => {
const state = {
blockListSettings: {
'test-1-dummy-clientId': {
setting1: false,
},
'test-2-dummy-clientId': {
setting1: true,
setting2: false,
},
'test-3-dummy-clientId': {
setting1: true,
setting2: false,
},
'test-4-dummy-clientId': {
setting1: true,
},
},
};

const targetBlocksClientIds = [ 'test-1-dummy-clientId', 'test-3-dummy-clientId' ];

expect( __experimentalGetBlockListSettingsForBlocks( state, targetBlocksClientIds ) ).toEqual( [
{
setting1: false,
},
{
setting1: true,
setting2: false,
},
] );
} );

it( 'should return empty array if settings for the blocks don’t exist', () => {
// Does not include target Block clientIds
const state = {
blockListSettings: {
'test-2-dummy-clientId': {
setting1: true,
setting2: false,
},
'test-4-dummy-clientId': {
setting1: true,
},
},
};

const targetBlocksClientIds = [ 'test-1-dummy-clientId', 'test-3-dummy-clientId' ];

expect( __experimentalGetBlockListSettingsForBlocks( state, targetBlocksClientIds ) ).toEqual( [] );
} );
} );

describe( '__experimentalGetLastBlockAttributeChanges', () => {
it( 'returns the last block attributes change', () => {
const state = {
Expand Down
8 changes: 4 additions & 4 deletions packages/data/src/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
);

/**
* Given the name of a registered store, returns an object containing the store's
* selectors pre-bound to state so that you only need to supply additional arguments,
* and modified so that they return promises that resolve to their eventual values,
* after any resolvers have ran.
* Given the name of a registered store, returns an object containing the store's
* selectors pre-bound to state so that you only need to supply additional arguments,
* and modified so that they return promises that resolve to their eventual values,
* after any resolvers have ran.
*
* @param {string} reducerKey Part of the state shape to register the
* selectors for.
Expand Down