-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Components: Refactor SlotFill #19242
Changes from all commits
6061ecd
7e38b5a
d032bee
13b60ef
0c77a8a
bd84603
23d3abc
229d432
7aff149
e8bbb9b
df31e9c
1fc8026
81c4741
c6f6341
7085a53
4ab2b9e
ba2ea9f
2db94e9
a275949
0b11256
94020b0
fcf9398
104335e
aa61605
d1bfe35
64987ca
7c3e5fc
2b31dbd
04f44ce
ce97491
d3ebcc2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ import { | |
} from '@wordpress/blocks'; | ||
import { | ||
PanelBody, | ||
__experimentalSlotFillConsumer, | ||
__experimentalUseSlot as useSlot, | ||
} from '@wordpress/components'; | ||
import { withSelect } from '@wordpress/data'; | ||
|
||
|
@@ -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 />; | ||
} | ||
|
@@ -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> | ||
) } | ||
Comment on lines
+75
to
+83
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. Related discussion: #16807 (review) |
||
</div> | ||
<SkipToSelectedBlock key="back" /> | ||
</div> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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( | ||
|
@@ -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; | ||
|
@@ -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>; | ||
Comment on lines
+606
to
+614
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. Looks like PopoverSlot always uses bubblesVirtually, so I also changed this snippet to use |
||
}; | ||
|
||
const PopoverContainer = Popover; | ||
|
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 } ) { | ||
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. This is a simple and nice implementation but I believe we had issues with fills being reordered when we switch between blocks (toolbar and inspector) and we had to add a custom logic to keep the fills rendering in the exact same order they appear on the React tree (even with conditions). I see that this new implementation passes the existing unit tests right which I believe account for this (and I'm surprised it does without any tweaks to the rendering order). We might just want to confirm in the UI too. 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. Hmm, from what I can see, the current unit tests are not covering that case for Also, I can't find how it's been solved for the current implementation (using I'm pretty sure that my implementation doesn't cover that. I've just made some experiments with the 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 related to the |
||
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 ) { | ||
Comment on lines
+29
to
+32
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. Should it be safe to assume In debugging the issue mentioned by @draganescu at #18560 (comment) , the exact error occurs at this line of code: It's not yet clear to me if the bug is in how the slot came to be |
||
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> | ||
); | ||
} |
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 } />; | ||
} |
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, | ||
}; | ||
} |
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, let's keep
__experimentalUseSlot
experimental until we come up with a better way to handle it. It seems fine for the time being.