From 2564c2122f75541cbdf4fcfdebdfc839dadeeb7a Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 6 Jun 2019 18:27:01 +0100 Subject: [PATCH] Refactor the hovered area component to a hook (#15038) --- .../src/components/block-list/block.js | 461 +++++++++--------- .../src/components/block-list/hover-area.js | 99 ++-- 2 files changed, 259 insertions(+), 301 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 82a237625e6a77..aac68e2e6faa88 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -48,7 +48,7 @@ import BlockInsertionPoint from './insertion-point'; import IgnoreNestedEvents from '../ignore-nested-events'; import InserterWithShortcuts from '../inserter-with-shortcuts'; import Inserter from '../inserter'; -import HoverArea from './hover-area'; +import useHoveredArea from './hover-area'; import { isInsideRootBlock } from '../../utils/dom'; /** @@ -79,6 +79,7 @@ function BlockListBlock( { isParentOfSelectedBlock, isDraggable, isSelectionEnabled, + isRTL, className, name, isValid, @@ -105,13 +106,14 @@ function BlockListBlock( { const wrapper = useRef( null ); useEffect( () => { blockRef( wrapper.current, clientId ); - // We need to rerender to trigger a rerendering of HoverArea. - rerender(); }, [] ); // Reference to the block edit node const blockNodeRef = useRef(); + // Hovered area of the block + const hoverArea = useHoveredArea( wrapper ); + // Keep track of touchstart to disable hover on iOS const hadTouchStart = useRef( false ); const onTouchStart = () => { @@ -333,240 +335,236 @@ function BlockListBlock( { } }; + // Rendering the output + const isHovered = isBlockHovered && ! isPartOfMultiSelection; + const blockType = getBlockType( name ); + // 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 = 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 showInserterShortcuts = ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid; + const showEmptyBlockSideInserter = ( isSelected || isHovered || isLast ) && isEmptyDefaultBlock && isValid; + const shouldAppearSelected = + ! isFocusMode && + ! showEmptyBlockSideInserter && + isSelected && + ! isTypingWithinBlock; + const shouldAppearHovered = + ! isFocusMode && + ! hasFixedToolbar && + isHovered && + ! isEmptyDefaultBlock; + // We render block movers and block settings to keep them tabbale even if hidden + const shouldRenderMovers = + ( isSelected || hoverArea === ( isRTL ? 'right' : 'left' ) ) && + ! showEmptyBlockSideInserter && + ! isPartOfMultiSelection && + ! isTypingWithinBlock; + const shouldShowBreadcrumb = + ! isFocusMode && isHovered && ! isEmptyDefaultBlock; + const shouldShowContextualToolbar = + ! hasFixedToolbar && + ! showEmptyBlockSideInserter && + ( + ( isSelected && ( ! isTypingWithinBlock || isCaretWithinFormattedText ) ) || + isFirstMultiSelected + ); + const shouldShowMobileToolbar = shouldAppearSelected; + + // 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; + + // 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 block-editor-block-list__block', + { + 'has-warning': ! isValid || !! hasError || isUnregisteredBlock, + 'is-selected': shouldAppearSelected, + 'is-multi-selected': isPartOfMultiSelection, + 'is-hovered': shouldAppearHovered, + 'is-reusable': isReusableBlock( blockType ), + 'is-dragging': isDragging, + 'is-typing': isTypingWithinBlock, + 'is-focused': isFocusMode && ( isSelected || isParentOfSelectedBlock ), + 'is-focus-mode': isFocusMode, + }, + className + ); + + // Determine whether the block has props to apply to the wrapper. + let blockWrapperProps = wrapperProps; + if ( blockType.getEditWrapperProps ) { + blockWrapperProps = { + ...blockWrapperProps, + ...blockType.getEditWrapperProps( 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 ( - - { ( { hoverArea } ) => { - const isHovered = isBlockHovered && ! isPartOfMultiSelection; - const blockType = getBlockType( name ); - // 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 = 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 showInserterShortcuts = ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid; - const showEmptyBlockSideInserter = ( isSelected || isHovered || isLast ) && isEmptyDefaultBlock && isValid; - const shouldAppearSelected = - ! isFocusMode && - ! showEmptyBlockSideInserter && + + { shouldShowInsertionPoint && ( + + ) } + + { isFirstMultiSelected && ( + + ) } +
+ { shouldRenderMovers && ( + + ) } + { shouldShowBreadcrumb && ( + + ) } + { ( shouldShowContextualToolbar || isForcingContextualToolbar.current ) && ( + + ) } + { + ! shouldShowContextualToolbar && isSelected && - ! isTypingWithinBlock; - const shouldAppearHovered = - ! isFocusMode && - ! hasFixedToolbar && - isHovered && - ! isEmptyDefaultBlock; - // We render block movers and block settings to keep them tabbale even if hidden - const shouldRenderMovers = - ( isSelected || hoverArea === 'left' ) && - ! showEmptyBlockSideInserter && - ! isPartOfMultiSelection && - ! isTypingWithinBlock; - const shouldShowBreadcrumb = - ! isFocusMode && isHovered && ! isEmptyDefaultBlock; - const shouldShowContextualToolbar = ! hasFixedToolbar && - ! showEmptyBlockSideInserter && - ( ( isSelected && - ( ! isTypingWithinBlock || isCaretWithinFormattedText ) ) || - isFirstMultiSelected ); - const shouldShowMobileToolbar = shouldAppearSelected; - - // 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; - - // 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 block-editor-block-list__block', - { - 'has-warning': ! isValid || !! hasError || isUnregisteredBlock, - 'is-selected': shouldAppearSelected, - 'is-multi-selected': isPartOfMultiSelection, - 'is-hovered': shouldAppearHovered, - 'is-reusable': isReusableBlock( blockType ), - 'is-dragging': isDragging, - 'is-typing': isTypingWithinBlock, - 'is-focused': - isFocusMode && ( isSelected || isParentOfSelectedBlock ), - 'is-focus-mode': isFocusMode, - }, - className - ); - - // Determine whether the block has props to apply to the wrapper. - let blockWrapperProps = wrapperProps; - if ( blockType.getEditWrapperProps ) { - blockWrapperProps = { - ...blockWrapperProps, - ...blockType.getEditWrapperProps( attributes ), - }; + ! isEmptyDefaultBlock && ( + + ) } - 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 = ( - + + { isValid && blockEdit } + { isValid && mode === 'html' && ( + + ) } + { ! isValid && [ + , +
+ { getSaveElement( blockType, attributes ) } +
, + ] } +
+ { shouldShowMobileToolbar && ( + + ) } + { !! hasError && } + +
+ { showInserterShortcuts && ( +
+ - ); - 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 && ( - - ) } - - { isFirstMultiSelected && ( - - ) } -
- { shouldRenderMovers && ( - - ) } - { shouldShowBreadcrumb && ( - - ) } - { ( shouldShowContextualToolbar || - isForcingContextualToolbar.current ) && ( - - ) } - { ! shouldShowContextualToolbar && - isSelected && - ! hasFixedToolbar && - ! isEmptyDefaultBlock && ( - - ) } - - - { isValid && blockEdit } - { isValid && mode === 'html' && ( - - ) } - { ! isValid && [ - , -
- { getSaveElement( blockType, attributes ) } -
, - ] } -
- { shouldShowMobileToolbar && ( - - ) } - { !! hasError && } -
-
- { showInserterShortcuts && ( -
- -
- ) } - { showEmptyBlockSideInserter && ( -
- -
- ) } -
- ); - /* eslint-enable 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 */ - } } - +
+ ) } + { showEmptyBlockSideInserter && ( +
+ +
+ ) } +
); + /* eslint-enable 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 */ } const applyWithSelect = withSelect( @@ -590,7 +588,7 @@ const applyWithSelect = withSelect( } = select( 'core/block-editor' ); const block = __unstableGetBlockWithoutInnerBlocks( clientId ); const isSelected = isBlockSelected( clientId ); - const { hasFixedToolbar, focusMode } = getSettings(); + const { hasFixedToolbar, focusMode, isRTL } = getSettings(); const templateLock = getTemplateLock( rootClientId ); const isParentOfSelectedBlock = hasSelectedInnerBlock( clientId, true ); const index = getBlockIndex( clientId, rootClientId ); @@ -620,6 +618,7 @@ const applyWithSelect = withSelect( isFocusMode: focusMode && isLargeViewport, hasFixedToolbar: hasFixedToolbar && isLargeViewport, isLast: index === blockOrder.length - 1, + isRTL, // Users of the editor.BlockListBlock filter used to be able to access the block prop // Ideally these blocks would rely on the clientId prop only. diff --git a/packages/block-editor/src/components/block-list/hover-area.js b/packages/block-editor/src/components/block-list/hover-area.js index a79b0bcd9b088b..36664e3c9d797c 100644 --- a/packages/block-editor/src/components/block-list/hover-area.js +++ b/packages/block-editor/src/components/block-list/hover-area.js @@ -1,82 +1,41 @@ /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; -import { withSelect } from '@wordpress/data'; +import { useState, useEffect } from '@wordpress/element'; -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 ); - } - } +const useHoveredArea = ( wrapper ) => { + const [ hoveredArea, setHoveredArea ] = useState( null ); - 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 } ); - } - } + useEffect( () => { + const onMouseLeave = () => { + if ( hoveredArea ) { + setHoveredArea( null ); + } + }; - onMouseMove( event ) { - const { isRTL, container } = this.props; - const { width, left, right } = container.getBoundingClientRect(); + const onMouseMove = ( event ) => { + const { width, left, right } = wrapper.current.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'; - } + let newHoveredArea = null; + if ( ( event.clientX - left ) < width / 3 ) { + newHoveredArea = 'left'; + } else if ( ( right - event.clientX ) < width / 3 ) { + newHoveredArea = 'right'; + } - if ( hoverArea !== this.state.hoverArea ) { - this.setState( { hoverArea } ); - } - } + setHoveredArea( newHoveredArea ); + }; - render() { - const { hoverArea } = this.state; - const { children } = this.props; + wrapper.current.addEventListener( 'mousemove', onMouseMove ); + wrapper.current.addEventListener( 'mouseleave', onMouseLeave ); - return children( { hoverArea } ); - } -} + return () => { + wrapper.current.removeEventListener( 'mousemove', onMouseMove ); + wrapper.current.removeEventListener( 'mouseleave', onMouseLeave ); + }; + }, [] ); -export default withSelect( ( select ) => { - return { - isRTL: select( 'core/block-editor' ).getSettings().isRTL, - }; -} )( HoverArea ); + return hoveredArea; +}; +export default useHoveredArea;