Skip to content

Commit

Permalink
[F] 3-level header nav
Browse files Browse the repository at this point in the history
  • Loading branch information
Blake Mason authored and blnkt committed Apr 17, 2024
1 parent 648eb91 commit 5439030
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 57 deletions.
26 changes: 19 additions & 7 deletions components/global/Header/NavItemWithChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import internalLinkShape, { internalLinkInternalShape } from "@/shapes/link";
export default function NavItemWithChildren({
id,
active,
parentActive,
title,
uri,
childItems,
onToggleClick,
onEsc,
theme,
baseClassName = "c-nav-list",
level = 2,
}) {
const ref = useRef(null);

Expand All @@ -30,24 +33,30 @@ export default function NavItemWithChildren({
const childrenWithParent = [parent].concat(childItems);

return (
<div ref={ref}>
<div ref={ref} className={`${baseClassName}__item-inner`}>
<button
onClick={() => onToggleClick(id)}
aria-expanded={active}
aria-haspopup
className={classNames({
"c-nav-list__link": true,
"c-nav-list__link--is-active": active,
[`c-nav-list__link--${theme}`]: !!theme,
[`${baseClassName}__link`]: true,
[`${baseClassName}__link--is-active`]: active,
[`${baseClassName}__link--${theme}`]: !!theme,
})}
>
<IconComposer icon="ChevronThin" className="c-nav-list__link-icon" />
<span className="c-nav-list__link-text">{title}</span>
<IconComposer
icon="ChevronThin"
className={`${baseClassName}__link-icon`}
/>
<span className={`${baseClassName}__link-text`}>{title}</span>
</button>
<Subnavigation
items={childrenWithParent}
active={active}
active={level === 3 ? parentActive && active : active}
onClick={onEsc}
theme={theme}
level={3}
baseClassName={level === 3 ? baseClassName : "c-subnav-list"}
/>
</div>
);
Expand All @@ -59,8 +68,11 @@ NavItemWithChildren.displayName =
NavItemWithChildren.propTypes = {
...internalLinkInternalShape,
active: PropTypes.bool,
parentActive: PropTypes.bool,
childItems: PropTypes.arrayOf(internalLinkShape),
onToggleClick: PropTypes.func.isRequired,
onEsc: PropTypes.func.isRequired,
theme: PropTypes.oneOf(["desktop", "mobile"]),
baseClassName: PropTypes.string,
level: PropTypes.number,
};
9 changes: 8 additions & 1 deletion components/global/Header/Navigation.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useRef } from "react";
import { useEffect, useState, useRef } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
Expand All @@ -13,6 +13,7 @@ export default function Navigation({
items,
userProfilePage,
theme,
isOpenSetter,
mobileActive,
mobileSetter,
}) {
Expand All @@ -27,12 +28,17 @@ export default function Navigation({

useClickEvent(handleClick);

useEffect(() => {
if (isOpenSetter) isOpenSetter(!!active);
}, [mobileSetter, isOpenSetter, active]);

function handleClick(e) {
const isLink =
e.target.nodeName === "A" || e.target.parentElement?.nodeName === "A";
const isMobileLangSelect = e.target.id === mobileLangSelectId;
if (mobileSetter && mobileActive && (isLink || isMobileLangSelect)) {
mobileSetter(false);
setActive(null);
}
if (navList?.current?.contains(e.target)) return;
setActive(null);
Expand Down Expand Up @@ -159,6 +165,7 @@ Navigation.propTypes = {
items: PropTypes.arrayOf(internalLinkWithChildrenShape),
userProfilePage: PropTypes.object,
theme: PropTypes.oneOf(["desktop", "mobile"]),
isOpenSetter: PropTypes.func,
mobileActive: PropTypes.bool,
mobileSetter: PropTypes.func,
};
Expand Down
86 changes: 65 additions & 21 deletions components/global/Header/Subnavigation.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,75 @@
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
/*
* 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";

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));
}

export default function Subnavigation({ items, active, onClick }) {
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
/*
* 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)
*/
return (
<ul
className={classNames({
"c-subnav-list": true,
"c-subnav-list--is-active": active,
[baseClassName]: true,
[`${baseClassName}--is-active`]: active,
})}
>
{items.map(({ id, title, uri }) => (
<li key={id} className="c-subnav-list__item">
<Link
prefetch={false}
href={`/${uri}`}
className="c-subnav-list__link"
tabIndex={active ? 0 : -1}
onClick={onClick}
>
{title}
</Link>
</li>
))}
{items.map(({ id, title, uri, children }) => {
const hasChildren = children && children.length > 0;
const isActiveSub = id === activeSub;

return (
<li key={id} className={`${baseClassName}__item`}>
{hasChildren && (
<NavItemWithChildren
id={id}
active={active && isActiveSub}
title={title}
uri={uri}
childItems={children}
onToggleClick={handleToggleClick}
onEsc={() => setActiveSub(null)}
theme={theme}
baseClassName="c-sub-subnav-list"
level={3}
parentActive={active}
/>
)}
{!hasChildren && (
<Link
prefetch={false}
href={`/${uri}`}
className={`${baseClassName}__link`}
tabIndex={active ? 0 : -1}
onClick={onClick}
>
{title}
</Link>
)}
</li>
);
})}
</ul>
);
}
Expand All @@ -39,4 +80,7 @@ Subnavigation.propTypes = {
items: PropTypes.arrayOf(internalLinkShape),
active: PropTypes.bool,
onClick: PropTypes.func,
theme: PropTypes.oneOf(["desktop", "mobile"]),
baseClassName: PropTypes.string,
level: PropTypes.number,
};
3 changes: 3 additions & 0 deletions components/global/Header/_header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ $_breakpoint: functions.header(layout-breakpoint);

&__search-block {
grid-area: search;
background-color: functions.palette(turquoise60);

@include base.respond($_breakpoint) {
justify-self: end;
Expand All @@ -70,6 +71,7 @@ $_breakpoint: functions.header(layout-breakpoint);
display: grid;
grid-auto-flow: column;
place-content: center;
background-color: functions.palette(turquoise60);

fieldset {
border: 0;
Expand All @@ -85,6 +87,7 @@ $_breakpoint: functions.header(layout-breakpoint);
grid-area: user-nav;
align-self: center;
padding-right: 20px;
background-color: functions.palette(turquoise60);

@include base.respond($_breakpoint) {
padding-left: 0.65em;
Expand Down
22 changes: 10 additions & 12 deletions components/global/Header/_navigation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,11 @@ $_breakpoint: functions.header(layout-breakpoint);
$_link-gap: 14px;

.c-nav-list {
@include base.fluid-scale(
nav-width,
functions.header(nav-width-desktop),
functions.header(nav-width-mobile),
functions.break(tablet),
functions.break(mobile)
);
@include base.fluid-scale(
font-size,
20px,
16px,
functions.break(tablet),
14px,
$_breakpoint,
functions.break(mobile)
);

Expand Down Expand Up @@ -67,12 +60,16 @@ $_link-gap: 14px;
}

&__lang {
width: var(--nav-width);
width: functions.header(nav-width-desktop);
max-width: 53vw;
padding-left: functions.header(nav-link-padding-lateral) * 1.5;
padding-right: functions.header(nav-link-padding-lateral);
background-color: functions.palette(turquoise60);

@include base.respond($_breakpoint) {
width: functions.header(nav-width-mobile);
}

fieldset {
height: var(--header-height);

Expand All @@ -83,7 +80,6 @@ $_link-gap: 14px;
}

&__link {
height: var(--header-height);
padding-right: functions.header(nav-link-padding-lateral);
padding-left: functions.header(nav-link-padding-lateral);
text-decoration: none;
Expand All @@ -101,15 +97,17 @@ $_link-gap: 14px;
display: flex;
flex-direction: row-reverse;
align-items: center;
height: var(--header-height);
white-space: nowrap;
}

&--mobile {
display: grid;
grid-template: "icon text" auto / 24px auto;
gap: $_link-gap;
width: var(--nav-width);
width: functions.header(nav-width-mobile);
max-width: 53vw;
padding: functions.header(nav-link-padding-lateral);
}
}

Expand Down
18 changes: 13 additions & 5 deletions components/global/Header/_subnavigation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,17 @@ $_bg-color: functions.sass-palette(neutral02);
.c-subnav-list {
position: absolute;
z-index: -5;
width: var(--nav-width);
max-width: 47vw;
max-height: calc(100vh - var(--header-height));
overflow: auto;
color: $_color;
background-color: $_bg-color;
visibility: hidden;
transition: transform 0.25s ease-in-out, visibility 0s 0.25s;

.c-nav-list--mobile & {
top: 0;
left: 0;
width: functions.header(nav-width-mobile);
height: 100%;

&--is-active {
Expand All @@ -30,6 +29,7 @@ $_bg-color: functions.sass-palette(neutral02);
.c-nav-list--desktop & {
top: 100%;
left: 0;
width: functions.header(nav-width-desktop);
transform: translateY(-100%);

&--is-active {
Expand All @@ -51,24 +51,32 @@ $_bg-color: functions.sass-palette(neutral02);
height: 1px;
content: "";
background-color: currentcolor;

.c-nav-list--mobile & {
left: functions.header(nav-link-padding-lateral) / 2;
width: calc(100% - #{2 * functions.header(nav-link-padding-lateral) / 2});
}
}

}

&__link {
display: flex;
align-items: center;
padding: 1.688em functions.header(nav-link-padding-lateral);
padding: functions.header(nav-link-padding-lateral);
text-decoration: none;
background-color: $_bg-color;
transition: color 0.2s, background-color 0.2s;

&:hover,
&:focus-visible {
&:focus-visible,
&--is-active {
color: functions.palette(white);
background-color: functions.palette(turquoise50);
}

.c-nav-list--mobile & {
height: functions.header(height);
padding: 0.8em functions.header(nav-link-padding-lateral);
}
}
}
Loading

0 comments on commit 5439030

Please sign in to comment.