fix(nav): stop the rail latching open after a portaled menu closes

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.
This commit is contained in:
2026-07-15 20:24:40 +02:00
parent 6456761861
commit bcec462ef6
+24
View File
@@ -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