From f1d10888eec91d897eecf3b1c13150a565f841f7 Mon Sep 17 00:00:00 2001 From: thatguywob Date: Mon, 26 Oct 2026 18:02:04 -0600 Subject: [PATCH] test(navbar): share hide-timer helper and cover re-entry regressions Share clearHideTimeout between navbar dropdowns and the language switcher. Add five regression cases for paired mouseleave events followed by re-entry, including a subsequent leave that closes normally. Refs hivecommons/docs#475. The behavior fix and changelog already landed in #476; this is only the helper refactor and regression coverage. Signed-off-by: thatguywob --- src/__tests__/useNavDropdowns.test.tsx | 52 ++++++++++++++++++++++++ src/components/navbar/useNavDropdowns.ts | 19 ++++----- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/__tests__/useNavDropdowns.test.tsx b/src/__tests__/useNavDropdowns.test.tsx index 9d89f4f..395ae40 100644 --- a/src/__tests__/useNavDropdowns.test.tsx +++ b/src/__tests__/useNavDropdowns.test.tsx @@ -137,6 +137,31 @@ describe("useNavDropdowns", () => { expect(menu.style.display).toBe("block"); }); + it.each(["container", "button", "menu"] as const)( + "cancels both leave events when re-entering the %s", + target => { + const dropdown = buildDropdown("community"); + const { result } = renderHook(() => useNavDropdowns()); + + mouseEnter(dropdown.container); + mouseEnter(dropdown.menu); + mouseLeave(dropdown.menu); + mouseLeave(dropdown.container); + advance(150); + mouseEnter(dropdown[target]); + advance(1000); + + expect(dropdown.menu.style.display).toBe("block"); + expect(result.current.isDropdownOpen).toBe(true); + expect(result.current.isCommunityOpen).toBe(true); + + mouseLeave(dropdown.container); + advance(300); + expect(dropdown.menu.style.display).toBe("none"); + expect(result.current.isDropdownOpen).toBe(false); + } + ); + it("dispatches close-lang-switcher when a dropdown opens", () => { const { container } = buildDropdown("contribute"); const seen = vi.fn(); @@ -365,6 +390,33 @@ describe("useNavDropdowns", () => { expect(click).toHaveBeenCalledTimes(2); }); + it.each(["wrap", "listbox"] as const)( + "cancels both language leave events when re-entering the %s", + async target => { + const { wrap, button } = buildLangSwitcher(); + const click = vi.spyOn(button, "click"); + const { result } = renderHook(() => useNavDropdowns()); + + mouseEnter(wrap); + const listbox = appendListbox(); + await flushObservers(); + mouseEnter(listbox); + mouseLeave(listbox); + mouseLeave(wrap); + advance(150); + mouseEnter(target === "wrap" ? wrap : listbox); + advance(1000); + + expect(click).toHaveBeenCalledTimes(1); + expect(result.current.isDropdownOpen).toBe(true); + + mouseLeave(wrap); + advance(300); + expect(click).toHaveBeenCalledTimes(2); + expect(result.current.isDropdownOpen).toBe(false); + } + ); + it("wires a listbox that is itself the added node", async () => { const { wrap, button } = buildLangSwitcher(); const click = vi.spyOn(button, "click"); diff --git a/src/components/navbar/useNavDropdowns.ts b/src/components/navbar/useNavDropdowns.ts index 3e31a39..d1aa904 100644 --- a/src/components/navbar/useNavDropdowns.ts +++ b/src/components/navbar/useNavDropdowns.ts @@ -24,6 +24,13 @@ export function useNavDropdowns() { useEffect(() => { const cleanups: (() => void)[] = []; + const clearHideTimeout = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + const dropdownContainers = document.querySelectorAll("[data-dropdown]"); @@ -32,13 +39,6 @@ export function useNavDropdowns() { if (!menu) return; - const clearHideTimeout = () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - }; - const showMenu = () => { clearHideTimeout(); @@ -180,10 +180,7 @@ export function useNavDropdowns() { }; const handleMouseLeave = () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } + clearHideTimeout(); timeoutRef.current = setTimeout(() => { const langButton = langSwitcher.querySelector("button"); const dropdown = document.querySelector('[role="listbox"]');