fix: correct Safari heading widget handling for Chinese IME (#12453)

Safari keeps the heading actions widget at the end of headings to avoid
selection issues, but the widget was still using side metadata suited to
the leading placement.

With Chinese IME composition at the end of a heading, same-position
insertion could interact with the contentEditable=false widget and leave
the editor selection stuck. After that, Backspace stopped working until
the page was refreshed.

Use positive side metadata for the Safari trailing widget so composed
text stays before it, allow relaxed selection around the widget, and add
a narrow Safari-only ArrowLeft fallback for the heading-end boundary.

Chinese IME has been manually verified. Other composition-based IMEs may
follow a similar path, but are not claimed as verified here.
This commit is contained in:
Wars
2026-05-26 04:51:35 +08:00
committed by GitHub
parent 6461aabc52
commit 38eda7fa61
+28 -2
View File
@@ -207,6 +207,30 @@ export default class Heading extends Node<HeadingOptions> {
...options,
Backspace: backspaceToParagraph(type),
Enter: splitHeading(type),
ArrowLeft: ((state, dispatch) => {
if (!isSafari) {
return false;
}
const { $from, empty } = state.selection;
if (!empty || $from.parent.type !== type) {
return false;
}
const end = $from.end();
if ($from.pos !== end || !$from.parent.lastChild?.isText) {
return false;
}
if (dispatch) {
dispatch(
state.tr
.setSelection(TextSelection.create(state.doc, end - 1))
.scrollIntoView()
);
}
return true;
}) as Command,
// Cmd+Left in Firefox lands the DOM caret inside the heading-actions
// widget (contentEditable=false, ignoreSelection: true), so Prosemirror
// does not update its model. Subsequent commands like Enter then operate
@@ -287,9 +311,11 @@ export default class Heading extends Node<HeadingOptions> {
isSafari ? pos + node.nodeSize - 1 : pos + 1,
container,
{
side: -1,
// Safari keeps this widget at the end; positive side preserves IME
// insertion order, while relaxed side preserves caret navigation.
side: isSafari ? 1 : -1,
ignoreSelection: true,
relaxedSide: false,
relaxedSide: isSafari,
key: pos.toString(),
}
)