From 90388ec650b4010278d41b5d7bd858223601302b Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 13 Mar 2019 13:03:53 +0100 Subject: [PATCH] Implement a constrained version of Slot/Fill --- .../components/inspector-controls/index.js | 6 +- .../src/components/provider/index.js | 9 +- packages/components/src/index.js | 1 + .../src/slot-fill-constrained/index.js | 106 ++++++++++++++++++ 4 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 packages/components/src/slot-fill-constrained/index.js diff --git a/packages/block-editor/src/components/inspector-controls/index.js b/packages/block-editor/src/components/inspector-controls/index.js index e808e987adbdcf..81ec64ea9d576a 100644 --- a/packages/block-editor/src/components/inspector-controls/index.js +++ b/packages/block-editor/src/components/inspector-controls/index.js @@ -1,17 +1,17 @@ /** * WordPress dependencies */ -import { createSlotFill } from '@wordpress/components'; +import { createConstrainedSlotFill } from '@wordpress/components'; /** * Internal dependencies */ import { ifBlockEditSelected } from '../block-edit/context'; -const { Fill, Slot } = createSlotFill( 'InspectorControls' ); +const { Fill, Slot, Provider } = createConstrainedSlotFill(); const InspectorControls = ifBlockEditSelected( Fill ); - InspectorControls.Slot = Slot; +InspectorControls.Provider = Provider; export default InspectorControls; diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index fd788477a0d6d1..b05d3158671326 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -6,6 +6,11 @@ import { DropZoneProvider, SlotFillProvider } from '@wordpress/components'; import { withDispatch, withRegistry } from '@wordpress/data'; import { compose } from '@wordpress/compose'; +/** + * Internal dependencies + */ +import InspectorControls from '../inspector-controls'; + class BlockEditorProvider extends Component { componentDidMount() { this.props.updateSettings( this.props.settings ); @@ -107,7 +112,9 @@ class BlockEditorProvider extends Component { return ( - { children } + + { children } + ); diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 6e45caa6e1e055..e5c638eaca71aa 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -63,6 +63,7 @@ export { default as Tooltip } from './tooltip'; export { default as TreeSelect } from './tree-select'; export { default as IsolatedEventContainer } from './isolated-event-container'; export { createSlotFill, Slot, Fill, Provider as SlotFillProvider } from './slot-fill'; +export { createConstrainedSlotFill } from './slot-fill-constrained'; // Higher-Order Components export { default as navigateRegions } from './higher-order/navigate-regions'; diff --git a/packages/components/src/slot-fill-constrained/index.js b/packages/components/src/slot-fill-constrained/index.js new file mode 100644 index 00000000000000..e63468049bc7bb --- /dev/null +++ b/packages/components/src/slot-fill-constrained/index.js @@ -0,0 +1,106 @@ +/** + * External dependencies + */ +import { + filter, + isFunction, + isString, + negate, + findIndex, +} from 'lodash'; + +/** + * WordPress dependencies + */ +import { + createContext, + useReducer, + useContext, + useEffect, + cloneElement, + Children, + isEmptyElement, + Fragment, +} from '@wordpress/element'; +import { withInstanceId } from '@wordpress/compose'; + +export function createConstrainedSlotFill() { + const Context = createContext( [ [], () => {} ] ); + + const initialFills = []; + function reducer( state, action ) { + switch ( action.type ) { + case 'remove': + return filter( state, ( fill ) => fill.key !== action.key ); + case 'add': { + const index = findIndex( state, ( fill ) => fill.key === action.key ); + if ( index === -1 ) { + return [ + ...state, + { key: action.key, children: action.children }, + ]; + } + return [ + ...state.slice( 0, index ), + { key: action.key, children: action.children }, + ...state.slice( index + 1 ), + ]; + } + default: + throw new Error(); + } + } + + function Provider( { children } ) { + const context = useReducer( reducer, initialFills ); + + return ( + + { children } + + ); + } + + function Slot( { children, fillProps = {} } ) { + const [ fills ] = useContext( Context ); + + const normalizedFills = fills.map( ( fill ) => { + const { children: fillChildren, key } = fill; + const element = isFunction( fillChildren ) ? fillChildren( fillProps ) : fillChildren; + return Children.map( element, ( child, childIndex ) => { + if ( ! child || isString( child ) ) { + return child; + } + + const childKey = `${ key }---${ child.key || childIndex }`; + return cloneElement( child, { key: childKey } ); + } ); + } ).filter( + // In some cases fills are rendered only when some conditions apply. + // This ensures that we only use non-empty fills when rendering, i.e., + // it allows us to render wrappers only when the fills are actually present. + negate( isEmptyElement ) + ); + + return ( + + { isFunction( children ) ? children( normalizedFills ) : normalizedFills } + + ); + } + + const Fill = withInstanceId( ( { children, instanceId } ) => { + const [ , dispatch ] = useContext( Context ); + useEffect( () => { + dispatch( { type: 'add', key: instanceId, children } ); + return () => dispatch( { type: 'remove', key: instanceId } ); + }, [ instanceId, children ] ); + return null; + } ); + + return { + Provider, + Slot, + Fill, + }; +}