Skip to content

Commit

Permalink
Lighter block DOM: put sibling inserter in popover (#19456)
Browse files Browse the repository at this point in the history
* Lighter block DOM: put sibling inserter in popover

* Optimise

* Fix multi select e2e test

* Prevent lingering inserter

* Don't show inserter above selected block

* Remove some dead CSS

* Add tabbable inserter for multi selected blocks

* Polish

* Simplify

* Clean up

* Simplify

* Fix nested block inserter
  • Loading branch information
ellatrix authored Jan 9, 2020
1 parent 18393a3 commit 8abebc6
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 102 deletions.
7 changes: 0 additions & 7 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import BlockCrashBoundary from './block-crash-boundary';
import BlockHtml from './block-html';
import BlockBreadcrumb from './breadcrumb';
import BlockContextualToolbar from './block-contextual-toolbar';
import BlockInsertionPoint from './insertion-point';
import Inserter from '../inserter';
import { isInsideRootBlock } from '../../utils/dom';
import useMovingAnimation from './moving-animation';
Expand Down Expand Up @@ -418,12 +417,6 @@ function BlockListBlock( {
animationStyle
}
>
{ shouldShowInsertionPoint && (
<BlockInsertionPoint
clientId={ clientId }
rootClientId={ rootClientId }
/>
) }
{ shouldRenderDropzone && <BlockDropZone
clientId={ clientId }
rootClientId={ rootClientId }
Expand Down
131 changes: 88 additions & 43 deletions packages/block-editor/src/components/block-list/insertion-point.js
Original file line number Diff line number Diff line change
@@ -1,74 +1,119 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { Popover } from '@wordpress/components';

/**
* Internal dependencies
*/
import Inserter from '../inserter';

export default function BlockInsertionPoint( { rootClientId, clientId } ) {
const [ isInserterFocused, setInserterFocused ] = useState( false );
function Indicator( { clientId } ) {
const showInsertionPoint = useSelect( ( select ) => {
const {
getBlockIndex,
getBlockInsertionPoint,
isBlockInsertionPointVisible,
getBlockRootClientId,
} = select( 'core/block-editor' );
const rootClientId = getBlockRootClientId( clientId );
const blockIndex = getBlockIndex( clientId, rootClientId );
const insertionPoint = getBlockInsertionPoint();
return (
isBlockInsertionPointVisible() &&
insertionPoint.index === blockIndex &&
insertionPoint.rootClientId === rootClientId
);
}, [ clientId, rootClientId ] );
}, [ clientId ] );

function onFocus( event ) {
// Stop propagation of the focus event to avoid selecting the current
// block while inserting a new block, as it is not relevant to sibling
// insertion and conflicts with contextual toolbar placement.
event.stopPropagation();
setInserterFocused( true );
if ( ! showInsertionPoint ) {
return null;
}

function onBlur() {
setInserterFocused( false );
return <div className="block-editor-block-list__insertion-point-indicator" />;
}

export default function InsertionPoint( {
className,
isMultiSelecting,
selectedBlockClientId,
children,
} ) {
const [ isInserterShown, setIsInserterShown ] = useState( false );
const [ isInserterForced, setIsInserterForced ] = useState( false );
const [ inserterElement, setInserterElement ] = useState( null );
const [ inserterClientId, setInserterClientId ] = useState( null );

function onMouseMove( event ) {
if ( event.target.className !== className ) {
if ( isInserterShown ) {
setIsInserterShown( false );
}
return;
}

const rect = event.target.getBoundingClientRect();
const offset = event.clientY - rect.top;
const element = Array.from( event.target.children ).find( ( blockEl ) => {
return blockEl.offsetTop > offset;
} );

if ( ! element ) {
return;
}

const clientId = element.id.slice( 'block-'.length );

if ( ! clientId || clientId === selectedBlockClientId ) {
return;
}

const elementRect = element.getBoundingClientRect();

if ( event.clientX > elementRect.right || event.clientX < elementRect.left ) {
if ( isInserterShown ) {
setIsInserterShown( false );
}
return;
}

setIsInserterShown( true );
setInserterElement( element );
setInserterClientId( clientId );
}

return (
<div className="block-editor-block-list__insertion-point">
{ showInsertionPoint && (
<div className="block-editor-block-list__insertion-point-indicator" />
) }
<div
onFocus={ onFocus }
onBlur={ onBlur }
// While ideally it would be enough to capture the
// bubbling focus event from the Inserter, due to the
// characteristics of click focusing of `button`s in
// Firefox and Safari, it is not reliable.
//
// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
tabIndex={ -1 }
className={
classnames( 'block-editor-block-list__insertion-point-inserter', {
'is-visible': isInserterFocused,
} )
}
>
<Inserter
rootClientId={ rootClientId }
clientId={ clientId }
/>
return <>
{ ! isMultiSelecting && ( isInserterShown || isInserterForced ) && <Popover
noArrow
animate={ false }
anchorRef={ inserterElement }
position="top right left"
focusOnMount={ false }
className="block-editor-block-list__block-popover"
__unstableSlotName="block-toolbar"
>
<div className="block-editor-block-list__insertion-point" style={ { width: inserterElement.offsetWidth } }>
<Indicator clientId={ inserterClientId } />
<div
onFocus={ () => setIsInserterForced( true ) }
onBlur={ () => setIsInserterForced( false ) }
// While ideally it would be enough to capture the
// bubbling focus event from the Inserter, due to the
// characteristics of click focusing of `button`s in
// Firefox and Safari, it is not reliable.
//
// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
tabIndex={ -1 }
className="block-editor-block-list__insertion-point-inserter"
>
<Inserter clientId={ inserterClientId } />
</div>
</div>
</Popover> }
<div onMouseMove={ ! isInserterForced && ! isMultiSelecting ? onMouseMove : undefined }>
{ children }
</div>
);
</>;
}
26 changes: 18 additions & 8 deletions packages/block-editor/src/components/block-list/root-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useSelect, useDispatch } from '@wordpress/data';
*/
import useMultiSelection from './use-multi-selection';
import { getBlockClientId } from '../../utils/dom';
import InsertionPoint from './insertion-point';

/** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */

Expand All @@ -18,11 +19,13 @@ function selector( select ) {
const {
getSelectedBlockClientId,
hasMultiSelection,
isMultiSelecting,
} = select( 'core/block-editor' );

return {
selectedBlockClientId: getSelectedBlockClientId(),
hasMultiSelection: hasMultiSelection(),
isMultiSelecting: isMultiSelecting(),
};
}

Expand All @@ -46,6 +49,7 @@ export default function RootContainer( { children, className } ) {
const {
selectedBlockClientId,
hasMultiSelection,
isMultiSelecting,
} = useSelect( selector, [] );
const { selectBlock } = useDispatch( 'core/block-editor' );
const onSelectionStart = useMultiSelection( ref );
Expand All @@ -70,15 +74,21 @@ export default function RootContainer( { children, className } ) {
}

return (
<div
ref={ ref }
<InsertionPoint
className={ className }
onFocus={ onFocus }
onDragStart={ onDragStart }
isMultiSelecting={ isMultiSelecting }
selectedBlockClientId={ selectedBlockClientId }
>
<Context.Provider value={ onSelectionStart }>
{ children }
</Context.Provider>
</div>
<div
ref={ ref }
className={ className }
onFocus={ onFocus }
onDragStart={ onDragStart }
>
<Context.Provider value={ onSelectionStart }>
{ children }
</Context.Provider>
</div>
</InsertionPoint>
);
}
32 changes: 1 addition & 31 deletions packages/block-editor/src/components/block-list/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
.block-editor-block-list__layout {
padding-left: $block-padding;
padding-right: $block-padding;
position: relative;

// Beyond the mobile breakpoint, compensate for side UI.
@include break-small() {
Expand Down Expand Up @@ -402,12 +403,7 @@
display: flex;
}

position: absolute;
bottom: auto;
left: 0;
right: 0;
justify-content: center;
height: $block-padding + 8px;

// Show a clickable plus.
.block-editor-inserter__toggle {
Expand All @@ -422,32 +418,6 @@
box-shadow: none;
}
}

// Hide both the button until hovered.
opacity: 0;
transition: opacity 0.1s linear;
@include reduce-motion("transition");

&:hover,
&.is-visible {
opacity: 1;
}
}

// Don't show the sibling inserter before the selected block.
.edit-post-layout:not(.has-fixed-toolbar) {
// The child selector is necessary for this to work properly in nested contexts.
.is-selected > .block-editor-block-list__insertion-point > .block-editor-block-list__insertion-point-inserter,
.is-focused > .block-editor-block-list__insertion-point > .block-editor-block-list__insertion-point-inserter {
opacity: 0;
pointer-events: none;

&:hover,
&.is-visible {
opacity: 1;
pointer-events: auto;
}
}
}

// This is the edge-to-edge hover area that contains the plus.
Expand Down
43 changes: 42 additions & 1 deletion packages/block-editor/src/components/block-toolbar/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
import { Toolbar } from '@wordpress/components';
import { useState } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -13,18 +20,21 @@ import BlockSettingsMenu from '../block-settings-menu';
import BlockSwitcher from '../block-switcher';
import MultiBlocksSwitcher from '../block-switcher/multi-blocks-switcher';
import BlockMover from '../block-mover';
import Inserter from '../inserter';

export default function BlockToolbar( { moverDirection, hasMovers = true } ) {
const { blockClientIds, isValid, mode } = useSelect( ( select ) => {
const { blockClientIds, isValid, mode, rootClientId } = useSelect( ( select ) => {
const {
getBlockMode,
getSelectedBlockClientIds,
isBlockValid,
getBlockRootClientId,
} = select( 'core/block-editor' );
const selectedBlockClientIds = getSelectedBlockClientIds();

return {
blockClientIds: selectedBlockClientIds,
rootClientId: getBlockRootClientId( selectedBlockClientIds[ 0 ] ),
isValid: selectedBlockClientIds.length === 1 ?
isBlockValid( selectedBlockClientIds[ 0 ] ) :
null,
Expand All @@ -33,11 +43,40 @@ export default function BlockToolbar( { moverDirection, hasMovers = true } ) {
null,
};
}, [] );
const [ isInserterShown, setIsInserterShown ] = useState( false );

if ( blockClientIds.length === 0 ) {
return null;
}

function onFocus() {
setIsInserterShown( true );
}

function onBlur() {
setIsInserterShown( false );
}

const inserter = (
<Toolbar
onFocus={ onFocus }
onBlur={ onBlur }
// While ideally it would be enough to capture the
// bubbling focus event from the Inserter, due to the
// characteristics of click focusing of `button`s in
// Firefox and Safari, it is not reliable.
//
// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
tabIndex={ -1 }
className={ classnames(
'block-editor-block-toolbar__inserter',
{ 'is-visible': isInserterShown }
) }
>
<Inserter clientId={ blockClientIds[ 0 ] } rootClientId={ rootClientId } />
</Toolbar>
);

if ( blockClientIds.length > 1 ) {
return (
<div className="block-editor-block-toolbar">
Expand All @@ -47,6 +86,7 @@ export default function BlockToolbar( { moverDirection, hasMovers = true } ) {
/> ) }
<MultiBlocksSwitcher />
<BlockSettingsMenu clientIds={ blockClientIds } />
{ inserter }
</div>
);
}
Expand All @@ -65,6 +105,7 @@ export default function BlockToolbar( { moverDirection, hasMovers = true } ) {
</>
) }
<BlockSettingsMenu clientIds={ blockClientIds } />
{ inserter }
</div>
);
}
Loading

0 comments on commit 8abebc6

Please sign in to comment.