Skip to content

Commit

Permalink
Components: Refactor SlotFill (#19242)
Browse files Browse the repository at this point in the history
  • Loading branch information
diegohaz authored and jorgefilipecosta committed Mar 16, 2020
1 parent b39fb21 commit 9b7eb2d
Show file tree
Hide file tree
Showing 15 changed files with 441 additions and 63 deletions.
29 changes: 13 additions & 16 deletions packages/block-editor/src/components/block-inspector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@wordpress/blocks';
import {
PanelBody,
__experimentalSlotFillConsumer,
__experimentalUseSlot as useSlot,
} from '@wordpress/components';
import { withSelect } from '@wordpress/data';

Expand All @@ -30,6 +30,9 @@ const BlockInspector = ( {
selectedBlockName,
showNoBlockSelectedMessage = true,
} ) => {
const slot = useSlot( InspectorAdvancedControls.slotName );
const hasFills = Boolean( slot.fills && slot.fills.length );

if ( count > 1 ) {
return <MultiSelectionInspector />;
}
Expand Down Expand Up @@ -69,21 +72,15 @@ const BlockInspector = ( {
) }
<InspectorControls.Slot bubblesVirtually />
<div>
<__experimentalSlotFillConsumer>
{ ( { hasFills } ) =>
hasFills( InspectorAdvancedControls.slotName ) && (
<PanelBody
className="block-editor-block-inspector__advanced"
title={ __( 'Advanced' ) }
initialOpen={ false }
>
<InspectorAdvancedControls.Slot
bubblesVirtually
/>
</PanelBody>
)
}
</__experimentalSlotFillConsumer>
{ hasFills && (
<PanelBody
className="block-editor-block-inspector__advanced"
title={ __( 'Advanced' ) }
initialOpen={ false }
>
<InspectorAdvancedControls.Slot bubblesVirtually />
</PanelBody>
) }
</div>
<SkipToSelectedBlock key="back" />
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export {
Slot,
Fill,
Provider as SlotFillProvider,
Consumer as __experimentalSlotFillConsumer,
useSlot as __experimentalUseSlot,
} from './slot-fill';

// Higher-Order Components
Expand Down
27 changes: 9 additions & 18 deletions packages/components/src/popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import PopoverDetectOutside from './detect-outside';
import Button from '../button';
import ScrollLock from '../scroll-lock';
import IsolatedEventContainer from '../isolated-event-container';
import { Slot, Fill, Consumer } from '../slot-fill';
import { Slot, Fill, useSlot } from '../slot-fill';
import Animate from '../animate';

const FocusManaged = withConstrainedTabbing(
Expand Down Expand Up @@ -261,6 +261,7 @@ const Popover = ( {
const contentRect = useRef();
const isMobileViewport = useViewportMatch( 'medium', '<' );
const [ animateOrigin, setAnimateOrigin ] = useState();
const slot = useSlot( __unstableSlotName );
const isExpanded = expandOnMobile && isMobileViewport;

noArrow = isExpanded || noArrow;
Expand Down Expand Up @@ -602,25 +603,15 @@ const Popover = ( {
content = <FocusManaged>{ content }</FocusManaged>;
}

return (
<Consumer>
{ ( { getSlot } ) => {
// In case there is no slot context in which to render,
// default to an in-place rendering.
if ( getSlot && getSlot( __unstableSlotName ) ) {
content = (
<Fill name={ __unstableSlotName }>{ content }</Fill>
);
}
if ( slot.ref ) {
content = <Fill name={ __unstableSlotName }>{ content }</Fill>;
}

if ( anchorRef || anchorRect ) {
return content;
}
if ( anchorRef || anchorRect ) {
return content;
}

return <span ref={ anchorRefFallback }>{ content }</span>;
} }
</Consumer>
);
return <span ref={ anchorRefFallback }>{ content }</span>;
};

const PopoverContainer = Popover;
Expand Down
34 changes: 34 additions & 0 deletions packages/components/src/slot-fill/bubbles-virtually/fill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* WordPress dependencies
*/
import { useRef, useEffect, createPortal } from '@wordpress/element';

/**
* Internal dependencies
*/
import useSlot from './use-slot';

export default function Fill( { name, children } ) {
const slot = useSlot( name );
const ref = useRef();

useEffect( () => {
// We register fills so we can keep track of their existance.
// Some Slot implementations need to know if there're already fills
// registered so they can choose to render themselves or not.
slot.registerFill( ref );
return () => {
slot.unregisterFill( ref );
};
}, [ slot.registerFill, slot.unregisterFill ] );

if ( ! slot.ref || ! slot.ref.current ) {
return null;
}

if ( typeof children === 'function' ) {
children = children( slot.fillProps );
}

return createPortal( children, slot.ref.current );
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* WordPress dependencies
*/
import { createContext } from '@wordpress/element';

const SlotFillContext = createContext( {
slots: {},
fills: {},
registerSlot: () => {},
unregisterSlot: () => {},
registerFill: () => {},
unregisterFill: () => {},
} );

export default SlotFillContext;
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* WordPress dependencies
*/
import { useMemo, useCallback, useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import SlotFillContext from './slot-fill-context';

function useSlotRegistry() {
const [ slots, setSlots ] = useState( {} );
const [ fills, setFills ] = useState( {} );

const registerSlot = useCallback( ( name, ref, fillProps ) => {
setSlots( ( prevSlots ) => ( {
...prevSlots,
[ name ]: {
...prevSlots[ name ],
ref: ref || prevSlots[ name ].ref,
fillProps: fillProps || prevSlots[ name ].fillProps || {},
},
} ) );
}, [] );

const unregisterSlot = useCallback( ( name, ref ) => {
setSlots( ( prevSlots ) => {
// eslint-disable-next-line no-unused-vars
const { [ name ]: slot, ...nextSlots } = prevSlots;
// Make sure we're not unregistering a slot registered by another element
// See https://github.com/WordPress/gutenberg/pull/19242#issuecomment-590295412
if ( slot.ref === ref ) {
return nextSlots;
}
return prevSlots;
} );
}, [] );

const registerFill = useCallback( ( name, ref ) => {
setFills( ( prevFills ) => ( {
...prevFills,
[ name ]: [ ...( prevFills[ name ] || [] ), ref ],
} ) );
}, [] );

const unregisterFill = useCallback( ( name, ref ) => {
setFills( ( prevFills ) => {
if ( prevFills[ name ] ) {
return {
...prevFills,
[ name ]: prevFills[ name ].filter(
( fillRef ) => fillRef !== ref
),
};
}
return prevFills;
} );
}, [] );

// Memoizing the return value so it can be directly passed to Provider value
const registry = useMemo(
() => ( {
slots,
fills,
registerSlot,
// Just for readability
updateSlot: registerSlot,
unregisterSlot,
registerFill,
unregisterFill,
} ),
[
slots,
fills,
registerSlot,
unregisterSlot,
registerFill,
unregisterFill,
]
);

return registry;
}

export default function SlotFillProvider( { children } ) {
const registry = useSlotRegistry();
return (
<SlotFillContext.Provider value={ registry }>
{ children }
</SlotFillContext.Provider>
);
}
48 changes: 48 additions & 0 deletions packages/components/src/slot-fill/bubbles-virtually/slot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* WordPress dependencies
*/
import {
useEffect,
useRef,
useLayoutEffect,
useContext,
} from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';

/**
* Internal dependencies
*/
import SlotFillContext from './slot-fill-context';
import useSlot from './use-slot';

export default function Slot( {
name,
fillProps = {},
as: Component = 'div',
...props
} ) {
const registry = useContext( SlotFillContext );
const ref = useRef();
const slot = useSlot( name );

useEffect( () => {
registry.registerSlot( name, ref, fillProps );
return () => {
registry.unregisterSlot( name, ref );
};
// We are not including fillProps in the deps because we don't want to
// unregister and register the slot whenever fillProps change, which would
// cause the fill to be re-mounted. We are only considering the initial value
// of fillProps.
}, [ registry.registerSlot, registry.unregisterSlot, name ] );

// fillProps may be an update that interact with the layout, so
// we useLayoutEffect
useLayoutEffect( () => {
if ( slot.fillProps && ! isShallowEqual( slot.fillProps, fillProps ) ) {
registry.updateSlot( name, ref, fillProps );
}
} );

return <Component ref={ ref } { ...props } />;
}
54 changes: 54 additions & 0 deletions packages/components/src/slot-fill/bubbles-virtually/use-slot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* WordPress dependencies
*/
import { useCallback, useContext, useMemo } from '@wordpress/element';

/**
* Internal dependencies
*/
import SlotFillContext from './slot-fill-context';

export default function useSlot( name ) {
const registry = useContext( SlotFillContext );

const slot = registry.slots[ name ] || {};
const slotFills = registry.fills[ name ];
const fills = useMemo( () => slotFills || [], [ slotFills ] );

const updateSlot = useCallback(
( slotRef, slotFillProps ) => {
registry.updateSlot( name, slotRef, slotFillProps );
},
[ name, registry.updateSlot ]
);

const unregisterSlot = useCallback(
( slotRef ) => {
registry.unregisterSlot( name, slotRef );
},
[ name, registry.unregisterSlot ]
);

const registerFill = useCallback(
( fillRef ) => {
registry.registerFill( name, fillRef );
},
[ name, registry.registerFill ]
);

const unregisterFill = useCallback(
( fillRef ) => {
registry.unregisterFill( name, fillRef );
},
[ name, registry.unregisterFill ]
);

return {
...slot,
updateSlot,
unregisterSlot,
fills,
registerFill,
unregisterFill,
};
}
9 changes: 8 additions & 1 deletion packages/components/src/slot-fill/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import {
useEffect,
} from '@wordpress/element';

/**
* Internal dependencies
*/
import SlotFillBubblesVirtuallyProvider from './bubbles-virtually/slot-fill-provider';

const SlotFillContext = createContext( {
registerSlot: () => {},
unregisterSlot: () => {},
Expand Down Expand Up @@ -140,7 +145,9 @@ class SlotFillProvider extends Component {
render() {
return (
<Provider value={ this.contextValue }>
{ this.props.children }
<SlotFillBubblesVirtuallyProvider>
{ this.props.children }
</SlotFillBubblesVirtuallyProvider>
</Provider>
);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/slot-fill/fill.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function FillComponent( { name, children, registerFill, unregisterFill } ) {

useLayoutEffect( () => {
ref.current.children = children;
if ( slot && ! slot.props.bubblesVirtually ) {
if ( slot ) {
slot.forceUpdate();
}
}, [ children ] );
Expand All @@ -49,7 +49,7 @@ function FillComponent( { name, children, registerFill, unregisterFill } ) {
registerFill( name, ref.current );
}, [ name ] );

if ( ! slot || ! slot.node || ! slot.props.bubblesVirtually ) {
if ( ! slot || ! slot.node ) {
return null;
}

Expand Down
Loading

0 comments on commit 9b7eb2d

Please sign in to comment.