Skip to content

Commit d0519bf

Browse files
authored
Merge pull request #348 from processing/nav-state
Nav state
2 parents 9a1c2bc + 4e0de9e commit d0519bf

5 files changed

Lines changed: 129 additions & 51 deletions

File tree

src/components/Nav/JumpToLinks.tsx

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,36 @@
11
import styles from "./styles.module.scss";
2-
import { useEffect, useState } from "preact/hooks";
32
import { Icon } from "../Icon";
43
import type { JumpToLink } from "@/src/globals/state";
54

65
type JumpToLinksProps = {
76
links?: JumpToLink[];
87
heading: string;
8+
handleToggle: () => void;
9+
isOpen: boolean;
910
};
1011

11-
export const JumpToLinks = ({ links, heading }: JumpToLinksProps) => {
12-
const [open, setOpen] = useState(true);
13-
14-
const handleClick = () => {
15-
setOpen(!open);
16-
};
17-
18-
// Defaults to closed on mobile, open on desktop
19-
// Have to do this in a lifecycle method
20-
// so that we can still server-side render
21-
useEffect(() => {
22-
const isMobile = window.innerWidth <= 768;
23-
setOpen(!isMobile);
24-
}, []);
25-
12+
export const JumpToLinks = ({
13+
links,
14+
heading,
15+
isOpen,
16+
handleToggle,
17+
}: JumpToLinksProps) => {
2618
if (!links || links?.length <= 0) return null;
2719

2820
return (
29-
<div class={`${styles.jumpto} ${open && "open"}`}>
21+
<div class={`${styles.jumpto} ${isOpen && "open"}`}>
3022
<button
3123
class={styles.toggle}
32-
onClick={handleClick}
24+
onClick={handleToggle}
3325
aria-hidden="true"
3426
tabIndex={-1}
3527
>
3628
<span>{heading}</span>
3729
<div class="pt-xs">
38-
<Icon kind={open ? "chevron-up" : "chevron-down"} />
30+
<Icon kind={isOpen ? "chevron-up" : "chevron-down"} />
3931
</div>
4032
</button>
41-
{open && (
33+
{isOpen && (
4234
<ul>
4335
{links?.map((link) => (
4436
<li

src/components/Nav/MainNavLinks.tsx

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import styles from "./styles.module.scss";
22
import { Logo } from "../Logo";
33
import { Icon } from "../Icon";
4-
import { useEffect, useState } from "preact/hooks";
54

65
type MainNavLinksProps = {
76
links: {
@@ -13,6 +12,8 @@ type MainNavLinksProps = {
1312
mobileMenuLabel: string;
1413
isHomepage: boolean;
1514
hasJumpTo: boolean;
15+
handleToggle: () => void;
16+
isOpen: boolean;
1617
};
1718

1819
export const MainNavLinks = ({
@@ -21,24 +22,10 @@ export const MainNavLinks = ({
2122
editorButtonLabel,
2223
mobileMenuLabel,
2324
isHomepage = false,
25+
handleToggle,
26+
isOpen,
2427
hasJumpTo,
2528
}: MainNavLinksProps) => {
26-
const [isMobile, setIsMobile] = useState(false);
27-
const [open, setOpen] = useState(!isMobile);
28-
29-
const handleClick = () => {
30-
setOpen(!open);
31-
};
32-
33-
// Defaults to closed on mobile, open on desktop
34-
// Have to do this in a lifecycle method
35-
// so that we can still server-side render
36-
useEffect(() => {
37-
const _isMobile = window.innerWidth < 768;
38-
setIsMobile(_isMobile);
39-
setOpen(!_isMobile);
40-
}, []);
41-
4229
if (!links || links?.length <= 0) return null;
4330

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

5845
<button
5946
class={styles.toggle}
60-
onClick={handleClick}
47+
onClick={handleToggle}
6148
aria-hidden="true"
6249
tabIndex={-1}
6350
>
6451
<div class={styles.mobileMenuLabel}>
65-
{open ? (
52+
{isOpen ? (
6653
<Icon kind="close" />
6754
) : (
6855
<>
@@ -72,15 +59,15 @@ export const MainNavLinks = ({
7259
)}
7360
</div>
7461
<span class={styles.desktopMenuLabel}>
75-
<Icon kind={open ? "chevron-up" : "chevron-down"} />
62+
<Icon kind={isOpen ? "chevron-up" : "chevron-down"} />
7663
</span>
7764
</button>
7865
</div>
7966
);
8067

8168
return (
8269
<div
83-
class={`${styles.mainlinks} ${open && "open"} ${
70+
class={`${styles.mainlinks} ${isOpen && "open"} ${
8471
!hasJumpTo && "noJumpTo"
8572
}`}
8673
>

src/components/Nav/NavPanels.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type { JumpToState } from "@/src/globals/state";
2+
import { JumpToLinks } from "./JumpToLinks";
3+
import { MainNavLinks } from "./MainNavLinks";
4+
import { useEffect, useState } from "preact/hooks";
5+
6+
interface NavPanelsProps {
7+
mainLinks: {
8+
label: string;
9+
url: string;
10+
}[];
11+
editorButtonLabel: string;
12+
donateButtonLabel: string;
13+
mobileMenuLabel: string;
14+
jumpToLabel: string;
15+
isHomepage: boolean;
16+
jumpToState: JumpToState | null;
17+
}
18+
19+
/**
20+
* This component primarily exists to manage open/closed state between
21+
* the two link menus, which behaves differently on mobile than on desktop.
22+
*
23+
*/
24+
export const NavPanels = (props: NavPanelsProps) => {
25+
const {
26+
mainLinks,
27+
isHomepage,
28+
editorButtonLabel,
29+
donateButtonLabel,
30+
mobileMenuLabel,
31+
jumpToLabel,
32+
jumpToState,
33+
} = props;
34+
35+
const [isOpen, setIsOpen] = useState({ main: false, jump: false });
36+
const [isMobile, setIsMobile] = useState(true);
37+
38+
// Defaults to closed on mobile, open on desktop
39+
// Have to do this in a lifecycle method
40+
// so that we can still server-side render
41+
useEffect(() => {
42+
const startsMobile = window.innerWidth < 768;
43+
setIsMobile(startsMobile);
44+
setIsOpen({ main: !startsMobile, jump: !startsMobile });
45+
// We use a resize observer to the user's window crosses the
46+
// threshhold between mobile and desktop
47+
const documentObserver = new ResizeObserver((entries) => {
48+
for (const entry of entries) {
49+
if (!isMobile && entry.contentRect.width < 768) {
50+
setIsMobile(true);
51+
setIsOpen({
52+
main: false,
53+
jump: false,
54+
});
55+
} else if (isMobile && entry.contentRect.width >= 768) {
56+
setIsMobile(false);
57+
setIsOpen({
58+
main: true,
59+
jump: true,
60+
});
61+
}
62+
}
63+
});
64+
documentObserver.observe(document.body);
65+
return () => documentObserver.disconnect();
66+
}, [setIsMobile, setIsOpen, isMobile]);
67+
68+
const handleMainNavToggle = () => {
69+
setIsOpen((prev) => ({
70+
main: !prev.main,
71+
jump: isMobile ? false : prev.jump || prev.main,
72+
}));
73+
};
74+
75+
const handleJumpToToggle = () => {
76+
setIsOpen((prev) => ({
77+
jump: !prev.jump,
78+
main: isMobile ? false : prev.main || prev.jump,
79+
}));
80+
};
81+
82+
return (
83+
<>
84+
<MainNavLinks
85+
links={mainLinks}
86+
isHomepage={isHomepage}
87+
editorButtonLabel={editorButtonLabel}
88+
donateButtonLabel={donateButtonLabel}
89+
mobileMenuLabel={mobileMenuLabel}
90+
hasJumpTo={jumpToState !== null}
91+
isOpen={isOpen.main}
92+
handleToggle={handleMainNavToggle}
93+
/>
94+
<JumpToLinks
95+
heading={jumpToState?.heading || jumpToLabel}
96+
links={jumpToState?.links}
97+
isOpen={isOpen.jump}
98+
handleToggle={handleJumpToToggle}
99+
/>
100+
</>
101+
);
102+
};

src/components/Nav/index.astro

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
---
22
import { getCurrentLocale, getUiTranslator } from "@/src/i18n/utils";
33
import { jumpToState } from "@/src/globals/state";
4-
import { JumpToLinks } from "./JumpToLinks";
5-
import { MainNavLinks } from "./MainNavLinks";
64
import styles from "./styles.module.scss";
5+
import { NavPanels } from "./NavPanels";
76
87
const currentLocale = getCurrentLocale(Astro.url.pathname);
98
// We force the logo to the accent color only on the homepage
@@ -23,24 +22,21 @@ const mainLinks = [
2322
const editorButtonLabel = t("Start Coding");
2423
const donateButtonLabel = t("Donate");
2524
const mobileMenuLabel = t("Menu");
25+
const jumpToLabel = t("Jump To");
2626
---
2727

2828
<nav class={styles.container}>
2929
<a href="#main-content" class="skip-to-main">
3030
{t("Skip to main content")}
3131
</a>
32-
<MainNavLinks
33-
links={mainLinks}
32+
<NavPanels
33+
mainLinks={mainLinks}
3434
isHomepage={isHomepage}
3535
editorButtonLabel={editorButtonLabel as string}
3636
donateButtonLabel={donateButtonLabel as string}
3737
mobileMenuLabel={mobileMenuLabel as string}
38-
hasJumpTo={jumpToState !== null}
39-
client:load
40-
/>
41-
<JumpToLinks
42-
heading={jumpToState?.heading || (t("Jump To") as string)}
43-
links={jumpToState?.links}
38+
jumpToLabel={jumpToLabel as string}
39+
jumpToState={jumpToState}
4440
client:load
4541
/>
4642
</nav>

src/components/Nav/styles.module.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
@media (min-width: $breakpoint-tablet) {
157157
height: 80px;
158158
border-top-width: 1px;
159+
margin-top: auto;
159160

160161
&:global(.open) {
161162
height: 100%;

0 commit comments

Comments
 (0)