Fix Slate crash after inserting emoji from picker/autocomplete
This commit is contained in:
@@ -82,13 +82,21 @@ function RenderEmoticonElement({
|
|||||||
const selected = useSelected();
|
const selected = useSelected();
|
||||||
const focused = useFocused();
|
const focused = useFocused();
|
||||||
|
|
||||||
|
// Void inline: attributes + contentEditable={false} on the same root, children
|
||||||
|
// as a sibling of the visual (not nested inside another non-editable span).
|
||||||
|
// Nesting {children} under an inner contentEditable={false} breaks Slate's
|
||||||
|
// DOM↔node map and crashes ReactEditor.focus after insert.
|
||||||
return (
|
return (
|
||||||
<span className={css.EmoticonBase} {...attributes}>
|
<span
|
||||||
|
{...attributes}
|
||||||
|
contentEditable={false}
|
||||||
|
className={css.EmoticonBase}
|
||||||
|
style={{ userSelect: 'none' }}
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
className={css.Emoticon({
|
className={css.Emoticon({
|
||||||
focus: selected && focused,
|
focus: selected && focused,
|
||||||
})}
|
})}
|
||||||
contentEditable={false}
|
|
||||||
>
|
>
|
||||||
{element.key.startsWith('mxc://') ? (
|
{element.key.startsWith('mxc://') ? (
|
||||||
<img
|
<img
|
||||||
@@ -99,8 +107,8 @@ function RenderEmoticonElement({
|
|||||||
) : (
|
) : (
|
||||||
element.key
|
element.key
|
||||||
)}
|
)}
|
||||||
{children}
|
|
||||||
</span>
|
</span>
|
||||||
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BasePoint, BaseRange, Editor, Element, Point, Range, Text, Transforms } from 'slate';
|
import { BasePoint, BaseRange, Editor, Element, Point, Range, Text, Transforms } from 'slate';
|
||||||
|
import { ReactEditor } from 'slate-react';
|
||||||
import { BlockType, MarkType } from './types';
|
import { BlockType, MarkType } from './types';
|
||||||
import {
|
import {
|
||||||
CommandElement,
|
CommandElement,
|
||||||
@@ -206,6 +207,29 @@ export const moveCursor = (editor: Editor, withSpace?: boolean) => {
|
|||||||
if (withSpace) editor.insertText(' ');
|
if (withSpace) editor.insertText(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focus the editor after React has committed DOM for recent transforms.
|
||||||
|
* Calling ReactEditor.focus immediately after insertNode/insertText (e.g. emoji +
|
||||||
|
* trailing space) races Slate's DOM map and throws:
|
||||||
|
* "Cannot resolve a DOM node from Slate node: {"text":" "}"
|
||||||
|
*/
|
||||||
|
export const safeFocusEditor = (editor: Editor) => {
|
||||||
|
const tryFocus = () => {
|
||||||
|
try {
|
||||||
|
ReactEditor.focus(editor);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Defer past the current React commit; retry once if the DOM map isn't ready yet.
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (tryFocus()) return;
|
||||||
|
setTimeout(tryFocus, 0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
interface PointUntilCharOptions {
|
interface PointUntilCharOptions {
|
||||||
match: (char: string) => boolean;
|
match: (char: string) => boolean;
|
||||||
reverse?: boolean;
|
reverse?: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { KeyboardEventHandler, useCallback, useRef, useState } from 'react';
|
import React, { KeyboardEventHandler, useCallback, useRef, useState } from 'react';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import { Editor } from 'slate';
|
import { Editor } from 'slate';
|
||||||
import { ReactEditor } from 'slate-react';
|
|
||||||
import { IconButton, Line, PopOut } from 'folds';
|
import { IconButton, Line, PopOut } from 'folds';
|
||||||
import { Icon, Icons } from '../../components/icons';
|
import { Icon, Icons } from '../../components/icons';
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +12,7 @@ import {
|
|||||||
getAutocompleteQuery,
|
getAutocompleteQuery,
|
||||||
createEmoticonElement,
|
createEmoticonElement,
|
||||||
moveCursor,
|
moveCursor,
|
||||||
|
safeFocusEditor,
|
||||||
} from '../../components/editor';
|
} from '../../components/editor';
|
||||||
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
@@ -84,7 +84,7 @@ export function ForumChatComposer({
|
|||||||
|
|
||||||
const handleCloseAutocomplete = useCallback(() => {
|
const handleCloseAutocomplete = useCallback(() => {
|
||||||
setAutocompleteQuery(undefined);
|
setAutocompleteQuery(undefined);
|
||||||
ReactEditor.focus(editor);
|
safeFocusEditor(editor);
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -139,7 +139,7 @@ export function ForumChatComposer({
|
|||||||
requestClose={() => {
|
requestClose={() => {
|
||||||
setEmojiBoardTab((t) => {
|
setEmojiBoardTab((t) => {
|
||||||
if (t) {
|
if (t) {
|
||||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
if (!mobileOrTablet()) safeFocusEditor(editor);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import React, {
|
|||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import { EventType, IContent, MsgType, RelationType, Room } from 'matrix-js-sdk';
|
import { EventType, IContent, MsgType, RelationType, Room } from 'matrix-js-sdk';
|
||||||
import { ReactEditor } from 'slate-react';
|
|
||||||
import { Transforms, Editor } from 'slate';
|
import { Transforms, Editor } from 'slate';
|
||||||
import { Box, Dialog, IconButton, Line, Overlay, OverlayBackdrop, OverlayCenter, PopOut, Scroll, Text, color, config, toRem } from 'folds';
|
import { Box, Dialog, IconButton, Line, Overlay, OverlayBackdrop, OverlayCenter, PopOut, Scroll, Text, color, config, toRem } from 'folds';
|
||||||
|
|
||||||
@@ -32,6 +31,7 @@ import {
|
|||||||
EmoticonAutocomplete,
|
EmoticonAutocomplete,
|
||||||
createEmoticonElement,
|
createEmoticonElement,
|
||||||
moveCursor,
|
moveCursor,
|
||||||
|
safeFocusEditor,
|
||||||
resetEditorHistory,
|
resetEditorHistory,
|
||||||
customHtmlEqualsPlainText,
|
customHtmlEqualsPlainText,
|
||||||
trimCustomHtml,
|
trimCustomHtml,
|
||||||
@@ -559,7 +559,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
|
|
||||||
const handleCloseAutocomplete = useCallback(() => {
|
const handleCloseAutocomplete = useCallback(() => {
|
||||||
setAutocompleteQuery(undefined);
|
setAutocompleteQuery(undefined);
|
||||||
ReactEditor.focus(editor);
|
safeFocusEditor(editor);
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
const handleEmoticonSelect = (key: string, shortcode: string) => {
|
const handleEmoticonSelect = (key: string, shortcode: string) => {
|
||||||
@@ -803,7 +803,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
requestClose={() => {
|
requestClose={() => {
|
||||||
setEmojiBoardTab((t) => {
|
setEmojiBoardTab((t) => {
|
||||||
if (t) {
|
if (t) {
|
||||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
if (!mobileOrTablet()) safeFocusEditor(editor);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import React, {
|
|||||||
import { Box, Chip, IconButton, Line, PopOut, RectCords, Spinner, Text, as, config } from 'folds';
|
import { Box, Chip, IconButton, Line, PopOut, RectCords, Spinner, Text, as, config } from 'folds';
|
||||||
import { Icon, Icons } from '../../../components/icons';
|
import { Icon, Icons } from '../../../components/icons';
|
||||||
import { Editor, Transforms } from 'slate';
|
import { Editor, Transforms } from 'slate';
|
||||||
import { ReactEditor } from 'slate-react';
|
|
||||||
import { IContent, IMentions, MatrixEvent, RelationType, Room } from 'matrix-js-sdk';
|
import { IContent, IMentions, MatrixEvent, RelationType, Room } from 'matrix-js-sdk';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
htmlToEditorInput,
|
htmlToEditorInput,
|
||||||
moveCursor,
|
moveCursor,
|
||||||
plainToEditorInput,
|
plainToEditorInput,
|
||||||
|
safeFocusEditor,
|
||||||
toMatrixCustomHTML,
|
toMatrixCustomHTML,
|
||||||
toPlainText,
|
toPlainText,
|
||||||
trimCustomHtml,
|
trimCustomHtml,
|
||||||
@@ -185,7 +185,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleCloseAutocomplete = useCallback(() => {
|
const handleCloseAutocomplete = useCallback(() => {
|
||||||
ReactEditor.focus(editor);
|
safeFocusEditor(editor);
|
||||||
setAutocompleteQuery(undefined);
|
setAutocompleteQuery(undefined);
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
editor.insertFragment(initialValue);
|
editor.insertFragment(initialValue);
|
||||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
if (!mobileOrTablet()) safeFocusEditor(editor);
|
||||||
}, [editor, getPrevBodyAndFormattedBody, isMarkdown]);
|
}, [editor, getPrevBodyAndFormattedBody, isMarkdown]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -300,7 +300,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
|||||||
requestClose={() => {
|
requestClose={() => {
|
||||||
setAnchor((v) => {
|
setAnchor((v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
if (!mobileOrTablet()) safeFocusEditor(editor);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
|
|||||||
Reference in New Issue
Block a user