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'));
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user