From 9496ff2795f95abde1df86f26519fff9ffa68463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Fri, 4 Dec 2020 11:58:16 +0200 Subject: [PATCH 1/4] Block wrapper: isolate functionality into smaller hooks --- .../components/block-list/block-wrapper.js | 289 ++++++++++-------- 1 file changed, 158 insertions(+), 131 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block-wrapper.js b/packages/block-editor/src/components/block-list/block-wrapper.js index a29898fae5557..27233ce6756b3 100644 --- a/packages/block-editor/src/components/block-list/block-wrapper.js +++ b/packages/block-editor/src/components/block-list/block-wrapper.js @@ -31,6 +31,159 @@ import { SelectionStart } from '../writing-flow'; import { BlockListBlockContext } from './block'; import ELEMENTS from './block-wrapper-elements'; +/** + * Transitions focus to the block or inner tabbable when the block becomes + * selected. + * + * @param {Object} ref React ref with the block element. + * @param {string} clientId Block client ID. + */ +function useFocusFirstElement( ref, clientId ) { + const initialPosition = useSelect( + ( select ) => { + const { + getSelectedBlocksInitialCaretPosition, + isMultiSelecting, + isNavigationMode, + isBlockSelected, + } = select( 'core/block-editor' ); + + if ( ! isBlockSelected( clientId ) ) { + return; + } + + if ( isMultiSelecting() || isNavigationMode() ) { + return; + } + + return getSelectedBlocksInitialCaretPosition() || 1; + }, + [ clientId ] + ); + + useEffect( () => { + if ( ! initialPosition ) { + return; + } + + const { ownerDocument } = ref.current; + + // Focus is captured by the wrapper node, so while focus transition + // should only consider tabbables within editable display, since it + // may be the wrapper itself or a side control which triggered the + // focus event, don't unnecessary transition to an inner tabbable. + if ( + ownerDocument.activeElement && + isInsideRootBlock( ref.current, ownerDocument.activeElement ) + ) { + return; + } + + // Find all tabbables within node. + const textInputs = focus.tabbable.find( ref.current ).filter( + ( node ) => + isTextField( node ) && + // Exclude inner blocks and block appenders + isInsideRootBlock( ref.current, node ) && + ! node.closest( '.block-list-appender' ) + ); + + // If reversed (e.g. merge via backspace), use the last in the set of + // tabbables. + const isReverse = -1 === initialPosition; + const target = + ( isReverse ? last : first )( textInputs ) || ref.current; + + placeCaretAtHorizontalEdge( target, isReverse ); + }, [ initialPosition ] ); +} + +/** + * Returns true when the block is hovered and in navigation mode, false if not. + * + * @param {Object} ref React ref with the block element. + * + * @return {boolean} Hovered state. + */ +function useIsHovered( ref ) { + const [ isHovered, setHovered ] = useState( false ); + const isNavigationMode = useSelect( + ( select ) => select( 'core/block-editor' ).isNavigationMode(), + [] + ); + + useEffect( () => { + if ( ! isNavigationMode ) { + return; + } + + function addListener( eventType, value ) { + function listener( event ) { + if ( event.defaultPrevented ) { + return; + } + + event.preventDefault(); + setHovered( value ); + } + + ref.current.addEventListener( eventType, listener ); + return () => { + ref.current.removeEventListener( eventType, listener ); + }; + } + + if ( isHovered ) { + return addListener( 'mouseout', false ); + } + + return addListener( 'mouseover', true ); + }, [ isNavigationMode, isHovered, setHovered ] ); + + return isHovered; +} + +/** + * Returns the class names used for block moving mode. + * + * @param {string} clientId The block client ID to insert above. + * + * @return {string} The class names. + */ +function useBlockMovingModeClassNames( clientId ) { + return useSelect( + ( select ) => { + const { + hasBlockMovingClientId, + canInsertBlockType, + getBlockName, + getBlockRootClientId, + isBlockSelected, + } = select( 'core/block-editor' ); + + // The classes are only relevant for the selected block. Avoid + // re-rendering all blocks! + if ( ! isBlockSelected( clientId ) ) { + return; + } + + const movingClientId = hasBlockMovingClientId(); + + if ( ! movingClientId ) { + return; + } + + return classnames( 'is-block-moving-mode', { + 'can-insert-moving-block': canInsertBlockType( + getBlockName( movingClientId ), + getBlockRootClientId( clientId ) + ), + } ); + }, + [ clientId ] + ); +} + /** * This hook is used to lightly mark an element as a block element. The element * should be the outermost element of a block. Call this hook and pass the @@ -67,51 +220,9 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { blockTitle, wrapperProps = {}, } = useContext( BlockListBlockContext ); - const { - initialPosition, - shouldFocusFirstElement, - isNavigationMode, - isBlockMovingMode, - canInsertMovingBlock, - } = useSelect( - ( select ) => { - const { - getSelectedBlocksInitialCaretPosition, - isMultiSelecting: _isMultiSelecting, - isNavigationMode: _isNavigationMode, - hasBlockMovingClientId, - canInsertBlockType, - getBlockName, - getBlockRootClientId, - } = select( 'core/block-editor' ); - - const movingClientId = hasBlockMovingClientId(); - const _isBlockMovingMode = isSelected && !! movingClientId; - - return { - shouldFocusFirstElement: - isSelected && - ! _isMultiSelecting() && - ! _isNavigationMode(), - initialPosition: isSelected - ? getSelectedBlocksInitialCaretPosition() - : undefined, - isNavigationMode: _isNavigationMode, - isBlockMovingMode: _isBlockMovingMode, - canInsertMovingBlock: - _isBlockMovingMode && - canInsertBlockType( - getBlockName( movingClientId ), - getBlockRootClientId( clientId ) - ), - }; - }, - [ isSelected, clientId ] - ); const { insertDefaultBlock, removeBlock, selectBlock } = useDispatch( 'core/block-editor' ); - const [ isHovered, setHovered ] = useState( false ); // Provide the selected node, or the first and last nodes of a multi- // selection, so it can be used to position the contextual block toolbar. @@ -147,48 +258,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { // translators: %s: Type of block (i.e. Text, Image etc) const blockLabel = sprintf( __( 'Block: %s' ), blockTitle ); - // Handing the focus of the block on creation and update - - /** - * When a block becomes selected, transition focus to an inner tabbable. - */ - const focusTabbable = () => { - const { ownerDocument } = ref.current; - - // Focus is captured by the wrapper node, so while focus transition - // should only consider tabbables within editable display, since it - // may be the wrapper itself or a side control which triggered the - // focus event, don't unnecessary transition to an inner tabbable. - if ( - ownerDocument.activeElement && - isInsideRootBlock( ref.current, ownerDocument.activeElement ) - ) { - return; - } - - // Find all tabbables within node. - const textInputs = focus.tabbable.find( ref.current ).filter( - ( node ) => - isTextField( node ) && - // Exclude inner blocks and block appenders - isInsideRootBlock( ref.current, node ) && - ! node.closest( '.block-list-appender' ) - ); - - // If reversed (e.g. merge via backspace), use the last in the set of - // tabbables. - const isReverse = -1 === initialPosition; - const target = - ( isReverse ? last : first )( textInputs ) || ref.current; - - placeCaretAtHorizontalEdge( target, isReverse ); - }; - - useEffect( () => { - if ( shouldFocusFirstElement ) { - focusTabbable(); - } - }, [ shouldFocusFirstElement ] ); + useFocusFirstElement( ref, clientId ); // Block Reordering animation useMovingAnimation( @@ -288,48 +358,8 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { }; }, [ isSelected, onSelectionStart, insertDefaultBlock, removeBlock ] ); - useEffect( () => { - if ( ! isNavigationMode ) { - return; - } - - function onMouseOver( event ) { - if ( event.defaultPrevented ) { - return; - } - - event.preventDefault(); - - if ( isHovered ) { - return; - } - - setHovered( true ); - } - - function onMouseOut( event ) { - if ( event.defaultPrevented ) { - return; - } - - event.preventDefault(); - - if ( ! isHovered ) { - return; - } - - setHovered( false ); - } - - ref.current.addEventListener( 'mouseover', onMouseOver ); - ref.current.addEventListener( 'mouseout', onMouseOut ); - - return () => { - ref.current.removeEventListener( 'mouseover', onMouseOver ); - ref.current.removeEventListener( 'mouseout', onMouseOut ); - }; - }, [ isNavigationMode, isHovered, setHovered ] ); - + const isHovered = useIsHovered( ref ); + const blockMovingModeClassNames = useBlockMovingModeClassNames( clientId ); const htmlSuffix = mode === 'html' && ! __unstableIsHtml ? '-visual' : ''; return { @@ -347,11 +377,8 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { className, props.className, wrapperProps.className, - { - 'is-hovered': isHovered, - 'is-block-moving-mode': isBlockMovingMode, - 'can-insert-moving-block': canInsertMovingBlock, - } + blockMovingModeClassNames, + { 'is-hovered': isHovered } ), style: { ...wrapperProps.style, ...props.style }, }; From 2f665936406a7f29948a184e6a334d70fcfb9054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Fri, 4 Dec 2020 12:46:43 +0200 Subject: [PATCH 2/4] Move --- .../components/block-list/block-wrapper.js | 385 +----------------- .../block-list/block-wrapper.native.js | 11 - .../src/components/block-list/block.js | 2 +- .../block-list/use-block-props/index.js | 234 +++++++++++ .../use-block-props/index.native.js | 10 + .../use-block-moving-mode-class-names.js | 50 +++ .../use-focus-first-element.js | 83 ++++ .../use-block-props/use-is-hovered.js | 50 +++ packages/block-editor/src/components/index.js | 6 +- .../src/components/index.native.js | 6 +- 10 files changed, 434 insertions(+), 403 deletions(-) create mode 100644 packages/block-editor/src/components/block-list/use-block-props/index.js create mode 100644 packages/block-editor/src/components/block-list/use-block-props/index.native.js create mode 100644 packages/block-editor/src/components/block-list/use-block-props/use-block-moving-mode-class-names.js create mode 100644 packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js create mode 100644 packages/block-editor/src/components/block-list/use-block-props/use-is-hovered.js diff --git a/packages/block-editor/src/components/block-list/block-wrapper.js b/packages/block-editor/src/components/block-list/block-wrapper.js index 27233ce6756b3..469b77c3e34a5 100644 --- a/packages/block-editor/src/components/block-list/block-wrapper.js +++ b/packages/block-editor/src/components/block-list/block-wrapper.js @@ -1,395 +1,14 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; -import { first, last, omit } from 'lodash'; - /** * WordPress dependencies */ -import { - useRef, - useEffect, - useState, - useContext, - forwardRef, -} from '@wordpress/element'; -import { focus, isTextField, placeCaretAtHorizontalEdge } from '@wordpress/dom'; -import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes'; -import { __, sprintf } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { forwardRef } from '@wordpress/element'; import deprecated from '@wordpress/deprecated'; -import { __unstableGetBlockProps as getBlockProps } from '@wordpress/blocks'; /** * Internal dependencies */ -import { isInsideRootBlock } from '../../utils/dom'; -import useMovingAnimation from '../use-moving-animation'; -import { SetBlockNodes } from './root-container'; -import { SelectionStart } from '../writing-flow'; -import { BlockListBlockContext } from './block'; import ELEMENTS from './block-wrapper-elements'; - -/** - * Transitions focus to the block or inner tabbable when the block becomes - * selected. - * - * @param {Object} ref React ref with the block element. - * @param {string} clientId Block client ID. - */ -function useFocusFirstElement( ref, clientId ) { - const initialPosition = useSelect( - ( select ) => { - const { - getSelectedBlocksInitialCaretPosition, - isMultiSelecting, - isNavigationMode, - isBlockSelected, - } = select( 'core/block-editor' ); - - if ( ! isBlockSelected( clientId ) ) { - return; - } - - if ( isMultiSelecting() || isNavigationMode() ) { - return; - } - - return getSelectedBlocksInitialCaretPosition() || 1; - }, - [ clientId ] - ); - - useEffect( () => { - if ( ! initialPosition ) { - return; - } - - const { ownerDocument } = ref.current; - - // Focus is captured by the wrapper node, so while focus transition - // should only consider tabbables within editable display, since it - // may be the wrapper itself or a side control which triggered the - // focus event, don't unnecessary transition to an inner tabbable. - if ( - ownerDocument.activeElement && - isInsideRootBlock( ref.current, ownerDocument.activeElement ) - ) { - return; - } - - // Find all tabbables within node. - const textInputs = focus.tabbable.find( ref.current ).filter( - ( node ) => - isTextField( node ) && - // Exclude inner blocks and block appenders - isInsideRootBlock( ref.current, node ) && - ! node.closest( '.block-list-appender' ) - ); - - // If reversed (e.g. merge via backspace), use the last in the set of - // tabbables. - const isReverse = -1 === initialPosition; - const target = - ( isReverse ? last : first )( textInputs ) || ref.current; - - placeCaretAtHorizontalEdge( target, isReverse ); - }, [ initialPosition ] ); -} - -/** - * Returns true when the block is hovered and in navigation mode, false if not. - * - * @param {Object} ref React ref with the block element. - * - * @return {boolean} Hovered state. - */ -function useIsHovered( ref ) { - const [ isHovered, setHovered ] = useState( false ); - const isNavigationMode = useSelect( - ( select ) => select( 'core/block-editor' ).isNavigationMode(), - [] - ); - - useEffect( () => { - if ( ! isNavigationMode ) { - return; - } - - function addListener( eventType, value ) { - function listener( event ) { - if ( event.defaultPrevented ) { - return; - } - - event.preventDefault(); - setHovered( value ); - } - - ref.current.addEventListener( eventType, listener ); - return () => { - ref.current.removeEventListener( eventType, listener ); - }; - } - - if ( isHovered ) { - return addListener( 'mouseout', false ); - } - - return addListener( 'mouseover', true ); - }, [ isNavigationMode, isHovered, setHovered ] ); - - return isHovered; -} - -/** - * Returns the class names used for block moving mode. - * - * @param {string} clientId The block client ID to insert above. - * - * @return {string} The class names. - */ -function useBlockMovingModeClassNames( clientId ) { - return useSelect( - ( select ) => { - const { - hasBlockMovingClientId, - canInsertBlockType, - getBlockName, - getBlockRootClientId, - isBlockSelected, - } = select( 'core/block-editor' ); - - // The classes are only relevant for the selected block. Avoid - // re-rendering all blocks! - if ( ! isBlockSelected( clientId ) ) { - return; - } - - const movingClientId = hasBlockMovingClientId(); - - if ( ! movingClientId ) { - return; - } - - return classnames( 'is-block-moving-mode', { - 'can-insert-moving-block': canInsertBlockType( - getBlockName( movingClientId ), - getBlockRootClientId( clientId ) - ), - } ); - }, - [ clientId ] - ); -} - -/** - * This hook is used to lightly mark an element as a block element. The element - * should be the outermost element of a block. Call this hook and pass the - * returned props to the element to mark as a block. 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 Options for internal use only. - * @param {boolean} options.__unstableIsHtml - * - * @return {Object} Props to pass to the element to mark as a block. - */ -export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { - const fallbackRef = useRef(); - const ref = props.ref || fallbackRef; - const onSelectionStart = useContext( SelectionStart ); - const setBlockNodes = useContext( SetBlockNodes ); - const { - clientId, - rootClientId, - isSelected, - isFirstMultiSelected, - isLastMultiSelected, - isPartOfMultiSelection, - enableAnimation, - index, - className, - name, - mode, - blockTitle, - wrapperProps = {}, - } = useContext( BlockListBlockContext ); - const { insertDefaultBlock, removeBlock, selectBlock } = useDispatch( - 'core/block-editor' - ); - - // Provide the selected node, or the first and last nodes of a multi- - // selection, so it can be used to position the contextual block toolbar. - // We only provide what is necessary, and remove the nodes again when they - // are no longer selected. - useEffect( () => { - if ( isSelected || isFirstMultiSelected || isLastMultiSelected ) { - const node = ref.current; - setBlockNodes( ( nodes ) => ( { - ...nodes, - [ clientId ]: node, - } ) ); - return () => { - setBlockNodes( ( nodes ) => omit( nodes, clientId ) ); - }; - } - }, [ isSelected, isFirstMultiSelected, isLastMultiSelected ] ); - - // Set new block node if it changes. - // This effect should happen on every render, so no dependencies should be - // added. - useEffect( () => { - const node = ref.current; - setBlockNodes( ( nodes ) => { - if ( ! nodes[ clientId ] || nodes[ clientId ] === node ) { - return nodes; - } - - return { ...nodes, [ clientId ]: node }; - } ); - } ); - - // translators: %s: Type of block (i.e. Text, Image etc) - const blockLabel = sprintf( __( 'Block: %s' ), blockTitle ); - - useFocusFirstElement( ref, clientId ); - - // Block Reordering animation - useMovingAnimation( - ref, - isSelected || isPartOfMultiSelection, - isSelected || isFirstMultiSelected, - enableAnimation, - index - ); - - useEffect( () => { - if ( ! isSelected ) { - /** - * Marks the block as selected when focused and not already - * selected. This specifically handles the case where block does not - * set focus on its own (via `setFocus`), typically if there is no - * focusable input in the block. - * - * @param {FocusEvent} event Focus event. - */ - function onFocus( event ) { - // If an inner block is focussed, that block is resposible for - // setting the selected block. - if ( ! isInsideRootBlock( ref.current, event.target ) ) { - return; - } - - selectBlock( clientId ); - } - - ref.current.addEventListener( 'focus', onFocus, true ); - - return () => { - ref.current.removeEventListener( 'focus', onFocus, true ); - }; - } - - /** - * Interprets keydown event intent to remove or insert after block if - * key event occurs on wrapper node. This can occur when the block has - * no text fields of its own, particularly after initial insertion, to - * allow for easy deletion and continuous writing flow to add additional - * content. - * - * @param {KeyboardEvent} event Keydown event. - */ - function onKeyDown( event ) { - const { keyCode, target } = event; - - if ( - keyCode !== ENTER && - keyCode !== BACKSPACE && - keyCode !== DELETE - ) { - return; - } - - if ( target !== ref.current || isTextField( target ) ) { - return; - } - - event.preventDefault(); - - if ( keyCode === ENTER ) { - insertDefaultBlock( {}, rootClientId, index + 1 ); - } else { - removeBlock( clientId ); - } - } - - function onMouseLeave( { buttons } ) { - // The primary button must be pressed to initiate selection. - // See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons - if ( buttons === 1 ) { - onSelectionStart( clientId ); - } - } - - /** - * Prevents default dragging behavior within a block. To do: we must - * handle this in the future and clean up the drag target. - * - * @param {DragEvent} event Drag event. - */ - function onDragStart( event ) { - event.preventDefault(); - } - - ref.current.addEventListener( 'keydown', onKeyDown ); - ref.current.addEventListener( 'mouseleave', onMouseLeave ); - ref.current.addEventListener( 'dragstart', onDragStart ); - - return () => { - ref.current.removeEventListener( 'mouseleave', onMouseLeave ); - ref.current.removeEventListener( 'keydown', onKeyDown ); - ref.current.removeEventListener( 'dragstart', onDragStart ); - }; - }, [ isSelected, onSelectionStart, insertDefaultBlock, removeBlock ] ); - - const isHovered = useIsHovered( ref ); - const blockMovingModeClassNames = useBlockMovingModeClassNames( clientId ); - const htmlSuffix = mode === 'html' && ! __unstableIsHtml ? '-visual' : ''; - - return { - ...wrapperProps, - ...props, - ref, - id: `block-${ clientId }${ htmlSuffix }`, - tabIndex: 0, - role: 'group', - 'aria-label': blockLabel, - 'data-block': clientId, - 'data-type': name, - 'data-title': blockTitle, - className: classnames( - className, - props.className, - wrapperProps.className, - blockMovingModeClassNames, - { 'is-hovered': isHovered } - ), - style: { ...wrapperProps.style, ...props.style }, - }; -} - -/** - * Call within a save function to get the props for the block wrapper. - * - * @param {Object} props Optional. Props to pass to the element. - */ -useBlockProps.save = getBlockProps; +import { useBlockProps } from './use-block-props'; const BlockComponent = forwardRef( ( { children, tagName: TagName = 'div', ...props }, ref ) => { diff --git a/packages/block-editor/src/components/block-list/block-wrapper.native.js b/packages/block-editor/src/components/block-list/block-wrapper.native.js index 122ff1e4f514c..507e17bb982ec 100644 --- a/packages/block-editor/src/components/block-list/block-wrapper.native.js +++ b/packages/block-editor/src/components/block-list/block-wrapper.native.js @@ -1,19 +1,8 @@ -/** - * WordPress dependencies - */ -import { __unstableGetBlockProps as getBlockProps } from '@wordpress/blocks'; - /** * Internal dependencies */ import ELEMENTS from './block-wrapper-elements'; -export function useBlockProps( props = {} ) { - return props; -} - -useBlockProps.save = getBlockProps; - const ExtendedBlockComponent = ELEMENTS.reduce( ( acc, element ) => { acc[ element ] = element; return acc; diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index d2ebae05d2471..a364150a4fb3d 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -44,7 +44,7 @@ import BlockInvalidWarning from './block-invalid-warning'; import BlockCrashWarning from './block-crash-warning'; import BlockCrashBoundary from './block-crash-boundary'; import BlockHtml from './block-html'; -import { useBlockProps } from './block-wrapper'; +import { useBlockProps } from './use-block-props'; export const BlockListBlockContext = createContext(); diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js new file mode 100644 index 0000000000000..a7a8e67479f23 --- /dev/null +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -0,0 +1,234 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { omit } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useRef, useEffect, useContext } from '@wordpress/element'; +import { isTextField } from '@wordpress/dom'; +import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes'; +import { __, sprintf } from '@wordpress/i18n'; +import { useDispatch } from '@wordpress/data'; +import { __unstableGetBlockProps as getBlockProps } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { isInsideRootBlock } from '../../../utils/dom'; +import useMovingAnimation from '../../use-moving-animation'; +import { SetBlockNodes } from '../root-container'; +import { SelectionStart } from '../../writing-flow'; +import { BlockListBlockContext } from '../block'; +import { useFocusFirstElement } from './use-focus-first-element'; +import { useIsHovered } from './use-is-hovered'; +import { useBlockMovingModeClassNames } from './use-block-moving-mode-class-names'; + +/** + * This hook is used to lightly mark an element as a block element. The element + * should be the outermost element of a block. Call this hook and pass the + * returned props to the element to mark as a block. 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 Options for internal use only. + * @param {boolean} options.__unstableIsHtml + * + * @return {Object} Props to pass to the element to mark as a block. + */ +export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { + const fallbackRef = useRef(); + const ref = props.ref || fallbackRef; + const onSelectionStart = useContext( SelectionStart ); + const setBlockNodes = useContext( SetBlockNodes ); + const { + clientId, + rootClientId, + isSelected, + isFirstMultiSelected, + isLastMultiSelected, + isPartOfMultiSelection, + enableAnimation, + index, + className, + name, + mode, + blockTitle, + wrapperProps = {}, + } = useContext( BlockListBlockContext ); + const { insertDefaultBlock, removeBlock, selectBlock } = useDispatch( + 'core/block-editor' + ); + + // Provide the selected node, or the first and last nodes of a multi- + // selection, so it can be used to position the contextual block toolbar. + // We only provide what is necessary, and remove the nodes again when they + // are no longer selected. + useEffect( () => { + if ( isSelected || isFirstMultiSelected || isLastMultiSelected ) { + const node = ref.current; + setBlockNodes( ( nodes ) => ( { + ...nodes, + [ clientId ]: node, + } ) ); + return () => { + setBlockNodes( ( nodes ) => omit( nodes, clientId ) ); + }; + } + }, [ isSelected, isFirstMultiSelected, isLastMultiSelected ] ); + + // Set new block node if it changes. + // This effect should happen on every render, so no dependencies should be + // added. + useEffect( () => { + const node = ref.current; + setBlockNodes( ( nodes ) => { + if ( ! nodes[ clientId ] || nodes[ clientId ] === node ) { + return nodes; + } + + return { ...nodes, [ clientId ]: node }; + } ); + } ); + + // translators: %s: Type of block (i.e. Text, Image etc) + const blockLabel = sprintf( __( 'Block: %s' ), blockTitle ); + + useFocusFirstElement( ref, clientId ); + + // Block Reordering animation + useMovingAnimation( + ref, + isSelected || isPartOfMultiSelection, + isSelected || isFirstMultiSelected, + enableAnimation, + index + ); + + useEffect( () => { + if ( ! isSelected ) { + /** + * Marks the block as selected when focused and not already + * selected. This specifically handles the case where block does not + * set focus on its own (via `setFocus`), typically if there is no + * focusable input in the block. + * + * @param {FocusEvent} event Focus event. + */ + function onFocus( event ) { + // If an inner block is focussed, that block is resposible for + // setting the selected block. + if ( ! isInsideRootBlock( ref.current, event.target ) ) { + return; + } + + selectBlock( clientId ); + } + + ref.current.addEventListener( 'focus', onFocus, true ); + + return () => { + ref.current.removeEventListener( 'focus', onFocus, true ); + }; + } + + /** + * Interprets keydown event intent to remove or insert after block if + * key event occurs on wrapper node. This can occur when the block has + * no text fields of its own, particularly after initial insertion, to + * allow for easy deletion and continuous writing flow to add additional + * content. + * + * @param {KeyboardEvent} event Keydown event. + */ + function onKeyDown( event ) { + const { keyCode, target } = event; + + if ( + keyCode !== ENTER && + keyCode !== BACKSPACE && + keyCode !== DELETE + ) { + return; + } + + if ( target !== ref.current || isTextField( target ) ) { + return; + } + + event.preventDefault(); + + if ( keyCode === ENTER ) { + insertDefaultBlock( {}, rootClientId, index + 1 ); + } else { + removeBlock( clientId ); + } + } + + function onMouseLeave( { buttons } ) { + // The primary button must be pressed to initiate selection. + // See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons + if ( buttons === 1 ) { + onSelectionStart( clientId ); + } + } + + /** + * Prevents default dragging behavior within a block. To do: we must + * handle this in the future and clean up the drag target. + * + * @param {DragEvent} event Drag event. + */ + function onDragStart( event ) { + event.preventDefault(); + } + + ref.current.addEventListener( 'keydown', onKeyDown ); + ref.current.addEventListener( 'mouseleave', onMouseLeave ); + ref.current.addEventListener( 'dragstart', onDragStart ); + + return () => { + ref.current.removeEventListener( 'mouseleave', onMouseLeave ); + ref.current.removeEventListener( 'keydown', onKeyDown ); + ref.current.removeEventListener( 'dragstart', onDragStart ); + }; + }, [ isSelected, onSelectionStart, insertDefaultBlock, removeBlock ] ); + + const isHovered = useIsHovered( ref ); + const blockMovingModeClassNames = useBlockMovingModeClassNames( clientId ); + const htmlSuffix = mode === 'html' && ! __unstableIsHtml ? '-visual' : ''; + + return { + ...wrapperProps, + ...props, + ref, + id: `block-${ clientId }${ htmlSuffix }`, + tabIndex: 0, + role: 'group', + 'aria-label': blockLabel, + 'data-block': clientId, + 'data-type': name, + 'data-title': blockTitle, + className: classnames( + className, + props.className, + wrapperProps.className, + blockMovingModeClassNames, + { 'is-hovered': isHovered } + ), + style: { ...wrapperProps.style, ...props.style }, + }; +} + +/** + * Call within a save function to get the props for the block wrapper. + * + * @param {Object} props Optional. Props to pass to the element. + */ +useBlockProps.save = getBlockProps; diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.native.js b/packages/block-editor/src/components/block-list/use-block-props/index.native.js new file mode 100644 index 0000000000000..5055a15d7bdde --- /dev/null +++ b/packages/block-editor/src/components/block-list/use-block-props/index.native.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __unstableGetBlockProps as getBlockProps } from '@wordpress/blocks'; + +export function useBlockProps( props = {} ) { + return props; +} + +useBlockProps.save = getBlockProps; diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-block-moving-mode-class-names.js b/packages/block-editor/src/components/block-list/use-block-props/use-block-moving-mode-class-names.js new file mode 100644 index 0000000000000..5e5f3f799dc7b --- /dev/null +++ b/packages/block-editor/src/components/block-list/use-block-props/use-block-moving-mode-class-names.js @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Returns the class names used for block moving mode. + * + * @param {string} clientId The block client ID to insert above. + * + * @return {string} The class names. + */ +export function useBlockMovingModeClassNames( clientId ) { + return useSelect( + ( select ) => { + const { + hasBlockMovingClientId, + canInsertBlockType, + getBlockName, + getBlockRootClientId, + isBlockSelected, + } = select( 'core/block-editor' ); + + // The classes are only relevant for the selected block. Avoid + // re-rendering all blocks! + if ( ! isBlockSelected( clientId ) ) { + return; + } + + const movingClientId = hasBlockMovingClientId(); + + if ( ! movingClientId ) { + return; + } + + return classnames( 'is-block-moving-mode', { + 'can-insert-moving-block': canInsertBlockType( + getBlockName( movingClientId ), + getBlockRootClientId( clientId ) + ), + } ); + }, + [ clientId ] + ); +} diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js new file mode 100644 index 0000000000000..74fdfa1d4d021 --- /dev/null +++ b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js @@ -0,0 +1,83 @@ +/** + * External dependencies + */ +import { first, last } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useEffect } from '@wordpress/element'; +import { focus, isTextField, placeCaretAtHorizontalEdge } from '@wordpress/dom'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { isInsideRootBlock } from '../../../utils/dom'; + +/** + * Transitions focus to the block or inner tabbable when the block becomes + * selected. + * + * @param {Object} ref React ref with the block element. + * @param {string} clientId Block client ID. + */ +export function useFocusFirstElement( ref, clientId ) { + const initialPosition = useSelect( + ( select ) => { + const { + getSelectedBlocksInitialCaretPosition, + isMultiSelecting, + isNavigationMode, + isBlockSelected, + } = select( 'core/block-editor' ); + + if ( ! isBlockSelected( clientId ) ) { + return; + } + + if ( isMultiSelecting() || isNavigationMode() ) { + return; + } + + return getSelectedBlocksInitialCaretPosition() || 1; + }, + [ clientId ] + ); + + useEffect( () => { + if ( ! initialPosition ) { + return; + } + + const { ownerDocument } = ref.current; + + // Focus is captured by the wrapper node, so while focus transition + // should only consider tabbables within editable display, since it + // may be the wrapper itself or a side control which triggered the + // focus event, don't unnecessary transition to an inner tabbable. + if ( + ownerDocument.activeElement && + isInsideRootBlock( ref.current, ownerDocument.activeElement ) + ) { + return; + } + + // Find all tabbables within node. + const textInputs = focus.tabbable.find( ref.current ).filter( + ( node ) => + isTextField( node ) && + // Exclude inner blocks and block appenders + isInsideRootBlock( ref.current, node ) && + ! node.closest( '.block-list-appender' ) + ); + + // If reversed (e.g. merge via backspace), use the last in the set of + // tabbables. + const isReverse = -1 === initialPosition; + const target = + ( isReverse ? last : first )( textInputs ) || ref.current; + + placeCaretAtHorizontalEdge( target, isReverse ); + }, [ initialPosition ] ); +} diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-is-hovered.js b/packages/block-editor/src/components/block-list/use-block-props/use-is-hovered.js new file mode 100644 index 0000000000000..50e7cbf34cb37 --- /dev/null +++ b/packages/block-editor/src/components/block-list/use-block-props/use-is-hovered.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; + +/** + * Returns true when the block is hovered and in navigation mode, false if not. + * + * @param {Object} ref React ref with the block element. + * + * @return {boolean} Hovered state. + */ +export function useIsHovered( ref ) { + const [ isHovered, setHovered ] = useState( false ); + const isNavigationMode = useSelect( + ( select ) => select( 'core/block-editor' ).isNavigationMode(), + [] + ); + + useEffect( () => { + if ( ! isNavigationMode ) { + return; + } + + function addListener( eventType, value ) { + function listener( event ) { + if ( event.defaultPrevented ) { + return; + } + + event.preventDefault(); + setHovered( value ); + } + + ref.current.addEventListener( eventType, listener ); + return () => { + ref.current.removeEventListener( eventType, listener ); + }; + } + + if ( isHovered ) { + return addListener( 'mouseout', false ); + } + + return addListener( 'mouseover', true ); + }, [ isNavigationMode, isHovered, setHovered ] ); + + return isHovered; +} diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 9d619e94ba6e3..3e845204bed82 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -78,10 +78,8 @@ export { default as __experimentalPreviewOptions } from './preview-options'; export { default as __experimentalUseResizeCanvas } from './use-resize-canvas'; export { default as BlockInspector } from './block-inspector'; export { default as BlockList } from './block-list'; -export { - Block as __experimentalBlock, - useBlockProps, -} from './block-list/block-wrapper'; +export { useBlockProps } from './block-list/use-block-props'; +export { Block as __experimentalBlock } from './block-list/block-wrapper'; export { default as BlockMover } from './block-mover'; export { default as BlockPreview } from './block-preview'; export { diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 29a3b085c09ba..81ca7273118cd 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -59,10 +59,8 @@ export { default as BlockStyles } from './block-styles'; export { default as DefaultBlockAppender } from './default-block-appender'; export { default as __unstableEditorStyles } from './editor-styles'; export { default as Inserter } from './inserter'; -export { - Block as __experimentalBlock, - useBlockProps, -} from './block-list/block-wrapper'; +export { useBlockProps } from './block-list/use-block-props'; +export { Block as __experimentalBlock } from './block-list/block-wrapper'; export { default as FloatingToolbar } from './floating-toolbar'; // State Related Components From df42e1ddff7a1c3bb43b76e87101e59efcbafd87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Fri, 4 Dec 2020 14:20:48 +0200 Subject: [PATCH 3/4] Polish --- .../block-list/use-block-props/index.js | 101 +------------ .../use-block-props/use-event-handlers.js | 133 ++++++++++++++++++ .../use-focus-first-element.js | 27 +++- .../src/components/writing-flow/index.js | 24 +--- 4 files changed, 161 insertions(+), 124 deletions(-) create mode 100644 packages/block-editor/src/components/block-list/use-block-props/use-event-handlers.js diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index a7a8e67479f23..4d53507bef06b 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -8,23 +8,19 @@ import { omit } from 'lodash'; * WordPress dependencies */ import { useRef, useEffect, useContext } from '@wordpress/element'; -import { isTextField } from '@wordpress/dom'; -import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes'; import { __, sprintf } from '@wordpress/i18n'; -import { useDispatch } from '@wordpress/data'; import { __unstableGetBlockProps as getBlockProps } from '@wordpress/blocks'; /** * Internal dependencies */ -import { isInsideRootBlock } from '../../../utils/dom'; import useMovingAnimation from '../../use-moving-animation'; import { SetBlockNodes } from '../root-container'; -import { SelectionStart } from '../../writing-flow'; import { BlockListBlockContext } from '../block'; import { useFocusFirstElement } from './use-focus-first-element'; import { useIsHovered } from './use-is-hovered'; import { useBlockMovingModeClassNames } from './use-block-moving-mode-class-names'; +import { useEventHandlers } from './use-event-handlers'; /** * This hook is used to lightly mark an element as a block element. The element @@ -45,11 +41,9 @@ import { useBlockMovingModeClassNames } from './use-block-moving-mode-class-name export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { const fallbackRef = useRef(); const ref = props.ref || fallbackRef; - const onSelectionStart = useContext( SelectionStart ); const setBlockNodes = useContext( SetBlockNodes ); const { clientId, - rootClientId, isSelected, isFirstMultiSelected, isLastMultiSelected, @@ -62,9 +56,6 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { blockTitle, wrapperProps = {}, } = useContext( BlockListBlockContext ); - const { insertDefaultBlock, removeBlock, selectBlock } = useDispatch( - 'core/block-editor' - ); // Provide the selected node, or the first and last nodes of a multi- // selection, so it can be used to position the contextual block toolbar. @@ -101,6 +92,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { const blockLabel = sprintf( __( 'Block: %s' ), blockTitle ); useFocusFirstElement( ref, clientId ); + useEventHandlers( ref, clientId ); // Block Reordering animation useMovingAnimation( @@ -111,95 +103,6 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { index ); - useEffect( () => { - if ( ! isSelected ) { - /** - * Marks the block as selected when focused and not already - * selected. This specifically handles the case where block does not - * set focus on its own (via `setFocus`), typically if there is no - * focusable input in the block. - * - * @param {FocusEvent} event Focus event. - */ - function onFocus( event ) { - // If an inner block is focussed, that block is resposible for - // setting the selected block. - if ( ! isInsideRootBlock( ref.current, event.target ) ) { - return; - } - - selectBlock( clientId ); - } - - ref.current.addEventListener( 'focus', onFocus, true ); - - return () => { - ref.current.removeEventListener( 'focus', onFocus, true ); - }; - } - - /** - * Interprets keydown event intent to remove or insert after block if - * key event occurs on wrapper node. This can occur when the block has - * no text fields of its own, particularly after initial insertion, to - * allow for easy deletion and continuous writing flow to add additional - * content. - * - * @param {KeyboardEvent} event Keydown event. - */ - function onKeyDown( event ) { - const { keyCode, target } = event; - - if ( - keyCode !== ENTER && - keyCode !== BACKSPACE && - keyCode !== DELETE - ) { - return; - } - - if ( target !== ref.current || isTextField( target ) ) { - return; - } - - event.preventDefault(); - - if ( keyCode === ENTER ) { - insertDefaultBlock( {}, rootClientId, index + 1 ); - } else { - removeBlock( clientId ); - } - } - - function onMouseLeave( { buttons } ) { - // The primary button must be pressed to initiate selection. - // See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons - if ( buttons === 1 ) { - onSelectionStart( clientId ); - } - } - - /** - * Prevents default dragging behavior within a block. To do: we must - * handle this in the future and clean up the drag target. - * - * @param {DragEvent} event Drag event. - */ - function onDragStart( event ) { - event.preventDefault(); - } - - ref.current.addEventListener( 'keydown', onKeyDown ); - ref.current.addEventListener( 'mouseleave', onMouseLeave ); - ref.current.addEventListener( 'dragstart', onDragStart ); - - return () => { - ref.current.removeEventListener( 'mouseleave', onMouseLeave ); - ref.current.removeEventListener( 'keydown', onKeyDown ); - ref.current.removeEventListener( 'dragstart', onDragStart ); - }; - }, [ isSelected, onSelectionStart, insertDefaultBlock, removeBlock ] ); - const isHovered = useIsHovered( ref ); const blockMovingModeClassNames = useBlockMovingModeClassNames( clientId ); const htmlSuffix = mode === 'html' && ! __unstableIsHtml ? '-visual' : ''; diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-event-handlers.js b/packages/block-editor/src/components/block-list/use-block-props/use-event-handlers.js new file mode 100644 index 0000000000000..8aa8cbf8622bf --- /dev/null +++ b/packages/block-editor/src/components/block-list/use-block-props/use-event-handlers.js @@ -0,0 +1,133 @@ +/** + * WordPress dependencies + */ +import { useEffect, useContext } from '@wordpress/element'; +import { isTextField } from '@wordpress/dom'; +import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes'; +import { useSelect, useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { isInsideRootBlock } from '../../../utils/dom'; +import { SelectionStart } from '../../writing-flow'; + +export function useEventHandlers( ref, clientId ) { + const onSelectionStart = useContext( SelectionStart ); + const { isSelected, rootClientId, index } = useSelect( + ( select ) => { + const { + isBlockSelected, + getBlockRootClientId, + getBlockIndex, + } = select( 'core/block-editor' ); + + return { + isSelected: isBlockSelected( clientId ), + rootClientId: getBlockRootClientId( clientId ), + index: getBlockIndex( clientId ), + }; + }, + [ clientId ] + ); + const { insertDefaultBlock, removeBlock, selectBlock } = useDispatch( + 'core/block-editor' + ); + + useEffect( () => { + if ( ! isSelected ) { + /** + * Marks the block as selected when focused and not already + * selected. This specifically handles the case where block does not + * set focus on its own (via `setFocus`), typically if there is no + * focusable input in the block. + * + * @param {FocusEvent} event Focus event. + */ + function onFocus( event ) { + // If an inner block is focussed, that block is resposible for + // setting the selected block. + if ( ! isInsideRootBlock( ref.current, event.target ) ) { + return; + } + + selectBlock( clientId ); + } + + ref.current.addEventListener( 'focus', onFocus, true ); + + return () => { + ref.current.removeEventListener( 'focus', onFocus, true ); + }; + } + + /** + * Interprets keydown event intent to remove or insert after block if + * key event occurs on wrapper node. This can occur when the block has + * no text fields of its own, particularly after initial insertion, to + * allow for easy deletion and continuous writing flow to add additional + * content. + * + * @param {KeyboardEvent} event Keydown event. + */ + function onKeyDown( event ) { + const { keyCode, target } = event; + + if ( + keyCode !== ENTER && + keyCode !== BACKSPACE && + keyCode !== DELETE + ) { + return; + } + + if ( target !== ref.current || isTextField( target ) ) { + return; + } + + event.preventDefault(); + + if ( keyCode === ENTER ) { + insertDefaultBlock( {}, rootClientId, index + 1 ); + } else { + removeBlock( clientId ); + } + } + + function onMouseLeave( { buttons } ) { + // The primary button must be pressed to initiate selection. + // See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons + if ( buttons === 1 ) { + onSelectionStart( clientId ); + } + } + + /** + * Prevents default dragging behavior within a block. To do: we must + * handle this in the future and clean up the drag target. + * + * @param {DragEvent} event Drag event. + */ + function onDragStart( event ) { + event.preventDefault(); + } + + ref.current.addEventListener( 'keydown', onKeyDown ); + ref.current.addEventListener( 'mouseleave', onMouseLeave ); + ref.current.addEventListener( 'dragstart', onDragStart ); + + return () => { + ref.current.removeEventListener( 'mouseleave', onMouseLeave ); + ref.current.removeEventListener( 'keydown', onKeyDown ); + ref.current.removeEventListener( 'dragstart', onDragStart ); + }; + }, [ + isSelected, + rootClientId, + index, + onSelectionStart, + insertDefaultBlock, + removeBlock, + selectBlock, + ] ); +} diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js index 74fdfa1d4d021..c1a62bb3a25db 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js @@ -16,14 +16,15 @@ import { useSelect } from '@wordpress/data'; import { isInsideRootBlock } from '../../../utils/dom'; /** - * Transitions focus to the block or inner tabbable when the block becomes - * selected. + * Returns the initial position if the block needs to be focussed, `undefined` + * otherwise. The initial position is either 0 (start) or -1 (end). * - * @param {Object} ref React ref with the block element. * @param {string} clientId Block client ID. + * + * @return {number} The initial position, either 0 (start) or -1 (end). */ -export function useFocusFirstElement( ref, clientId ) { - const initialPosition = useSelect( +function useInitialPosition( clientId ) { + return useSelect( ( select ) => { const { getSelectedBlocksInitialCaretPosition, @@ -40,13 +41,25 @@ export function useFocusFirstElement( ref, clientId ) { return; } - return getSelectedBlocksInitialCaretPosition() || 1; + // If there's no initial position, return 0 to focus the start. + return getSelectedBlocksInitialCaretPosition() || 0; }, [ clientId ] ); +} + +/** + * Transitions focus to the block or inner tabbable when the block becomes + * selected. + * + * @param {Object} ref React ref with the block element. + * @param {string} clientId Block client ID. + */ +export function useFocusFirstElement( ref, clientId ) { + const initialPosition = useInitialPosition( clientId ); useEffect( () => { - if ( ! initialPosition ) { + if ( initialPosition === undefined ) { return; } diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index 133c62d993e62..e9cc4132ce79f 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -191,11 +191,6 @@ function selector( select ) { hasMultiSelection, getBlockOrder, isNavigationMode, - getBlockIndex, - getBlockRootClientId, - getClientIdsOfDescendants, - canInsertBlockType, - getBlockName, isSelectionEnabled, getBlockSelectionStart, isMultiSelecting, @@ -205,6 +200,7 @@ function selector( select ) { const selectedBlockClientId = getSelectedBlockClientId(); const selectionStartClientId = getMultiSelectedBlocksStartClientId(); const selectionEndClientId = getMultiSelectedBlocksEndClientId(); + const blocks = getBlockOrder(); return { selectedBlockClientId, @@ -218,13 +214,9 @@ function selector( select ) { selectedFirstClientId: getFirstMultiSelectedBlockClientId(), selectedLastClientId: getLastMultiSelectedBlockClientId(), hasMultiSelection: hasMultiSelection(), - blocks: getBlockOrder(), + firstBlock: first( blocks ), + lastBlock: last( blocks ), isNavigationMode: isNavigationMode(), - getBlockIndex, - getBlockRootClientId, - getClientIdsOfDescendants, - canInsertBlockType, - getBlockName, isSelectionEnabled: isSelectionEnabled(), blockSelectionStart: getBlockSelectionStart(), isMultiSelecting: isMultiSelecting(), @@ -266,7 +258,8 @@ export default function WritingFlow( { children } ) { selectedFirstClientId, selectedLastClientId, hasMultiSelection, - blocks, + firstBlock, + lastBlock, isNavigationMode, isSelectionEnabled, blockSelectionStart, @@ -396,11 +389,6 @@ export default function WritingFlow( { children } ) { const { ownerDocument } = container.current; const { defaultView } = ownerDocument; - // In navigation mode, tab and arrows navigate from block to block. - if ( isNavigationMode ) { - return; - } - // In Edit mode, Tab should focus the first tabbable element after the // content, which is normally the sidebar (with block controls) and // Shift+Tab should focus the first tabbable element before the content, @@ -471,7 +459,7 @@ export default function WritingFlow( { children } ) { ? entirelySelected.current : isEntirelySelected( target ) ) { - multiSelect( first( blocks ), last( blocks ) ); + multiSelect( firstBlock, lastBlock ); event.preventDefault(); } From ca84332324519ea99c968ce7b9eaeec332729b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Fri, 4 Dec 2020 14:41:42 +0200 Subject: [PATCH 4/4] Polish --- .../block-editor/src/components/block-list/block.js | 2 -- .../block-editor/src/components/block-list/index.js | 1 - .../use-block-props/use-event-handlers.js | 13 +++++++++++++ .../use-block-props/use-focus-first-element.js | 6 ++++-- .../block-list/use-block-props/use-is-hovered.js | 4 +++- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index a364150a4fb3d..137625a6e6ecb 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -84,7 +84,6 @@ function BlockListBlock( { mode, isLocked, clientId, - rootClientId, isSelected, isMultiSelected, isPartOfMultiSelection, @@ -216,7 +215,6 @@ function BlockListBlock( { const value = { clientId, - rootClientId, isSelected, isFirstMultiSelected, isLastMultiSelected, diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index f78c09330fb25..ba1200e24652d 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -119,7 +119,6 @@ function Items( { value={ ! isBlockInSelection } >