From 830080aa44672b8805361b4d9927116d89710ca3 Mon Sep 17 00:00:00 2001 From: BitPhinix Date: Fri, 19 Nov 2021 13:28:35 +0100 Subject: [PATCH 1/2] fix: flush selection change on before input --- .changeset/chilled-bears-lick.md | 5 + .../slate-react/src/components/editable.tsx | 116 +++++++++--------- 2 files changed, 66 insertions(+), 55 deletions(-) create mode 100644 .changeset/chilled-bears-lick.md diff --git a/.changeset/chilled-bears-lick.md b/.changeset/chilled-bears-lick.md new file mode 100644 index 0000000000..228c9f5658 --- /dev/null +++ b/.changeset/chilled-bears-lick.md @@ -0,0 +1,5 @@ +--- +'slate-react': patch +--- + +Flush scheduleOnDOMSelectionChange on beforeinput diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index d729854208..7dffa4badf 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -10,6 +10,7 @@ import { Path, } from 'slate' import getDirection from 'direction' +import debounce from 'lodash/debounce' import throttle from 'lodash/throttle' import scrollIntoView from 'scroll-into-view-if-needed' @@ -253,6 +254,61 @@ export const Editable = (props: EditableProps) => { } }, [autoFocus]) + // Listen on the native `selectionchange` event to be able to update any time + // the selection changes. This is required because React's `onSelect` is leaky + // and non-standard so it doesn't fire until after a selection has been + // released. This causes issues in situations where another change happens + // while a selection is being dragged. + const onDOMSelectionChange = useCallback( + throttle(() => { + if ( + !state.isComposing && + !state.isUpdatingSelection && + !state.isDraggingInternally + ) { + const root = ReactEditor.findDocumentOrShadowRoot(editor) + const { activeElement } = root + const el = ReactEditor.toDOMNode(editor, editor) + const domSelection = root.getSelection() + + if (activeElement === el) { + state.latestElement = activeElement + IS_FOCUSED.set(editor, true) + } else { + IS_FOCUSED.delete(editor) + } + + if (!domSelection) { + return Transforms.deselect(editor) + } + + const { anchorNode, focusNode } = domSelection + + const anchorNodeSelectable = + hasEditableTarget(editor, anchorNode) || + isTargetInsideVoid(editor, anchorNode) + + const focusNodeSelectable = + hasEditableTarget(editor, focusNode) || + isTargetInsideVoid(editor, focusNode) + + if (anchorNodeSelectable && focusNodeSelectable) { + const range = ReactEditor.toSlateRange(editor, domSelection, { + exactMatch: false, + suppressThrow: false, + }) + Transforms.select(editor, range) + } + } + }, 100), + [readOnly] + ) + + const scheduleOnDOMSelectionChange = useMemo( + () => debounce(onDOMSelectionChange, 0, { leading: false }), + [onDOMSelectionChange] + ) + // Listen on the native `beforeinput` event to get real "Level 2" events. This // is required because React's `beforeinput` is fake and never really attaches // to the real event sadly. (2019/11/01) @@ -264,6 +320,11 @@ export const Editable = (props: EditableProps) => { hasEditableTarget(editor, event.target) && !isDOMEventHandled(event, propsOnDOMBeforeInput) ) { + // Some IMEs/Chrome extensions like e.g. Grammarly set the selection immediately before + // triggering a `beforeinput` expecting the change to be applied to the immediately before + // set selection. + scheduleOnDOMSelectionChange.flush() + const { selection } = editor const { inputType: type } = event const data = (event as any).dataTransfer || event.data || undefined @@ -472,61 +533,6 @@ export const Editable = (props: EditableProps) => { } }, [onDOMBeforeInput]) - // Listen on the native `selectionchange` event to be able to update any time - // the selection changes. This is required because React's `onSelect` is leaky - // and non-standard so it doesn't fire until after a selection has been - // released. This causes issues in situations where another change happens - // while a selection is being dragged. - const onDOMSelectionChange = useCallback( - throttle(() => { - if ( - !state.isComposing && - !state.isUpdatingSelection && - !state.isDraggingInternally - ) { - const root = ReactEditor.findDocumentOrShadowRoot(editor) - const { activeElement } = root - const el = ReactEditor.toDOMNode(editor, editor) - const domSelection = root.getSelection() - - if (activeElement === el) { - state.latestElement = activeElement - IS_FOCUSED.set(editor, true) - } else { - IS_FOCUSED.delete(editor) - } - - if (!domSelection) { - return Transforms.deselect(editor) - } - - const { anchorNode, focusNode } = domSelection - - const anchorNodeSelectable = - hasEditableTarget(editor, anchorNode) || - isTargetInsideVoid(editor, anchorNode) - - const focusNodeSelectable = - hasEditableTarget(editor, focusNode) || - isTargetInsideVoid(editor, focusNode) - - if (anchorNodeSelectable && focusNodeSelectable) { - const range = ReactEditor.toSlateRange(editor, domSelection, { - exactMatch: false, - suppressThrow: false, - }) - Transforms.select(editor, range) - } - } - }, 100), - [readOnly] - ) - - const scheduleOnDOMSelectionChange = useCallback( - () => setTimeout(onDOMSelectionChange), - [onDOMSelectionChange] - ) - // Attach a native DOM event handler for `selectionchange`, because React's // built-in `onSelect` handler doesn't fire for all selection changes. It's a // leaky polyfill that only fires on keypresses or clicks. Instead, we want to From 79928a9503b39ebe86a7b573263860192e2a4af1 Mon Sep 17 00:00:00 2001 From: BitPhinix Date: Fri, 19 Nov 2021 18:46:25 +0100 Subject: [PATCH 2/2] chore: remove redundant option from debounce --- packages/slate-react/src/components/editable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 7dffa4badf..648c5d54c1 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -305,7 +305,7 @@ export const Editable = (props: EditableProps) => { ) const scheduleOnDOMSelectionChange = useMemo( - () => debounce(onDOMSelectionChange, 0, { leading: false }), + () => debounce(onDOMSelectionChange, 0), [onDOMSelectionChange] )