diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js
index 1d8561de8d913..8b656f0616944 100644
--- a/packages/editor/src/components/block-list/block.js
+++ b/packages/editor/src/components/block-list/block.js
@@ -46,7 +46,7 @@ import BlockInsertionPoint from './insertion-point';
import IgnoreNestedEvents from '../ignore-nested-events';
import InserterWithShortcuts from '../inserter-with-shortcuts';
import Inserter from '../inserter';
-import withHoverAreas from './with-hover-areas';
+import HoverArea from './hover-area';
import { isInsideRootBlock } from '../../utils/dom';
export class BlockListBlock extends Component {
@@ -104,6 +104,11 @@ export class BlockListBlock extends Component {
setBlockListRef( node ) {
this.wrapperNode = node;
this.props.blockRef( node, this.props.clientId );
+
+ // We need to rerender to trigger a rerendering of HoverArea
+ // it depents on this.wrapperNode but we can't keep this.wrapperNode in state
+ // Because we need it to be immediately availeble for `focusableTabbable` to work.
+ this.forceUpdate();
}
bindBlockNode( node ) {
@@ -353,219 +358,225 @@ export class BlockListBlock extends Component {
}
render() {
- const {
- block,
- order,
- mode,
- isFocusMode,
- hasFixedToolbar,
- isLocked,
- isFirst,
- isLast,
- clientId,
- rootClientId,
- isSelected,
- isPartOfMultiSelection,
- isFirstMultiSelected,
- isTypingWithinBlock,
- isCaretWithinFormattedText,
- isMultiSelecting,
- hoverArea,
- isEmptyDefaultBlock,
- isMovable,
- isPreviousBlockADefaultEmptyBlock,
- isParentOfSelectedBlock,
- isDraggable,
- } = this.props;
- const isHovered = this.state.isHovered && ! isMultiSelecting;
- const { name: blockName, isValid } = block;
- const blockType = getBlockType( blockName );
- // translators: %s: Type of block (i.e. Text, Image etc)
- const blockLabel = sprintf( __( 'Block: %s' ), blockType.title );
- // The block as rendered in the editor is composed of general block UI
- // (mover, toolbar, wrapper) and the display of the block content.
-
- const isUnregisteredBlock = block.name === getUnregisteredTypeHandlerName();
-
- // If the block is selected and we're typing the block should not appear.
- // Empty paragraph blocks should always show up as unselected.
- const showEmptyBlockSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid;
- const showSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock;
- const shouldAppearSelected = ! isFocusMode && ! hasFixedToolbar && ! showSideInserter && isSelected && ! isTypingWithinBlock;
- const shouldAppearHovered = ! isFocusMode && ! hasFixedToolbar && isHovered && ! isEmptyDefaultBlock;
- // We render block movers and block settings to keep them tabbale even if hidden
- const shouldRenderMovers = ! isFocusMode && ( isSelected || hoverArea === 'left' ) && ! showEmptyBlockSideInserter && ! isMultiSelecting && ! isPartOfMultiSelection && ! isTypingWithinBlock;
- const shouldShowBreadcrumb = ! isFocusMode && isHovered && ! isEmptyDefaultBlock;
- const shouldShowContextualToolbar = ! hasFixedToolbar && ! showSideInserter && ( ( isSelected && ( ! isTypingWithinBlock || isCaretWithinFormattedText ) ) || isFirstMultiSelected );
- const shouldShowMobileToolbar = shouldAppearSelected;
- const { error, dragging } = this.state;
-
- // Insertion point can only be made visible if the block is at the
- // the extent of a multi-selection, or not in a multi-selection.
- const shouldShowInsertionPoint = ( isPartOfMultiSelection && isFirstMultiSelected ) || ! isPartOfMultiSelection;
- const canShowInBetweenInserter = ! isEmptyDefaultBlock && ! isPreviousBlockADefaultEmptyBlock;
-
- // The wp-block className is important for editor styles.
- // Generate the wrapper class names handling the different states of the block.
- const wrapperClassName = classnames( 'wp-block editor-block-list__block', {
- 'has-warning': ! isValid || !! error || isUnregisteredBlock,
- 'is-selected': shouldAppearSelected,
- 'is-multi-selected': isPartOfMultiSelection,
- 'is-hovered': shouldAppearHovered,
- 'is-reusable': isReusableBlock( blockType ),
- 'is-dragging': dragging,
- 'is-typing': isTypingWithinBlock,
- 'is-focused': isFocusMode && ( isSelected || isParentOfSelectedBlock ),
- 'is-focus-mode': isFocusMode,
- } );
-
- const { onReplace } = this.props;
-
- // Determine whether the block has props to apply to the wrapper.
- let wrapperProps = this.props.wrapperProps;
- if ( blockType.getEditWrapperProps ) {
- wrapperProps = {
- ...wrapperProps,
- ...blockType.getEditWrapperProps( block.attributes ),
- };
- }
- const blockElementId = `block-${ clientId }`;
-
- // We wrap the BlockEdit component in a div that hides it when editing in
- // HTML mode. This allows us to render all of the ancillary pieces
- // (InspectorControls, etc.) which are inside `BlockEdit` but not
- // `BlockHTML`, even in HTML mode.
- let blockEdit = (
-
- );
- if ( mode !== 'visual' ) {
- blockEdit =
{ blockEdit }
;
- }
-
- // Disable reasons:
- //
- // jsx-a11y/mouse-events-have-key-events:
- // - onMouseOver is explicitly handling hover effects
- //
- // jsx-a11y/no-static-element-interactions:
- // - Each block can be selected by clicking on it
-
- /* eslint-disable jsx-a11y/mouse-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
return (
-
- { shouldShowInsertionPoint && (
-
- ) }
-
- { shouldRenderMovers && (
-
- ) }
- { shouldShowBreadcrumb && (
-
- ) }
- { shouldShowContextualToolbar && }
- { isFirstMultiSelected && (
-
- ) }
-
-
- { isValid && blockEdit }
- { isValid && mode === 'html' && (
-
- ) }
- { ! isValid && [
- ,
-
- { getSaveElement( blockType, block.attributes ) }
-
,
- ] }
-
- { shouldShowMobileToolbar && (
-
+ { ( { hoverArea } ) => {
+ const {
+ block,
+ order,
+ mode,
+ isFocusMode,
+ hasFixedToolbar,
+ isLocked,
+ isFirst,
+ isLast,
+ clientId,
+ rootClientId,
+ isSelected,
+ isPartOfMultiSelection,
+ isFirstMultiSelected,
+ isTypingWithinBlock,
+ isCaretWithinFormattedText,
+ isMultiSelecting,
+ isEmptyDefaultBlock,
+ isMovable,
+ isPreviousBlockADefaultEmptyBlock,
+ isParentOfSelectedBlock,
+ isDraggable,
+ } = this.props;
+ const isHovered = this.state.isHovered && ! isMultiSelecting;
+ const { name: blockName, isValid } = block;
+ const blockType = getBlockType( blockName );
+ // translators: %s: Type of block (i.e. Text, Image etc)
+ const blockLabel = sprintf( __( 'Block: %s' ), blockType.title );
+ // The block as rendered in the editor is composed of general block UI
+ // (mover, toolbar, wrapper) and the display of the block content.
+
+ const isUnregisteredBlock = block.name === getUnregisteredTypeHandlerName();
+
+ // If the block is selected and we're typing the block should not appear.
+ // Empty paragraph blocks should always show up as unselected.
+ const showEmptyBlockSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid;
+ const showSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock;
+ const shouldAppearSelected = ! isFocusMode && ! hasFixedToolbar && ! showSideInserter && isSelected && ! isTypingWithinBlock;
+ const shouldAppearHovered = ! isFocusMode && ! hasFixedToolbar && isHovered && ! isEmptyDefaultBlock;
+ // We render block movers and block settings to keep them tabbale even if hidden
+ const shouldRenderMovers = ! isFocusMode && ( isSelected || hoverArea === 'left' ) && ! showEmptyBlockSideInserter && ! isMultiSelecting && ! isPartOfMultiSelection && ! isTypingWithinBlock;
+ const shouldShowBreadcrumb = ! isFocusMode && isHovered && ! isEmptyDefaultBlock;
+ const shouldShowContextualToolbar = ! hasFixedToolbar && ! showSideInserter && ( ( isSelected && ( ! isTypingWithinBlock || isCaretWithinFormattedText ) ) || isFirstMultiSelected );
+ const shouldShowMobileToolbar = shouldAppearSelected;
+ const { error, dragging } = this.state;
+
+ // Insertion point can only be made visible if the block is at the
+ // the extent of a multi-selection, or not in a multi-selection.
+ const shouldShowInsertionPoint = ( isPartOfMultiSelection && isFirstMultiSelected ) || ! isPartOfMultiSelection;
+ const canShowInBetweenInserter = ! isEmptyDefaultBlock && ! isPreviousBlockADefaultEmptyBlock;
+
+ // The wp-block className is important for editor styles.
+ // Generate the wrapper class names handling the different states of the block.
+ const wrapperClassName = classnames( 'wp-block editor-block-list__block', {
+ 'has-warning': ! isValid || !! error || isUnregisteredBlock,
+ 'is-selected': shouldAppearSelected,
+ 'is-multi-selected': isPartOfMultiSelection,
+ 'is-hovered': shouldAppearHovered,
+ 'is-reusable': isReusableBlock( blockType ),
+ 'is-dragging': dragging,
+ 'is-typing': isTypingWithinBlock,
+ 'is-focused': isFocusMode && ( isSelected || isParentOfSelectedBlock ),
+ 'is-focus-mode': isFocusMode,
+ } );
+
+ const { onReplace } = this.props;
+
+ // Determine whether the block has props to apply to the wrapper.
+ let wrapperProps = this.props.wrapperProps;
+ if ( blockType.getEditWrapperProps ) {
+ wrapperProps = {
+ ...wrapperProps,
+ ...blockType.getEditWrapperProps( block.attributes ),
+ };
+ }
+ const blockElementId = `block-${ clientId }`;
+
+ // We wrap the BlockEdit component in a div that hides it when editing in
+ // HTML mode. This allows us to render all of the ancillary pieces
+ // (InspectorControls, etc.) which are inside `BlockEdit` but not
+ // `BlockHTML`, even in HTML mode.
+ let blockEdit = (
+
- ) }
- { !! error && }
-
- { showEmptyBlockSideInserter && (
-
-
- { blockEdit }
;
+ }
+
+ // Disable reasons:
+ //
+ // jsx-a11y/mouse-events-have-key-events:
+ // - onMouseOver is explicitly handling hover effects
+ //
+ // jsx-a11y/no-static-element-interactions:
+ // - Each block can be selected by clicking on it
+
+ /* eslint-disable jsx-a11y/mouse-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
+
+ return (
+
+ { shouldShowInsertionPoint && (
+
+ ) }
+
-
-
-
-
-
- ) }
-
+ { shouldRenderMovers && (
+
+ ) }
+ { shouldShowBreadcrumb && (
+
+ ) }
+ { shouldShowContextualToolbar && }
+ { isFirstMultiSelected && (
+
+ ) }
+
+
+ { isValid && blockEdit }
+ { isValid && mode === 'html' && (
+
+ ) }
+ { ! isValid && [
+ ,
+
+ { getSaveElement( blockType, block.attributes ) }
+
,
+ ] }
+
+ { shouldShowMobileToolbar && (
+
+ ) }
+ { !! error && }
+
+ { showEmptyBlockSideInserter && (
+
+
+
+
+
+
+
+
+ ) }
+
+ );
+ /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
+ } }
+
);
- /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
}
}
@@ -676,5 +687,4 @@ export default compose(
applyWithSelect,
applyWithDispatch,
withFilters( 'editor.BlockListBlock' ),
- withHoverAreas,
)( BlockListBlock );
diff --git a/packages/editor/src/components/block-list/hover-area.js b/packages/editor/src/components/block-list/hover-area.js
new file mode 100644
index 0000000000000..ff0a16144b4f0
--- /dev/null
+++ b/packages/editor/src/components/block-list/hover-area.js
@@ -0,0 +1,82 @@
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+import { withSelect } from '@wordpress/data';
+
+class HoverArea extends Component {
+ constructor() {
+ super( ...arguments );
+ this.state = {
+ hoverArea: null,
+ };
+ this.onMouseLeave = this.onMouseLeave.bind( this );
+ this.onMouseMove = this.onMouseMove.bind( this );
+ }
+
+ componentWillUnmount() {
+ if ( this.props.container ) {
+ this.toggleListeners( this.props.container, false );
+ }
+ }
+
+ componentDidMount() {
+ if ( this.props.container ) {
+ this.toggleListeners( this.props.container );
+ }
+ }
+
+ componentDidUpdate( prevProps ) {
+ if ( prevProps.container === this.props.container ) {
+ return;
+ }
+ if ( prevProps.container ) {
+ this.toggleListeners( prevProps.container, false );
+ }
+ if ( this.props.container ) {
+ this.toggleListeners( this.props.container, true );
+ }
+ }
+
+ toggleListeners( container, shouldListnerToEvents = true ) {
+ const method = shouldListnerToEvents ? 'addEventListener' : 'removeEventListener';
+ container[ method ]( 'mousemove', this.onMouseMove );
+ container[ method ]( 'mouseleave', this.onMouseLeave );
+ }
+
+ onMouseLeave() {
+ if ( this.state.hoverArea ) {
+ this.setState( { hoverArea: null } );
+ }
+ }
+
+ onMouseMove( event ) {
+ const { isRTL, container } = this.props;
+ const { width, left, right } = container.getBoundingClientRect();
+
+ let hoverArea = null;
+ if ( ( event.clientX - left ) < width / 3 ) {
+ hoverArea = isRTL ? 'right' : 'left';
+ } else if ( ( right - event.clientX ) < width / 3 ) {
+ hoverArea = isRTL ? 'left' : 'right';
+ }
+
+ if ( hoverArea !== this.state.hoverArea ) {
+ this.setState( { hoverArea } );
+ }
+ }
+
+ render() {
+ const { hoverArea } = this.state;
+ const { children } = this.props;
+
+ return children( { hoverArea } );
+ }
+}
+
+export default withSelect( ( select ) => {
+ return {
+ isRTL: select( 'core/editor' ).getEditorSettings().isRTL,
+ };
+} )( HoverArea );
+
diff --git a/packages/editor/src/components/block-list/with-hover-areas.js b/packages/editor/src/components/block-list/with-hover-areas.js
deleted file mode 100644
index d5f51f6e1bfd8..0000000000000
--- a/packages/editor/src/components/block-list/with-hover-areas.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { Component, findDOMNode } from '@wordpress/element';
-import { withSelect } from '@wordpress/data';
-import { createHigherOrderComponent } from '@wordpress/compose';
-
-const withHoverAreas = createHigherOrderComponent( ( WrappedComponent ) => {
- class WithHoverAreasComponent extends Component {
- constructor() {
- super( ...arguments );
- this.state = {
- hoverArea: null,
- };
- this.onMouseLeave = this.onMouseLeave.bind( this );
- this.onMouseMove = this.onMouseMove.bind( this );
- }
-
- componentDidMount() {
- // Disable reason: We use findDOMNode to avoid unnecessary extra dom Nodes
- // eslint-disable-next-line react/no-find-dom-node
- this.container = findDOMNode( this );
- this.container.addEventListener( 'mousemove', this.onMouseMove );
- this.container.addEventListener( 'mouseleave', this.onMouseLeave );
- }
-
- componentWillUnmount() {
- this.container.removeEventListener( 'mousemove', this.onMouseMove );
- this.container.removeEventListener( 'mouseleave', this.onMouseLeave );
- }
-
- onMouseLeave() {
- if ( this.state.hoverArea ) {
- this.setState( { hoverArea: null } );
- }
- }
-
- onMouseMove( event ) {
- const { isRTL } = this.props;
- const { width, left, right } = this.container.getBoundingClientRect();
-
- let hoverArea = null;
- if ( ( event.clientX - left ) < width / 3 ) {
- hoverArea = isRTL ? 'right' : 'left';
- } else if ( ( right - event.clientX ) < width / 3 ) {
- hoverArea = isRTL ? 'left' : 'right';
- }
-
- if ( hoverArea !== this.state.hoverArea ) {
- this.setState( { hoverArea } );
- }
- }
-
- render() {
- const { hoverArea } = this.state;
- return (
-
- );
- }
- }
-
- return withSelect( ( select ) => {
- return {
- isRTL: select( 'core/editor' ).getEditorSettings().isRTL,
- };
- } )( WithHoverAreasComponent );
-} );
-
-export default withHoverAreas;