diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 66ef0c6bccb14..d79992ab35a46 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -9,26 +9,19 @@ import { omit } from 'lodash'; */ import { RawHTML, useRef, useCallback, forwardRef } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; -import { - children as childrenSource, - getBlockTransforms, - findTransform, -} from '@wordpress/blocks'; +import { children as childrenSource } from '@wordpress/blocks'; import { useInstanceId, useMergeRefs } from '@wordpress/compose'; import { __unstableUseRichText as useRichText, __unstableCreateElement, isEmpty, - __unstableIsEmptyLine as isEmptyLine, - insert, - __unstableInsertLineSeparator as insertLineSeparator, split, toHTMLString, isCollapsed, removeFormat, } from '@wordpress/rich-text'; import deprecated from '@wordpress/deprecated'; -import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes'; +import { BACKSPACE, DELETE } from '@wordpress/keycodes'; /** * Internal dependencies @@ -43,6 +36,7 @@ import { useCaretInFormat } from './use-caret-in-format'; import { useMarkPersistent } from './use-mark-persistent'; import { usePasteHandler } from './use-paste-handler'; import { useInputRules } from './use-input-rules'; +import { useEnter } from './use-enter'; import { useFormatTypes } from './use-format-types'; import FormatEdit from './format-edit'; import { getMultilineTag, getAllowedFormats } from './utils'; @@ -119,9 +113,7 @@ function RichTextWrapper( const { selectionStart, selectionEnd, isSelected, disabled } = useSelect( selector ); - const { selectionChange, __unstableMarkAutomaticChange } = useDispatch( - blockEditorStore - ); + const { selectionChange } = useDispatch( blockEditorStore ); const multilineTag = getMultilineTag( multiline ); const adjustedAllowedFormats = getAllowedFormats( { allowedFormats, @@ -313,57 +305,7 @@ function RichTextWrapper( return; } - if ( event.keyCode === ENTER ) { - event.preventDefault(); - - const _value = { ...value }; - _value.formats = removeEditorOnlyFormats( value ); - const canSplit = onReplace && onSplit; - - if ( onReplace ) { - const transforms = getBlockTransforms( 'from' ).filter( - ( { type } ) => type === 'enter' - ); - const transformation = findTransform( transforms, ( item ) => { - return item.regExp.test( _value.text ); - } ); - - if ( transformation ) { - onReplace( [ - transformation.transform( { - content: _value.text, - } ), - ] ); - __unstableMarkAutomaticChange(); - } - } - - if ( multiline ) { - if ( event.shiftKey ) { - if ( ! disableLineBreaks ) { - onChange( insert( _value, '\n' ) ); - } - } else if ( canSplit && isEmptyLine( _value ) ) { - splitValue( _value ); - } else { - onChange( insertLineSeparator( _value ) ); - } - } else { - const { text, start, end } = _value; - const canSplitAtEnd = - onSplitAtEnd && start === end && end === text.length; - - if ( event.shiftKey || ( ! canSplit && ! canSplitAtEnd ) ) { - if ( ! disableLineBreaks ) { - onChange( insert( _value, '\n' ) ); - } - } else if ( ! canSplit && canSplitAtEnd ) { - onSplitAtEnd(); - } else if ( canSplit ) { - splitValue( _value ); - } - } - } else if ( keyCode === DELETE || keyCode === BACKSPACE ) { + if ( keyCode === DELETE || keyCode === BACKSPACE ) { const { start, end, text } = value; const isReverse = keyCode === BACKSPACE; const hasActiveFormats = @@ -450,6 +392,17 @@ function RichTextWrapper( preserveWhiteSpace, pastePlainText, } ), + useEnter( { + removeEditorOnlyFormats, + value, + onReplace, + onSplit, + multiline, + onChange, + disableLineBreaks, + splitValue, + onSplitAtEnd, + } ), anchorRef, forwardedRef, ] ) } @@ -462,10 +415,7 @@ function RichTextWrapper( 'rich-text' ) } onFocus={ unstableOnFocus } - onKeyDown={ ( event ) => { - autocompleteProps.onKeyDown( event ); - onKeyDown( event ); - } } + onKeyDown={ onKeyDown } /> ); diff --git a/packages/block-editor/src/components/rich-text/use-enter.js b/packages/block-editor/src/components/rich-text/use-enter.js new file mode 100644 index 0000000000000..52962124724ac --- /dev/null +++ b/packages/block-editor/src/components/rich-text/use-enter.js @@ -0,0 +1,105 @@ +/** + * WordPress dependencies + */ +/** + * WordPress dependencies + */ +import { useRef } from '@wordpress/element'; +import { useRefEffect } from '@wordpress/compose'; +import { ENTER } from '@wordpress/keycodes'; +import { + insert, + __unstableIsEmptyLine as isEmptyLine, + __unstableInsertLineSeparator as insertLineSeparator, +} from '@wordpress/rich-text'; +import { getBlockTransforms, findTransform } from '@wordpress/blocks'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +export function useEnter( props ) { + const { __unstableMarkAutomaticChange } = useDispatch( blockEditorStore ); + const propsRef = useRef( props ); + propsRef.current = props; + return useRefEffect( ( element ) => { + function onKeyDown( event ) { + if ( event.defaultPrevented ) { + return; + } + + const { + removeEditorOnlyFormats, + value, + onReplace, + onSplit, + multiline, + onChange, + disableLineBreaks, + splitValue, + onSplitAtEnd, + } = propsRef.current; + + if ( event.keyCode !== ENTER ) { + return; + } + + event.preventDefault(); + + const _value = { ...value }; + _value.formats = removeEditorOnlyFormats( value ); + const canSplit = onReplace && onSplit; + + if ( onReplace ) { + const transforms = getBlockTransforms( 'from' ).filter( + ( { type } ) => type === 'enter' + ); + const transformation = findTransform( transforms, ( item ) => { + return item.regExp.test( _value.text ); + } ); + + if ( transformation ) { + onReplace( [ + transformation.transform( { + content: _value.text, + } ), + ] ); + __unstableMarkAutomaticChange(); + } + } + + if ( multiline ) { + if ( event.shiftKey ) { + if ( ! disableLineBreaks ) { + onChange( insert( _value, '\n' ) ); + } + } else if ( canSplit && isEmptyLine( _value ) ) { + splitValue( _value ); + } else { + onChange( insertLineSeparator( _value ) ); + } + } else { + const { text, start, end } = _value; + const canSplitAtEnd = + onSplitAtEnd && start === end && end === text.length; + + if ( event.shiftKey || ( ! canSplit && ! canSplitAtEnd ) ) { + if ( ! disableLineBreaks ) { + onChange( insert( _value, '\n' ) ); + } + } else if ( ! canSplit && canSplitAtEnd ) { + onSplitAtEnd(); + } else if ( canSplit ) { + splitValue( _value ); + } + } + } + + element.addEventListener( 'keydown', onKeyDown ); + return () => { + element.removeEventListener( 'keydown', onKeyDown ); + }; + }, [] ); +} diff --git a/packages/components/src/autocomplete/index.js b/packages/components/src/autocomplete/index.js index 100672f811d44..87dd92eac2633 100644 --- a/packages/components/src/autocomplete/index.js +++ b/packages/components/src/autocomplete/index.js @@ -24,7 +24,12 @@ import { BACKSPACE, } from '@wordpress/keycodes'; import { __, _n, sprintf } from '@wordpress/i18n'; -import { useInstanceId, useDebounce } from '@wordpress/compose'; +import { + useInstanceId, + useDebounce, + useMergeRefs, + useRefEffect, +} from '@wordpress/compose'; import { create, slice, @@ -565,15 +570,26 @@ function useAutocomplete( { export function useAutocompleteProps( options ) { const ref = useRef(); + const onKeyDownRef = useRef(); const { popover, listBoxId, activeId, onKeyDown } = useAutocomplete( { ...options, contentRef: ref, } ); - + onKeyDownRef.current = onKeyDown; return { - ref, + ref: useMergeRefs( [ + ref, + useRefEffect( ( element ) => { + function _onKeyDown( event ) { + onKeyDownRef.current( event ); + } + element.addEventListener( 'keydown', _onKeyDown ); + return () => { + element.removeEventListener( 'keydown', _onKeyDown ); + }; + }, [] ), + ] ), children: popover, - onKeyDown, 'aria-autocomplete': listBoxId ? 'list' : undefined, 'aria-owns': listBoxId, 'aria-activedescendant': activeId, diff --git a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js index df255f39bec8a..f9d40c07e2bd7 100644 --- a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js +++ b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js @@ -36,8 +36,9 @@ async function getSelectedFlatIndices() { * Tests if the native selection matches the block selection. */ async function testNativeSelection() { - // Wait for the selection to update. - await page.evaluate( () => new Promise( window.requestAnimationFrame ) ); + // Wait for the selection to update and async mode to update classes of + // deselected blocks. + await page.evaluate( () => new Promise( window.requestIdleCallback ) ); await page.evaluate( () => { const selection = window.getSelection(); const elements = Array.from( diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index d258b8b33ca96..7d0ef2ba28c6f 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -93,8 +93,20 @@ export function useRichText( { record.current.end = selectionEnd; } + const hadSelectionUpdate = useRef( false ); + if ( ! record.current ) { setRecordFromProps(); + } else if ( + selectionStart !== record.current.start || + selectionEnd !== record.current.end + ) { + hadSelectionUpdate.current = isSelected; + record.current = { + ...record.current, + start: selectionStart, + end: selectionEnd, + }; } /** @@ -149,24 +161,13 @@ export function useRichText( { // Value updates must happen synchonously to avoid overwriting newer values. useLayoutEffect( () => { - if ( ! didMount.current ) { + if ( ! hadSelectionUpdate.current ) { return; } - if ( - isSelected && - ( selectionStart !== record.current.start || - selectionEnd !== record.current.end ) - ) { - applyFromProps(); - } else { - record.current = { - ...record.current, - start: selectionStart, - end: selectionEnd, - }; - } - }, [ selectionStart, selectionEnd, isSelected ] ); + applyFromProps(); + hadSelectionUpdate.current = false; + }, [ hadSelectionUpdate.current ] ); function focus() { ref.current.focus();