Skip to content

Commit

Permalink
Add animated mobile menu button
Browse files Browse the repository at this point in the history
  • Loading branch information
pookmish committed Mar 18, 2024
1 parent aead4fa commit b4263cf
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 113 deletions.
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
"@mui/base": "^5.0.0-beta.39",
"@next/third-parties": "^14.1.3",
"@tailwindcss/container-queries": "^0.1.1",
"@types/node": "^20.11.27",
"@types/node": "^20.11.28",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"algoliasearch": "^4.22.1",
"autoprefixer": "^10.4.18",
"axios": "^1.6.7",
"axios": "^1.6.8",
"clsx": "^2.1.0",
"decanter": "^7.2.0",
"drupal-jsonapi-params": "^2.3.1",
Expand All @@ -35,9 +35,9 @@
"graphql-tag": "^2.12.6",
"html-entities": "^2.5.2",
"html-react-parser": "^5.1.8",
"next": "^14.2.0-canary.22",
"next": "^14.2.0-canary.27",
"next-drupal": "^1.6.0",
"postcss": "^8.4.35",
"postcss": "^8.4.36",
"qs": "^6.12.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand All @@ -46,7 +46,7 @@
"react-instantsearch-nextjs": "^0.1.14",
"react-tiny-oembed": "^1.1.0",
"sharp": "^0.33.2",
"tailwind-merge": "^2.2.1",
"tailwind-merge": "^2.2.2",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2",
"usehooks-ts": "^3.0.1",
Expand Down
51 changes: 26 additions & 25 deletions src/components/menu/main-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
"use client";

import Link from "@components/elements/link";
import {useCallback, useEffect, useId, useLayoutEffect, useRef, useState} from "react";
import {Bars3Icon, ChevronDownIcon} from "@heroicons/react/20/solid";
import {XCircleIcon} from "@heroicons/react/24/outline";
import SiteSearchForm from "@components/search/site-search-form";
import useActiveTrail from "@lib/hooks/useActiveTrail";
import useOutsideClick from "@lib/hooks/useOutsideClick";
import {usePathname} from "next/navigation";
import {useBoolean, useEventListener} from "usehooks-ts";
import {clsx} from "clsx";
import {ChevronDownIcon} from "@heroicons/react/20/solid";
import {MenuItem as MenuItemType} from "@lib/gql/__generated__/drupal.d";
import {clsx} from "clsx";
import {useBoolean, useEventListener, useIsClient} from "usehooks-ts";
import {useCallback, useEffect, useId, useLayoutEffect, useRef, useState} from "react";
import {usePathname} from "next/navigation";

const menuLevelsToShow = 2;

const MainMenu = ({menuItems}: { menuItems: MenuItemType[] }) => {
const buttonRef = useRef<HTMLButtonElement>(null)
const menuRef = useRef<HTMLDivElement>(null);
const navId = useId();

const {value: menuOpen, setFalse: closeMenu, toggle: toggleMenu} = useBoolean(false)
const browserUrl = usePathname()
Expand All @@ -35,16 +35,14 @@ const MainMenu = ({menuItems}: { menuItems: MenuItemType[] }) => {
useEventListener("keydown", handleEscape, menuRef);

return (
<nav aria-label="Main Navigation" className="lg:centered" ref={menuRef}>
<button
ref={buttonRef}
className="flex flex-col items-center lg:hidden absolute top-5 right-10 group"
onClick={toggleMenu}
aria-expanded={menuOpen}
>
{menuOpen && <XCircleIcon height={40}/>}
{!menuOpen && <Bars3Icon height={40}/>}
<span className="group-hocus:underline">{menuOpen ? "Close" : "Menu"}</span>
<nav id={navId} aria-label="Main Navigation" className="lg:centered" ref={menuRef}>
<button ref={buttonRef} className="flex flex-col items-center lg:hidden absolute top-5 right-10 group" onClick={toggleMenu} aria-expanded={menuOpen} aria-labelledby={navId}>
<span className="flex flex-col justify-center items-center w-[30px] h-[30px]">
<span className={clsx('bg-black-true block transition-all duration-300 ease-out h-[3px] w-full rounded-sm', {'rotate-45 translate-y-4': menuOpen, '-translate-y-0.5': !menuOpen})}/>
<span className={clsx('bg-black-true block transition-all duration-300 ease-out h-[3px] w-full rounded-sm my-3',{'opacity-0' :menuOpen , 'opacity-100': !menuOpen})}/>
<span className={clsx('bg-black-true block transition-all duration-300 ease-out h-[3px] w-full rounded-sm', {'-rotate-45 -translate-y-4': menuOpen, 'translate-y-0.5': !menuOpen})}/>
</span>
<span className="group-hocus:underline" aria-hidden>{menuOpen ? "Close" : "Menu"}</span>
</button>

<div
Expand All @@ -66,21 +64,24 @@ type MenuItemProps = MenuItemType & {
}

const MenuItem = ({id, url, title, activeTrail, children, level}: MenuItemProps) => {
const isClient = useIsClient();
const linkId = useId();
const sublistRef = useRef<HTMLLIElement>(null);
const menuItemRef = useRef<HTMLLIElement>(null);
const belowListRef = useRef<HTMLUListElement>(null);

const [positionRight, setPositionRight] = useState<boolean>(true)
const buttonRef = useRef<HTMLButtonElement>(null)
const {value: submenuOpen, setFalse: closeSubmenu, toggle: toggleSubmenu} = useBoolean(false)
const browserUrl = usePathname()

useOutsideClick(sublistRef, closeSubmenu);
useOutsideClick(menuItemRef, closeSubmenu);

// Close the submenu if the url changes.
useEffect(() => closeSubmenu(), [browserUrl, closeSubmenu]);

useLayoutEffect(() => {
// If the right side of the submenu is not visible, set the position to be on the left of the menu item.
const {x, width} = sublistRef.current?.getBoundingClientRect() || {x: 0, width: 0}
const {x, width} = belowListRef.current?.getBoundingClientRect() || {x: 0, width: 0}
if (x + width > window.innerWidth) setPositionRight(false);
}, [submenuOpen])

Expand All @@ -92,7 +93,7 @@ const MenuItem = ({id, url, title, activeTrail, children, level}: MenuItemProps)
if (level === 0) buttonRef.current?.focus();
}, [level, submenuOpen, closeSubmenu]);

useEventListener("keydown", handleEscape, sublistRef)
useEventListener("keydown", handleEscape, menuItemRef)

// List out the specific classes so tailwind will include them. Dynamic classes values don't get compiled.
const zIndexes = ["z-[1]", "z-[2]", "z-[3]", "z-[4]", "z-[5]"]
Expand Down Expand Up @@ -135,8 +136,8 @@ const MenuItem = ({id, url, title, activeTrail, children, level}: MenuItemProps)

return (
<li
ref={sublistRef}
className={clsx("m-0 py-2 lg:py-0 relative border-b first:border-t last:border-0 border-cool-grey lg:border-black-20 lg:relative lg:mr-5 last:lg:mr-0", level === 0 && "lg:border-b-0 first:border-t-0")}
ref={menuItemRef}
className={clsx("m-0 py-2 lg:py-0 relative border-b first:border-t last:border-0 border-cool-grey lg:border-black-20 lg:relative lg:mr-5 last:lg:mr-0", {"lg:border-b-0 first:border-t-0": level === 0})}
>
<div className="flex items-center justify-between lg:justify-end">
<Link
Expand All @@ -150,7 +151,7 @@ const MenuItem = ({id, url, title, activeTrail, children, level}: MenuItemProps)

{(children.length > 0 && level < menuLevelsToShow) &&
<>
{level === 0 && <div className="block ml-5 w-[1px] h-[25px] mb-[6px] bg-archway-light shrink-0"/>}
{level === 0 && <div className="block ml-5 w-[1px] h-[25px] mb-[6px] bg-archway-light shrink-0"/>}
<button
ref={buttonRef}
className="shrink-0 relative right-10 lg:right-0 text-white lg:text-digital-red bg-digital-red lg:bg-transparent rounded-full lg:rounded-none group border-b border-transparent hocus:border-black hocus:bg-white"
Expand All @@ -160,7 +161,7 @@ const MenuItem = ({id, url, title, activeTrail, children, level}: MenuItemProps)
>
<ChevronDownIcon
height={35}
className={clsx("transition group-hocus:scale-125 group-hocus:text-black ease-in-out duration-150", submenuOpen && "rotate-180")}
className={clsx("transition group-hocus:scale-125 group-hocus:text-black ease-in-out duration-150", {"rotate-180": submenuOpen})}
/>
</button>

Expand All @@ -170,7 +171,7 @@ const MenuItem = ({id, url, title, activeTrail, children, level}: MenuItemProps)
</div>

{(children.length > 0 && level < menuLevelsToShow) &&
<ul className={subMenuStyles}>
<ul className={subMenuStyles} ref={belowListRef}>
{children.map(item =>
<MenuItem
key={item.id}
Expand Down
Loading

0 comments on commit b4263cf

Please sign in to comment.