diff --git a/.changeset/many-baboons-stare.md b/.changeset/many-baboons-stare.md new file mode 100644 index 000000000..04ef86890 --- /dev/null +++ b/.changeset/many-baboons-stare.md @@ -0,0 +1,5 @@ +--- +'slate-react': patch +--- + +defer native events within Editable to avoid bugs with Editor diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index a9f0f20fe..f0c45fe62 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -48,7 +48,8 @@ import { PLACEHOLDER_SYMBOL, EDITOR_TO_WINDOW, } from '../utils/weak-maps' -import { asNative, flushNativeEvents } from '../utils/native' + +type DeferredOperation = () => void const Children = (props: Parameters[0]) => ( {useChildren(props)} @@ -124,6 +125,7 @@ export const Editable = (props: EditableProps) => { // Rerender editor when composition status changed const [isComposing, setIsComposing] = useState(false) const ref = useRef(null) + const deferredOperations = useRef([]) // Update internal state on each render. IS_READ_ONLY.set(editor, readOnly) @@ -433,9 +435,9 @@ export const Editable = (props: EditableProps) => { // Only insertText operations use the native functionality, for now. // Potentially expand to single character deletes, as well. if (native) { - asNative(editor, () => Editor.insertText(editor, data), { - onFlushed: () => event.preventDefault(), - }) + deferredOperations.current.push(() => + Editor.insertText(editor, data) + ) } else { Editor.insertText(editor, data) } @@ -622,7 +624,10 @@ export const Editable = (props: EditableProps) => { // and we can correctly compare DOM text values in components // to stop rendering, so that browser functions like autocorrect // and spellcheck work as expected. - flushNativeEvents(editor) + for (const op of deferredOperations.current) { + op() + } + deferredOperations.current = [] }, [])} onBlur={useCallback( (event: React.FocusEvent) => { diff --git a/packages/slate-react/src/plugin/with-react.ts b/packages/slate-react/src/plugin/with-react.ts index d73609731..e2fbd57ff 100644 --- a/packages/slate-react/src/plugin/with-react.ts +++ b/packages/slate-react/src/plugin/with-react.ts @@ -8,11 +8,6 @@ import { EDITOR_TO_ON_CHANGE, NODE_TO_KEY, } from '../utils/weak-maps' -import { - AS_NATIVE, - NATIVE_OPERATIONS, - flushNativeEvents, -} from '../utils/native' import { isDOMText, getPlainText, @@ -66,31 +61,6 @@ export const withReact = (editor: T) => { } e.apply = (op: Operation) => { - // if we're NOT an insert_text and there's a queue - // of native events, bail out and flush the queue. - // otherwise transforms as part of this cycle will - // be incorrect. - // - // This is needed as overriden operations (e.g. `insertText`) - // can call additional transforms, which will need accurate - // content, and will be called _before_ `onInput` is fired. - if (op.type !== 'insert_text') { - AS_NATIVE.set(editor, false) - flushNativeEvents(editor) - } - - // If we're in native mode, queue the operation - // and it will be applied later. - if (AS_NATIVE.get(editor)) { - const nativeOps = NATIVE_OPERATIONS.get(editor) - if (nativeOps) { - nativeOps.push(op) - } else { - NATIVE_OPERATIONS.set(editor, [op]) - } - return - } - const matches: [Path, Key][] = [] switch (op.type) { diff --git a/packages/slate-react/src/utils/native.ts b/packages/slate-react/src/utils/native.ts deleted file mode 100644 index 12a2857d6..000000000 --- a/packages/slate-react/src/utils/native.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Editor, Operation } from 'slate' - -export const AS_NATIVE: WeakMap = new WeakMap() -export const NATIVE_OPERATIONS: WeakMap = new WeakMap() - -/** - * `asNative` queues operations as native, meaning native browser events will - * not have been prevented, and we need to flush the operations - * after the native events have propogated to the DOM. - * @param {Editor} editor - Editor on which the operations are being applied - * @param {callback} fn - Function containing .exec calls which will be queued as native - */ -export const asNative = ( - editor: Editor, - fn: () => void, - { onFlushed }: { onFlushed?: () => void } = {} -) => { - const isNative = AS_NATIVE.get(editor) - - AS_NATIVE.set(editor, true) - try { - fn() - } finally { - if (isNative !== undefined) { - AS_NATIVE.set(editor, isNative) - } - } - - if (!NATIVE_OPERATIONS.get(editor)) { - onFlushed?.() - } -} - -/** - * `flushNativeEvents` applies any queued native events. - * @param {Editor} editor - Editor on which the operations are being applied - */ -export const flushNativeEvents = (editor: Editor) => { - const nativeOps = NATIVE_OPERATIONS.get(editor) - - // Clear list _before_ applying, as we might flush - // events in each op, as well. - NATIVE_OPERATIONS.delete(editor) - - if (nativeOps) { - Editor.withoutNormalizing(editor, () => { - nativeOps.forEach(op => { - editor.apply(op) - }) - }) - } -}