Compare commits

2 Commits

Author SHA1 Message Date
c7067021ba Fix white timestamps when Twemoji is selected
InterVariable did not match Fontsource's Inter Variable name, so digits
fell through to Twemoji COLR keycaps. Scope Twemoji off ASCII digits.
2026-07-13 02:40:03 +10:00
687f139719 Fix Slate crash after inserting emoji from picker/autocomplete 2026-07-13 02:40:02 +10:00
5 changed files with 53 additions and 21 deletions

View File

@@ -82,13 +82,21 @@ function RenderEmoticonElement({
const selected = useSelected();
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 (
<span className={css.EmoticonBase} {...attributes}>
<span
{...attributes}
contentEditable={false}
className={css.EmoticonBase}
style={{ userSelect: 'none' }}
>
<span
className={css.Emoticon({
focus: selected && focused,
})}
contentEditable={false}
>
{element.key.startsWith('mxc://') ? (
<img
@@ -99,8 +107,8 @@ function RenderEmoticonElement({
) : (
element.key
)}
{children}
</span>
{children}
</span>
);
}

View File

@@ -1,4 +1,5 @@
import { BasePoint, BaseRange, Editor, Element, Point, Range, Text, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
import { BlockType, MarkType } from './types';
import {
CommandElement,
@@ -206,6 +207,29 @@ export const moveCursor = (editor: Editor, withSpace?: boolean) => {
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 {
match: (char: string) => boolean;
reverse?: boolean;

View File

@@ -1,7 +1,6 @@
import React, { KeyboardEventHandler, useCallback, useRef, useState } from 'react';
import { isKeyHotkey } from 'is-hotkey';
import { Editor } from 'slate';
import { ReactEditor } from 'slate-react';
import { IconButton, Line, PopOut } from 'folds';
import { Icon, Icons } from '../../components/icons';
import {
@@ -13,6 +12,7 @@ import {
getAutocompleteQuery,
createEmoticonElement,
moveCursor,
safeFocusEditor,
} from '../../components/editor';
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
import { UseStateProvider } from '../../components/UseStateProvider';
@@ -84,7 +84,7 @@ export function ForumChatComposer({
const handleCloseAutocomplete = useCallback(() => {
setAutocompleteQuery(undefined);
ReactEditor.focus(editor);
safeFocusEditor(editor);
}, [editor]);
return (
@@ -136,15 +136,15 @@ export function ForumChatComposer({
returnFocusOnDeactivate={false}
onEmojiSelect={handleEmoticonSelect}
onCustomEmojiSelect={handleEmoticonSelect}
requestClose={() => {
setEmojiBoardTab((t) => {
if (t) {
if (!mobileOrTablet()) ReactEditor.focus(editor);
return undefined;
}
return t;
});
}}
requestClose={() => {
setEmojiBoardTab((t) => {
if (t) {
if (!mobileOrTablet()) safeFocusEditor(editor);
return undefined;
}
return t;
});
}}
/>
}
>

View File

@@ -10,7 +10,6 @@ import React, {
import { useAtom, useAtomValue } from 'jotai';
import { isKeyHotkey } from 'is-hotkey';
import { EventType, IContent, MsgType, RelationType, Room } from 'matrix-js-sdk';
import { ReactEditor } from 'slate-react';
import { Transforms, Editor } from 'slate';
import { Box, Dialog, IconButton, Line, Overlay, OverlayBackdrop, OverlayCenter, PopOut, Scroll, Text, color, config, toRem } from 'folds';
@@ -32,6 +31,7 @@ import {
EmoticonAutocomplete,
createEmoticonElement,
moveCursor,
safeFocusEditor,
resetEditorHistory,
customHtmlEqualsPlainText,
trimCustomHtml,
@@ -559,7 +559,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const handleCloseAutocomplete = useCallback(() => {
setAutocompleteQuery(undefined);
ReactEditor.focus(editor);
safeFocusEditor(editor);
}, [editor]);
const handleEmoticonSelect = (key: string, shortcode: string) => {
@@ -803,7 +803,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
requestClose={() => {
setEmojiBoardTab((t) => {
if (t) {
if (!mobileOrTablet()) ReactEditor.focus(editor);
if (!mobileOrTablet()) safeFocusEditor(editor);
return undefined;
}
return t;

View File

@@ -8,7 +8,6 @@ import React, {
import { Box, Chip, IconButton, Line, PopOut, RectCords, Spinner, Text, as, config } from 'folds';
import { Icon, Icons } from '../../../components/icons';
import { Editor, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
import { IContent, IMentions, MatrixEvent, RelationType, Room } from 'matrix-js-sdk';
import { isKeyHotkey } from 'is-hotkey';
import {
@@ -27,6 +26,7 @@ import {
htmlToEditorInput,
moveCursor,
plainToEditorInput,
safeFocusEditor,
toMatrixCustomHTML,
toPlainText,
trimCustomHtml,
@@ -185,7 +185,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
);
const handleCloseAutocomplete = useCallback(() => {
ReactEditor.focus(editor);
safeFocusEditor(editor);
setAutocompleteQuery(undefined);
}, [editor]);
@@ -208,7 +208,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
});
editor.insertFragment(initialValue);
if (!mobileOrTablet()) ReactEditor.focus(editor);
if (!mobileOrTablet()) safeFocusEditor(editor);
}, [editor, getPrevBodyAndFormattedBody, isMarkdown]);
useEffect(() => {
@@ -300,7 +300,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
requestClose={() => {
setAnchor((v) => {
if (v) {
if (!mobileOrTablet()) ReactEditor.focus(editor);
if (!mobileOrTablet()) safeFocusEditor(editor);
return undefined;
}
return v;