Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { omit } from 'lodash';
import { useRef, useEffect, useContext } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { __unstableGetBlockProps as getBlockProps } from '@wordpress/blocks';
import { useMergeRefs } from '@wordpress/compose';

/**
* Internal dependencies
Expand Down Expand Up @@ -92,7 +93,6 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) {
const blockLabel = sprintf( __( 'Block: %s' ), blockTitle );

useFocusFirstElement( ref, clientId );
useEventHandlers( ref, clientId );

// Block Reordering animation
useMovingAnimation(
Expand All @@ -106,11 +106,12 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) {
const isHovered = useIsHovered( ref );
const blockMovingModeClassNames = useBlockMovingModeClassNames( clientId );
const htmlSuffix = mode === 'html' && ! __unstableIsHtml ? '-visual' : '';
const mergedRefs = useMergeRefs( [ ref, useEventHandlers( clientId ) ] );

return {
...wrapperProps,
...props,
ref,
ref: mergedRefs,
id: `block-${ clientId }${ htmlSuffix }`,
tabIndex: 0,
role: 'group',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/**
* WordPress dependencies
*/
import { useEffect, useContext } from '@wordpress/element';
import { useContext } from '@wordpress/element';
import { isTextField } from '@wordpress/dom';
import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes';
import { useSelect, useDispatch } from '@wordpress/data';
import { useRefEffect } from '@wordpress/compose';

/**
* Internal dependencies
Expand All @@ -13,8 +14,6 @@ import { isInsideRootBlock } from '../../../utils/dom';
import { SelectionStart } from '../../writing-flow';
import { store as blockEditorStore } from '../../../store';

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

/**
* Adds block behaviour:
* - Selects the block if it receives focus.
Expand All @@ -23,10 +22,9 @@ import { store as blockEditorStore } from '../../../store';
* - Initiates selection start for multi-selection.
* - Disables dragging of block contents.
*
* @param {RefObject} ref React ref with the block element.
* @param {string} clientId Block client ID.
* @param {string} clientId Block client ID.
*/
export function useEventHandlers( ref, clientId ) {
export function useEventHandlers( clientId ) {
const onSelectionStart = useContext( SelectionStart );
const { isSelected, rootClientId, index } = useSelect(
( select ) => {
Expand All @@ -48,100 +46,103 @@ export function useEventHandlers( ref, clientId ) {
blockEditorStore
);

useEffect( () => {
if ( ! isSelected ) {
return useRefEffect(
( node ) => {
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( node, event.target ) ) {
return;
}

selectBlock( clientId );
}

node.addEventListener( 'focusin', onFocus );

return () => {
node.removeEventListener( 'focusin', onFocus );
};
}

/**
* 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.
* 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 {FocusEvent} event Focus event.
* @param {KeyboardEvent} event Keydown 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 ) ) {
function onKeyDown( event ) {
const { keyCode, target } = event;

if (
keyCode !== ENTER &&
keyCode !== BACKSPACE &&
keyCode !== DELETE
) {
return;
}

selectBlock( clientId );
}
if ( target !== node || isTextField( target ) ) {
return;
}

ref.current.addEventListener( 'focusin', onFocus );
event.preventDefault();

return () => {
ref.current.removeEventListener( 'focusin', onFocus );
};
}

/**
* 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 ( keyCode === ENTER ) {
insertDefaultBlock( {}, rootClientId, index + 1 );
} else {
removeBlock( clientId );
}
}

if ( target !== ref.current || isTextField( target ) ) {
return;
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 );
}
}

event.preventDefault();

if ( keyCode === ENTER ) {
insertDefaultBlock( {}, rootClientId, index + 1 );
} else {
removeBlock( 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();
}
}

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,
] );
node.addEventListener( 'keydown', onKeyDown );
node.addEventListener( 'mouseleave', onMouseLeave );
node.addEventListener( 'dragstart', onDragStart );

return () => {
node.removeEventListener( 'mouseleave', onMouseLeave );
node.removeEventListener( 'keydown', onKeyDown );
node.removeEventListener( 'dragstart', onDragStart );
};
},
[
isSelected,
rootClientId,
index,
onSelectionStart,
insertDefaultBlock,
removeBlock,
selectBlock,
]
);
}
24 changes: 24 additions & 0 deletions packages/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,30 @@ _Returns_

- `boolean`: Reduced motion preference value.

<a name="useRefEffect" href="#useRefEffect">#</a> **useRefEffect**

Effect-like ref callback. Just like with `useEffect`, this allows you to
return a cleanup function to be run if the ref changes or one of the
dependencies changes. The ref is provided as an argument to the callback
functions. The main difference between this and `useEffect` is that
the `useEffect` callback is not called when the ref changes, but this is.
Pass the returned ref callback as the component's ref and merge multiple refs
with `useMergeRefs`.

It's worth noting that if the dependencies array is empty, there's not
strictly a need to clean up event handlers for example, because the node is
to be removed. It _is_ necessary if you add dependencies because the ref
callback will be called multiple times for the same node.

_Parameters_

- _calllback_ `Function`: Callback with ref as argument.
- _dependencies_ `Array`: Dependencies of the callback.

_Returns_

- `Function`: Ref callback.

<a name="useResizeObserver" href="#useResizeObserver">#</a> **useResizeObserver**

Hook which allows to listen the resize event of any target element when it changes sizes.
Expand Down
34 changes: 34 additions & 0 deletions packages/compose/src/hooks/use-ref-effect/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* WordPress dependencies
*/
import { useCallback, useRef } from '@wordpress/element';

/**
* Effect-like ref callback. Just like with `useEffect`, this allows you to
* return a cleanup function to be run if the ref changes or one of the
* dependencies changes. The ref is provided as an argument to the callback
* functions. The main difference between this and `useEffect` is that
* the `useEffect` callback is not called when the ref changes, but this is.
* Pass the returned ref callback as the component's ref and merge multiple refs
* with `useMergeRefs`.
*
* It's worth noting that if the dependencies array is empty, there's not
* strictly a need to clean up event handlers for example, because the node is
* to be removed. It *is* necessary if you add dependencies because the ref
* callback will be called multiple times for the same node.
*
* @param {Function} calllback Callback with ref as argument.
* @param {Array} dependencies Dependencies of the callback.
*
* @return {Function} Ref callback.
*/
export default function useRefEffect( calllback, dependencies ) {
const cleanup = useRef();
return useCallback( ( node ) => {
if ( node ) {
cleanup.current = calllback( node );
} else if ( cleanup.current ) {
cleanup.current();
}
}, dependencies );
}
1 change: 1 addition & 0 deletions packages/compose/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ export { default as useWarnOnChange } from './hooks/use-warn-on-change';
export { default as useDebounce } from './hooks/use-debounce';
export { default as useThrottle } from './hooks/use-throttle';
export { default as useMergeRefs } from './hooks/use-merge-refs';
export { default as useRefEffect } from './hooks/use-ref-effect';
6 changes: 4 additions & 2 deletions packages/rich-text/src/component/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from '@wordpress/keycodes';
import deprecated from '@wordpress/deprecated';
import { getFilesFromDataTransfer } from '@wordpress/dom';
import { useMergeRefs } from '@wordpress/compose';

/**
* Internal dependencies
Expand Down Expand Up @@ -162,8 +163,9 @@ function RichText(
__unstableOnCreateUndoLevel: onCreateUndoLevel,
__unstableIsSelected: isSelected,
},
ref
forwardedRef
) {
const ref = useRef();
const [ activeFormats = [], setActiveFormats ] = useState();
const {
formatTypes,
Expand Down Expand Up @@ -1073,7 +1075,7 @@ function RichText(
role: 'textbox',
'aria-multiline': true,
'aria-label': placeholder,
ref,
ref: useMergeRefs( [ forwardedRef, ref ] ),
style: defaultStyle,
className: 'rich-text',
onPaste: handlePaste,
Expand Down