diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index 082345d2d241a..8e8d1fa8f1ffe 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -17,6 +17,7 @@ import { useCopyHandler } from './use-copy-handler'; import { useFormatBoundaries } from './use-format-boundaries'; import { useSelectObject } from './use-select-object'; import { useInputAndSelection } from './use-input-and-selection'; +import { useSelectionChangeCompat } from './use-selection-change-compat'; import { useDelete } from './use-delete'; export function useRichText( { @@ -240,6 +241,7 @@ export function useRichText( { isSelected, onSelectionChange, } ), + useSelectionChangeCompat(), useRefEffect( () => { applyFromProps(); didMount.current = true; diff --git a/packages/rich-text/src/component/use-input-and-selection.js b/packages/rich-text/src/component/use-input-and-selection.js index c145669d5fbc5..870bfe170635c 100644 --- a/packages/rich-text/src/component/use-input-and-selection.js +++ b/packages/rich-text/src/component/use-input-and-selection.js @@ -113,18 +113,11 @@ export function useInputAndSelection( props ) { /** * Syncs the selection to local state. A callback for the - * `selectionchange`, `keyup`, `mouseup` and `touchend` events. - * - * @param {Event} event + * `selectionchange` event. */ - function handleSelectionChange( event ) { - const { - record, - applyRecord, - createRecord, - isSelected, - onSelectionChange, - } = propsRef.current; + function handleSelectionChange() { + const { record, applyRecord, createRecord, onSelectionChange } = + propsRef.current; // Check if the implementor disabled editing. `contentEditable` // does disable input, but not text selection, so we must ignore @@ -178,10 +171,6 @@ export function useInputAndSelection( props ) { return; } - if ( event.type !== 'selectionchange' && ! isSelected ) { - return; - } - // In case of a keyboard event, ignore selection changes during // composition. if ( isComposing ) { @@ -295,13 +284,6 @@ export function useInputAndSelection( props ) { element.addEventListener( 'compositionstart', onCompositionStart ); element.addEventListener( 'compositionend', onCompositionEnd ); element.addEventListener( 'focus', onFocus ); - // Selection updates must be done at these events as they - // happen before the `selectionchange` event. In some cases, - // the `selectionchange` event may not even fire, for - // example when the window receives focus again on click. - element.addEventListener( 'keyup', handleSelectionChange ); - element.addEventListener( 'mouseup', handleSelectionChange ); - element.addEventListener( 'touchend', handleSelectionChange ); ownerDocument.addEventListener( 'selectionchange', handleSelectionChange @@ -314,9 +296,6 @@ export function useInputAndSelection( props ) { ); element.removeEventListener( 'compositionend', onCompositionEnd ); element.removeEventListener( 'focus', onFocus ); - element.removeEventListener( 'keyup', handleSelectionChange ); - element.removeEventListener( 'mouseup', handleSelectionChange ); - element.removeEventListener( 'touchend', handleSelectionChange ); ownerDocument.removeEventListener( 'selectionchange', handleSelectionChange diff --git a/packages/rich-text/src/component/use-selection-change-compat.js b/packages/rich-text/src/component/use-selection-change-compat.js new file mode 100644 index 0000000000000..41783c9e034d1 --- /dev/null +++ b/packages/rich-text/src/component/use-selection-change-compat.js @@ -0,0 +1,59 @@ +/** + * WordPress dependencies + */ +import { useRefEffect } from '@wordpress/compose'; + +/** + * Sometimes some browsers are not firing a `selectionchange` event when + * changing the selection by mouse or keyboard. This hook makes sure that, if we + * detect no `selectionchange` or `input` event between the up and down events, + * we fire a `selectionchange` event. + * + * @return {import('@wordpress/compose').RefEffect} A ref effect attaching the + * listeners. + */ +export function useSelectionChangeCompat() { + return useRefEffect( ( element ) => { + const { ownerDocument } = element; + const { defaultView } = ownerDocument; + const selection = defaultView.getSelection(); + + let range; + + function getRange() { + return selection.rangeCount ? selection.getRangeAt( 0 ) : null; + } + + function onDown( event ) { + const type = event.type === 'keydown' ? 'keyup' : 'pointerup'; + + function onCancel() { + ownerDocument.removeEventListener( type, onUp ); + ownerDocument.removeEventListener( + 'selectionchange', + onCancel + ); + ownerDocument.removeEventListener( 'input', onCancel ); + } + + function onUp() { + onCancel(); + if ( range === getRange() ) return; + ownerDocument.dispatchEvent( new Event( 'selectionchange' ) ); + } + + ownerDocument.addEventListener( type, onUp ); + ownerDocument.addEventListener( 'selectionchange', onCancel ); + ownerDocument.addEventListener( 'input', onCancel ); + + range = getRange(); + } + + element.addEventListener( 'pointerdown', onDown ); + element.addEventListener( 'keydown', onDown ); + return () => { + element.removeEventListener( 'pointerdown', onDown ); + element.removeEventListener( 'keydown', onDown ); + }; + }, [] ); +}