fix(sidebar): fix rename input losing its selection on open (#5563)

* fix(sidebar): fix rename input losing its selection on open

Radix's FocusScope defers close-time focus teardown to a setTimeout(0),
which can occasionally run after the rename input's own focus()/select()
and clobber the selection. Focus the input from onCloseAutoFocus instead,
which runs inside that same deferred teardown and always wins the race.

Affects workflow, folder, and workspace rename (all route through the
shared sidebar ContextMenu component).

* fix(sidebar): only refocus the rename input when Rename triggered the close

onCloseAutoFocus fires on every menu close, not just after selecting
Rename. Gate the refocus behind a ref set only when the Rename item
was selected, so closing the menu for an unrelated action (Delete,
Duplicate, ...) while an earlier rename is still live doesn't steal
focus back into it and delay its blur-save.
This commit is contained in:
Waleed
2026-07-10 10:55:34 -07:00
committed by GitHub
parent 8bf942407c
commit 08320d5ab0
4 changed files with 37 additions and 1 deletions
@@ -1,5 +1,6 @@
'use client'
import { useRef } from 'react'
import {
DropdownMenu,
DropdownMenuContent,
@@ -34,6 +35,18 @@ interface ContextMenuProps {
onMarkAsUnread?: () => void
onTogglePin?: () => void
onRename?: () => void
/**
* Ref to the rename input rendered by the "Rename" action, if any. Radix's
* FocusScope defers its close-time focus teardown to a `setTimeout(0)`, which
* can run after the rename input's own mount-time `focus()`/`select()` and
* clobber the selection (the "rename deselects the text" bug). Focusing from
* `onCloseAutoFocus` runs synchronously inside that same deferred teardown, so
* it always wins the race regardless of scheduler timing. Only applied when
* this specific close was caused by selecting "Rename" (see
* `justSelectedRenameRef`) — an unrelated action closing the menu while an
* earlier rename is still live must not steal focus back into it.
*/
renameInputRef?: React.RefObject<HTMLInputElement | null>
onCreate?: () => void
onCreateFolder?: () => void
onDuplicate?: () => void
@@ -84,6 +97,7 @@ export function ContextMenu({
onMarkAsUnread,
onTogglePin,
onRename,
renameInputRef,
onCreate,
onCreateFolder,
onDuplicate,
@@ -132,6 +146,13 @@ export function ContextMenu({
(showUploadLogo && onUploadLogo)
const hasCopySection = (showDuplicate && onDuplicate) || (showExport && onExport)
/**
* Only the "Rename" item should trigger the `onCloseAutoFocus` refocus below —
* an unrelated action (Delete, Duplicate, ...) closing this menu while a rename
* from an earlier interaction is still live must not steal focus back into it.
*/
const justSelectedRenameRef = useRef(false)
return (
<DropdownMenu open={isOpen} onOpenChange={(open) => !open && onClose()} modal={false}>
<DropdownMenuTrigger asChild>
@@ -152,7 +173,16 @@ export function ContextMenu({
side='bottom'
sideOffset={4}
className='max-h-[var(--radix-dropdown-menu-content-available-height,400px)]'
onCloseAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => {
e.preventDefault()
const shouldFocusRenameInput = justSelectedRenameRef.current
justSelectedRenameRef.current = false
const input = shouldFocusRenameInput ? renameInputRef?.current : null
if (input) {
input.focus()
input.select()
}
}}
>
{showOpenInNewTab && onOpenInNewTab && (
<DropdownMenuItem
@@ -210,6 +240,7 @@ export function ContextMenu({
<DropdownMenuItem
disabled={disableRename}
onSelect={() => {
justSelectedRenameRef.current = true
onRename()
onClose()
}}
@@ -575,6 +575,7 @@ export const FolderItem = memo(function FolderItem({ workspaceId, folder }: Fold
menuRef={menuRef}
onClose={closeMenu}
onRename={handleStartEdit}
renameInputRef={inputRef}
onCreate={handleCreateWorkflowInFolder}
onCreateFolder={handleCreateFolderInFolder}
onDuplicate={handleDuplicate}
@@ -487,6 +487,7 @@ export const WorkflowItem = memo(function WorkflowItem({
onClose={closeMenu}
onOpenInNewTab={handleOpenInNewTab}
onRename={handleStartEdit}
renameInputRef={inputRef}
onDuplicate={handleDuplicate}
onExport={handleExport}
onDelete={handleOpenDeleteModal}
@@ -113,6 +113,7 @@ function WorkspaceHeaderImpl({
const isContextMenuOpeningRef = useRef(false)
const contextMenuClosedRef = useRef(true)
const hasInputFocusedRef = useRef(false)
const renameInputRef = useRef<HTMLInputElement | null>(null)
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
@@ -408,6 +409,7 @@ function WorkspaceHeaderImpl({
)}
<input
ref={(el) => {
renameInputRef.current = el
if (el && !hasInputFocusedRef.current) {
hasInputFocusedRef.current = true
el.focus()
@@ -643,6 +645,7 @@ function WorkspaceHeaderImpl({
menuRef={contextMenuRef}
onClose={closeContextMenu}
onRename={handleRenameAction}
renameInputRef={renameInputRef}
onDelete={handleDeleteAction}
onLeave={handleLeaveAction}
onUploadLogo={handleUploadLogoAction}