fix(composer): stop the intermittent autocomplete-insert crash
Picking an autocomplete item (mention/emoji/command — all inline voids)
occasionally tripped the composer error boundary, forcing a page refresh, even
though the element had already inserted. Root cause (traced through slate-react):
moveCursor deferred its cursor work to setTimeout(0), leaving the caret on the
just-inserted void's zero-width edge whose DOM (a U+FEFF node) isn't populated on
that tick. slate-react's commit-phase selection sync then calls
setBaseAndExtent(voidEdge, 1) and throws IndexSizeError mid-render → boundary.
Prevention: do the cursor work SYNCHRONOUSLY, in the same commit as the insert —
Transforms.move (escapes the void into the real trailing text node) then
insertText(' '). The caret is then always a resolvable text point when the
selection sync runs. (moveCursor's focus stays deferred+guarded, unchanged.)
Recovery (belt-and-suspenders): the composer error boundary is now recoverable —
a "Reload composer" button (resetErrorBoundary) + onReset Transforms.deselect
clears a transient bad selection so it remounts with the draft intact, no page
refresh. + role="alert" for screen readers.
Three review agents: two root-caused the exact slate-react throw and proved the
try/catch-only version merely recovered; a third reproduced the transforms
headlessly and caught that a first "sync insertText WITHOUT move" attempt hit
Slate's void guard (space dropped, caret trapped) — the move is required to
escape the void. Not unit-testable (needs the live DOM + the timing race).
Gate-green (tsc, eslint, prettier, 925 tests, build).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Box, Text, config } from 'folds';
|
||||
import { Box, Button, Text, config } from 'folds';
|
||||
import { EventType } from 'matrix-js-sdk';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { Transforms } from 'slate';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { useStateEvent } from '../../hooks/useStateEvent';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
@@ -152,17 +153,38 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||
<>
|
||||
{canMessage && (
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
onReset={() => {
|
||||
// The composer crash is a transient bad-selection render
|
||||
// (e.g. after an autocomplete insert); the draft content is
|
||||
// intact. Clear the selection so the remounted composer can
|
||||
// render — the user clicks in to continue, no page refresh.
|
||||
try {
|
||||
Transforms.deselect(editor);
|
||||
} catch {
|
||||
/* editor already in a safe state */
|
||||
}
|
||||
}}
|
||||
fallbackRender={({ resetErrorBoundary }) => (
|
||||
<RoomInputPlaceholder
|
||||
role="alert"
|
||||
style={{ padding: config.space.S200 }}
|
||||
direction="Column"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
gap="200"
|
||||
>
|
||||
<Text align="Center">
|
||||
Message composer encountered an error. Try refreshing.
|
||||
</Text>
|
||||
<Text align="Center">The message composer hit a snag.</Text>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
onClick={resetErrorBoundary}
|
||||
>
|
||||
<Text size="B300">Reload composer</Text>
|
||||
</Button>
|
||||
</RoomInputPlaceholder>
|
||||
}
|
||||
)}
|
||||
>
|
||||
<RoomInput
|
||||
room={room}
|
||||
|
||||
Reference in New Issue
Block a user