Skip to content

Commit

Permalink
Merge pull request #13647 from ethereum/shadcn-nav
Browse files Browse the repository at this point in the history
Shadcn migration - desktop nav menu & search modal fixes
  • Loading branch information
wackerow authored Sep 10, 2024
2 parents 816648c + 154bc3c commit 1d4e2cc
Show file tree
Hide file tree
Showing 24 changed files with 436 additions and 390 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"remark-gfm": "^3.0.1",
"swiper": "^11.1.10",
"tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.2.1",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0",
"yaml-loader": "^0.8.0"
Expand Down
34 changes: 11 additions & 23 deletions src/components/Nav/Desktop/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { useRouter } from "next/router"
import { useTranslation } from "next-i18next"
import { BsTranslate } from "react-icons/bs"
import { MdBrightness2, MdWbSunny } from "react-icons/md"
import { HStack, useColorModeValue, useEventListener } from "@chakra-ui/react"

import { IconButton } from "@/components/Buttons"
import LanguagePicker from "@/components/LanguagePicker"
import { Button } from "@/components/ui/buttons/Button"
import { HStack } from "@/components/ui/flex"

import { DESKTOP_LANGUAGE_BUTTON_NAME } from "@/lib/constants"

import useColorModeValue from "@/hooks/useColorModeValue"
import { useEventListener } from "@/hooks/useEventListener"

type DesktopNavMenuProps = {
toggleColorMode: () => void
}
Expand All @@ -20,20 +22,12 @@ const DesktopNavMenu = ({ toggleColorMode }: DesktopNavMenuProps) => {
const { locale } = useRouter()
const languagePickerRef = useRef<HTMLButtonElement>(null)

const ThemeIcon = useColorModeValue(<MdBrightness2 />, <MdWbSunny />)
const ThemeIcon = useColorModeValue(MdBrightness2, MdWbSunny)
const themeIconAriaLabel = useColorModeValue(
"Switch to Dark Theme",
"Switch to Light Theme"
)

const desktopHoverFocusStyles = {
"& > svg": {
transform: "rotate(10deg)",
color: "primary.hover",
transition: "transform 0.5s, color 0.2s",
},
}

/**
* Adds a keydown event listener to toggle color mode (ctrl|cmd + \)
* or open the language picker (\).
Expand All @@ -52,22 +46,16 @@ const DesktopNavMenu = ({ toggleColorMode }: DesktopNavMenuProps) => {
})

return (
<HStack hideBelow="md" gap="0">
<IconButton
icon={ThemeIcon}
<HStack className="hidden gap-0 md:flex">
<Button
aria-label={themeIconAriaLabel}
variant="ghost"
isSecondary
px={{ base: "2", xl: "3" }}
sx={{
"& > svg": {
transition: "transform 0.5s, color 0.2s",
},
}}
_hover={desktopHoverFocusStyles}
_focus={desktopHoverFocusStyles}
className="px-2 xl:px-3 [&>svg]:transition-all [&>svg]:duration-500 [&>svg]:hover:rotate-12 [&>svg]:hover:text-primary-hover"
onClick={toggleColorMode}
/>
>
<ThemeIcon />
</Button>

{/* Locale-picker menu */}
<LanguagePicker>
Expand Down
57 changes: 44 additions & 13 deletions src/components/Nav/Menu/MenuContent.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
import { motion } from "framer-motion"
import { Box } from "@chakra-ui/react"
import { tv } from "tailwind-variants"
import { Content } from "@radix-ui/react-navigation-menu"

import { cn } from "@/lib/utils/cn"

import { NavItem, NavSections } from "../types"

import SubMenu from "./SubMenu"
import { useNavMenu } from "./useNavMenu"

export const navMenuVariants = tv({
slots: {
base: "text-body",
item: "has-[button[data-state=open]]:rounded-s-md has-[button[data-state=open]]:rounded-e-none has-[button[data-state=open]]:-me-2 has-[button[data-state=open]]:pe-2",
link: "w-full relative py-4 hover:text-menu-active [&:hover_p]:text-menu-active focus-visible:text-menu-active [&:focus-visible_p]:text-menu-active hover:outline-0 rounded-md hover:shadow-none focus-visible:outline-0 focus-visible:rounded-md focus-visible:shadow-none",
submenu: "grid h-full w-full grid-cols-1",
},
variants: {
level: {
1: {
submenu: "grid-cols-3 bg-menu-1-background",
item: "has-[button[data-state=open]]:bg-menu-1-active-background",
link: "data-[active=true]:bg-menu-1-active-background hover:bg-menu-1-active-background focus-visible:bg-menu-1-active-background",
},
2: {
submenu: "grid-cols-2 bg-menu-2-background",
item: "has-[button[data-state=open]]:bg-menu-2-active-background",
link: "hover:bg-menu-2-active-background focus-visible:bg-menu-2-active-background data-[active=true]:bg-menu-2-active-background",
},
3: {
submenu: "grid-cols-1 bg-menu-3-background",
item: "has-[button[data-state=open]]:bg-menu-3-active-background",
link: "data-[active=true]:bg-menu-3-active-background hover:bg-menu-3-active-background",
},
4: {
submenu: "grid-cols-1 bg-menu-4-background",
item: "has-[button[data-state=open]]:bg-menu-4-active-background",
link: "data-[active=true]:bg-menu-4-active-background hover:bg-menu-4-active-background",
},
},
},
})

type MenuContentProps = {
items: NavItem[]
isOpen: boolean
Expand All @@ -15,31 +50,27 @@ type MenuContentProps = {

// Desktop Menu content
const MenuContent = ({ items, isOpen, sections }: MenuContentProps) => {
const { activeSection, containerVariants, menuColors, onClose } =
useNavMenu(sections)
const { activeSection, containerVariants, onClose } = useNavMenu(sections)
const { base } = navMenuVariants()

return (
<Content asChild>
<Box
as={motion.div}
<motion.div
className={cn(
"absolute inset-x-0 top-19 border border-body-light bg-menu-1-background shadow-md",
base()
)}
variants={containerVariants}
initial={false}
animate={isOpen ? "open" : "closed"}
position="absolute"
top="19"
insetInline="0"
shadow="md"
border="1px"
borderColor={menuColors.stroke}
bg={menuColors.lvl[1].background}
>
<SubMenu
lvl={1}
items={items}
activeSection={activeSection}
onClose={onClose}
/>
</Box>
</motion.div>
</Content>
)
}
Expand Down
103 changes: 35 additions & 68 deletions src/components/Nav/Menu/SubMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import { AnimatePresence, motion } from "framer-motion"
import NextLink from "next/link"
import {
Box,
Button,
Grid,
Icon,
ListItem,
UnorderedList,
} from "@chakra-ui/react"
import {
Content,
Item,
Expand All @@ -18,16 +10,19 @@ import {
Viewport,
} from "@radix-ui/react-navigation-menu"

import { ButtonProps } from "@/components/Buttons"
import { ChevronNext } from "@/components/Chevron"
import Link from "@/components/Link"
import { Button } from "@/components/ui/buttons/Button"
import { BaseLink } from "@/components/ui/Link"
import { ListItem, UnorderedList } from "@/components/ui/list"

import { cn } from "@/lib/utils/cn"
import { trackCustomEvent } from "@/lib/utils/matomo"
import { cleanPath } from "@/lib/utils/url"

import type { Level, NavItem, NavSectionKey } from "../types"

import ItemContent from "./ItemContent"
import { navMenuVariants } from "./MenuContent"
import { useSubMenu } from "./useSubMenu"

type LvlContentProps = {
Expand All @@ -47,77 +42,41 @@ type LvlContentProps = {
* @returns The JSX element representing the menu content.
*/
const SubMenu = ({ lvl, items, activeSection, onClose }: LvlContentProps) => {
const { asPath, locale, menuColors, menuVariants, PADDING } = useSubMenu()
const { asPath, locale, menuVariants } = useSubMenu()
const { submenu, item: itemClasses, link } = navMenuVariants({ level: lvl })

if (lvl > 3) return null

const templateColumns = `repeat(${4 - lvl}, 1fr)`

return (
<Sub orientation="vertical" asChild>
<AnimatePresence>
<Grid
as={motion.div}
<motion.div
className={submenu()}
variants={menuVariants}
initial="closed"
animate="open"
exit="closed"
w="full"
h="full"
gridTemplateColumns={templateColumns}
>
<List asChild>
<UnorderedList listStyleType="none" p={PADDING / 2} m="0">
<UnorderedList className="m-0 list-none p-2">
{items.map((item) => {
const { label, icon, ...action } = item
const { label, icon: Icon, ...action } = item
const subItems = action.items || []
const isLink = "href" in action
const isActivePage = isLink && cleanPath(asPath) === action.href
const activeStyles = {
outline: "none",
rounded: "md",
"p, svg": { color: menuColors.highlight },
bg: menuColors.lvl[lvl].activeBackground,
boxShadow: "none",
}
const buttonProps: ButtonProps = {
color: menuColors.body,
leftIcon: lvl === 1 && icon ? <Icon as={icon} /> : undefined,
rightIcon: isLink ? undefined : <ChevronNext />,
position: "relative",
w: "full",
me: -PADDING,
sx: {
"span:first-of-type": { m: 0, me: 4 }, // Spacing for icon
},
py: PADDING,
bg: isActivePage
? menuColors.lvl[lvl].activeBackground
: "none",
_hover: activeStyles,
_focus: activeStyles,
variant: "ghost",
}

const buttonClasses = cn("no-underline text-body", link())

return (
<Item key={label} asChild>
<ListItem
mb={PADDING / 2}
_last={{ mb: 0 }}
sx={{
'&:has(button[data-state="open"])': {
roundedStart: "md",
roundedEnd: "none",
bg: menuColors.lvl[lvl].activeBackground,
me: -PADDING,
pe: PADDING,
},
}}
>
<ListItem className={cn("mb-2 last:mb-0", itemClasses())}>
{isLink ? (
<NextLink href={action.href!} passHref legacyBehavior>
<NavigationMenuLink asChild>
<Button
as={Link}
variant="ghost"
className={buttonClasses}
data-active={isActivePage}
onClick={() => {
onClose()
trackCustomEvent({
Expand All @@ -126,31 +85,39 @@ const SubMenu = ({ lvl, items, activeSection, onClose }: LvlContentProps) => {
eventName: action.href!,
})
}}
{...buttonProps}
asChild
>
<ItemContent item={item} lvl={lvl} />
<BaseLink>
{lvl === 1 && Icon ? (
<Icon className="me-4 h-6 w-6" />
) : null}

<ItemContent item={item} lvl={lvl} />
</BaseLink>
</Button>
</NavigationMenuLink>
</NextLink>
) : (
<>
<Trigger asChild>
<Button {...buttonProps}>
<Button variant="ghost" className={buttonClasses}>
{lvl === 1 && Icon ? (
<Icon className="me-4 h-6 w-6" />
) : null}

<ItemContent item={item} lvl={lvl} />
<ChevronNext />
</Button>
</Trigger>
<Content asChild>
<Box
bg={menuColors.lvl[lvl + 1].background}
h="full"
>
<div className="h-full">
<SubMenu
lvl={(lvl + 1) as Level}
items={subItems}
activeSection={activeSection}
onClose={onClose}
/>
</Box>
</div>
</Content>
</>
)}
Expand All @@ -161,7 +128,7 @@ const SubMenu = ({ lvl, items, activeSection, onClose }: LvlContentProps) => {
</UnorderedList>
</List>
<Viewport style={{ gridColumn: "2/4" }} />
</Grid>
</motion.div>
</AnimatePresence>
</Sub>
)
Expand Down
4 changes: 2 additions & 2 deletions src/components/Nav/Menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type { NavSections } from "../types"

import { useNavMenu } from "./useNavMenu"

const MenuContent = dynamic(() => import("./MenuContent"))

type NavMenuProps = BaseHTMLAttributes<HTMLDivElement> & {
sections: NavSections
}
Expand All @@ -26,8 +28,6 @@ const Menu = ({ sections, ...props }: NavMenuProps) => {
const { activeSection, direction, handleSectionChange, isOpen } =
useNavMenu(sections)

const MenuContent = dynamic(() => import("./MenuContent"))

return (
<div {...props}>
<Root
Expand Down
5 changes: 1 addition & 4 deletions src/components/Nav/Menu/useNavMenu.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { useState } from "react"
import type { MotionProps } from "framer-motion"
import { useEventListener } from "@chakra-ui/react"

import { isModified } from "@/lib/utils/keyboard"

import { MAIN_NAV_ID, SECTION_LABELS } from "@/lib/constants"

import type { NavSectionKey, NavSections } from "../types"

import { useNavMenuColors } from "@/hooks/useNavMenuColors"
import { useEventListener } from "@/hooks/useEventListener"
import { useRtlFlip } from "@/hooks/useRtlFlip"

export const useNavMenu = (sections: NavSections) => {
const { direction } = useRtlFlip()
const menuColors = useNavMenuColors()
const [activeSection, setActiveSection] = useState<NavSectionKey | null>(null)

// Focus corresponding nav section when number keys pressed
Expand Down Expand Up @@ -72,7 +70,6 @@ export const useNavMenu = (sections: NavSections) => {
direction,
handleSectionChange,
isOpen,
menuColors,
onClose,
}
}
Loading

0 comments on commit 1d4e2cc

Please sign in to comment.