From 4c927670d3b1cdb01090ac5b32d87054a11dcc80 Mon Sep 17 00:00:00 2001 From: Haohua-Sun Date: Sat, 16 May 2026 01:47:59 +0800 Subject: [PATCH] Fix mobile sidebar logout interaction --- frontend/src/components/Sidebar/User.tsx | 25 ++++++++-- frontend/src/components/ui/sidebar.tsx | 58 +++++++++++++++++++++++- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/Sidebar/User.tsx b/frontend/src/components/Sidebar/User.tsx index 12c6362aff..1b438c57b9 100644 --- a/frontend/src/components/Sidebar/User.tsx +++ b/frontend/src/components/Sidebar/User.tsx @@ -1,5 +1,6 @@ import { Link as RouterLink } from "@tanstack/react-router" import { ChevronsUpDown, LogOut, Settings } from "lucide-react" +import { useState } from "react" import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { @@ -42,23 +43,32 @@ function UserInfo({ fullName, email }: UserInfoProps) { export function User({ user }: { user: any }) { const { logout } = useAuth() - const { isMobile, setOpenMobile } = useSidebar() + const { isMobile, setOpenMobile, closeMobileSidebar } = useSidebar() + const [isMenuOpen, setIsMenuOpen] = useState(false) if (!user) return null const handleMenuClick = () => { + setIsMenuOpen(false) if (isMobile) { setOpenMobile(false) } } - const handleLogout = async () => { - logout() + const handleLogout = () => { + setIsMenuOpen(false) + + if (isMobile) { + void closeMobileSidebar().then(logout) + return + } + + window.requestAnimationFrame(logout) } return ( - + - + { + event.preventDefault() + handleLogout() + }} + > Log Out diff --git a/frontend/src/components/ui/sidebar.tsx b/frontend/src/components/ui/sidebar.tsx index 60c0115ee8..b4392bb8a4 100644 --- a/frontend/src/components/ui/sidebar.tsx +++ b/frontend/src/components/ui/sidebar.tsx @@ -36,6 +36,8 @@ type SidebarContextProps = { setOpen: (open: boolean) => void openMobile: boolean setOpenMobile: (open: boolean) => void + closeMobileSidebar: () => Promise + completeMobileSidebarClose: () => void isMobile: boolean toggleSidebar: () => void } @@ -66,6 +68,36 @@ function SidebarProvider({ }) { const isMobile = useIsMobile() const [openMobile, setOpenMobile] = React.useState(false) + const mobileCloseResolversRef = React.useRef(new Set<() => void>()) + + const completeMobileSidebarClose = React.useCallback(() => { + const resolvers = Array.from(mobileCloseResolversRef.current) + mobileCloseResolversRef.current.clear() + resolvers.forEach((resolve) => resolve()) + }, []) + + const closeMobileSidebar = React.useCallback(() => { + if (!isMobile || !openMobile) { + return Promise.resolve() + } + + setOpenMobile(false) + + return new Promise((resolve) => { + let timeoutId: number | undefined + const resolveOnce = () => { + if (timeoutId !== undefined) { + window.clearTimeout(timeoutId) + } + mobileCloseResolversRef.current.delete(resolveOnce) + resolve() + } + + mobileCloseResolversRef.current.add(resolveOnce) + // Fallback for environments that do not fire CSS animation events. + timeoutId = window.setTimeout(resolveOnce, 350) + }) + }, [isMobile, openMobile]) const getInitialOpen = () => { if (typeof document === "undefined") return defaultOpen @@ -131,9 +163,20 @@ function SidebarProvider({ isMobile, openMobile, setOpenMobile, + closeMobileSidebar, + completeMobileSidebarClose, toggleSidebar, }), - [state, open, setOpen, isMobile, openMobile, toggleSidebar], + [ + state, + open, + setOpen, + isMobile, + openMobile, + closeMobileSidebar, + completeMobileSidebarClose, + toggleSidebar, + ], ) return ( @@ -173,7 +216,13 @@ function Sidebar({ variant?: "sidebar" | "floating" | "inset" collapsible?: "offcanvas" | "icon" | "none" }) { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + const { + isMobile, + state, + openMobile, + setOpenMobile, + completeMobileSidebarClose, + } = useSidebar() if (collapsible === "none") { return ( @@ -198,6 +247,11 @@ function Sidebar({ data-slot="sidebar" data-mobile="true" className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" + onAnimationEnd={(event) => { + if (event.currentTarget === event.target && !openMobile) { + completeMobileSidebarClose() + } + }} style={ { "--sidebar-width": SIDEBAR_WIDTH_MOBILE,