Skip to content

Commit

Permalink
Merge pull request #348 from processing/nav-state
Browse files Browse the repository at this point in the history
Nav state
  • Loading branch information
outofambit committed May 9, 2024
2 parents 9a1c2bc + 4e0de9e commit d0519bf
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 51 deletions.
32 changes: 12 additions & 20 deletions src/components/Nav/JumpToLinks.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,36 @@
import styles from "./styles.module.scss";
import { useEffect, useState } from "preact/hooks";
import { Icon } from "../Icon";
import type { JumpToLink } from "@/src/globals/state";

type JumpToLinksProps = {
links?: JumpToLink[];
heading: string;
handleToggle: () => void;
isOpen: boolean;
};

export const JumpToLinks = ({ links, heading }: JumpToLinksProps) => {
const [open, setOpen] = useState(true);

const handleClick = () => {
setOpen(!open);
};

// Defaults to closed on mobile, open on desktop
// Have to do this in a lifecycle method
// so that we can still server-side render
useEffect(() => {
const isMobile = window.innerWidth <= 768;
setOpen(!isMobile);
}, []);

export const JumpToLinks = ({
links,
heading,
isOpen,
handleToggle,
}: JumpToLinksProps) => {
if (!links || links?.length <= 0) return null;

return (
<div class={`${styles.jumpto} ${open && "open"}`}>
<div class={`${styles.jumpto} ${isOpen && "open"}`}>
<button
class={styles.toggle}
onClick={handleClick}
onClick={handleToggle}
aria-hidden="true"
tabIndex={-1}
>
<span>{heading}</span>
<div class="pt-xs">
<Icon kind={open ? "chevron-up" : "chevron-down"} />
<Icon kind={isOpen ? "chevron-up" : "chevron-down"} />
</div>
</button>
{open && (
{isOpen && (
<ul>
{links?.map((link) => (
<li
Expand Down
29 changes: 8 additions & 21 deletions src/components/Nav/MainNavLinks.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import styles from "./styles.module.scss";
import { Logo } from "../Logo";
import { Icon } from "../Icon";
import { useEffect, useState } from "preact/hooks";

type MainNavLinksProps = {
links: {
Expand All @@ -13,6 +12,8 @@ type MainNavLinksProps = {
mobileMenuLabel: string;
isHomepage: boolean;
hasJumpTo: boolean;
handleToggle: () => void;
isOpen: boolean;
};

export const MainNavLinks = ({
Expand All @@ -21,24 +22,10 @@ export const MainNavLinks = ({
editorButtonLabel,
mobileMenuLabel,
isHomepage = false,
handleToggle,
isOpen,
hasJumpTo,
}: MainNavLinksProps) => {
const [isMobile, setIsMobile] = useState(false);
const [open, setOpen] = useState(!isMobile);

const handleClick = () => {
setOpen(!open);
};

// Defaults to closed on mobile, open on desktop
// Have to do this in a lifecycle method
// so that we can still server-side render
useEffect(() => {
const _isMobile = window.innerWidth < 768;
setIsMobile(_isMobile);
setOpen(!_isMobile);
}, []);

if (!links || links?.length <= 0) return null;

const renderLogo = () => (
Expand All @@ -57,12 +44,12 @@ export const MainNavLinks = ({

<button
class={styles.toggle}
onClick={handleClick}
onClick={handleToggle}
aria-hidden="true"
tabIndex={-1}
>
<div class={styles.mobileMenuLabel}>
{open ? (
{isOpen ? (
<Icon kind="close" />
) : (
<>
Expand All @@ -72,15 +59,15 @@ export const MainNavLinks = ({
)}
</div>
<span class={styles.desktopMenuLabel}>
<Icon kind={open ? "chevron-up" : "chevron-down"} />
<Icon kind={isOpen ? "chevron-up" : "chevron-down"} />
</span>
</button>
</div>
);

return (
<div
class={`${styles.mainlinks} ${open && "open"} ${
class={`${styles.mainlinks} ${isOpen && "open"} ${
!hasJumpTo && "noJumpTo"
}`}
>
Expand Down
102 changes: 102 additions & 0 deletions src/components/Nav/NavPanels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { JumpToState } from "@/src/globals/state";
import { JumpToLinks } from "./JumpToLinks";
import { MainNavLinks } from "./MainNavLinks";
import { useEffect, useState } from "preact/hooks";

interface NavPanelsProps {
mainLinks: {
label: string;
url: string;
}[];
editorButtonLabel: string;
donateButtonLabel: string;
mobileMenuLabel: string;
jumpToLabel: string;
isHomepage: boolean;
jumpToState: JumpToState | null;
}

/**
* This component primarily exists to manage open/closed state between
* the two link menus, which behaves differently on mobile than on desktop.
*
*/
export const NavPanels = (props: NavPanelsProps) => {
const {
mainLinks,
isHomepage,
editorButtonLabel,
donateButtonLabel,
mobileMenuLabel,
jumpToLabel,
jumpToState,
} = props;

const [isOpen, setIsOpen] = useState({ main: false, jump: false });
const [isMobile, setIsMobile] = useState(true);

// Defaults to closed on mobile, open on desktop
// Have to do this in a lifecycle method
// so that we can still server-side render
useEffect(() => {
const startsMobile = window.innerWidth < 768;
setIsMobile(startsMobile);
setIsOpen({ main: !startsMobile, jump: !startsMobile });
// We use a resize observer to the user's window crosses the
// threshhold between mobile and desktop
const documentObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (!isMobile && entry.contentRect.width < 768) {
setIsMobile(true);
setIsOpen({
main: false,
jump: false,
});
} else if (isMobile && entry.contentRect.width >= 768) {
setIsMobile(false);
setIsOpen({
main: true,
jump: true,
});
}
}
});
documentObserver.observe(document.body);
return () => documentObserver.disconnect();
}, [setIsMobile, setIsOpen, isMobile]);

const handleMainNavToggle = () => {
setIsOpen((prev) => ({
main: !prev.main,
jump: isMobile ? false : prev.jump || prev.main,
}));
};

const handleJumpToToggle = () => {
setIsOpen((prev) => ({
jump: !prev.jump,
main: isMobile ? false : prev.main || prev.jump,
}));
};

return (
<>
<MainNavLinks
links={mainLinks}
isHomepage={isHomepage}
editorButtonLabel={editorButtonLabel}
donateButtonLabel={donateButtonLabel}
mobileMenuLabel={mobileMenuLabel}
hasJumpTo={jumpToState !== null}
isOpen={isOpen.main}
handleToggle={handleMainNavToggle}
/>
<JumpToLinks
heading={jumpToState?.heading || jumpToLabel}
links={jumpToState?.links}
isOpen={isOpen.jump}
handleToggle={handleJumpToToggle}
/>
</>
);
};
16 changes: 6 additions & 10 deletions src/components/Nav/index.astro
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
---
import { getCurrentLocale, getUiTranslator } from "@/src/i18n/utils";
import { jumpToState } from "@/src/globals/state";
import { JumpToLinks } from "./JumpToLinks";
import { MainNavLinks } from "./MainNavLinks";
import styles from "./styles.module.scss";
import { NavPanels } from "./NavPanels";
const currentLocale = getCurrentLocale(Astro.url.pathname);
// We force the logo to the accent color only on the homepage
Expand All @@ -23,24 +22,21 @@ const mainLinks = [
const editorButtonLabel = t("Start Coding");
const donateButtonLabel = t("Donate");
const mobileMenuLabel = t("Menu");
const jumpToLabel = t("Jump To");
---

<nav class={styles.container}>
<a href="#main-content" class="skip-to-main">
{t("Skip to main content")}
</a>
<MainNavLinks
links={mainLinks}
<NavPanels
mainLinks={mainLinks}
isHomepage={isHomepage}
editorButtonLabel={editorButtonLabel as string}
donateButtonLabel={donateButtonLabel as string}
mobileMenuLabel={mobileMenuLabel as string}
hasJumpTo={jumpToState !== null}
client:load
/>
<JumpToLinks
heading={jumpToState?.heading || (t("Jump To") as string)}
links={jumpToState?.links}
jumpToLabel={jumpToLabel as string}
jumpToState={jumpToState}
client:load
/>
</nav>
1 change: 1 addition & 0 deletions src/components/Nav/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
@media (min-width: $breakpoint-tablet) {
height: 80px;
border-top-width: 1px;
margin-top: auto;

&:global(.open) {
height: 100%;
Expand Down

0 comments on commit d0519bf

Please sign in to comment.