-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Block editor: new editor.BlockControls
filter
#48809
Changes from all commits
c5abcef
56c6679
2b6f49e
b2f240a
724a151
49e6aa6
afd49e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,23 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { v4 as uuid } from 'uuid'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useMemo } from '@wordpress/element'; | ||
import { useMemo, useEffect, useReducer } from '@wordpress/element'; | ||
|
||
import { hasBlockSupport } from '@wordpress/blocks'; | ||
import { filters, addAction, removeAction } from '@wordpress/hooks'; | ||
import { useSelect } from '@wordpress/data'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import Edit from './edit'; | ||
import { BlockEditContextProvider, useBlockEditContext } from './context'; | ||
import { store as blockEditorStore } from '../../store'; | ||
|
||
/** | ||
* The `useBlockEditContext` hook provides information about the block this hook is being used in. | ||
|
@@ -20,6 +29,79 @@ import { BlockEditContextProvider, useBlockEditContext } from './context'; | |
*/ | ||
export { useBlockEditContext }; | ||
|
||
// Please do not export this at the package level until we have a stable API. | ||
// Hook names must start with a letter. | ||
export const blockControlsFilterName = 'a' + uuid(); | ||
|
||
function BlockControlFilters( props ) { | ||
const { name, isSelected, clientId } = props; | ||
const shouldDisplayControls = useSelect( | ||
( select ) => { | ||
if ( isSelected ) { | ||
return true; | ||
} | ||
|
||
const { | ||
getBlockName, | ||
isFirstMultiSelectedBlock, | ||
getMultiSelectedBlockClientIds, | ||
hasSelectedInnerBlock, | ||
} = select( blockEditorStore ); | ||
|
||
if ( isFirstMultiSelectedBlock( clientId ) ) { | ||
return getMultiSelectedBlockClientIds().every( | ||
( id ) => getBlockName( id ) === name | ||
); | ||
} | ||
|
||
return ( | ||
hasBlockSupport( | ||
getBlockName( clientId ), | ||
'__experimentalExposeControlsToChildren', | ||
false | ||
) && hasSelectedInnerBlock( clientId ) | ||
); | ||
}, | ||
[ clientId, isSelected, name ] | ||
); | ||
|
||
const [ , forceRender ] = useReducer( () => [] ); | ||
|
||
useEffect( () => { | ||
const namespace = 'core/block-edit/block-controls'; | ||
|
||
function onHooksUpdated( updatedHookName ) { | ||
if ( updatedHookName === blockControlsFilterName ) { | ||
forceRender(); | ||
} | ||
} | ||
|
||
addAction( 'hookRemoved', namespace, onHooksUpdated ); | ||
addAction( 'hookAdded', namespace, onHooksUpdated ); | ||
|
||
return () => { | ||
removeAction( 'hookRemoved', namespace ); | ||
removeAction( 'hookAdded', namespace ); | ||
}; | ||
}, [] ); | ||
|
||
if ( ! shouldDisplayControls ) { | ||
return; | ||
} | ||
|
||
const blockControlFilters = filters[ blockControlsFilterName ]; | ||
|
||
if ( ! blockControlFilters ) { | ||
return; | ||
} | ||
|
||
return blockControlFilters.handlers.map( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's probably not quite how WordPress filters were designed to work. I'm not sure I've ever seen usage where the previous value would not get set at all. It works here really well though 😄 |
||
( { callback: Controls, namespace } ) => { | ||
return <Controls { ...props } key={ namespace } />; | ||
} | ||
); | ||
} | ||
|
||
export default function BlockEdit( props ) { | ||
const { | ||
name, | ||
|
@@ -48,6 +130,7 @@ export default function BlockEdit( props ) { | |
// See https://reactjs.org/docs/context.html#caveats. | ||
value={ useMemo( () => context, Object.values( context ) ) } | ||
> | ||
<BlockControlFilters { ...props } /> | ||
<Edit { ...props } /> | ||
</BlockEditContextProvider> | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ import classnames from 'classnames'; | |
/** | ||
* WordPress dependencies | ||
*/ | ||
import { createHigherOrderComponent } from '@wordpress/compose'; | ||
import { createHigherOrderComponent, ifCondition } from '@wordpress/compose'; | ||
import { addFilter } from '@wordpress/hooks'; | ||
import { | ||
getBlockSupport, | ||
|
@@ -21,6 +21,7 @@ import { useSelect } from '@wordpress/data'; | |
import { BlockControls, BlockAlignmentControl } from '../components'; | ||
import useAvailableAlignments from '../components/block-alignment-control/use-available-alignments'; | ||
import { store as blockEditorStore } from '../store'; | ||
import { blockControlsFilterName } from '../components/block-edit'; | ||
|
||
/** | ||
* An array which includes all possible valid alignments, | ||
|
@@ -113,64 +114,55 @@ export function addAttribute( settings ) { | |
* Override the default edit UI to include new toolbar controls for block | ||
* alignment, if block defines support. | ||
* | ||
* @param {Function} BlockEdit Original component. | ||
* | ||
* @return {Function} Wrapped component. | ||
* @param {Object} props | ||
*/ | ||
export const withToolbarControls = createHigherOrderComponent( | ||
( BlockEdit ) => ( props ) => { | ||
const blockEdit = <BlockEdit key="edit" { ...props } />; | ||
const { name: blockName } = props; | ||
// Compute the block valid alignments by taking into account, | ||
// if the theme supports wide alignments or not and the layout's | ||
// availble alignments. We do that for conditionally rendering | ||
// Slot. | ||
const blockAllowedAlignments = getValidAlignments( | ||
getBlockSupport( blockName, 'align' ), | ||
hasBlockSupport( blockName, 'alignWide', true ) | ||
); | ||
|
||
const validAlignments = useAvailableAlignments( | ||
blockAllowedAlignments | ||
).map( ( { name } ) => name ); | ||
const isContentLocked = useSelect( | ||
( select ) => { | ||
return select( | ||
blockEditorStore | ||
).__unstableGetContentLockingParent( props.clientId ); | ||
}, | ||
[ props.clientId ] | ||
); | ||
if ( ! validAlignments.length || isContentLocked ) { | ||
return blockEdit; | ||
} | ||
export const ToolbarControls = ( props ) => { | ||
const { name: blockName } = props; | ||
// Compute the block valid alignments by taking into account, | ||
// if the theme supports wide alignments or not and the layout's | ||
// availble alignments. We do that for conditionally rendering | ||
// Slot. | ||
const blockAllowedAlignments = getValidAlignments( | ||
getBlockSupport( blockName, 'align' ), | ||
hasBlockSupport( blockName, 'alignWide', true ) | ||
); | ||
|
||
const validAlignments = useAvailableAlignments( | ||
blockAllowedAlignments | ||
).map( ( { name } ) => name ); | ||
const isContentLocked = useSelect( | ||
( select ) => { | ||
return select( blockEditorStore ).__unstableGetContentLockingParent( | ||
props.clientId | ||
); | ||
}, | ||
[ props.clientId ] | ||
); | ||
if ( ! validAlignments.length || isContentLocked ) { | ||
return null; | ||
} | ||
|
||
const updateAlignment = ( nextAlign ) => { | ||
if ( ! nextAlign ) { | ||
const blockType = getBlockType( props.name ); | ||
const blockDefaultAlign = blockType?.attributes?.align?.default; | ||
if ( blockDefaultAlign ) { | ||
nextAlign = ''; | ||
} | ||
const updateAlignment = ( nextAlign ) => { | ||
if ( ! nextAlign ) { | ||
const blockType = getBlockType( props.name ); | ||
const blockDefaultAlign = blockType?.attributes?.align?.default; | ||
if ( blockDefaultAlign ) { | ||
nextAlign = ''; | ||
} | ||
props.setAttributes( { align: nextAlign } ); | ||
}; | ||
|
||
return ( | ||
<> | ||
<BlockControls group="block" __experimentalShareWithChildBlocks> | ||
<BlockAlignmentControl | ||
value={ props.attributes.align } | ||
onChange={ updateAlignment } | ||
controls={ validAlignments } | ||
/> | ||
</BlockControls> | ||
{ blockEdit } | ||
</> | ||
); | ||
}, | ||
'withToolbarControls' | ||
); | ||
} | ||
props.setAttributes( { align: nextAlign } ); | ||
}; | ||
|
||
return ( | ||
<BlockControls group="block" __experimentalShareWithChildBlocks> | ||
<BlockAlignmentControl | ||
value={ props.attributes.align } | ||
onChange={ updateAlignment } | ||
controls={ validAlignments } | ||
/> | ||
</BlockControls> | ||
); | ||
}; | ||
|
||
/** | ||
* Override the default block element to add alignment wrapper props. | ||
|
@@ -248,9 +240,11 @@ addFilter( | |
withDataAlign | ||
); | ||
addFilter( | ||
'editor.BlockEdit', | ||
blockControlsFilterName, | ||
'core/editor/align/with-toolbar-controls', | ||
withToolbarControls | ||
ifCondition( ( { name } ) => hasBlockSupport( name, 'align' ) )( | ||
ToolbarControls | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's worth noting that these checks aren't contributing much to the performance improvement, but still, it's good practice to avoid as much calls to hooks as possible. Hopefully new hooks and plugins copy this pattern. |
||
); | ||
addFilter( | ||
'blocks.getSaveContent.extraProps', | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this the same thing as just adding an empty component and wrapping it with
withFilters
or something like that?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess there are a few differences:
shouldDisplayControls
: I'm guessing that's to improve performance and avoid rendering these controls if not necessary. While this is a good thing probably, it does add a bit of overhead in the sense that most of these filters actually renderInspectorControls
orBlockControls
that do these checks as well. So using a context value for these checks might still be a good idea. Also, should we support the case where we pass flag to enable__experimentalExposeControlsToChildren
behavior or not? (so let's say that this is generally a good thing)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the performance aspect, we should be a bit careful here to not actually degrade performance. Some existing hooks might have existed earlier (like by checking a block support) while here we first check
shouldDisplayControls
then check the block supports ... within the hook. So I wonder if we should first landshouldDisplayControls
as a context value before this PR.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The overhead is almost negligible. I'd prefer keeping all this internal instead of exposing it through context, although this context in particular can be entirely internal if we don't use the existing context.
It supports it. The filters will render.
Not sure how this would degrade performance. We still guard hasSelectedInnerBlock with a __experimentalExposeControlsToChildren check. Using context for InspectorControls and BlockControls would be better yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct. If this is a good pattern for other filters, at some point we could move this to the components package next to
withFilters
, but let's see how it goes.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, they could do this previously too. I do like that idea! We should be as restrictive as possible with this new filter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean some controls won't render if the block itself is not selected and that additional check and rendering is not necessary in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I'm following 😅