Skip to content

Commit

Permalink
Implement a constrained version of Slot/Fill
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad committed Mar 13, 2019
1 parent 630e7bb commit 571a2e0
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 8 additions & 1 deletion packages/block-editor/src/components/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -107,7 +112,9 @@ class BlockEditorProvider extends Component {
return (
<SlotFillProvider>
<DropZoneProvider>
{ children }
<InspectorControls.Provider>
{ children }
</InspectorControls.Provider>
</DropZoneProvider>
</SlotFillProvider>
);
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
106 changes: 106 additions & 0 deletions packages/components/src/slot-fill-constrained/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<Context.Provider value={ context }>
{ children }
</Context.Provider>
);
}

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 (
<Fragment>
{ isFunction( children ) ? children( normalizedFills ) : normalizedFills }
</Fragment>
);
}

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,
};
}

0 comments on commit 571a2e0

Please sign in to comment.