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;