Focus terminal tab rename input from ref (#3314)
* Focus terminal tab rename input from ref * Preserve tab rename focus after menu selection Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
35bcf4af49
commit
34c90eaab4
|
|
@ -115,7 +115,7 @@ export default function SortableTab({
|
|||
// (e.g. showing the bell without the wash, or vice versa).
|
||||
const showActivityAffordance = hasUnreadActivity && !isEditing
|
||||
const [renameValue, setRenameValue] = useState('')
|
||||
const renameInputRef = useRef<HTMLInputElement>(null)
|
||||
const renameFocusFrameRef = useRef<number | null>(null)
|
||||
// Why: React's synthetic onBlur fires during the Input's unmount when isEditing flips
|
||||
// to false. Without this guard, pressing Escape (or committing via Enter) would cause
|
||||
// the blur handler to run commitRename a second time and overwrite the title with the
|
||||
|
|
@ -148,21 +148,22 @@ export default function SortableTab({
|
|||
setIsEditing(false)
|
||||
}, [])
|
||||
|
||||
// Why: rAF defers focus()+select() until after the Input mounts so the text
|
||||
// is pre-selected (overwriting the old title is the common case). Deps are
|
||||
// intentionally just [isEditing] — we do NOT re-run when tab.title or
|
||||
// tab.customTitle change mid-edit, so external title updates cannot
|
||||
// re-focus/re-select and disrupt the user's typing.
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
const setRenameInputElement = useCallback((input: HTMLInputElement | null) => {
|
||||
if (renameFocusFrameRef.current !== null) {
|
||||
cancelAnimationFrame(renameFocusFrameRef.current)
|
||||
renameFocusFrameRef.current = null
|
||||
}
|
||||
if (!input) {
|
||||
return
|
||||
}
|
||||
const frame = requestAnimationFrame(() => {
|
||||
renameInputRef.current?.focus()
|
||||
renameInputRef.current?.select()
|
||||
// Why: defer past Radix menu teardown/focus restore while still keying off
|
||||
// input mount only; terminal title updates must not re-select in-progress text.
|
||||
renameFocusFrameRef.current = requestAnimationFrame(() => {
|
||||
renameFocusFrameRef.current = null
|
||||
input.focus()
|
||||
input.select()
|
||||
})
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [isEditing])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const closeMenu = (): void => setMenuOpen(false)
|
||||
|
|
@ -297,7 +298,7 @@ export default function SortableTab({
|
|||
)}
|
||||
{isEditing ? (
|
||||
<Input
|
||||
ref={renameInputRef}
|
||||
ref={setRenameInputElement}
|
||||
data-tab-rename-input="true"
|
||||
value={renameValue}
|
||||
aria-label={`Rename tab ${tabTitle}`}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,35 @@ test.describe('Tab Rename (Inline)', () => {
|
|||
await expect(tabLocatorByTitle(orcaPage, 'My Custom Title')).toBeVisible()
|
||||
})
|
||||
|
||||
test('context-menu Change Title opens a focused select-all rename input', async ({
|
||||
orcaPage
|
||||
}) => {
|
||||
const worktreeId = (await getActiveWorktreeId(orcaPage))!
|
||||
const originalTitle = await getActiveTabTitle(orcaPage, worktreeId)
|
||||
expect(originalTitle.length).toBeGreaterThan(0)
|
||||
|
||||
await tabLocatorByTitle(orcaPage, originalTitle).click({ button: 'right' })
|
||||
await orcaPage.getByRole('menuitem', { name: 'Change Title', exact: true }).click()
|
||||
|
||||
const renameInput = orcaPage.getByRole('textbox', {
|
||||
name: `Rename tab ${originalTitle}`,
|
||||
exact: true
|
||||
})
|
||||
await expect(renameInput).toBeVisible()
|
||||
await expect(renameInput).toBeFocused()
|
||||
|
||||
// Why: plain keyboard typing exercises the real focused selection. If the
|
||||
// context menu steals focus back or the title is not selected, this will not
|
||||
// replace the original text with the intended custom title.
|
||||
await orcaPage.keyboard.type('Context Menu Title')
|
||||
await renameInput.press('Enter')
|
||||
|
||||
await expect
|
||||
.poll(async () => getActiveCustomTitle(orcaPage, worktreeId), { timeout: 3_000 })
|
||||
.toBe('Context Menu Title')
|
||||
await expect(tabLocatorByTitle(orcaPage, 'Context Menu Title')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Escape during inline rename discards the edit', async ({ orcaPage }) => {
|
||||
const worktreeId = (await getActiveWorktreeId(orcaPage))!
|
||||
const originalTitle = await getActiveTabTitle(orcaPage, worktreeId)
|
||||
|
|
|
|||
Loading…
Reference in New Issue