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:
youfch
2026-05-26 11:50:16 +03:00
committed by GitHub
parent 4c34031f8d
commit cc7969ac00
@@ -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'));
const alwaysActionPaddingClass = showQuickArchiveAction ? 'pr-13' : 'pr-7';
const suppressNextSelectRef = React.useRef(false);
const editCancelledRef = React.useRef(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 liveSession = useSession(session.id);
@@ -447,6 +452,18 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
});
}, [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) {
return (
<div
@@ -455,8 +472,8 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
>
<div className="flex min-w-0 flex-1 flex-col gap-0">
<form
ref={formRef}
className="flex w-full items-center gap-2"
onSubmit={(event) => {
event.preventDefault();
handleSaveEdit();
@@ -471,7 +488,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
onKeyDown={(event) => {
if (event.key === 'Escape') {
event.stopPropagation();
editCancelledRef.current = true;
handleCancelEdit();
return;
}
@@ -479,13 +495,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
event.stopPropagation();
}
}}
onBlur={() => {
if (editCancelledRef.current) {
editCancelledRef.current = false;
return;
}
handleSaveEdit();
}}
/>
<button
type="submit"
@@ -592,6 +601,15 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
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>) => {
event.preventDefault();
event.stopPropagation();
@@ -685,11 +703,13 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
};
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
onClick={() => {
setEditingId(session.id);
setEditTitle(sessionTitle);
// Defer rename until dropdown close transition completes.
// 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"
>
@@ -991,7 +1011,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
</TooltipContent>
</Tooltip>
) : null}
<DropdownMenu open={isMenuOpen} onOpenChange={handleMenuOpenChange}>
<DropdownMenu open={isMenuOpen} onOpenChange={handleMenuOpenChange} onOpenChangeComplete={handleMenuOpenChangeComplete}>
<DropdownMenuTrigger asChild>
<button
type="button"