Skip to content

Commit e1083bb

Browse files
committed
Make header close once clicked outside
1 parent d75d821 commit e1083bb

File tree

3 files changed

+55
-7
lines changed

3 files changed

+55
-7
lines changed

src/components/controls/Button.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ButtonHTMLAttributes, DetailedHTMLProps, HTMLAttributes, useMemo } from "react";;
1+
import { ButtonHTMLAttributes, DetailedHTMLProps, ForwardedRef, forwardRef, HTMLAttributes, useMemo } from "react";;
22
import { cva, VariantProps as GetVariantProps } from "class-variance-authority";
33
import { twMerge } from "tailwind-merge";
44

@@ -19,16 +19,16 @@ type ButtonProps = {
1919
} & RequiredKeys<VariantProps, "color">
2020
& DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
2121

22-
const Button = ({ className, color, ...props }: ButtonProps) => {
22+
const Button = forwardRef(({ className, color, ...props }: ButtonProps, ref: ForwardedRef<HTMLButtonElement>) => {
2323

2424
const computedClassName = useMemo(
2525
() => twMerge(style({ color }), className),
2626
[className, color]
2727
);
2828

2929
return (
30-
<button className={computedClassName} {...props} />
30+
<button ref={ref} className={computedClassName} {...props} />
3131
);
32-
};
32+
});
3333

3434
export default Button;

src/components/navigation/Header.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode, useState } from "react";
1+
import { ReactNode, useRef, useState } from "react";
22
import { useRouter } from "next/router";
33
import Bars3Icon from "@heroicons/react/24/solid/Bars3Icon";
44

@@ -7,12 +7,20 @@ import Logo from "@/assets/images/brand/logo-200x200.webp";
77
import Link from "./Link";
88
import Button from "../controls/Button";
99
import NavLink from "./NavLink";
10+
import useOutsideClick from "@/hooks/useOutsideClick";
1011

1112
const Header = () => {
1213
const router = useRouter();
1314

15+
const buttonRef = useRef<HTMLButtonElement>(null);
16+
const itemsContainerRef = useRef<HTMLDivElement>(null);
17+
1418
const [open, setOpen] = useState(false);
1519

20+
useOutsideClick([buttonRef, itemsContainerRef], (e) => {
21+
setOpen(false);
22+
});
23+
1624
return (
1725
<header className="relative flex items-center w-full gap-4 p-4 transition-all md:px-8 md:gap-12">
1826
<Link color="primary" className="flex items-center justify-center text-2xl font-bold transition-all lg:text-4xl" href="/">
@@ -25,10 +33,22 @@ const Header = () => {
2533
/>
2634
<span>Commit Rocket</span>
2735
</Link>
28-
<Button color="secondary" className="p-2 ml-auto rounded-full md:hidden" aria-expanded={open} aria-controls="header-items" onClick={() => setOpen(!open)}>
36+
<Button
37+
ref={buttonRef}
38+
className="p-2 ml-auto rounded-full md:hidden"
39+
color="secondary"
40+
aria-expanded={open}
41+
aria-controls="header-items"
42+
onClick={() => setOpen(!open)}
43+
>
2944
<Bars3Icon className="w-6 h-6" />
3045
</Button>
31-
<div id="header-items" className="absolute flex flex-col bg-fill gap-0 p-4 top-full inset-x-4 rounded-md shadow shadow-primary z-10 aria-[expanded='false']:hidden md:aria-[expanded='false']:flex md:flex-row md:items-center md:p-0 md:shadow-none md:static md:bg-transparent md:gap-12">
46+
<div
47+
ref={itemsContainerRef}
48+
className="absolute flex flex-col bg-fill gap-0 p-4 top-full inset-x-4 rounded-md shadow shadow-primary z-10 data-[expanded='false']:hidden md:data-[expanded='false']:flex md:flex-row md:items-center md:p-0 md:shadow-none md:static md:bg-transparent md:gap-12"
49+
id="header-items"
50+
data-expanded={open}
51+
>
3252
<NavLink href="/" currentHref={router.pathname}>
3353
Home
3454
</NavLink>

src/hooks/useOutsideClick.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { MutableRefObject, useEffect, useRef, RefObject } from "react";
2+
3+
const useOutsideClick = (refs: (RefObject<Element> | MutableRefObject<Element>)[], handler: (event: TouchEvent | MouseEvent) => void) => {
4+
const handlerRef = useRef(handler);
5+
handlerRef.current = handler;
6+
7+
useEffect(() => {
8+
const listener = (event: TouchEvent | MouseEvent) => {
9+
const isContained = refs.some((ref) => {
10+
if (!event.target || !ref.current) return true;
11+
return ref.current.contains(event.target as Element);
12+
});
13+
if (isContained) return;
14+
15+
handlerRef.current(event);
16+
};
17+
18+
document.addEventListener("mousedown", listener);
19+
document.addEventListener("touchstart", listener);
20+
21+
return () => {
22+
document.removeEventListener("mousedown", listener);
23+
document.removeEventListener("touchstart", listener);
24+
};
25+
}, [...refs]);
26+
};
27+
28+
export default useOutsideClick;

0 commit comments

Comments
 (0)