From dbfb947faf4f9d0249953cb80f533c0cdbf89968 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 17:57:15 -0700 Subject: [PATCH] Focus editor header rename input from ref (#3328) --- .../editor/editor-header-file-rename.ts | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/renderer/src/components/editor/editor-header-file-rename.ts b/src/renderer/src/components/editor/editor-header-file-rename.ts index 51af400c258..2d601b99d6d 100644 --- a/src/renderer/src/components/editor/editor-header-file-rename.ts +++ b/src/renderer/src/components/editor/editor-header-file-rename.ts @@ -1,4 +1,5 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useRef, useState } from 'react' +import type { RefCallback } from 'react' import type { OpenFile } from '@/store/slices/editor' import { useWorktreeById } from '@/store/selectors' import { basename } from '@/lib/path' @@ -9,7 +10,7 @@ type EditorHeaderFileRenameState = { canRename: boolean currentFileName: string isRenaming: boolean - renameInputRef: React.RefObject + renameInputRef: RefCallback openRenameInput: () => void commitRename: () => void cancelRename: () => void @@ -18,8 +19,9 @@ type EditorHeaderFileRenameState = { export function useEditorHeaderFileRename(activeFile: OpenFile): EditorHeaderFileRenameState { const worktree = useWorktreeById(activeFile.worktreeId) const [isRenaming, setIsRenaming] = useState(false) - const renameInputRef = useRef(null) + const renameInputElementRef = useRef(null) const renameCancelledRef = useRef(false) + const renameFocusFrameRef = useRef(null) const currentFileName = basename(activeFile.filePath) const canRename = activeFile.mode === 'edit' && !activeFile.diffSource && !activeFile.conflict && !isRenaming @@ -38,7 +40,7 @@ export function useEditorHeaderFileRename(activeFile: OpenFile): EditorHeaderFil setIsRenaming(false) return } - const input = renameInputRef.current + const input = renameInputElementRef.current if (!input) { setIsRenaming(false) return @@ -62,25 +64,40 @@ export function useEditorHeaderFileRename(activeFile: OpenFile): EditorHeaderFil setIsRenaming(false) } - useEffect(() => { - if (!isRenaming) { + const clearRenameFocusFrame = useCallback((): void => { + if (renameFocusFrameRef.current === null) { return } - const raf = requestAnimationFrame(() => { - const el = renameInputRef.current - if (!el) { + cancelAnimationFrame(renameFocusFrameRef.current) + renameFocusFrameRef.current = null + }, []) + + const renameInputRef = useCallback>( + (el) => { + renameInputElementRef.current = el + clearRenameFocusFrame() + if (!el || !isRenaming) { return } - el.focus() - const dotIndex = currentFileName.lastIndexOf('.') - if (dotIndex > 0) { - el.setSelectionRange(0, dotIndex) - } else { - el.select() - } - }) - return () => cancelAnimationFrame(raf) - }, [currentFileName, isRenaming]) + + // Why: focus belongs to the rename input mount; the frame preserves the + // previous timing so header layout settles before selecting text. + renameFocusFrameRef.current = requestAnimationFrame(() => { + renameFocusFrameRef.current = null + if (renameInputElementRef.current !== el) { + return + } + el.focus() + const dotIndex = currentFileName.lastIndexOf('.') + if (dotIndex > 0) { + el.setSelectionRange(0, dotIndex) + } else { + el.select() + } + }) + }, + [clearRenameFocusFrame, currentFileName, isRenaming] + ) return { canRename,