NavigationIconLink.tsx109 lines · main
1import { LOCAL_STORAGE_KEYS } from 'common'
2import { noop } from 'lodash'
3import Link from 'next/link'
4import {
5 AnchorHTMLAttributes,
6 cloneElement,
7 ComponentPropsWithoutRef,
8 forwardRef,
9 isValidElement,
10} from 'react'
11import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
12
13import type { Route } from '@/components/ui/ui.types'
14import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
15
16interface NavigationIconButtonProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
17 route: Route
18 isActive?: boolean
19}
20
21const NavigationIconLink = forwardRef<HTMLAnchorElement, NavigationIconButtonProps>(
22 ({ route, isActive = false, onClick = noop, ...props }, ref) => {
23 const [storedAllowNavPanel] = useLocalStorageQuery(
24 LOCAL_STORAGE_KEYS.EXPAND_NAVIGATION_PANEL,
25 true
26 )
27 // Don't allow the nav panel to expand in playwright tests
28 const allowNavPanelToExpand = process.env.NEXT_PUBLIC_NODE_ENV !== 'test' && storedAllowNavPanel
29
30 const iconClasses = [
31 'absolute left-0 top-0 flex rounded-sm h-10 w-10 items-center justify-center text-foreground-lighter', // Layout
32 'group-hover/item:text-foreground-light',
33 isActive ? 'text-foreground! [&_svg]:stroke-[1.5]' : '[&_svg]:stroke-1',
34 'transition-all',
35 ]
36
37 const classes = [
38 'relative',
39 'h-10 w-full md:w-10 md:group-data-[state=expanded]:w-full',
40 'transition-all duration-200',
41 'flex items-center rounded-sm',
42 'group-data-[state=collapsed]:justify-center',
43 'group-data-[state=expanded]:-space-x-2',
44 'hover:bg-surface-200',
45 'group/item',
46 `${isActive && 'bg-selection! shadow-xs'}`,
47 ]
48
49 const LinkComponent = forwardRef<HTMLAnchorElement, ComponentPropsWithoutRef<typeof Link>>(
50 function LinkComponent(props, ref) {
51 if (route.linkElement && isValidElement(route.linkElement)) {
52 return cloneElement<any>(route.linkElement, { ...props, ref })
53 }
54
55 return <Link ref={ref} {...props} />
56 }
57 )
58
59 const linkContent = (
60 <LinkComponent
61 role="button"
62 aria-current={isActive}
63 ref={ref}
64 href={route.link || '#'} // Provide a fallback href
65 {...props}
66 onClick={(e) => {
67 if (!route.link) {
68 e.preventDefault() // Prevent navigation if there's no link
69 }
70 onClick(e)
71 }}
72 className={cn(classes, props.className)}
73 >
74 <span id="icon-link" className={cn(...iconClasses)} {...props}>
75 {route.icon}
76 </span>
77 <span
78 className={cn(
79 'min-w-[128px] text-sm text-foreground-light',
80 'group-hover/item:text-foreground',
81 'group-aria-current/item:text-foreground',
82 'absolute left-10 md:left-7 md:group-data-[state=expanded]:left-12',
83 'opacity-100 md:opacity-0 md:group-data-[state=expanded]:opacity-100',
84 `${isActive && 'text-foreground hover:text-foreground'}`,
85 'transition-all'
86 )}
87 >
88 {route.label}
89 </span>
90 </LinkComponent>
91 )
92
93 if (!allowNavPanelToExpand) {
94 return (
95 <Tooltip>
96 <TooltipTrigger asChild>{linkContent}</TooltipTrigger>
97 <TooltipContent side="right">
98 <span>{route.label}</span>
99 </TooltipContent>
100 </Tooltip>
101 )
102 }
103
104 return linkContent
105 }
106)
107
108NavigationIconLink.displayName = 'NavigationIconLink'
109export default NavigationIconLink