From fa53c79dffb0d16191eeab4a0ae4c6fcd1b4cdcf Mon Sep 17 00:00:00 2001 From: Kyle Foster Date: Thu, 9 Jul 2026 13:54:19 -0400 Subject: [PATCH] fix: keep mobile sidebar open for portaled focus --- .changeset/sidebar-mobile-portal-focus.md | 5 +++++ packages/kumo/src/components/sidebar/sidebar.test.tsx | 6 +++--- packages/kumo/src/components/sidebar/sidebar.tsx | 11 +---------- 3 files changed, 9 insertions(+), 13 deletions(-) create mode 100644 .changeset/sidebar-mobile-portal-focus.md diff --git a/.changeset/sidebar-mobile-portal-focus.md b/.changeset/sidebar-mobile-portal-focus.md new file mode 100644 index 000000000..332d0313b --- /dev/null +++ b/.changeset/sidebar-mobile-portal-focus.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/kumo": patch +--- + +Keep the mobile Sidebar open when focus moves to portaled interactive content. diff --git a/packages/kumo/src/components/sidebar/sidebar.test.tsx b/packages/kumo/src/components/sidebar/sidebar.test.tsx index c69a25f8e..30e4d40c8 100644 --- a/packages/kumo/src/components/sidebar/sidebar.test.tsx +++ b/packages/kumo/src/components/sidebar/sidebar.test.tsx @@ -683,7 +683,7 @@ describe("Sidebar mobile behavior", () => { await waitFor(() => expect(document.activeElement).toBe(toggle)); }); - it("should close on focus leave without stealing focus back", async () => { + it("should NOT close when focus moves outside the sidebar (e.g. to portaled content)", async () => { setMobileMatchMedia(true); const user = userEvent.setup(); render(); @@ -700,7 +700,7 @@ describe("Sidebar mobile behavior", () => { afterSidebar.focus(); fireEvent.focusOut(nav, { relatedTarget: afterSidebar }); - await waitFor(() => expect(nav.getAttribute("aria-hidden")).toBe("true")); - expect(document.activeElement).toBe(afterSidebar); + expect(nav.getAttribute("aria-hidden")).toBe("false"); + expect(nav.hasAttribute("inert")).toBe(false); }); }); diff --git a/packages/kumo/src/components/sidebar/sidebar.tsx b/packages/kumo/src/components/sidebar/sidebar.tsx index 010bada8d..c896d5321 100644 --- a/packages/kumo/src/components/sidebar/sidebar.tsx +++ b/packages/kumo/src/components/sidebar/sidebar.tsx @@ -477,27 +477,18 @@ const SidebarRoot = forwardRef( const mobileNodeRef = useRef(null); const shouldRestoreFocusRef = useRef(false); - // Escape key and focus-leave close the mobile sidebar + // Escape key closes the mobile sidebar useEffect(() => { if (!isMobile || !openMobile) return; - const node = mobileNodeRef.current; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { shouldRestoreFocusRef.current = true; setOpenMobile(false); } }; - const handleFocusOut = (e: FocusEvent) => { - if (node && !node.contains(e.relatedTarget as Node)) { - shouldRestoreFocusRef.current = false; - setOpenMobile(false); - } - }; document.addEventListener("keydown", handleKeyDown); - node?.addEventListener("focusout", handleFocusOut); return () => { document.removeEventListener("keydown", handleKeyDown); - node?.removeEventListener("focusout", handleFocusOut); }; }, [isMobile, openMobile, setOpenMobile]);