Skip to content

Commit

Permalink
Inner blocks: try hook approach
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed Oct 8, 2020
1 parent e376eab commit c49c73d
Show file tree
Hide file tree
Showing 13 changed files with 309 additions and 267 deletions.
157 changes: 80 additions & 77 deletions packages/block-editor/src/components/block-list/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,36 @@ const appenderNodesMap = new Map();
*/
const BLOCK_ANIMATION_THRESHOLD = 200;

function BlockList(
{
className,
rootClientId,
renderAppender,
__experimentalTagName = 'div',
__experimentalAppenderTagName,
__experimentalPassedProps = {},
},
ref
) {
function BlockList( { className, rootClientId, renderAppender }, ref ) {
const Container = rootClientId ? 'div' : RootContainer;
const fallbackRef = useRef();
const wrapperRef = ref || fallbackRef;

return (
<AppenderNodesContext.Provider value={ appenderNodesMap }>
<Container
ref={ wrapperRef }
className={ classnames(
'block-editor-block-list__layout',
className
) }
>
<BlockListItems
rootClientId={ rootClientId }
renderAppender={ renderAppender }
wrapperRef={ wrapperRef }
/>
</Container>
</AppenderNodesContext.Provider>
);
}

function Items( {
rootClientId,
renderAppender,
__experimentalAppenderTagName,
wrapperRef,
} ) {
function selector( select ) {
const {
getBlockOrder,
Expand Down Expand Up @@ -76,85 +95,69 @@ function BlockList(
isDraggingBlocks,
} = useSelect( selector, [ rootClientId ] );

const fallbackRef = useRef();
const element = __experimentalPassedProps.ref || ref || fallbackRef;

const Container = rootClientId ? __experimentalTagName : RootContainer;
const dropTargetIndex = useBlockDropZone( {
element,
element: wrapperRef,
rootClientId,
} );

const isAppenderDropTarget =
dropTargetIndex === blockClientIds.length && isDraggingBlocks;

return (
<AppenderNodesContext.Provider value={ appenderNodesMap }>
<Container
{ ...__experimentalPassedProps }
ref={ element }
className={ classnames(
'block-editor-block-list__layout',
className,
__experimentalPassedProps.className
) }
>
{ blockClientIds.map( ( clientId, index ) => {
const isBlockInSelection = hasMultiSelection
? multiSelectedBlockClientIds.includes( clientId )
: selectedBlockClientId === clientId;

const isDropTarget =
dropTargetIndex === index && isDraggingBlocks;

return (
<AsyncModeProvider
key={ clientId }
value={ ! isBlockInSelection }
>
<BlockListBlock
rootClientId={ rootClientId }
clientId={ clientId }
// This prop is explicitely computed and passed down
// to avoid being impacted by the async mode
// otherwise there might be a small delay to trigger the animation.
index={ index }
enableAnimation={ enableAnimation }
className={ classnames( {
'is-drop-target': isDropTarget,
'is-dropping-horizontally':
isDropTarget &&
orientation === 'horizontal',
} ) }
/>
</AsyncModeProvider>
);
<>
{ blockClientIds.map( ( clientId, index ) => {
const isBlockInSelection = hasMultiSelection
? multiSelectedBlockClientIds.includes( clientId )
: selectedBlockClientId === clientId;

const isDropTarget =
dropTargetIndex === index && isDraggingBlocks;

return (
<AsyncModeProvider
key={ clientId }
value={ ! isBlockInSelection }
>
<BlockListBlock
rootClientId={ rootClientId }
clientId={ clientId }
// This prop is explicitely computed and passed down
// to avoid being impacted by the async mode
// otherwise there might be a small delay to trigger the animation.
index={ index }
enableAnimation={ enableAnimation }
className={ classnames( {
'is-drop-target': isDropTarget,
'is-dropping-horizontally':
isDropTarget &&
orientation === 'horizontal',
} ) }
/>
</AsyncModeProvider>
);
} ) }
<BlockListAppender
tagName={ __experimentalAppenderTagName }
rootClientId={ rootClientId }
renderAppender={ renderAppender }
className={ classnames( {
'is-drop-target': isAppenderDropTarget,
'is-dropping-horizontally':
isAppenderDropTarget && orientation === 'horizontal',
} ) }
<BlockListAppender
tagName={ __experimentalAppenderTagName }
rootClientId={ rootClientId }
renderAppender={ renderAppender }
className={ classnames( {
'is-drop-target': isAppenderDropTarget,
'is-dropping-horizontally':
isAppenderDropTarget &&
orientation === 'horizontal',
} ) }
/>
</Container>
</AppenderNodesContext.Provider>
/>
</>
);
}

const ForwardedBlockList = forwardRef( BlockList );

// This component needs to always be synchronous
// as it's the one changing the async mode
// depending on the block selection.
export default forwardRef( ( props, ref ) => {
export function BlockListItems( props ) {
// This component needs to always be synchronous as it's the one changing
// the async mode depending on the block selection.
return (
<AsyncModeProvider value={ false }>
<ForwardedBlockList ref={ ref } { ...props } />
<Items { ...props } />
</AsyncModeProvider>
);
} );
}

export default forwardRef( BlockList );
5 changes: 4 additions & 1 deletion packages/block-editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export { default as __experimentalGradientPickerPanel } from './gradient-picker/
export { default as __experimentalColorGradientControl } from './colors-gradients/control';
export { default as __experimentalPanelColorGradientSettings } from './colors-gradients/panel-color-gradient-settings';
export { default as __experimentalImageSizeControl } from './image-size-control';
export { default as InnerBlocks } from './inner-blocks';
export {
default as InnerBlocks,
useInnerBlocksProps as __experimentalUseInnerBlocksProps,
} from './inner-blocks';
export { default as InspectorAdvancedControls } from './inspector-advanced-controls';
export { default as InspectorControls } from './inspector-controls';
export { default as __experimentalLinkControl } from './link-control';
Expand Down
155 changes: 89 additions & 66 deletions packages/block-editor/src/components/inner-blocks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import getBlockContext from './get-block-context';
/**
* Internal dependencies
*/
import BlockList from '../block-list';
import { BlockListItems } from '../block-list';
import { BlockContextProvider } from '../block-context';
import { useBlockEditContext } from '../block-edit/context';
import useBlockSync from '../provider/use-block-sync';
Expand All @@ -42,31 +42,27 @@ function UncontrolledInnerBlocks( props ) {
allowedBlocks,
template,
templateLock,
forwardedRef,
wrapperRef,
templateInsertUpdatesSelection,
__experimentalCaptureToolbars: captureToolbars,
__experimentalAppenderTagName,
renderAppender,
orientation,
} = props;

const isSmallScreen = useViewportMatch( 'medium', '<' );
useNestedSettingsUpdate(
clientId,
allowedBlocks,
templateLock,
captureToolbars,
orientation
);

const hasOverlay = useSelect(
( select ) => {
const {
getBlockName,
isBlockSelected,
hasSelectedInnerBlock,
isNavigationMode,
} = select( 'core/block-editor' );
const enableClickThrough = isNavigationMode() || isSmallScreen;
return (
getBlockName( clientId ) !== 'core/template' &&
! isBlockSelected( clientId ) &&
! hasSelectedInnerBlock( clientId, true ) &&
enableClickThrough
);
},
[ clientId, isSmallScreen ]
useInnerBlockTemplateSync(
clientId,
template,
templateLock,
templateInsertUpdatesSelection
);

const context = useSelect(
Expand All @@ -83,42 +79,18 @@ function UncontrolledInnerBlocks( props ) {
[ clientId ]
);

useNestedSettingsUpdate(
clientId,
allowedBlocks,
templateLock,
captureToolbars,
orientation
);

useInnerBlockTemplateSync(
clientId,
template,
templateLock,
templateInsertUpdatesSelection
);

const classes = classnames( {
'has-overlay': hasOverlay,
'is-capturing-toolbar': captureToolbars,
} );

const blockList = (
// This component needs to always be synchronous as it's the one changing
// the async mode depending on the block selection.
return (
<BlockContextProvider value={ context }>
<BlockList
{ ...props }
ref={ forwardedRef }
<BlockListItems
rootClientId={ clientId }
className={ classes }
renderAppender={ renderAppender }
__experimentalAppenderTagName={ __experimentalAppenderTagName }
wrapperRef={ wrapperRef }
/>
</BlockContextProvider>
);

if ( props.__experimentalTagName ) {
return blockList;
}

return <div className="block-editor-inner-blocks">{ blockList }</div>;
}

/**
Expand All @@ -135,27 +107,78 @@ function ControlledInnerBlocks( props ) {
return <UncontrolledInnerBlocks { ...props } />;
}

const ForwardedInnerBlocks = forwardRef( ( props, ref ) => {
const innerBlocksProps = useInnerBlocksProps( { ref }, props );
return (
<div className="block-editor-inner-blocks">
<div { ...innerBlocksProps } />
</div>
);
} );

/**
* Wrapped InnerBlocks component which detects whether to use the controlled or
* uncontrolled variations of the InnerBlocks component. This is the component
* which should be used throughout the application.
* This hook is used to lightly mark an element as an inner blocks wrapper
* element. Call this hook and pass the returned props to the element to mark as
* an inner blocks wrapper, automatically rendering inner blocks as children. If
* you define a ref for the element, it is important to pass the ref to this
* hook, which the hook in turn will pass to the component through the props it
* returns. Optionally, you can also pass any other props through this hook, and
* they will be merged and returned.
*
* @param {Object} props Optional. Props to pass to the element. Must contain
* the ref if one is defined.
* @param {Object} options Optional. Inner blocks options.
*
* @see https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/inner-blocks/README.md
*/
const ForwardedInnerBlocks = forwardRef( ( props, ref ) => {
const { clientId } = useBlockEditContext();
export function useInnerBlocksProps( props = {}, options = {} ) {
const fallbackRef = useRef();
const { clientId } = useBlockEditContext();
const isSmallScreen = useViewportMatch( 'medium', '<' );
const hasOverlay = useSelect(
( select ) => {
const {
getBlockName,
isBlockSelected,
hasSelectedInnerBlock,
isNavigationMode,
} = select( 'core/block-editor' );
const enableClickThrough = isNavigationMode() || isSmallScreen;
return (
getBlockName( clientId ) !== 'core/template' &&
! isBlockSelected( clientId ) &&
! hasSelectedInnerBlock( clientId, true ) &&
enableClickThrough
);
},
[ clientId, isSmallScreen ]
);

const allProps = {
clientId,
forwardedRef: ref || fallbackRef,
const ref = props.ref || fallbackRef;
const InnerBlocks =
options.value && options.onChange
? ControlledInnerBlocks
: UncontrolledInnerBlocks;

return {
...props,
ref,
className: classnames(
props.className,
'block-editor-block-list__layout',
{
'has-overlay': hasOverlay,
}
),
children: (
<InnerBlocks
{ ...options }
clientId={ clientId }
wrapperRef={ ref }
/>
),
};

// Detects if the InnerBlocks should be controlled by an incoming value.
if ( props.value && props.onChange ) {
return <ControlledInnerBlocks { ...allProps } />;
}
return <UncontrolledInnerBlocks { ...allProps } />;
} );
}

// Expose default appender placeholders as components.
ForwardedInnerBlocks.DefaultBlockAppender = DefaultBlockAppender;
Expand Down
Loading

0 comments on commit c49c73d

Please sign in to comment.