chore: better focus timning

This commit is contained in:
Nathan
2025-08-27 20:20:19 +08:00
parent cefdd08200
commit bf10df8f61

View File

@@ -52,12 +52,14 @@ function TitleEditable({
onUpdateName, onUpdateName,
onEnter, onEnter,
onFocus, onFocus,
autoFocus = true,
}: { }: {
viewId: string; viewId: string;
name: string; name: string;
onUpdateName: (name: string) => void; onUpdateName: (name: string) => void;
onEnter?: (text: string) => void; onEnter?: (text: string) => void;
onFocus?: () => void; onFocus?: () => void;
autoFocus?: boolean;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -68,7 +70,7 @@ function TitleEditable({
pendingUpdate: null, pendingUpdate: null,
updateId: null, updateId: null,
}); });
const [isEditing, setIsEditing] = useState(false); const [isFocused, setIsFocused] = useState(false);
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const cursorPositionRef = useRef<number>(0); const cursorPositionRef = useRef<number>(0);
@@ -135,11 +137,11 @@ function TitleEditable({
newName: name, newName: name,
pendingUpdate: updateStateRef.current.pendingUpdate, pendingUpdate: updateStateRef.current.pendingUpdate,
lastConfirmedName: updateStateRef.current.lastConfirmedName, lastConfirmedName: updateStateRef.current.lastConfirmedName,
isEditing, isFocused,
}); });
// If editing, ignore all remote updates // If focused, ignore all remote updates
if (isEditing) { if (isFocused) {
return; return;
} }
@@ -197,7 +199,7 @@ function TitleEditable({
if (contentRef.current) { if (contentRef.current) {
contentRef.current.textContent = name; contentRef.current.textContent = name;
} }
}, [name, isEditing, smartSendUpdate]); }, [name, isFocused, smartSendUpdate]);
const focusedTextbox = useCallback(() => { const focusedTextbox = useCallback(() => {
const contentBox = contentRef.current; const contentBox = contentRef.current;
@@ -209,7 +211,7 @@ function TitleEditable({
textbox?.focus(); textbox?.focus();
}, [viewId]); }, [viewId]);
// Initialize settings // Initialize content and handle autoFocus
useEffect(() => { useEffect(() => {
const contentBox = contentRef.current; const contentBox = contentRef.current;
@@ -219,15 +221,22 @@ function TitleEditable({
contentBox.textContent = updateStateRef.current.localName; contentBox.textContent = updateStateRef.current.localName;
initialEditValueRef.current = updateStateRef.current.localName; initialEditValueRef.current = updateStateRef.current.localName;
contentBox.focus(); // Ensure focus if autoFocus is true
if (autoFocus) {
// Move cursor to end // Use requestAnimationFrame for next paint cycle to ensure DOM is fully ready
if (contentBox.textContent !== '') { requestAnimationFrame(() => {
setTimeout(() => { // Double-check the element still exists and is in the document
setCursorPosition(contentBox, contentBox.textContent?.length || 0); if (contentBox && document.contains(contentBox)) {
}, 0); contentBox.focus();
// Move cursor to end if there's content
if (contentBox.textContent) {
setCursorPosition(contentBox, contentBox.textContent.length);
}
}
});
} }
}, []); // Only execute once when component mounts // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only execute once when component mounts - autoFocus is intentionally not in deps
return ( return (
<div <div
@@ -243,14 +252,14 @@ function TitleEditable({
data-placeholder={t('menuAppHeader.defaultNewPageName')} data-placeholder={t('menuAppHeader.defaultNewPageName')}
contentEditable={true} contentEditable={true}
aria-readonly={false} aria-readonly={false}
autoFocus={true} autoFocus={autoFocus}
onFocus={() => { onFocus={() => {
// Record initial value when starting to edit // Record initial value when starting to edit
if (contentRef.current) { if (contentRef.current) {
initialEditValueRef.current = contentRef.current.textContent || ''; initialEditValueRef.current = contentRef.current.textContent || '';
} }
setIsEditing(true); setIsFocused(true);
onFocus?.(); onFocus?.();
}} }}
onBlur={() => { onBlur={() => {
@@ -272,10 +281,10 @@ function TitleEditable({
} }
} }
// Delay setting editing state to avoid issues with rapid focus switching // Use microtask to avoid race conditions
setTimeout(() => { void Promise.resolve().then(() => {
setIsEditing(false); setIsFocused(false);
}, 100); });
}} }}
onInput={() => { onInput={() => {
if (!contentRef.current) return; if (!contentRef.current) return;
@@ -314,7 +323,7 @@ function TitleEditable({
if (offset >= currentText.length || offset <= 0) { if (offset >= currentText.length || offset <= 0) {
// Cursor at end or position inaccurate, keep all text // Cursor at end or position inaccurate, keep all text
setIsEditing(false); setIsFocused(false);
updateStateRef.current = { updateStateRef.current = {
...updateStateRef.current, ...updateStateRef.current,
localName: currentText, localName: currentText,
@@ -327,7 +336,7 @@ function TitleEditable({
const afterText = currentText.slice(offset); const afterText = currentText.slice(offset);
contentRef.current.textContent = beforeText; contentRef.current.textContent = beforeText;
setIsEditing(false); setIsFocused(false);
updateStateRef.current = { updateStateRef.current = {
...updateStateRef.current, ...updateStateRef.current,
localName: beforeText, localName: beforeText,
@@ -343,7 +352,7 @@ function TitleEditable({
// Escape key: complete editing and save current content // Escape key: complete editing and save current content
const currentText = contentRef.current.textContent || ''; const currentText = contentRef.current.textContent || '';
setIsEditing(false); setIsFocused(false);
updateStateRef.current = { updateStateRef.current = {
...updateStateRef.current, ...updateStateRef.current,
localName: currentText, localName: currentText,