NavigationIconLink.tsx 3.4 KB

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