From a984ce6ab79cc246bfa25ad38ab2e40575f3ef01 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 13 Jun 2026 02:28:40 +0800 Subject: [PATCH] refactor(layout): share popup menu dismiss handling The language and theme menus carried two identical outside-click/Escape effects; fold them into one useMenuDismiss hook. --- src/components/layout/MainLayout.tsx | 89 ++++++++++++---------------- 1 file changed, 38 insertions(+), 51 deletions(-) diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 4d9889c..a3e114c 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -1,5 +1,6 @@ import { ReactNode, + RefObject, SVGProps, useCallback, useEffect, @@ -87,6 +88,39 @@ interface SidebarNavGroup { const flattenNavItems = (items: SidebarNavItem[]): SidebarNavLinkItem[] => items.flatMap((item) => item.kind === 'drawer' ? item.children : [item]); +/** 点击菜单外或按下 Escape 时关闭弹出菜单 */ +function useMenuDismiss( + open: boolean, + menuRef: RefObject, + onClose: () => void +) { + useEffect(() => { + if (!open) { + return; + } + + const handlePointerDown = (event: MouseEvent) => { + if (!menuRef.current?.contains(event.target as Node)) { + onClose(); + } + }; + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onClose(); + } + }; + + document.addEventListener('mousedown', handlePointerDown); + document.addEventListener('keydown', handleEscape); + + return () => { + document.removeEventListener('mousedown', handlePointerDown); + document.removeEventListener('keydown', handleEscape); + }; + }, [open, menuRef, onClose]); +} + function PluginSidebarIcon({ src }: { src: string }) { const [failed, setFailed] = useState(false); const showImage = Boolean(src) && !failed; @@ -355,57 +389,10 @@ export function MainLayout() { }; }, []); - useEffect(() => { - if (!languageMenuOpen) { - return; - } - - const handlePointerDown = (event: MouseEvent) => { - if (!languageMenuRef.current?.contains(event.target as Node)) { - setLanguageMenuOpen(false); - } - }; - - const handleEscape = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - setLanguageMenuOpen(false); - } - }; - - document.addEventListener('mousedown', handlePointerDown); - document.addEventListener('keydown', handleEscape); - - return () => { - document.removeEventListener('mousedown', handlePointerDown); - document.removeEventListener('keydown', handleEscape); - }; - }, [languageMenuOpen]); - - useEffect(() => { - if (!themeMenuOpen) { - return; - } - - const handlePointerDown = (event: MouseEvent) => { - if (!themeMenuRef.current?.contains(event.target as Node)) { - setThemeMenuOpen(false); - } - }; - - const handleEscape = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - setThemeMenuOpen(false); - } - }; - - document.addEventListener('mousedown', handlePointerDown); - document.addEventListener('keydown', handleEscape); - - return () => { - document.removeEventListener('mousedown', handlePointerDown); - document.removeEventListener('keydown', handleEscape); - }; - }, [themeMenuOpen]); + const closeLanguageMenu = useCallback(() => setLanguageMenuOpen(false), []); + const closeThemeMenu = useCallback(() => setThemeMenuOpen(false), []); + useMenuDismiss(languageMenuOpen, languageMenuRef, closeLanguageMenu); + useMenuDismiss(themeMenuOpen, themeMenuRef, closeThemeMenu); const toggleLanguageMenu = useCallback(() => { setLanguageMenuOpen((prev) => !prev);