From 6f3f306458a737c529766b2aa7a75a4068a73612 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Tue, 17 Sep 2024 09:42:06 -0700 Subject: [PATCH] Revert "Allow multi-select on iOS Safari/touch devices (#63671)" This reverts commit 6aeba745bcba51621310a318e8dccfdf3eb4c92e. --- .../use-focus-first-element.js | 1 - .../event-listeners/paste-handler.js | 7 +- .../src/components/rich-text/index.js | 14 +--- .../src/components/writing-flow/index.js | 2 - .../components/writing-flow/use-arrow-nav.js | 11 +-- .../writing-flow/use-event-redirect.js | 72 ------------------- .../src/components/writing-flow/use-input.js | 37 +--------- .../components/writing-flow/use-select-all.js | 19 +---- .../writing-flow/use-selection-observer.js | 17 +---- .../src/components/writing-flow/utils.js | 30 -------- packages/dom/src/dom/place-caret-at-edge.js | 12 ++-- .../component/event-listeners/copy-handler.js | 17 ++--- .../event-listeners/input-and-selection.js | 19 +++-- .../editor/various/block-deletion.spec.js | 11 ++- .../editor/various/pattern-overrides.spec.js | 4 +- 15 files changed, 36 insertions(+), 237 deletions(-) delete mode 100644 packages/block-editor/src/components/writing-flow/use-event-redirect.js 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 a6308f48005f9..27f72d1a100d3 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 @@ -68,7 +68,6 @@ export function useFocusFirstElement( { clientId, initialPosition } ) { textInputs[ isReverse ? textInputs.length - 1 : 0 ] || ref.current; if ( ! isInsideRootBlock( ref.current, target ) ) { - ownerDocument.defaultView.getSelection().removeAllRanges(); ref.current.focus(); return; } diff --git a/packages/block-editor/src/components/rich-text/event-listeners/paste-handler.js b/packages/block-editor/src/components/rich-text/event-listeners/paste-handler.js index 9266cd2553754..59633f4750ff9 100644 --- a/packages/block-editor/src/components/rich-text/event-listeners/paste-handler.js +++ b/packages/block-editor/src/components/rich-text/event-listeners/paste-handler.js @@ -26,15 +26,10 @@ export default ( props ) => ( element ) => { preserveWhiteSpace, pastePlainText, } = props.current; - const { ownerDocument } = element; - const { defaultView } = ownerDocument; - const { anchorNode, focusNode } = defaultView.getSelection(); - const containsSelection = - element.contains( anchorNode ) && element.contains( focusNode ); // The event listener is attached to the window, so we need to check if // the target is the element. - if ( ! containsSelection ) { + if ( event.target !== element ) { return; } diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 732b8dbf2c089..3a7cf77649896 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -356,19 +356,7 @@ export function RichTextWrapper( const inputEvents = useRef( new Set() ); function onFocus() { - let element = anchorRef.current; - - if ( ! element ) { - return; - } - - // Writing flow might be editable, so we should make sure focus goes to - // the root editable element. - while ( element.parentElement?.isContentEditable ) { - element = element.parentElement; - } - - element.focus(); + anchorRef.current?.focus(); } const registry = useRegistry(); diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index cea3e4b19707d..7e6b36b0e2214 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -23,7 +23,6 @@ import useSelectionObserver from './use-selection-observer'; import useClickSelection from './use-click-selection'; import useInput from './use-input'; import useClipboardHandler from './use-clipboard-handler'; -import useEventRedirect from './use-event-redirect'; import { store as blockEditorStore } from '../../store'; export function useWritingFlow() { @@ -66,7 +65,6 @@ export function useWritingFlow() { }, [ hasMultiSelection ] ), - useEventRedirect(), ] ), after, ]; diff --git a/packages/block-editor/src/components/writing-flow/use-arrow-nav.js b/packages/block-editor/src/components/writing-flow/use-arrow-nav.js index fda5a0133ee00..44051b324ff64 100644 --- a/packages/block-editor/src/components/writing-flow/use-arrow-nav.js +++ b/packages/block-editor/src/components/writing-flow/use-arrow-nav.js @@ -19,7 +19,6 @@ import { useRefEffect } from '@wordpress/compose'; */ import { getBlockClientId, isInSameBlock } from '../../utils/dom'; import { store as blockEditorStore } from '../../store'; -import { getSelectionRoot } from './utils'; /** * Returns true if the element should consider edge navigation upon a keyboard @@ -191,7 +190,8 @@ export default function useArrowNav() { return; } - const { keyCode, shiftKey, ctrlKey, altKey, metaKey } = event; + const { keyCode, target, shiftKey, ctrlKey, altKey, metaKey } = + event; const isUp = keyCode === UP; const isDown = keyCode === DOWN; const isLeft = keyCode === LEFT; @@ -233,11 +233,6 @@ export default function useArrowNav() { return; } - const target = - ownerDocument.activeElement === node - ? getSelectionRoot( ownerDocument ) - : event.target; - // Abort if our current target is not a candidate for navigation // (e.g. preserve native input behaviors). if ( ! isNavigationCandidate( target, keyCode, hasModifier ) ) { @@ -279,7 +274,6 @@ export default function useArrowNav() { ( altKey ? isHorizontalEdge( target, isReverseDir ) : true ) && ! keepCaretInsideBlock ) { - node.contentEditable = false; const closestTabbable = getClosestTabbable( target, isReverse, @@ -303,7 +297,6 @@ export default function useArrowNav() { isHorizontalEdge( target, isReverseDir ) && ! keepCaretInsideBlock ) { - node.contentEditable = false; const closestTabbable = getClosestTabbable( target, isReverseDir, diff --git a/packages/block-editor/src/components/writing-flow/use-event-redirect.js b/packages/block-editor/src/components/writing-flow/use-event-redirect.js deleted file mode 100644 index b8dcd7eda6969..0000000000000 --- a/packages/block-editor/src/components/writing-flow/use-event-redirect.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * WordPress dependencies - */ -import { useRefEffect } from '@wordpress/compose'; - -/** - * Internal dependencies - */ -import { getSelectionRoot } from './utils'; - -/** - * Whenever content editable is enabled on writing flow, it will have focus, so - * we need to dispatch some events to the root of the selection to ensure - * compatibility with rich text. In the future, perhaps the rich text event - * handlers should be attached to the window instead. - * - * Alternatively, we could try to find a way to always maintain rich text focus. - */ -export default function useEventRedirect() { - return useRefEffect( ( node ) => { - function onInput( event ) { - if ( event.target !== node ) { - return; - } - - const { ownerDocument } = node; - const { defaultView } = ownerDocument; - const prototype = Object.getPrototypeOf( event ); - const constructorName = prototype.constructor.name; - const Constructor = defaultView[ constructorName ]; - const root = getSelectionRoot( ownerDocument ); - - if ( ! root || root === node ) { - return; - } - - const init = {}; - - for ( const key in event ) { - init[ key ] = event[ key ]; - } - - init.bubbles = false; - - const newEvent = new Constructor( event.type, init ); - const cancelled = ! root.dispatchEvent( newEvent ); - - if ( cancelled ) { - event.preventDefault(); - } - } - - const events = [ - 'beforeinput', - 'input', - 'compositionstart', - 'compositionend', - 'compositionupdate', - 'keydown', - ]; - - events.forEach( ( eventType ) => { - node.addEventListener( eventType, onInput ); - } ); - - return () => { - events.forEach( ( eventType ) => { - node.removeEventListener( eventType, onInput ); - } ); - }; - }, [] ); -} diff --git a/packages/block-editor/src/components/writing-flow/use-input.js b/packages/block-editor/src/components/writing-flow/use-input.js index 31c5d769834c0..0f10cc9c2d1c7 100644 --- a/packages/block-editor/src/components/writing-flow/use-input.js +++ b/packages/block-editor/src/components/writing-flow/use-input.js @@ -16,7 +16,6 @@ import { * Internal dependencies */ import { store as blockEditorStore } from '../../store'; -import { getSelectionRoot } from './utils'; /** * Handles input for selections across blocks. @@ -50,24 +49,7 @@ export default function useInput() { // DOM. This will cause React errors (and the DOM should only be // altered in a controlled fashion). if ( node.contentEditable === 'true' ) { - const selection = node.ownerDocument.defaultView.getSelection(); - const range = selection.rangeCount - ? selection.getRangeAt( 0 ) - : null; - const root = getSelectionRoot( node.ownerDocument ); - - // If selection is contained within a nested editable, allow - // input. We need to ensure that selection is maintained. - if ( root ) { - node.contentEditable = false; - root.focus(); - selection.removeAllRanges(); - if ( range ) { - selection.addRange( range ); - } - } else { - event.preventDefault(); - } + event.preventDefault(); } } @@ -77,23 +59,6 @@ export default function useInput() { } if ( ! hasMultiSelection() ) { - const { ownerDocument } = node; - if ( node === ownerDocument.activeElement ) { - if ( event.key === 'End' || event.key === 'Home' ) { - const selectionRoot = getSelectionRoot( ownerDocument ); - const selection = - ownerDocument.defaultView.getSelection(); - selection.selectAllChildren( selectionRoot ); - const method = - event.key === 'End' - ? 'collapseToEnd' - : 'collapseToStart'; - selection[ method ](); - event.preventDefault(); - return; - } - } - if ( event.keyCode === ENTER ) { if ( event.shiftKey || __unstableIsFullySelected() ) { return; diff --git a/packages/block-editor/src/components/writing-flow/use-select-all.js b/packages/block-editor/src/components/writing-flow/use-select-all.js index 5a7acb3a8783a..c56549acf54ad 100644 --- a/packages/block-editor/src/components/writing-flow/use-select-all.js +++ b/packages/block-editor/src/components/writing-flow/use-select-all.js @@ -10,7 +10,6 @@ import { useRefEffect } from '@wordpress/compose'; * Internal dependencies */ import { store as blockEditorStore } from '../../store'; -import { getSelectionRoot } from './utils'; export default function useSelectAll() { const { getBlockOrder, getSelectedBlockClientIds, getBlockRootClientId } = @@ -24,27 +23,12 @@ export default function useSelectAll() { return; } - const selectionRoot = getSelectionRoot( node.ownerDocument ); const selectedClientIds = getSelectedBlockClientIds(); - // Abort if there is selection, but it is not within a block. - if ( selectionRoot && ! selectedClientIds.length ) { - return; - } - if ( - selectionRoot && selectedClientIds.length < 2 && - ! isEntirelySelected( selectionRoot ) + ! isEntirelySelected( event.target ) ) { - if ( node === node.ownerDocument.activeElement ) { - event.preventDefault(); - node.ownerDocument.defaultView - .getSelection() - .selectAllChildren( selectionRoot ); - return; - } - return; } @@ -61,7 +45,6 @@ export default function useSelectAll() { node.ownerDocument.defaultView .getSelection() .removeAllRanges(); - node.contentEditable = 'false'; selectBlock( rootClientId ); } return; diff --git a/packages/block-editor/src/components/writing-flow/use-selection-observer.js b/packages/block-editor/src/components/writing-flow/use-selection-observer.js index 8ecba461d1025..c7ce5d259d875 100644 --- a/packages/block-editor/src/components/writing-flow/use-selection-observer.js +++ b/packages/block-editor/src/components/writing-flow/use-selection-observer.js @@ -107,12 +107,8 @@ function getRichTextElement( node ) { export default function useSelectionObserver() { const { multiSelect, selectBlock, selectionChange } = useDispatch( blockEditorStore ); - const { - getBlockParents, - getBlockSelectionStart, - isMultiSelecting, - getSelectedBlockClientId, - } = useSelect( blockEditorStore ); + const { getBlockParents, getBlockSelectionStart, isMultiSelecting } = + useSelect( blockEditorStore ); return useRefEffect( ( node ) => { const { ownerDocument } = node; @@ -195,17 +191,10 @@ export default function useSelectionObserver() { return; } - setContentEditableWrapper( - node, - !! ( startClientId && endClientId ) - ); - const isSingularSelection = startClientId === endClientId; if ( isSingularSelection ) { if ( ! isMultiSelecting() ) { - if ( getSelectedBlockClientId() !== startClientId ) { - selectBlock( startClientId ); - } + selectBlock( startClientId ); } else { multiSelect( startClientId, startClientId ); } diff --git a/packages/block-editor/src/components/writing-flow/utils.js b/packages/block-editor/src/components/writing-flow/utils.js index 0cd41eedd3f6f..2a2010854ed20 100644 --- a/packages/block-editor/src/components/writing-flow/utils.js +++ b/packages/block-editor/src/components/writing-flow/utils.js @@ -116,33 +116,3 @@ function toPlainText( html ) { // Merge any consecutive line breaks return plainText.replace( /\n\n+/g, '\n\n' ); } - -/** - * Gets the current content editable root element based on the selection. - * @param {Document} ownerDocument - * @return {Element|undefined} The content editable root element. - */ -export function getSelectionRoot( ownerDocument ) { - const { defaultView } = ownerDocument; - const { anchorNode, focusNode } = defaultView.getSelection(); - - if ( ! anchorNode || ! focusNode ) { - return; - } - - const anchorElement = ( - anchorNode.nodeType === anchorNode.ELEMENT_NODE - ? anchorNode - : anchorNode.parentElement - ).closest( '[contenteditable]' ); - - if ( ! anchorElement ) { - return; - } - - if ( ! anchorElement.contains( focusNode ) ) { - return; - } - - return anchorElement; -} diff --git a/packages/dom/src/dom/place-caret-at-edge.js b/packages/dom/src/dom/place-caret-at-edge.js index 013a64d076e55..4075fc7c43958 100644 --- a/packages/dom/src/dom/place-caret-at-edge.js +++ b/packages/dom/src/dom/place-caret-at-edge.js @@ -67,14 +67,7 @@ export default function placeCaretAtEdge( container, isReverse, x ) { return; } - const { ownerDocument } = container; - const { defaultView } = ownerDocument; - assertIsDefined( defaultView, 'defaultView' ); - const selection = defaultView.getSelection(); - assertIsDefined( selection, 'selection' ); - if ( ! container.isContentEditable ) { - selection.removeAllRanges(); return; } @@ -86,6 +79,11 @@ export default function placeCaretAtEdge( container, isReverse, x ) { return; } + const { ownerDocument } = container; + const { defaultView } = ownerDocument; + assertIsDefined( defaultView, 'defaultView' ); + const selection = defaultView.getSelection(); + assertIsDefined( selection, 'selection' ); selection.removeAllRanges(); selection.addRange( range ); } diff --git a/packages/rich-text/src/component/event-listeners/copy-handler.js b/packages/rich-text/src/component/event-listeners/copy-handler.js index 1a92237bb4c5b..0cc1594c3ab91 100644 --- a/packages/rich-text/src/component/event-listeners/copy-handler.js +++ b/packages/rich-text/src/component/event-listeners/copy-handler.js @@ -2,21 +2,18 @@ * Internal dependencies */ import { toHTMLString } from '../../to-html-string'; +import { isCollapsed } from '../../is-collapsed'; import { slice } from '../../slice'; -import { remove } from '../../remove'; import { getTextContent } from '../../get-text-content'; export default ( props ) => ( element ) => { function onCopy( event ) { - const { record, createRecord, handleChange } = props.current; + const { record } = props.current; const { ownerDocument } = element; - const { defaultView } = ownerDocument; - const { anchorNode, focusNode, isCollapsed } = - defaultView.getSelection(); - const containsSelection = - element.contains( anchorNode ) && element.contains( focusNode ); - - if ( isCollapsed || ! containsSelection ) { + if ( + isCollapsed( record.current ) || + ! element.contains( ownerDocument.activeElement ) + ) { return; } @@ -29,7 +26,7 @@ export default ( props ) => ( element ) => { event.preventDefault(); if ( event.type === 'cut' ) { - handleChange( remove( createRecord() ) ); + ownerDocument.execCommand( 'delete' ); } } diff --git a/packages/rich-text/src/component/event-listeners/input-and-selection.js b/packages/rich-text/src/component/event-listeners/input-and-selection.js index 11dcdb0d8ff9a..621f1c59fab04 100644 --- a/packages/rich-text/src/component/event-listeners/input-and-selection.js +++ b/packages/rich-text/src/component/event-listeners/input-and-selection.js @@ -114,13 +114,14 @@ export default ( props ) => ( element ) => { return; } - const { anchorNode, focusNode } = defaultView.getSelection(); - const containsSelection = - element.contains( anchorNode ) && - element.contains( focusNode ) && - ownerDocument.activeElement.contains( element ); - - if ( ! containsSelection ) { + // Ensure the active element is the rich text element. + if ( ownerDocument.activeElement !== element ) { + // If it is not, we can stop listening for selection changes. We + // resume listening when the element is focused. + ownerDocument.removeEventListener( + 'selectionchange', + handleSelectionChange + ); return; } @@ -254,9 +255,5 @@ export default ( props ) => ( element ) => { element.removeEventListener( 'compositionstart', onCompositionStart ); element.removeEventListener( 'compositionend', onCompositionEnd ); element.removeEventListener( 'focus', onFocus ); - ownerDocument.removeEventListener( - 'selectionchange', - handleSelectionChange - ); }; }; diff --git a/test/e2e/specs/editor/various/block-deletion.spec.js b/test/e2e/specs/editor/various/block-deletion.spec.js index 00b51a94668d5..9346412c46bcb 100644 --- a/test/e2e/specs/editor/various/block-deletion.spec.js +++ b/test/e2e/specs/editor/various/block-deletion.spec.js @@ -287,16 +287,15 @@ test.describe( 'Block deletion', () => { await expect.poll( editor.getBlocks ).toMatchObject( [ { name: 'core/paragraph', attributes: { content: 'First' } }, { name: 'core/paragraph', attributes: { content: 'Second' } }, + { name: 'core/paragraph', attributes: { content: '' } }, ] ); // Ensure that the newly created empty block is focused. - await expect.poll( editor.getBlocks ).toHaveLength( 2 ); + await expect.poll( editor.getBlocks ).toHaveLength( 3 ); await expect( - editor.canvas - .getByRole( 'document', { - name: 'Block: Paragraph', - } ) - .nth( 1 ) + editor.canvas.getByRole( 'document', { + name: 'Empty block', + } ) ).toBeFocused(); } ); diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js index 83f2f880f3bf1..5fbd0e66b5fd0 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -150,7 +150,7 @@ test.describe( 'Pattern Overrides', () => { name: 'Block: Paragraph', } ); // Ensure the first pattern is selected. - await editor.selectBlocks( patternBlocks.first() ); + await patternBlocks.first().selectText(); await expect( paragraphs.first() ).not.toHaveAttribute( 'inert', 'true' @@ -168,7 +168,7 @@ test.describe( 'Pattern Overrides', () => { await page.keyboard.type( 'I would word it this way' ); // Ensure the second pattern is selected. - await editor.selectBlocks( patternBlocks.last() ); + await patternBlocks.last().selectText(); await patternBlocks .last() .getByRole( 'document', {