Skip to content

Commit

Permalink
fix: menus close on click and navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
alexgoff committed Feb 6, 2025
1 parent a22629d commit 27c3dcb
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 141 deletions.
20 changes: 12 additions & 8 deletions components/molecules/CollapsibleHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
"use client";

import { FunctionComponent, PropsWithChildren, useState } from "react";
import { FunctionComponent, PropsWithChildren, useRef } from "react";
import Headroom from "react-headroom";
import styles from "./styles.module.scss";
import { HeadroomProvider } from "@/contexts/Headroom";
import useNavigationMenu from "@/contexts/NavigationMenu";
import Center from "@rubin-epo/epo-react-lib/Center";
import { useOnClickOutside } from "@/hooks/listeners";

const CollapsibleHeader: FunctionComponent<PropsWithChildren> = ({
children,
}) => {
const [pinned, setPinned] = useState(false);
const { pinned, close } = useNavigationMenu();
const ref = useRef(null);

useOnClickOutside(ref, close);

return (
<Headroom
downTolerance={2}
pin={pinned}
style={{ zIndex: "var(--elevation-element-header, 25)" }}
>
<HeadroomProvider {...{ pinned, setPinned }}>
<Center maxWidth="inherit">
<header className={styles.header}>{children}</header>
</Center>
</HeadroomProvider>
<Center maxWidth="inherit">
<header ref={ref} className={styles.header}>
{children}
</header>
</Center>
</Headroom>
);
};
Expand Down
35 changes: 21 additions & 14 deletions components/organisms/Header/NavItemWithChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,51 @@ import Subnavigation from "./Subnavigation";
import IconComposer from "@rubin-epo/epo-react-lib/IconComposer";
import { useKeyDownEvent, useFocusTrap } from "@/hooks";
import internalLinkShape, { internalLinkInternalShape } from "@/shapes/link";
import useNavigationMenu from "@/contexts/NavigationMenu";

export default function NavItemWithChildren({
id,
active,
parentActive,
title,
uri,
childItems,
onToggleClick,
onEsc,
theme,
baseClassName = "c-nav-list",
level = 2,
}) {
const { activeSubmenu, closeMenu, openMenu, close } = useNavigationMenu();
const isActive =
activeSubmenu.has(id) || childItems.find(({ id }) => activeSubmenu.has(id));
const ref = useRef(null);

useFocusTrap(ref, active);
useFocusTrap(ref, isActive);
useKeyDownEvent(handleKeyDown);

function handleKeyDown({ key }) {
if (!active || key !== "Escape") return;
onEsc();
if (!isActive || key !== "Escape") return;
close();
}

const handleToggle = () => {
if (isActive) {
closeMenu(id);
} else {
openMenu(id, true);
}
};

const parent = { id, title, uri };
const childrenWithParent = [parent].concat(childItems);

return (
<div ref={ref} className={`${baseClassName}__item-inner`}>
<button
onClick={() => onToggleClick(id)}
aria-expanded={active}
onClick={handleToggle}
aria-expanded={isActive}
aria-haspopup
className={classNames({
[`${baseClassName}__link`]: true,
[`${baseClassName}__link--is-active`]: active,
[`${baseClassName}__link--is-active`]: isActive,
[`${baseClassName}__link--${theme}`]: !!theme,
})}
>
Expand All @@ -53,8 +62,8 @@ export default function NavItemWithChildren({
</button>
<Subnavigation
items={childrenWithParent}
active={level === 3 ? parentActive && active : active}
onClick={onEsc}
active={level === 3 ? parentActive && isActive : isActive}
onClick={close}
theme={theme}
level={3}
baseClassName={level === 3 ? baseClassName : "c-subnav-list"}
Expand All @@ -68,11 +77,9 @@ NavItemWithChildren.displayName =

NavItemWithChildren.propTypes = {
...internalLinkInternalShape,
active: PropTypes.bool,
parentActive: PropTypes.bool,
childItems: PropTypes.arrayOf(internalLinkShape),
onToggleClick: PropTypes.func.isRequired,
onEsc: PropTypes.func.isRequired,
close: PropTypes.func.isRequired,
theme: PropTypes.oneOf(["desktop", "mobile"]),
baseClassName: PropTypes.string,
level: PropTypes.number,
Expand Down
22 changes: 5 additions & 17 deletions components/organisms/Header/Subnavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,20 @@
* Ignore false positives (key event exists on Navigation component; role attribute not needed
* since Link component validly handles href and window history, click just closes subnav)
*/
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import Link from "next/link";
import classNames from "classnames";
import internalLinkShape from "@/shapes/link";
import NavItemWithChildren from "./NavItemWithChildren";
import useNavigationMenu from "@/contexts/NavigationMenu";

export default function Subnavigation({
items,
active,
onClick,
theme,
baseClassName = "c-subnav-list",
level = 2,
}) {
const [activeSub, setActiveSub] = useState(null);

useEffect(() => {
if (!active) setActiveSub(null);
}, [active]);

function handleToggleClick(id) {
setActiveSub((prevActive) => (prevActive === id ? null : id));
}
const { activeSubmenu, closeMenu, close } = useNavigationMenu();

return (
<ul
Expand All @@ -37,7 +27,7 @@ export default function Subnavigation({
>
{items.map(({ id, title, uri, children }) => {
const hasChildren = children && children.length > 0;
const isActiveSub = id === activeSub;
const isActiveSub = activeSubmenu.has(id);

if (!uri && !hasChildren) return null;

Expand All @@ -50,8 +40,7 @@ export default function Subnavigation({
title={title}
uri={uri}
childItems={children}
onToggleClick={handleToggleClick}
onEsc={() => setActiveSub(null)}
onEsc={() => closeMenu(id)}
theme={theme}
baseClassName="c-sub-subnav-list"
level={3}
Expand All @@ -63,7 +52,7 @@ export default function Subnavigation({
href={`/${uri}`}
className={`${baseClassName}__link`}
tabIndex={active ? 0 : -1}
onClick={onClick}
onClick={close}
>
{title}
</Link>
Expand All @@ -80,7 +69,6 @@ Subnavigation.displayName = "Header.Navigation.Subnavigation";
Subnavigation.propTypes = {
items: PropTypes.arrayOf(internalLinkShape),
active: PropTypes.bool,
onClick: PropTypes.func,
theme: PropTypes.oneOf(["desktop", "mobile"]),
baseClassName: PropTypes.string,
level: PropTypes.number,
Expand Down
11 changes: 7 additions & 4 deletions components/organisms/Header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import PropTypes from "prop-types";
import CollapsibleHeader from "@/components/molecules/CollapsibleHeader";
import UpperHeader from "./Upper";
import LowerHeader from "./Lower";
import { NavigationMenuProvider } from "@/contexts/NavigationMenu";

export default function Header({ locale }) {
return (
<CollapsibleHeader>
<UpperHeader {...{ locale }} />
<LowerHeader {...{ locale }} />
</CollapsibleHeader>
<NavigationMenuProvider>
<CollapsibleHeader>
<UpperHeader {...{ locale }} />
<LowerHeader {...{ locale }} />
</CollapsibleHeader>
</NavigationMenuProvider>
);
}

Expand Down
42 changes: 6 additions & 36 deletions components/organisms/Header/navigation/Horizontal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
"use client";
import { FunctionComponent, useRef, useState } from "react";
import { FunctionComponent } from "react";
import classNames from "classnames";
import useHeadroom from "@/contexts/Headroom";
import useNavigationMenu from "@/contexts/NavigationMenu";
import NavItemWithChildren from "../../NavItemWithChildren";
import NavItem from "../../NavItem";
import { fallbackLng } from "@/lib/i18n/settings";
import { isDefaultLocale } from "@/lib/i18n";
import styles from "./styles.module.scss";
import { useOnClickOutside } from "@/hooks/listeners";

interface NavigationProps {
items: Array<InternalLinkWithChildren>;
Expand All @@ -20,54 +19,25 @@ const NavigationHorizontal: FunctionComponent<NavigationProps> = ({
locale = fallbackLng,
className,
}) => {
const ref = useRef(null);
const [active, setActive] = useState<string | null>(null);
const { setPinned } = useHeadroom();

function handleToggleClick(id: string) {
if (id === active) {
close();
} else {
open(id);
}
}

const open = (id: string) => {
setActive(id);
setPinned(true);
};

const close = () => {
setActive(null);
setPinned(false);
};

useOnClickOutside(ref, close);
const { close } = useNavigationMenu();

return (
<nav className={classNames(styles.horizontalNavigation, className)}>
<ul
ref={ref}
className={classNames("c-nav-list--desktop", styles.navigationList)}
>
<ul className={classNames("c-nav-list--desktop", styles.navigationList)}>
{items.map(({ id, title, uri, children }) => {
const hasChildren = children && children.length > 0;

return (
<li key={id} className={styles.navigationItem}>
{hasChildren && (
{hasChildren ? (
<NavItemWithChildren
id={id}
active={id === active}
title={title}
uri={uri}
childItems={children}
onToggleClick={handleToggleClick}
onEsc={close}
theme="desktop"
/>
)}
{!hasChildren && (
) : (
<NavItem
href={`${isDefaultLocale(locale) ? "" : `/${locale}`}/${uri}`}
onClick={close}
Expand Down
33 changes: 9 additions & 24 deletions components/organisms/Header/navigation/Vertical/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use client";
import { FunctionComponent, useState } from "react";
import { FunctionComponent, useCallback } from "react";
import classNames from "classnames";
import Slideout from "@rubin-epo/epo-react-lib/Slideout";
import { isDefaultLocale } from "@/lib/i18n";
import { fallbackLng } from "@/lib/i18n/settings";
import Hamburger from "@/components/atomic/Button/Hamburger";
import useHeadroom from "@/contexts/Headroom";
import useNavigationMenu from "@/contexts/NavigationMenu";
import NavItemWithChildren from "../../NavItemWithChildren";
import NavItem from "../../NavItem";
import styles from "./styles.module.scss";
Expand All @@ -21,28 +21,17 @@ const NavigationVertical: FunctionComponent<NavigationProps> = ({
locale = fallbackLng,
className,
}) => {
const [open, setOpen] = useState(false);
const [active, setActive] = useState<string | null>(null);
const { pinned, setPinned } = useHeadroom();
const { pinned, setPinned, open, setOpen, close } = useNavigationMenu();

const handleOpenToggle = () => {
const handleOpenToggle = useCallback(() => {
if (open) {
setOpen(false);
setPinned(false);
} else {
setOpen(true);
setPinned(true);
}
};

function handleToggleClick(id: string) {
if (active === id) {
setActive(null);
} else {
setPinned(true);
setActive(id);
}
}
}, [open, pinned]);

return (
<>
Expand All @@ -56,7 +45,6 @@ const NavigationVertical: FunctionComponent<NavigationProps> = ({
isOpen={open}
showBackground={false}
slideFrom="right"
onCloseCallback={() => setActive(null)}
>
<nav className={classNames(styles.verticalNavigation, className)}>
<ul className="c-nav-list--mobile">
Expand All @@ -65,24 +53,21 @@ const NavigationVertical: FunctionComponent<NavigationProps> = ({

return (
<li key={id} className="c-nav-list__item">
{hasChildren && (
{hasChildren ? (
<NavItemWithChildren
id={id}
active={open && id === active}
title={title}
uri={uri}
childItems={children}
onToggleClick={handleToggleClick}
onEsc={() => setActive(null)}
// onEsc={close}
theme="mobile"
/>
)}
{!hasChildren && (
) : (
<NavItem
href={`${
isDefaultLocale(locale) ? "" : `/${locale}`
}/${uri}`}
onClick={() => setActive(null)}
onClick={close}
title={title}
theme="mobile"
/>
Expand Down
Loading

0 comments on commit 27c3dcb

Please sign in to comment.