Focus editor header rename input from ref (#3328)

This commit is contained in:
Neil
2026-05-30 17:57:15 -07:00
committed by GitHub
parent 4a0f078817
commit dbfb947faf
@@ -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<HTMLInputElement | null>
renameInputRef: RefCallback<HTMLInputElement>
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<HTMLInputElement>(null)
const renameInputElementRef = useRef<HTMLInputElement | null>(null)
const renameCancelledRef = useRef(false)
const renameFocusFrameRef = useRef<number | null>(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<RefCallback<HTMLInputElement>>(
(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,