From d42cd005db862165f5ac63fba4d35f36d92864f6 Mon Sep 17 00:00:00 2001 From: Anthony Ciccarello Date: Fri, 17 Mar 2023 10:45:54 -0700 Subject: [PATCH] enable eslint hooks rules (#5363) --- .changeset/shaggy-donuts-applaud.md | 11 + .eslintrc.json | 5 +- package.json | 1 + .../slate-react/src/components/editable.tsx | 217 ++++++++++-------- packages/slate-react/src/components/leaf.tsx | 2 +- packages/slate-react/src/components/slate.tsx | 10 +- .../use-android-input-manager.ts | 61 +++-- .../src/hooks/use-mutation-observer.ts | 2 +- .../src/hooks/use-slate-selector.tsx | 15 +- .../src/hooks/use-track-user-input.ts | 2 +- packages/slate-react/test/index.spec.tsx | 14 +- site/examples/code-highlighting.tsx | 64 +++--- site/examples/markdown-shortcuts.tsx | 55 ++--- site/examples/mentions.tsx | 2 +- yarn.lock | 10 + 15 files changed, 270 insertions(+), 201 deletions(-) create mode 100644 .changeset/shaggy-donuts-applaud.md diff --git a/.changeset/shaggy-donuts-applaud.md b/.changeset/shaggy-donuts-applaud.md new file mode 100644 index 0000000000..b0450b2070 --- /dev/null +++ b/.changeset/shaggy-donuts-applaud.md @@ -0,0 +1,11 @@ +--- +'slate-react': minor +--- + +update dependencies on react hooks to be more senstive to changes + +The code should now meet eslint react hook standards + +This could result in more renders + +closes #3886 diff --git a/.eslintrc.json b/.eslintrc.json index 87bc42e0ca..ac7bd4c418 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,7 +4,8 @@ "plugin:import/typescript", "prettier", "prettier/@typescript-eslint", - "prettier/react" + "prettier/react", + "plugin:react-hooks/recommended" ], "plugins": ["@typescript-eslint", "import", "react", "prettier"], "parser": "@typescript-eslint/parser", @@ -19,7 +20,7 @@ "settings": { "import/extensions": [".js", ".ts", ".jsx", ".tsx"], "react": { - "version": "detect" + "version": "16" } }, "env": { diff --git a/package.json b/package.json index 50f28f142c..04e415fc9a 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "eslint-plugin-import": "^2.18.2", "eslint-plugin-prettier": "^3.1.1", "eslint-plugin-react": "^7.16.0", + "eslint-plugin-react-hooks": "^4.6.0", "faker": "^4.1.0", "image-extensions": "^1.1.0", "is-hotkey": "^0.1.6", diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 7e288bb8e9..dabd025c2a 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -69,6 +69,7 @@ import { import { RestoreDOM } from './restore-dom/restore-dom' import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager' import { useTrackUserInput } from '../hooks/use-track-user-input' +import { AndroidInputManager } from '../hooks/android-input-manager/android-input-manager' type DeferredOperation = () => void @@ -181,70 +182,82 @@ export const Editable = (props: EditableProps) => { } }, [autoFocus]) + /** + * The AndroidInputManager object has a cyclical dependency on onDOMSelectionChange + * + * It is defined as a reference to simplify hook dependencies and clarify that + * it needs to be initialized. + */ + const androidInputManagerRef = useRef< + AndroidInputManager | null | undefined + >() + // 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 ( - (IS_ANDROID || !ReactEditor.isComposing(editor)) && - (!state.isUpdatingSelection || androidInputManager?.isFlushing()) && - !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) - } + const onDOMSelectionChange = useMemo( + () => + throttle(() => { + const androidInputManager = androidInputManagerRef.current + if ( + (IS_ANDROID || !ReactEditor.isComposing(editor)) && + (!state.isUpdatingSelection || androidInputManager?.isFlushing()) && + !state.isDraggingInternally + ) { + const root = ReactEditor.findDocumentOrShadowRoot(editor) + const { activeElement } = root + const el = ReactEditor.toDOMNode(editor, editor) + const domSelection = root.getSelection() - if (!domSelection) { - return Transforms.deselect(editor) - } + if (activeElement === el) { + state.latestElement = activeElement + IS_FOCUSED.set(editor, true) + } else { + IS_FOCUSED.delete(editor) + } - const { anchorNode, focusNode } = domSelection + if (!domSelection) { + return Transforms.deselect(editor) + } - const anchorNodeSelectable = - ReactEditor.hasEditableTarget(editor, anchorNode) || - ReactEditor.isTargetInsideNonReadonlyVoid(editor, anchorNode) + const { anchorNode, focusNode } = domSelection - const focusNodeSelectable = - ReactEditor.hasEditableTarget(editor, focusNode) || - ReactEditor.isTargetInsideNonReadonlyVoid(editor, focusNode) + const anchorNodeSelectable = + ReactEditor.hasEditableTarget(editor, anchorNode) || + ReactEditor.isTargetInsideNonReadonlyVoid(editor, anchorNode) - if (anchorNodeSelectable && focusNodeSelectable) { - const range = ReactEditor.toSlateRange(editor, domSelection, { - exactMatch: false, - suppressThrow: true, - }) + const focusNodeSelectable = + ReactEditor.hasEditableTarget(editor, focusNode) || + ReactEditor.isTargetInsideNonReadonlyVoid(editor, focusNode) - if (range) { - if ( - !ReactEditor.isComposing(editor) && - !androidInputManager?.hasPendingChanges() && - !androidInputManager?.isFlushing() - ) { - Transforms.select(editor, range) - } else { - androidInputManager?.handleUserSelect(range) + if (anchorNodeSelectable && focusNodeSelectable) { + const range = ReactEditor.toSlateRange(editor, domSelection, { + exactMatch: false, + suppressThrow: true, + }) + + if (range) { + if ( + !ReactEditor.isComposing(editor) && + !androidInputManager?.hasPendingChanges() && + !androidInputManager?.isFlushing() + ) { + Transforms.select(editor, range) + } else { + androidInputManager?.handleUserSelect(range) + } } } - } - // Deselect the editor if the dom selection is not selectable in readonly mode - if (readOnly && (!anchorNodeSelectable || !focusNodeSelectable)) { - Transforms.deselect(editor) + // Deselect the editor if the dom selection is not selectable in readonly mode + if (readOnly && (!anchorNodeSelectable || !focusNodeSelectable)) { + Transforms.deselect(editor) + } } - } - }, 100), - [readOnly] + }, 100), + [editor, readOnly, state] ) const scheduleOnDOMSelectionChange = useMemo( @@ -252,7 +265,7 @@ export const Editable = (props: EditableProps) => { [onDOMSelectionChange] ) - const androidInputManager = useAndroidInputManager({ + androidInputManagerRef.current = useAndroidInputManager({ node: ref, onDOMSelectionChange, scheduleOnDOMSelectionChange, @@ -278,7 +291,7 @@ export const Editable = (props: EditableProps) => { if ( !domSelection || !ReactEditor.isFocused(editor) || - androidInputManager?.hasPendingAction() + androidInputManagerRef.current?.hasPendingAction() ) { return } @@ -376,7 +389,8 @@ export const Editable = (props: EditableProps) => { } const newDomRange = setDomSelection() - const ensureSelection = androidInputManager?.isFlushing() === 'action' + const ensureSelection = + androidInputManagerRef.current?.isFlushing() === 'action' if (!IS_ANDROID || !ensureSelection) { setTimeout(() => { @@ -444,8 +458,8 @@ export const Editable = (props: EditableProps) => { !isDOMEventHandled(event, propsOnDOMBeforeInput) ) { // COMPAT: BeforeInput events aren't cancelable on android, so we have to handle them differently using the android input manager. - if (androidInputManager) { - return androidInputManager.handleDOMBeforeInput(event) + if (androidInputManagerRef.current) { + return androidInputManagerRef.current.handleDOMBeforeInput(event) } // Some IMEs/Chrome extensions like e.g. Grammarly set the selection immediately before @@ -699,7 +713,14 @@ export const Editable = (props: EditableProps) => { } } }, - [readOnly, propsOnDOMBeforeInput] + [ + editor, + onDOMSelectionChange, + onUserInput, + propsOnDOMBeforeInput, + readOnly, + scheduleOnDOMSelectionChange, + ] ) const callbackRef = useCallback( @@ -728,7 +749,12 @@ export const Editable = (props: EditableProps) => { ref.current = node }, - [ref, onDOMBeforeInput, onDOMSelectionChange, scheduleOnDOMSelectionChange] + [ + onDOMSelectionChange, + scheduleOnDOMSelectionChange, + editor, + onDOMBeforeInput, + ] ) // Attach a native DOM event handler for `selectionchange`, because React's @@ -899,27 +925,30 @@ export const Editable = (props: EditableProps) => { } } }, - [readOnly] + [attributes.onBeforeInput, editor, readOnly] )} - onInput={useCallback((event: React.FormEvent) => { - if (isEventHandled(event, attributes.onInput)) { - return - } + onInput={useCallback( + (event: React.FormEvent) => { + if (isEventHandled(event, attributes.onInput)) { + return + } - if (androidInputManager) { - androidInputManager.handleInput() - return - } + if (androidInputManagerRef.current) { + androidInputManagerRef.current.handleInput() + return + } - // Flush native operations, as native events will have propogated - // and we can correctly compare DOM text values in components - // to stop rendering, so that browser functions like autocorrect - // and spellcheck work as expected. - for (const op of deferredOperations.current) { - op() - } - deferredOperations.current = [] - }, [])} + // Flush native operations, as native events will have propogated + // and we can correctly compare DOM text values in components + // to stop rendering, so that browser functions like autocorrect + // and spellcheck work as expected. + for (const op of deferredOperations.current) { + op() + } + deferredOperations.current = [] + }, + [attributes.onInput] + )} onBlur={useCallback( (event: React.FocusEvent) => { if ( @@ -984,7 +1013,13 @@ export const Editable = (props: EditableProps) => { IS_FOCUSED.delete(editor) }, - [readOnly, attributes.onBlur] + [ + readOnly, + state.isUpdatingSelection, + state.latestElement, + editor, + attributes.onBlur, + ] )} onClick={useCallback( (event: React.MouseEvent) => { @@ -1045,7 +1080,7 @@ export const Editable = (props: EditableProps) => { } } }, - [readOnly, attributes.onClick] + [editor, attributes.onClick, readOnly] )} onCompositionEnd={useCallback( (event: React.CompositionEvent) => { @@ -1055,7 +1090,7 @@ export const Editable = (props: EditableProps) => { IS_COMPOSING.set(editor, false) } - androidInputManager?.handleCompositionEnd(event) + androidInputManagerRef.current?.handleCompositionEnd(event) if ( isEventHandled(event, attributes.onCompositionEnd) || @@ -1097,7 +1132,7 @@ export const Editable = (props: EditableProps) => { } } }, - [attributes.onCompositionEnd] + [attributes.onCompositionEnd, editor] )} onCompositionUpdate={useCallback( (event: React.CompositionEvent) => { @@ -1111,12 +1146,12 @@ export const Editable = (props: EditableProps) => { } } }, - [attributes.onCompositionUpdate] + [attributes.onCompositionUpdate, editor] )} onCompositionStart={useCallback( (event: React.CompositionEvent) => { if (ReactEditor.hasSelectableTarget(editor, event.target)) { - androidInputManager?.handleCompositionStart(event) + androidInputManagerRef.current?.handleCompositionStart(event) if ( isEventHandled(event, attributes.onCompositionStart) || @@ -1151,7 +1186,7 @@ export const Editable = (props: EditableProps) => { } } }, - [attributes.onCompositionStart] + [attributes.onCompositionStart, editor] )} onCopy={useCallback( (event: React.ClipboardEvent) => { @@ -1168,7 +1203,7 @@ export const Editable = (props: EditableProps) => { ) } }, - [attributes.onCopy] + [attributes.onCopy, editor] )} onCut={useCallback( (event: React.ClipboardEvent) => { @@ -1198,7 +1233,7 @@ export const Editable = (props: EditableProps) => { } } }, - [readOnly, attributes.onCut] + [readOnly, editor, attributes.onCut] )} onDragOver={useCallback( (event: React.DragEvent) => { @@ -1216,7 +1251,7 @@ export const Editable = (props: EditableProps) => { } } }, - [attributes.onDragOver] + [attributes.onDragOver, editor] )} onDragStart={useCallback( (event: React.DragEvent) => { @@ -1247,7 +1282,7 @@ export const Editable = (props: EditableProps) => { ) } }, - [readOnly, attributes.onDragStart] + [readOnly, editor, attributes.onDragStart, state] )} onDrop={useCallback( (event: React.DragEvent) => { @@ -1290,7 +1325,7 @@ export const Editable = (props: EditableProps) => { state.isDraggingInternally = false }, - [readOnly, attributes.onDrop] + [readOnly, editor, attributes.onDrop, state] )} onDragEnd={useCallback( (event: React.DragEvent) => { @@ -1308,7 +1343,7 @@ export const Editable = (props: EditableProps) => { // Note: `onDragEnd` is only called when `onDrop` is not called state.isDraggingInternally = false }, - [readOnly, attributes.onDragEnd] + [readOnly, state, attributes, editor] )} onFocus={useCallback( (event: React.FocusEvent) => { @@ -1333,7 +1368,7 @@ export const Editable = (props: EditableProps) => { IS_FOCUSED.set(editor, true) } }, - [readOnly, attributes.onFocus] + [readOnly, state, editor, attributes.onFocus] )} onKeyDown={useCallback( (event: React.KeyboardEvent) => { @@ -1341,7 +1376,7 @@ export const Editable = (props: EditableProps) => { !readOnly && ReactEditor.hasEditableTarget(editor, event.target) ) { - androidInputManager?.handleKeyDown(event) + androidInputManagerRef.current?.handleKeyDown(event) const { nativeEvent } = event @@ -1608,7 +1643,7 @@ export const Editable = (props: EditableProps) => { } } }, - [readOnly, attributes.onKeyDown] + [readOnly, editor, attributes.onKeyDown] )} onPaste={useCallback( (event: React.ClipboardEvent) => { @@ -1634,7 +1669,7 @@ export const Editable = (props: EditableProps) => { } } }, - [readOnly, attributes.onPaste] + [readOnly, editor, attributes.onPaste] )} > { EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) } - }, [placeholderRef, leaf]) + }, [placeholderRef, leaf, editor]) let children = ( diff --git a/packages/slate-react/src/components/slate.tsx b/packages/slate-react/src/components/slate.tsx index 4a1b8829df..3c1b2a56c7 100644 --- a/packages/slate-react/src/components/slate.tsx +++ b/packages/slate-react/src/components/slate.tsx @@ -5,7 +5,7 @@ import { FocusedContext } from '../hooks/use-focused' import { EditorContext } from '../hooks/use-slate-static' import { SlateContext, SlateContextValue } from '../hooks/use-slate' import { - getSelectorContext, + useSelectorContext, SlateSelectorContext, } from '../hooks/use-slate-selector' import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps' @@ -47,7 +47,7 @@ export const Slate = (props: { const { selectorContext, onChange: handleSelectorChange, - } = getSelectorContext(editor) + } = useSelectorContext(editor) const onContextChange = useCallback(() => { if (onChange) { @@ -59,7 +59,7 @@ export const Slate = (props: { editor, })) handleSelectorChange(editor) - }, [onChange]) + }, [editor, handleSelectorChange, onChange]) useEffect(() => { EDITOR_TO_ON_CHANGE.set(editor, onContextChange) @@ -68,13 +68,13 @@ export const Slate = (props: { EDITOR_TO_ON_CHANGE.set(editor, () => {}) unmountRef.current = true } - }, [onContextChange]) + }, [editor, onContextChange]) const [isFocused, setIsFocused] = useState(ReactEditor.isFocused(editor)) useEffect(() => { setIsFocused(ReactEditor.isFocused(editor)) - }) + }, [editor]) useIsomorphicLayoutEffect(() => { const fn = () => setIsFocused(ReactEditor.isFocused(editor)) diff --git a/packages/slate-react/src/hooks/android-input-manager/use-android-input-manager.ts b/packages/slate-react/src/hooks/android-input-manager/use-android-input-manager.ts index 7091c3b75f..7b9195eab0 100644 --- a/packages/slate-react/src/hooks/android-input-manager/use-android-input-manager.ts +++ b/packages/slate-react/src/hooks/android-input-manager/use-android-input-manager.ts @@ -22,34 +22,33 @@ const MUTATION_OBSERVER_CONFIG: MutationObserverInit = { characterData: true, } -export function useAndroidInputManager({ - node, - ...options -}: UseAndroidInputManagerOptions) { - if (!IS_ANDROID) { - return null - } - - const editor = useSlateStatic() - const isMounted = useIsMounted() - - const [inputManager] = useState(() => - createAndroidInputManager({ - editor, - ...options, - }) - ) - - useMutationObserver( - node, - inputManager.handleDomMutations, - MUTATION_OBSERVER_CONFIG - ) - - EDITOR_TO_SCHEDULE_FLUSH.set(editor, inputManager.scheduleFlush) - if (isMounted) { - inputManager.flush() - } - - return inputManager -} +export const useAndroidInputManager = !IS_ANDROID + ? () => null + : ({ node, ...options }: UseAndroidInputManagerOptions) => { + if (!IS_ANDROID) { + return null + } + + const editor = useSlateStatic() + const isMounted = useIsMounted() + + const [inputManager] = useState(() => + createAndroidInputManager({ + editor, + ...options, + }) + ) + + useMutationObserver( + node, + inputManager.handleDomMutations, + MUTATION_OBSERVER_CONFIG + ) + + EDITOR_TO_SCHEDULE_FLUSH.set(editor, inputManager.scheduleFlush) + if (isMounted) { + inputManager.flush() + } + + return inputManager + } diff --git a/packages/slate-react/src/hooks/use-mutation-observer.ts b/packages/slate-react/src/hooks/use-mutation-observer.ts index c7341cf586..6fa393090b 100644 --- a/packages/slate-react/src/hooks/use-mutation-observer.ts +++ b/packages/slate-react/src/hooks/use-mutation-observer.ts @@ -23,5 +23,5 @@ export function useMutationObserver( mutationObserver.observe(node.current, options) return () => mutationObserver.disconnect() - }, []) + }, [mutationObserver, node, options]) } diff --git a/packages/slate-react/src/hooks/use-slate-selector.tsx b/packages/slate-react/src/hooks/use-slate-selector.tsx index 720f58f3a4..5da5cd4da7 100644 --- a/packages/slate-react/src/hooks/use-slate-selector.tsx +++ b/packages/slate-react/src/hooks/use-slate-selector.tsx @@ -112,17 +112,22 @@ export function useSlateSelector( /** * Create selector context with editor updating on every editor change */ -export function getSelectorContext(editor: Editor) { +export function useSelectorContext(editor: Editor) { const eventListeners = useRef([]).current const slateRef = useRef<{ editor: Editor }>({ editor, }).current - const onChange = useCallback((editor: Editor) => { - slateRef.editor = editor - eventListeners.forEach((listener: EditorChangeHandler) => listener(editor)) - }, []) + const onChange = useCallback( + (editor: Editor) => { + slateRef.editor = editor + eventListeners.forEach((listener: EditorChangeHandler) => + listener(editor) + ) + }, + [eventListeners, slateRef] + ) const selectorContext = useMemo(() => { return { diff --git a/packages/slate-react/src/hooks/use-track-user-input.ts b/packages/slate-react/src/hooks/use-track-user-input.ts index 5725eab2b4..ae9b0cf254 100644 --- a/packages/slate-react/src/hooks/use-track-user-input.ts +++ b/packages/slate-react/src/hooks/use-track-user-input.ts @@ -21,7 +21,7 @@ export function useTrackUserInput() { animationFrameIdRef.current = window.requestAnimationFrame(() => { receivedUserInput.current = false }) - }, []) + }, [editor]) useEffect(() => () => cancelAnimationFrame(animationFrameIdRef.current), []) diff --git a/packages/slate-react/test/index.spec.tsx b/packages/slate-react/test/index.spec.tsx index c482d1c2ed..91139db086 100644 --- a/packages/slate-react/test/index.spec.tsx +++ b/packages/slate-react/test/index.spec.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import { createEditor, Element, Transforms } from 'slate' import { create, act, ReactTestRenderer } from 'react-test-renderer' import { Slate, withReact, Editable } from '../src' @@ -22,7 +22,7 @@ describe('slate-react', () => { test('should not unmount the node that gets split on a split_node operation', async () => { const editor = withReact(createEditor()) const value = [{ type: 'block', children: [{ text: 'test' }] }] - const mounts = jest.fn() + const mounts = jest.fn() let el: ReactTestRenderer @@ -30,8 +30,8 @@ describe('slate-react', () => { el = create( {}}> { - React.useEffect(() => mounts(element), []) + renderElement={({ children }) => { + useEffect(() => mounts(), []) return children }} @@ -56,7 +56,7 @@ describe('slate-react', () => { { type: 'block', children: [{ text: 'te' }] }, { type: 'block', children: [{ text: 'st' }] }, ] - const mounts = jest.fn() + const mounts = jest.fn() let el: ReactTestRenderer @@ -64,8 +64,8 @@ describe('slate-react', () => { el = create( {}}> { - React.useEffect(() => mounts(element), []) + renderElement={({ children }) => { + useEffect(() => mounts(), []) return children }} diff --git a/site/examples/code-highlighting.tsx b/site/examples/code-highlighting.tsx index ee18236c75..e92357f696 100644 --- a/site/examples/code-highlighting.tsx +++ b/site/examples/code-highlighting.tsx @@ -8,7 +8,7 @@ import 'prismjs/components/prism-python' import 'prismjs/components/prism-php' import 'prismjs/components/prism-sql' import 'prismjs/components/prism-java' -import React, { useCallback, useMemo, useState } from 'react' +import React, { useCallback, useState } from 'react' import { createEditor, Node, @@ -51,7 +51,7 @@ const CodeHighlightingExample = () => { @@ -60,7 +60,7 @@ const CodeHighlightingExample = () => { ) } -const renderElement = (props: RenderElementProps) => { +const ElementWrapper = (props: RenderElementProps) => { const { attributes, children, element } = props const editor = useSlateStatic() @@ -161,14 +161,17 @@ const renderLeaf = (props: RenderLeafProps) => { } const useDecorate = (editor: Editor) => { - return useCallback(([node, path]) => { - if (Element.isElement(node) && node.type === CodeLineType) { - const ranges = editor.nodeToDecorations.get(node) || [] - return ranges - } + return useCallback( + ([node, path]) => { + if (Element.isElement(node) && node.type === CodeLineType) { + const ranges = editor.nodeToDecorations.get(node) || [] + return ranges + } - return [] - }, []) + return [] + }, + [editor.nodeToDecorations] + ) } const getChildNodeToDecorations = ([block, blockPath]: NodeEntry< @@ -220,34 +223,35 @@ const getChildNodeToDecorations = ([block, blockPath]: NodeEntry< const SetNodeToDecorations = () => { const editor = useSlate() - useMemo(() => { - const blockEntries = Array.from( - Editor.nodes(editor, { - at: [], - mode: 'highest', - match: n => Element.isElement(n) && n.type === CodeBlockType, - }) - ) + const blockEntries = Array.from( + Editor.nodes(editor, { + at: [], + mode: 'highest', + match: n => Element.isElement(n) && n.type === CodeBlockType, + }) + ) - const nodeToDecorations = mergeMaps( - ...blockEntries.map(getChildNodeToDecorations) - ) + const nodeToDecorations = mergeMaps( + ...blockEntries.map(getChildNodeToDecorations) + ) - editor.nodeToDecorations = nodeToDecorations - }, [editor.children]) + editor.nodeToDecorations = nodeToDecorations return null } const useOnKeydown = (editor: Editor) => { - const onKeyDown: React.KeyboardEventHandler = useCallback(e => { - if (isHotkey('tab', e)) { - // handle tab key, insert spaces - e.preventDefault() + const onKeyDown: React.KeyboardEventHandler = useCallback( + e => { + if (isHotkey('tab', e)) { + // handle tab key, insert spaces + e.preventDefault() - Editor.insertText(editor, ' ') - } - }, []) + Editor.insertText(editor, ' ') + } + }, + [editor] + ) return onKeyDown } diff --git a/site/examples/markdown-shortcuts.tsx b/site/examples/markdown-shortcuts.tsx index 86217d63e8..1d8a425704 100644 --- a/site/examples/markdown-shortcuts.tsx +++ b/site/examples/markdown-shortcuts.tsx @@ -33,38 +33,41 @@ const MarkdownShortcutsExample = () => { [] ) - const handleDOMBeforeInput = useCallback((e: InputEvent) => { - queueMicrotask(() => { - const pendingDiffs = ReactEditor.androidPendingDiffs(editor) + const handleDOMBeforeInput = useCallback( + (e: InputEvent) => { + queueMicrotask(() => { + const pendingDiffs = ReactEditor.androidPendingDiffs(editor) + + const scheduleFlush = pendingDiffs?.some(({ diff, path }) => { + if (!diff.text.endsWith(' ')) { + return false + } - const scheduleFlush = pendingDiffs?.some(({ diff, path }) => { - if (!diff.text.endsWith(' ')) { - return false - } + const { text } = SlateNode.leaf(editor, path) + const beforeText = text.slice(0, diff.start) + diff.text.slice(0, -1) + if (!(beforeText in SHORTCUTS)) { + return + } - const { text } = SlateNode.leaf(editor, path) - const beforeText = text.slice(0, diff.start) + diff.text.slice(0, -1) - if (!(beforeText in SHORTCUTS)) { - return - } + const blockEntry = Editor.above(editor, { + at: path, + match: n => SlateElement.isElement(n) && Editor.isBlock(editor, n), + }) + if (!blockEntry) { + return false + } - const blockEntry = Editor.above(editor, { - at: path, - match: n => SlateElement.isElement(n) && Editor.isBlock(editor, n), + const [, blockPath] = blockEntry + return Editor.isStart(editor, Editor.start(editor, path), blockPath) }) - if (!blockEntry) { - return false - } - const [, blockPath] = blockEntry - return Editor.isStart(editor, Editor.start(editor, path), blockPath) + if (scheduleFlush) { + ReactEditor.androidScheduleFlush(editor) + } }) - - if (scheduleFlush) { - ReactEditor.androidScheduleFlush(editor) - } - }) - }, []) + }, + [editor] + ) return ( diff --git a/site/examples/mentions.tsx b/site/examples/mentions.tsx index c7e1d1ca82..3967e9426f 100644 --- a/site/examples/mentions.tsx +++ b/site/examples/mentions.tsx @@ -57,7 +57,7 @@ const MentionExample = () => { } } }, - [index, search, target] + [chars, editor, index, target] ) useEffect(() => { diff --git a/yarn.lock b/yarn.lock index bc39279d54..e6ceaa77dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6769,6 +6769,15 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-react-hooks@npm:^4.6.0": + version: 4.6.0 + resolution: "eslint-plugin-react-hooks@npm:4.6.0" + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + checksum: 23001801f14c1d16bf0a837ca7970d9dd94e7b560384b41db378b49b6e32dc43d6e2790de1bd737a652a86f81a08d6a91f402525061b47719328f586a57e86c3 + languageName: node + linkType: hard + "eslint-plugin-react@npm:^7.16.0": version: 7.24.0 resolution: "eslint-plugin-react@npm:7.24.0" @@ -13872,6 +13881,7 @@ resolve@^2.0.0-next.3: eslint-plugin-import: ^2.18.2 eslint-plugin-prettier: ^3.1.1 eslint-plugin-react: ^7.16.0 + eslint-plugin-react-hooks: ^4.6.0 faker: ^4.1.0 image-extensions: ^1.1.0 is-hotkey: ^0.1.6