fix: improve editor auto-scroll and Safari IME handling (#5469)

- Use `textarea-caret` for precise cursor position calculation instead of line approximation
- Update `scrollToCursor` to scroll to the actual cursor position
- Fix Safari double-enter issue with IME in list completion
pull/5468/merge
Steven 2 days ago
parent 61dbca8dc2
commit 4e34ef22bf

@ -1,4 +1,5 @@
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from "react";
import getCaretCoordinates from "textarea-caret";
import { cn } from "@/lib/utils";
import { EDITOR_HEIGHT } from "../constants";
import type { EditorProps } from "../types";
@ -74,9 +75,12 @@ const Editor = forwardRef(function Editor(props: EditorProps, ref: React.Forward
getEditor: () => editorRef.current,
focus: () => editorRef.current?.focus(),
scrollToCursor: () => {
if (editorRef.current) {
editorRef.current.scrollTop = editorRef.current.scrollHeight;
}
const editor = editorRef.current;
if (!editor) return;
const caret = getCaretCoordinates(editor, editor.selectionEnd);
// Scroll to center cursor vertically
editor.scrollTop = Math.max(0, caret.top - editor.clientHeight / 2);
},
insertText: (content = "", prefix = "", suffix = "") => {
const editor = editorRef.current;
@ -148,6 +152,19 @@ const Editor = forwardRef(function Editor(props: EditorProps, ref: React.Forward
if (editorRef.current) {
handleContentChangeCallback(editorRef.current.value);
updateEditorHeight();
// Auto-scroll to keep cursor visible when typing
// See: https://github.com/usememos/memos/issues/5469
const editor = editorRef.current;
const caret = getCaretCoordinates(editor, editor.selectionEnd);
const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 24;
// Scroll if cursor is near or beyond bottom edge (within 2 lines)
const viewportBottom = editor.scrollTop + editor.clientHeight;
if (caret.top + lineHeight * 2 > viewportBottom) {
// Scroll to center cursor vertically
editor.scrollTop = Math.max(0, caret.top - editor.clientHeight / 2);
}
}
}, [handleContentChangeCallback, updateEditorHeight]);

@ -24,15 +24,30 @@ export function useListCompletion({ editorRef, editorActions, isInIME }: UseList
const editorActionsRef = useRef(editorActions);
editorActionsRef.current = editorActions;
// Track when composition ends to handle Safari race condition
// Safari fires keydown(Enter) immediately after compositionend, while Chrome doesn't
// See: https://github.com/usememos/memos/issues/5469
const lastCompositionEndRef = useRef(0);
useEffect(() => {
const editor = editorRef.current;
if (!editor) return;
const handleCompositionEnd = () => {
lastCompositionEndRef.current = Date.now();
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Enter" || isInIMERef.current || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) {
return;
}
// Safari fix: Ignore Enter key within 100ms of composition end
// This prevents double-enter behavior when confirming IME input in lists
if (Date.now() - lastCompositionEndRef.current < 100) {
return;
}
const actions = editorActionsRef.current;
const cursorPosition = actions.getCursorPosition();
const contentBeforeCursor = actions.getContent().substring(0, cursorPosition);
@ -51,10 +66,17 @@ export function useListCompletion({ editorRef, editorActions, isInIME }: UseList
} else {
const continuation = generateListContinuation(listInfo);
actions.insertText("\n" + continuation);
// Auto-scroll to keep cursor visible after inserting list item
setTimeout(() => actions.scrollToCursor(), 0);
}
};
editor.addEventListener("compositionend", handleCompositionEnd);
editor.addEventListener("keydown", handleKeyDown);
return () => editor.removeEventListener("keydown", handleKeyDown);
return () => {
editor.removeEventListener("compositionend", handleCompositionEnd);
editor.removeEventListener("keydown", handleKeyDown);
};
}, []);
}

Loading…
Cancel
Save