From c659e561211f06b099d91a257c04c8a036bcc924 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Tue, 25 Nov 2025 17:34:36 +0100 Subject: [PATCH 1/4] refactor mobilemenu to use PersistentPanel to keep menu content mounted and avoid re-renders --- .../Nav/MobileMenu/MobileMenuClient.tsx | 50 +++++++ src/components/Nav/MobileMenu/index.tsx | 126 ++++++++---------- src/components/ui/sheet-close-on-navigate.tsx | 12 +- src/hooks/useCloseOnNavigate.ts | 21 +++ 4 files changed, 127 insertions(+), 82 deletions(-) create mode 100644 src/components/Nav/MobileMenu/MobileMenuClient.tsx create mode 100644 src/hooks/useCloseOnNavigate.ts diff --git a/src/components/Nav/MobileMenu/MobileMenuClient.tsx b/src/components/Nav/MobileMenu/MobileMenuClient.tsx new file mode 100644 index 00000000000..5c4aca81c09 --- /dev/null +++ b/src/components/Nav/MobileMenu/MobileMenuClient.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" + +import { PersistentPanel } from "@/components/ui/persistent-panel" +import { Sheet, SheetTrigger } from "@/components/ui/sheet" + +import { cn } from "@/lib/utils/cn" + +import HamburgerButton from "./HamburgerButton" + +import { useCloseOnNavigate } from "@/hooks/useCloseOnNavigate" + +type MobileMenuClientProps = { + className?: string + side: "left" | "right" + children: React.ReactNode +} + +const MobileMenuClient = ({ + className, + side, + children, +}: MobileMenuClientProps) => { + const [open, setOpen] = useCloseOnNavigate() + + return ( + <> + + + + + + + {children} + + + + ) +} + +export default MobileMenuClient diff --git a/src/components/Nav/MobileMenu/index.tsx b/src/components/Nav/MobileMenu/index.tsx index 9163ef93f3f..8625b941f46 100644 --- a/src/components/Nav/MobileMenu/index.tsx +++ b/src/components/Nav/MobileMenu/index.tsx @@ -12,13 +12,7 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible" -import { - SheetContent, - SheetFooter, - SheetHeader, - SheetTrigger, -} from "@/components/ui/sheet" -import { SheetCloseOnNavigate } from "@/components/ui/sheet-close-on-navigate" +import { SheetFooter, SheetHeader } from "@/components/ui/sheet" import { cn } from "@/lib/utils/cn" import { isLangRightToLeft } from "@/lib/utils/translations" @@ -28,8 +22,8 @@ import { MOBILE_LANGUAGE_BUTTON_NAME, SECTION_LABELS } from "@/lib/constants" import FooterButton from "./FooterButton" import FooterItemText from "./FooterItemText" -import HamburgerButton from "./HamburgerButton" import MenuHeader from "./MenuHeader" +import MobileMenuClient from "./MobileMenuClient" import ThemeToggleFooterButton from "./ThemeToggleFooterButton" import { getLanguagesDisplayInfo, getNavigation } from "@/lib/nav/links" @@ -49,73 +43,59 @@ export default async function MobileMenu({ const dir = isRtl ? "rtl" : "ltr" return ( - - - - - + + + + + - - - - - - - - - - - - - - -
- - - {t("languages")} - - -
-
- -
-
- - - {t("menu")} - - -
-
-
-
-
-
+ + + + + + + + +
+ + + {t("languages")} + + +
+
+ +
+
+ + + {t("menu")} + + +
+
+
+ + ) } diff --git a/src/components/ui/sheet-close-on-navigate.tsx b/src/components/ui/sheet-close-on-navigate.tsx index 060c78abdd3..50f9fcb1aa2 100644 --- a/src/components/ui/sheet-close-on-navigate.tsx +++ b/src/components/ui/sheet-close-on-navigate.tsx @@ -1,24 +1,18 @@ "use client" import * as React from "react" -import { useState } from "react" -import { usePathname } from "next/navigation" import { Sheet as BaseSheet } from "./sheet" +import { useCloseOnNavigate } from "@/hooks/useCloseOnNavigate" + type BaseSheetProps = React.ComponentProps const SheetCloseOnNavigate: React.FC = ({ children, ...props }) => { - const pathname = usePathname() - const [open, setOpen] = useState(false) - - React.useEffect(() => { - if (open) setOpen(false) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pathname]) + const [open, setOpen] = useCloseOnNavigate() return ( diff --git a/src/hooks/useCloseOnNavigate.ts b/src/hooks/useCloseOnNavigate.ts new file mode 100644 index 00000000000..025d7b19aa5 --- /dev/null +++ b/src/hooks/useCloseOnNavigate.ts @@ -0,0 +1,21 @@ +"use client" + +import * as React from "react" +import { useState } from "react" +import { usePathname } from "next/navigation" + +/** + * Hook that provides open/close state that automatically closes on navigation. + * Useful for modals, sheets, panels, etc. that should close when the user navigates. + */ +export function useCloseOnNavigate(initialOpen = false) { + const pathname = usePathname() + const [open, setOpen] = useState(initialOpen) + + React.useEffect(() => { + if (open) setOpen(false) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pathname]) + + return [open, setOpen] as const +} From 7ac302ccaeb921edd1f5a67078b66962fef3221c Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 26 Nov 2025 12:17:21 +0100 Subject: [PATCH 2/4] remove redundant code --- .../Nav/MobileMenu/MobileMenuClient.tsx | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/components/Nav/MobileMenu/MobileMenuClient.tsx b/src/components/Nav/MobileMenu/MobileMenuClient.tsx index 5c4aca81c09..177616f2ac4 100644 --- a/src/components/Nav/MobileMenu/MobileMenuClient.tsx +++ b/src/components/Nav/MobileMenu/MobileMenuClient.tsx @@ -25,25 +25,20 @@ const MobileMenuClient = ({ const [open, setOpen] = useCloseOnNavigate() return ( - <> - - - - - - - {children} - - - + + + + + + + {children} + + ) } From ddba2fd18bc776f1c9a45b393331691733e5535b Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 26 Nov 2025 12:17:50 +0100 Subject: [PATCH 3/4] delete unused ui component --- src/components/ui/sheet-close-on-navigate.tsx | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 src/components/ui/sheet-close-on-navigate.tsx diff --git a/src/components/ui/sheet-close-on-navigate.tsx b/src/components/ui/sheet-close-on-navigate.tsx deleted file mode 100644 index 50f9fcb1aa2..00000000000 --- a/src/components/ui/sheet-close-on-navigate.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client" - -import * as React from "react" - -import { Sheet as BaseSheet } from "./sheet" - -import { useCloseOnNavigate } from "@/hooks/useCloseOnNavigate" - -type BaseSheetProps = React.ComponentProps - -const SheetCloseOnNavigate: React.FC = ({ - children, - ...props -}) => { - const [open, setOpen] = useCloseOnNavigate() - - return ( - - {children} - - ) -} - -export { SheetCloseOnNavigate } From ce0176dacb716fced445bf3a2665c1569e8ddcf6 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 26 Nov 2025 14:02:00 +0100 Subject: [PATCH 4/4] add focus management to persistent panel --- package.json | 1 + pnpm-lock.yaml | 65 +++++++++++++++++++ .../Nav/MobileMenu/MobileMenuClient.tsx | 8 ++- src/components/ProductTable/MobileFilters.tsx | 4 +- src/components/ui/persistent-panel.tsx | 45 +++++++++++-- 5 files changed, 115 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a52b510c1fc..5ffbb741d03 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@radix-ui/react-compose-refs": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-focus-scope": "^1.1.8", "@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-portal": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5cfe3f09a4..28bb2ec3a66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@radix-ui/react-dropdown-menu': specifier: ^2.1.1 version: 2.1.15(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-scope': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-navigation-menu': specifier: ^1.2.0 version: 1.2.13(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2431,6 +2434,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-scope@1.1.8': + resolution: {integrity: sha512-BFjgXkfyRXxFJ0t/Xs4QSsb2wmkDfJ983j4vzC95on81gKPtJdJ+5ESHOuwKGm/umcWd2En33AiEMgyUGSKWQw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-id@1.1.1': resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} peerDependencies: @@ -2544,6 +2560,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-progress@1.1.7': resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} peerDependencies: @@ -2618,6 +2647,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-switch@1.2.5': resolution: {integrity: sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==} peerDependencies: @@ -11870,6 +11908,17 @@ snapshots: '@types/react': 18.2.57 '@types/react-dom': 18.2.19 + '@radix-ui/react-focus-scope@1.1.8(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.57)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.2.57)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + '@radix-ui/react-id@1.1.1(@types/react@18.2.57)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.57)(react@18.3.1) @@ -12005,6 +12054,15 @@ snapshots: '@types/react': 18.2.57 '@types/react-dom': 18.2.19 + '@radix-ui/react-primitive@2.1.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@18.2.57)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + '@radix-ui/react-progress@1.1.7(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-context': 1.1.2(@types/react@18.2.57)(react@18.3.1) @@ -12103,6 +12161,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.57 + '@radix-ui/react-slot@1.2.4(@types/react@18.2.57)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.57)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.57 + '@radix-ui/react-switch@1.2.5(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 diff --git a/src/components/Nav/MobileMenu/MobileMenuClient.tsx b/src/components/Nav/MobileMenu/MobileMenuClient.tsx index 177616f2ac4..33d9b9b0d4d 100644 --- a/src/components/Nav/MobileMenu/MobileMenuClient.tsx +++ b/src/components/Nav/MobileMenu/MobileMenuClient.tsx @@ -23,11 +23,16 @@ const MobileMenuClient = ({ children, }: MobileMenuClientProps) => { const [open, setOpen] = useCloseOnNavigate() + const triggerRef = React.useRef(null) return ( - + {children} diff --git a/src/components/ProductTable/MobileFilters.tsx b/src/components/ProductTable/MobileFilters.tsx index 88d7d315d46..ec7b18d8c66 100644 --- a/src/components/ProductTable/MobileFilters.tsx +++ b/src/components/ProductTable/MobileFilters.tsx @@ -40,6 +40,7 @@ const MobileFilters = ({ mobileFiltersLabel, }: MobileFiltersProps) => { const { t } = useTranslation("table") + const triggerRef = React.useRef(null) const handleOpenChange = (open: boolean) => { setMobileFiltersOpen(open) @@ -59,6 +60,7 @@ const MobileFilters = ({