Skip to content

Commit 99c9b88

Browse files
committed
fix: display menu dropdown when sidebar collapsed
1 parent f9ed62d commit 99c9b88

File tree

2 files changed

+142
-69
lines changed

2 files changed

+142
-69
lines changed

src/components/layout/nav-group.tsx

+130-59
Original file line numberDiff line numberDiff line change
@@ -18,75 +18,35 @@ import {
1818
useSidebar,
1919
} from '@/components/ui/sidebar'
2020
import { Badge } from '../ui/badge'
21-
import { NavItem, type NavGroup } from './types'
21+
import {
22+
DropdownMenu,
23+
DropdownMenuContent,
24+
DropdownMenuItem,
25+
DropdownMenuLabel,
26+
DropdownMenuSeparator,
27+
DropdownMenuTrigger,
28+
} from '../ui/dropdown-menu'
29+
import { NavCollapsible, NavItem, NavLink, type NavGroup } from './types'
2230

2331
export function NavGroup({ title, items }: NavGroup) {
24-
const { setOpenMobile } = useSidebar()
32+
const { state } = useSidebar()
2533
const href = useLocation({ select: (location) => location.href })
2634
return (
2735
<SidebarGroup>
2836
<SidebarGroupLabel>{title}</SidebarGroupLabel>
2937
<SidebarMenu>
3038
{items.map((item) => {
31-
if (!item.items) {
39+
const key = `${item.title}-${item.url}`
40+
41+
if (!item.items)
42+
return <SidebarMenuLink key={key} item={item} href={href} />
43+
44+
if (state === 'collapsed')
3245
return (
33-
<SidebarMenuItem key={item.title}>
34-
<SidebarMenuButton
35-
asChild
36-
isActive={checkIsActive(href, item)}
37-
tooltip={item.title}
38-
>
39-
<Link to={item.url} onClick={() => setOpenMobile(false)}>
40-
{item.icon && <item.icon />}
41-
<span>{item.title}</span>
42-
{item.badge && <NavBadge>{item.badge}</NavBadge>}
43-
</Link>
44-
</SidebarMenuButton>
45-
</SidebarMenuItem>
46+
<SidebarMenuCollapsedDropdown key={key} item={item} href={href} />
4647
)
47-
}
48-
return (
49-
<Collapsible
50-
key={item.title}
51-
asChild
52-
defaultOpen={checkIsActive(href, item, true)}
53-
className='group/collapsible'
54-
>
55-
<SidebarMenuItem>
56-
<CollapsibleTrigger asChild>
57-
<SidebarMenuButton tooltip={item.title}>
58-
{item.icon && <item.icon />}
59-
<span>{item.title}</span>
60-
{item.badge && <NavBadge>{item.badge}</NavBadge>}
61-
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
62-
</SidebarMenuButton>
63-
</CollapsibleTrigger>
64-
<CollapsibleContent className='CollapsibleContent'>
65-
<SidebarMenuSub>
66-
{item.items.map((subItem) => (
67-
<SidebarMenuSubItem key={subItem.title}>
68-
<SidebarMenuSubButton
69-
asChild
70-
isActive={checkIsActive(href, subItem)}
71-
>
72-
<Link
73-
to={subItem.url}
74-
onClick={() => setOpenMobile(false)}
75-
>
76-
{subItem.icon && <subItem.icon />}
77-
<span>{subItem.title}</span>
78-
{subItem.badge && (
79-
<NavBadge>{subItem.badge}</NavBadge>
80-
)}
81-
</Link>
82-
</SidebarMenuSubButton>
83-
</SidebarMenuSubItem>
84-
))}
85-
</SidebarMenuSub>
86-
</CollapsibleContent>
87-
</SidebarMenuItem>
88-
</Collapsible>
89-
)
48+
49+
return <SidebarMenuCollapsible key={key} item={item} href={href} />
9050
})}
9151
</SidebarMenu>
9252
</SidebarGroup>
@@ -97,6 +57,117 @@ const NavBadge = ({ children }: { children: ReactNode }) => (
9757
<Badge className='text-xs rounded-full px-1 py-0'>{children}</Badge>
9858
)
9959

60+
const SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => {
61+
const { setOpenMobile } = useSidebar()
62+
return (
63+
<SidebarMenuItem>
64+
<SidebarMenuButton
65+
asChild
66+
isActive={checkIsActive(href, item)}
67+
tooltip={item.title}
68+
>
69+
<Link to={item.url} onClick={() => setOpenMobile(false)}>
70+
{item.icon && <item.icon />}
71+
<span>{item.title}</span>
72+
{item.badge && <NavBadge>{item.badge}</NavBadge>}
73+
</Link>
74+
</SidebarMenuButton>
75+
</SidebarMenuItem>
76+
)
77+
}
78+
79+
const SidebarMenuCollapsible = ({
80+
item,
81+
href,
82+
}: {
83+
item: NavCollapsible
84+
href: string
85+
}) => {
86+
const { setOpenMobile } = useSidebar()
87+
return (
88+
<Collapsible
89+
asChild
90+
defaultOpen={checkIsActive(href, item, true)}
91+
className='group/collapsible'
92+
>
93+
<SidebarMenuItem>
94+
<CollapsibleTrigger asChild>
95+
<SidebarMenuButton tooltip={item.title}>
96+
{item.icon && <item.icon />}
97+
<span>{item.title}</span>
98+
{item.badge && <NavBadge>{item.badge}</NavBadge>}
99+
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
100+
</SidebarMenuButton>
101+
</CollapsibleTrigger>
102+
<CollapsibleContent className='CollapsibleContent'>
103+
<SidebarMenuSub>
104+
{item.items.map((subItem) => (
105+
<SidebarMenuSubItem key={subItem.title}>
106+
<SidebarMenuSubButton
107+
asChild
108+
isActive={checkIsActive(href, subItem)}
109+
>
110+
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
111+
{subItem.icon && <subItem.icon />}
112+
<span>{subItem.title}</span>
113+
{subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
114+
</Link>
115+
</SidebarMenuSubButton>
116+
</SidebarMenuSubItem>
117+
))}
118+
</SidebarMenuSub>
119+
</CollapsibleContent>
120+
</SidebarMenuItem>
121+
</Collapsible>
122+
)
123+
}
124+
125+
const SidebarMenuCollapsedDropdown = ({
126+
item,
127+
href,
128+
}: {
129+
item: NavCollapsible
130+
href: string
131+
}) => {
132+
return (
133+
<SidebarMenuItem>
134+
<DropdownMenu>
135+
<DropdownMenuTrigger asChild>
136+
<SidebarMenuButton
137+
tooltip={item.title}
138+
isActive={checkIsActive(href, item)}
139+
>
140+
{item.icon && <item.icon />}
141+
<span>{item.title}</span>
142+
{item.badge && <NavBadge>{item.badge}</NavBadge>}
143+
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
144+
</SidebarMenuButton>
145+
</DropdownMenuTrigger>
146+
<DropdownMenuContent side='right' align='start' sideOffset={4}>
147+
<DropdownMenuLabel>
148+
{item.title} {item.badge ? `(${item.badge})` : ''}
149+
</DropdownMenuLabel>
150+
<DropdownMenuSeparator />
151+
{item.items.map((sub) => (
152+
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
153+
<Link
154+
to={sub.url}
155+
className={`${checkIsActive(href, sub) ? 'bg-secondary' : ''}`}
156+
>
157+
{sub.icon && <sub.icon />}
158+
<span className='max-w-52 text-wrap'>{sub.title}</span>
159+
{sub.badge && (
160+
<span className='ml-auto text-xs'>{sub.badge}</span>
161+
)}
162+
</Link>
163+
</DropdownMenuItem>
164+
))}
165+
</DropdownMenuContent>
166+
</DropdownMenu>
167+
</SidebarMenuItem>
168+
)
169+
}
170+
100171
function checkIsActive(href: string, item: NavItem, mainNav = false) {
101172
return (
102173
href === item.url || // /endpint?search=param

src/components/layout/types.ts

+12-10
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@ interface BaseNavItem {
1818
icon?: React.ElementType
1919
}
2020

21-
export type NavItem =
22-
| (BaseNavItem & {
23-
items: (BaseNavItem & { url: LinkProps['to'] })[]
24-
url?: never
25-
})
26-
| (BaseNavItem & {
27-
url: LinkProps['to']
28-
items?: never
29-
})
21+
type NavLink = BaseNavItem & {
22+
url: LinkProps['to']
23+
items?: never
24+
}
25+
26+
type NavCollapsible = BaseNavItem & {
27+
items: (BaseNavItem & { url: LinkProps['to'] })[]
28+
url?: never
29+
}
30+
31+
type NavItem = NavCollapsible | NavLink
3032

3133
interface NavGroup {
3234
title: string
@@ -39,4 +41,4 @@ interface SidebarData {
3941
navGroups: NavGroup[]
4042
}
4143

42-
export type { SidebarData, NavGroup }
44+
export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink }

0 commit comments

Comments
 (0)