Skip to content

Commit

Permalink
fix: flush selection change on before input (#4669)
Browse files Browse the repository at this point in the history
* fix: flush selection change on before input

* chore: remove redundant option from debounce
  • Loading branch information
BitPhinix authored Nov 20, 2021
1 parent 0f194a8 commit 807716d
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 55 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilled-bears-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'slate-react': patch
---

Flush scheduleOnDOMSelectionChange on beforeinput
116 changes: 61 additions & 55 deletions packages/slate-react/src/components/editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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),
[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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 807716d

Please sign in to comment.