fix: session rename exits immediately due to focus race (#1429)
* fix: session rename exits immediately due to focus race Replace fragile onBlur handler with document mousedown listener for click-outside detection. Defer dropdown-initiated rename to onOpenChangeComplete (fires after CSS transition + cleanup). Use finalFocus instead of dead onCloseAutoFocus (never forwarded to Base UI). * chore: remove dead editCancelledRef after onBlur removal onBlur was replaced by document mousedown listener. editCancelledRef was only read in the old onBlur handler — its write in the Escape key handler was the sole remaining use.
This commit is contained in:
@@ -284,8 +284,13 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
: (showQuickArchiveAction ? 'group-hover:pr-12 group-focus-within:pr-12' : 'group-hover:pr-5 group-focus-within:pr-5'));
|
: (showQuickArchiveAction ? 'group-hover:pr-12 group-focus-within:pr-12' : 'group-hover:pr-5 group-focus-within:pr-5'));
|
||||||
const alwaysActionPaddingClass = showQuickArchiveAction ? 'pr-13' : 'pr-7';
|
const alwaysActionPaddingClass = showQuickArchiveAction ? 'pr-13' : 'pr-7';
|
||||||
const suppressNextSelectRef = React.useRef(false);
|
const suppressNextSelectRef = React.useRef(false);
|
||||||
const editCancelledRef = React.useRef(false);
|
|
||||||
const [isTouchPressed, setIsTouchPressed] = React.useState(false);
|
const [isTouchPressed, setIsTouchPressed] = React.useState(false);
|
||||||
|
const editingIdRef = React.useRef(editingId);
|
||||||
|
editingIdRef.current = editingId;
|
||||||
|
const pendingRenameRef = React.useRef<{ id: string; title: string } | null>(null);
|
||||||
|
const handleSaveEditRef = React.useRef(handleSaveEdit);
|
||||||
|
handleSaveEditRef.current = handleSaveEdit;
|
||||||
|
const formRef = React.useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
const session = node.session;
|
const session = node.session;
|
||||||
const liveSession = useSession(session.id);
|
const liveSession = useSession(session.id);
|
||||||
@@ -447,6 +452,18 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
});
|
});
|
||||||
}, [session.id, sessionDirectory]);
|
}, [session.id, sessionDirectory]);
|
||||||
|
|
||||||
|
// Capture outside-clicks to save edits — immune to focus-race with onBlur.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (editingId !== session.id) return;
|
||||||
|
const handleDocMouseDown = (e: MouseEvent) => {
|
||||||
|
if (formRef.current && !formRef.current.contains(e.target as Node)) {
|
||||||
|
handleSaveEditRef.current();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleDocMouseDown);
|
||||||
|
return () => document.removeEventListener('mousedown', handleDocMouseDown);
|
||||||
|
}, [editingId, session.id]);
|
||||||
|
|
||||||
if (editingId === session.id) {
|
if (editingId === session.id) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -455,8 +472,8 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-0">
|
<div className="flex min-w-0 flex-1 flex-col gap-0">
|
||||||
<form
|
<form
|
||||||
|
ref={formRef}
|
||||||
className="flex w-full items-center gap-2"
|
className="flex w-full items-center gap-2"
|
||||||
|
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleSaveEdit();
|
handleSaveEdit();
|
||||||
@@ -471,7 +488,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
editCancelledRef.current = true;
|
|
||||||
handleCancelEdit();
|
handleCancelEdit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -479,13 +495,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
|
||||||
if (editCancelledRef.current) {
|
|
||||||
editCancelledRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleSaveEdit();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -592,6 +601,15 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
setOpenSidebarMenuKey(open ? menuInstanceKey : null);
|
setOpenSidebarMenuKey(open ? menuInstanceKey : null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMenuOpenChangeComplete = (open: boolean) => {
|
||||||
|
if (!open && pendingRenameRef.current) {
|
||||||
|
const { id, title } = pendingRenameRef.current;
|
||||||
|
pendingRenameRef.current = null;
|
||||||
|
setEditingId(id);
|
||||||
|
setEditTitle(title);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleMenuTriggerClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleMenuTriggerClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -685,11 +703,13 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sessionMenuContent = (
|
const sessionMenuContent = (
|
||||||
<DropdownMenuContent align="end" className="min-w-[180px]" onCloseAutoFocus={(event) => { if (renamingFolderId) event.preventDefault(); }}>
|
<DropdownMenuContent align="end" className="min-w-[180px]" finalFocus={() => (renamingFolderId || editingIdRef.current) ? false : true}>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingId(session.id);
|
// Defer rename until dropdown close transition completes.
|
||||||
setEditTitle(sessionTitle);
|
// onOpenChangeComplete fires after animation + focus cleanup are done,
|
||||||
|
// avoiding focus stealing from Base UI's unmount cleanup.
|
||||||
|
pendingRenameRef.current = { id: session.id, title: sessionTitle };
|
||||||
}}
|
}}
|
||||||
className="[&>svg]:mr-1"
|
className="[&>svg]:mr-1"
|
||||||
>
|
>
|
||||||
@@ -991,7 +1011,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
<DropdownMenu open={isMenuOpen} onOpenChange={handleMenuOpenChange}>
|
<DropdownMenu open={isMenuOpen} onOpenChange={handleMenuOpenChange} onOpenChangeComplete={handleMenuOpenChangeComplete}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user