mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
+32
-1
@@ -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()
|
||||
}}
|
||||
|
||||
+1
@@ -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}
|
||||
|
||||
+1
@@ -487,6 +487,7 @@ export const WorkflowItem = memo(function WorkflowItem({
|
||||
onClose={closeMenu}
|
||||
onOpenInNewTab={handleOpenInNewTab}
|
||||
onRename={handleStartEdit}
|
||||
renameInputRef={inputRef}
|
||||
onDuplicate={handleDuplicate}
|
||||
onExport={handleExport}
|
||||
onDelete={handleOpenDeleteModal}
|
||||
|
||||
+3
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user