Skip to content

Commit fb17c99

Browse files
committed
feat: improved mobile navigation
1 parent 8cb009a commit fb17c99

5 files changed

Lines changed: 62 additions & 15 deletions

File tree

src/components/navigation/NavigationMobile.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ const NavigationMobile: React.FC<NavigationMobileProps> = ({ locale, items, butt
2727
const { trigger } = useWebHaptics()
2828
const menuRef = useOutsideClick<HTMLElement>(() => setIsOpen(false))
2929
const rootRef = useRef<HTMLDivElement>(null)
30+
const menuContentRef = useRef<HTMLDivElement>(null)
3031
const wasOpenRef = useRef(false)
3132
const [isOpen, setIsOpen] = useState(false)
3233
const [mobileOpenKey, setMobileOpenKey] = useState<string | null>(null)
3334
const [isMenuClosing, setIsMenuClosing] = useState(false)
3435
const [shellInsetWidth, setShellInsetWidth] = useState(0)
36+
const [menuHeight, setMenuHeight] = useState(0)
3537
const { homeHref, navbarItems, navbarButtons, logo: navigationLogo } = useNavigationViewModel(locale, items, buttons, logo)
3638
const menuTransition = {
3739
duration: 0.34,
@@ -43,7 +45,7 @@ const NavigationMobile: React.FC<NavigationMobileProps> = ({ locale, items, butt
4345
}
4446
const isShellExpanded = isOpen || isMenuClosing
4547
const isScrolled = useNavigationScrollState({
46-
onScroll: () => setIsOpen((prevIsOpen) => (prevIsOpen ? false : prevIsOpen)),
48+
onScroll: isOpen ? () => setIsOpen(false) : undefined,
4749
})
4850
const expandedHeaderPadding = 8
4951
const shellInset = isScrolled && !isShellExpanded ? shellInsetWidth : 0
@@ -97,6 +99,26 @@ const NavigationMobile: React.FC<NavigationMobileProps> = ({ locale, items, butt
9799
return () => resizeObserver.disconnect()
98100
}, [])
99101

102+
useLayoutEffect(() => {
103+
const element = menuContentRef.current
104+
105+
if (!element || !isOpen) {
106+
setMenuHeight(0)
107+
return
108+
}
109+
110+
const measure = () => {
111+
setMenuHeight(element.scrollHeight)
112+
}
113+
114+
measure()
115+
116+
const resizeObserver = new ResizeObserver(measure)
117+
resizeObserver.observe(element)
118+
119+
return () => resizeObserver.disconnect()
120+
}, [isOpen, navbarItems, navbarButtons])
121+
100122
const closeMenu = () => {
101123
trigger("medium")
102124
setIsOpen(false)
@@ -113,7 +135,7 @@ const NavigationMobile: React.FC<NavigationMobileProps> = ({ locale, items, butt
113135
<motion.div
114136
className={cn(
115137
"pointer-events-none absolute inset-0 rounded-2xl shadow-sm",
116-
(isScrolled || isShellExpanded) ? "bg-primary/50 backdrop-blur-lg" : "bg-transparent shadow-none",
138+
(isScrolled || isShellExpanded) ? "bg-primary/70 backdrop-blur-md" : "bg-transparent shadow-none",
117139
)}
118140
initial={false}
119141
animate={{
@@ -175,15 +197,18 @@ const NavigationMobile: React.FC<NavigationMobileProps> = ({ locale, items, butt
175197
<motion.div
176198
id="mobile-navigation-menu"
177199
key="mobile-menu"
178-
layout
179200
initial={{ opacity: 0, y: -4, height: 0 }}
180-
animate={{ opacity: 1, y: 0, height: "auto" }}
201+
animate={{ opacity: 1, y: 0, height: menuHeight }}
181202
exit={{ opacity: 0, y: -4, height: 0 }}
182-
transition={menuTransition}
203+
transition={{
204+
height: menuTransition,
205+
opacity: { duration: 0.18, ease: "easeOut" },
206+
y: menuTransition,
207+
}}
183208
style={{ overflow: "hidden" }}
184209
className="flex flex-col gap-2 px-2"
185210
>
186-
<div className="flex flex-col gap-1">
211+
<div ref={menuContentRef} className="flex flex-col gap-1">
187212
{navbarItems.map((item) => {
188213
const isOpenAcc = mobileOpenKey === item.title
189214

src/components/navigation/NavigationMobileItem.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { cn } from "@/lib/utils"
66
import { IconChevronUp } from "@tabler/icons-react"
77
import { AnimatePresence, m as motion } from "motion/react"
88
import Link from "next/link"
9+
import { useLayoutEffect, useRef, useState } from "react"
910
import { NavigationSubMenu } from "./NavigationSubMenu"
1011

1112
interface NavigationMobileItemProps {
@@ -20,9 +21,31 @@ const mobileNavItemClassName = navigationMenuTriggerStyle({
2021
})
2122

2223
export function NavigationMobileItem({ item, isOpen, onToggle, onNavigate }: NavigationMobileItemProps) {
24+
const submenuContentRef = useRef<HTMLDivElement>(null)
25+
const [submenuHeight, setSubmenuHeight] = useState(0)
2326
const isAccordion = Boolean(item.subMenu?.length)
2427
const hasRoute = Boolean(item.href)
2528

29+
useLayoutEffect(() => {
30+
const element = submenuContentRef.current
31+
32+
if (!element || !isOpen) {
33+
setSubmenuHeight(0)
34+
return
35+
}
36+
37+
const measure = () => {
38+
setSubmenuHeight(element.scrollHeight)
39+
}
40+
41+
measure()
42+
43+
const resizeObserver = new ResizeObserver(measure)
44+
resizeObserver.observe(element)
45+
46+
return () => resizeObserver.disconnect()
47+
}, [isOpen, item.subMenu])
48+
2649
return (
2750
<div className="flex flex-col">
2851
{isAccordion ? (
@@ -55,9 +78,8 @@ export function NavigationMobileItem({ item, isOpen, onToggle, onNavigate }: Nav
5578
{isOpen && (
5679
<motion.div
5780
key={`${item.title}-submenu`}
58-
layout
5981
initial={{ height: 0, opacity: 0 }}
60-
animate={{ height: "auto", opacity: 1 }}
82+
animate={{ height: submenuHeight, opacity: 1 }}
6183
exit={{ height: 0, opacity: 0 }}
6284
transition={{
6385
height: {
@@ -71,7 +93,7 @@ export function NavigationMobileItem({ item, isOpen, onToggle, onNavigate }: Nav
7193
}}
7294
className="overflow-hidden"
7395
>
74-
<div className="mt-1 rounded-lg">
96+
<div ref={submenuContentRef} className="mt-1 rounded-lg">
7597
<NavigationSubMenu
7698
items={item.subMenu!}
7799
onSelect={onNavigate}

src/globals/navigation.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ export const Navigation: GlobalConfig = {
99
read: () => true,
1010
update: ({ req }) => Boolean(req.user),
1111
},
12-
admin: {
13-
group: "Navigation",
14-
},
1512
fields: [
1613
{
1714
name: "logo",
@@ -80,7 +77,7 @@ export const Navigation: GlobalConfig = {
8077
{
8178
name: "icon",
8279
type: "text",
83-
required: true,
80+
required: false,
8481
admin: {
8582
description: 'Tabler Icon Name. Unbekannte Werte fallen auf "cube" zurück.',
8683
},

src/migrations/20260531_084500_navigation_globals.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export async function up({ db }: MigrateUpArgs): Promise<void> {
3737
"id" varchar PRIMARY KEY NOT NULL,
3838
"key" varchar NOT NULL,
3939
"href" varchar NOT NULL,
40-
"icon" varchar NOT NULL
40+
"icon" varchar
4141
);
4242
4343
CREATE TABLE IF NOT EXISTS "navigation_items_items_sub_menu_locales" (
@@ -48,6 +48,9 @@ export async function up({ db }: MigrateUpArgs): Promise<void> {
4848
"_parent_id" varchar NOT NULL
4949
);
5050
51+
ALTER TABLE "navigation_items_items_sub_menu"
52+
ALTER COLUMN "icon" DROP NOT NULL;
53+
5154
CREATE TABLE IF NOT EXISTS "navigation_buttons_buttons" (
5255
"_order" integer NOT NULL,
5356
"_parent_id" integer NOT NULL,

src/payload-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1515,7 +1515,7 @@ export interface Navigation {
15151515
/**
15161516
* Tabler Icon Name. Unbekannte Werte fallen auf "cube" zurück.
15171517
*/
1518-
icon: string;
1518+
icon?: string | null;
15191519
id?: string | null;
15201520
}[]
15211521
| null;

0 commit comments

Comments
 (0)