From bcec462ef6a6e52c9a2f6a8881a18a29be25d36c Mon Sep 17 00:00:00 2001 From: npeter83 Date: Wed, 15 Jul 2026 20:24:40 +0200 Subject: [PATCH] fix(nav): stop the rail latching open after a portaled menu closes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening the language switcher (or the account popover) and then dismissing it left the unpinned rail stuck expanded. Those menus are portaled and unmount while focused, and browsers fire no blur when a focused node is removed — so the wrapper's onBlur never arrived and `focused` stayed true forever. Keep the React-tree onFocus/onBlur (portals bubble through it, which is what correctly holds the rail open while its own menu has focus) and add a safety net for the one case they miss: when focus has landed nowhere, clear it. --- frontend/src/components/NavSidebar.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/frontend/src/components/NavSidebar.tsx b/frontend/src/components/NavSidebar.tsx index 4a53ca1d..edd30a23 100644 --- a/frontend/src/components/NavSidebar.tsx +++ b/frontend/src/components/NavSidebar.tsx @@ -67,6 +67,30 @@ export default function NavSidebar({ // in-flow like before. `collapsed` (persisted per-account) now means "unpinned". const [hovered, setHovered] = useState(false); const [focused, setFocused] = useState(false); + // onFocus/onBlur (below) track focus through the REACT tree, so the rail's portaled menus + // (language switcher, account popover) count as "inside" and keep it open while you use them. + // The case they miss: a portal unmounts while focused, and browsers fire NO blur when a focused + // node is removed — focus silently falls to the body and `focused` would latch on forever. + // Safety net: whenever focus has landed nowhere, clear it. Deferred a tick so activeElement has + // settled, and rescheduled rather than queued — only the latest state matters. + useEffect(() => { + let timer: number | undefined; + const clearIfFocusLandedNowhere = () => { + const el = document.activeElement; + if (!el || el === document.body || el === document.documentElement) setFocused(false); + }; + const soon = () => { + window.clearTimeout(timer); + timer = window.setTimeout(clearIfFocusLandedNowhere, 0); + }; + document.addEventListener("focusout", soon); + document.addEventListener("click", soon); + return () => { + document.removeEventListener("focusout", soon); + document.removeEventListener("click", soon); + window.clearTimeout(timer); + }; + }, []); const pinned = !collapsed; const expanded = pinned || hovered || focused; const slim = !expanded; // visual: icon-only rail