Skip to content

Commit 2f609aa

Browse files
committed
Add a Navigation component that auto-hides and stuff
1 parent daaa5c0 commit 2f609aa

File tree

4 files changed

+277
-0
lines changed

4 files changed

+277
-0
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import type { Meta, StoryObj } from "@storybook/react-vite";
2+
3+
import Navigation from ".";
4+
import { Placeholder } from "../Placeholder";
5+
import Header from "../Header";
6+
import ColorSchemeToggle from "../ColorSchemeToggle";
7+
8+
const meta = {
9+
title: "Molecules/Navigation",
10+
component: Navigation,
11+
argTypes: {
12+
initialState: {
13+
options: ["open", "closed"],
14+
},
15+
},
16+
} satisfies Meta<typeof Navigation>;
17+
18+
export default meta;
19+
type Story = StoryObj<typeof meta>;
20+
21+
export const Default: Story = {
22+
args: {
23+
initialState: undefined,
24+
},
25+
render: ({ initialState }) => (
26+
<>
27+
<div
28+
style={{
29+
height: "300px",
30+
width: "500px",
31+
border: "1px solid black",
32+
display: "flex",
33+
justifyContent: "end",
34+
}}
35+
>
36+
<div>
37+
<div>
38+
<Navigation initialState={initialState}>
39+
<Navigation.Element>
40+
<Placeholder height="100%" label="Primary Element" />
41+
</Navigation.Element>
42+
43+
<Navigation.Element>
44+
<Placeholder height="100%" label="Navigation Link" />
45+
</Navigation.Element>
46+
</Navigation>
47+
</div>
48+
</div>
49+
</div>
50+
</>
51+
),
52+
};
53+
54+
export const InHeader: Story = {
55+
args: {
56+
initialState: undefined,
57+
},
58+
render: ({ initialState }) => (
59+
<>
60+
<Header>
61+
<Header.Logo>
62+
<Placeholder height="100%" label="DetSys Logo" />
63+
</Header.Logo>
64+
<Header.Element>
65+
<ColorSchemeToggle />
66+
<Navigation initialState={initialState}>
67+
<Navigation.Element>
68+
<Placeholder height="100%" label="Lorem" />
69+
</Navigation.Element>
70+
71+
<Navigation.Element>
72+
<Placeholder height="100%" label="Ipsum" />
73+
</Navigation.Element>
74+
</Navigation>
75+
</Header.Element>
76+
</Header>
77+
</>
78+
),
79+
};
80+
81+
export const DefaultOpen: Story = {
82+
args: {
83+
initialState: "open",
84+
},
85+
render: ({ initialState }) => (
86+
<>
87+
<div
88+
style={{
89+
height: "300px",
90+
width: "500px",
91+
border: "1px solid black",
92+
display: "flex",
93+
justifyContent: "end",
94+
}}
95+
>
96+
<div>
97+
<div>
98+
<Navigation initialState={initialState}>
99+
<Navigation.Element>
100+
<Placeholder height="100%" label="Primary Element" />
101+
</Navigation.Element>
102+
103+
<Navigation.Element>
104+
<Placeholder height="100%" label="Navigation Link" />
105+
</Navigation.Element>
106+
</Navigation>
107+
</div>
108+
</div>
109+
</div>
110+
</>
111+
),
112+
};
113+
114+
export const InHeaderOpen: Story = {
115+
args: {
116+
initialState: "open",
117+
},
118+
render: ({ initialState }) => (
119+
<>
120+
<Header>
121+
<Header.Logo>
122+
<Placeholder height="100%" label="DetSys Logo" />
123+
</Header.Logo>
124+
<Header.Element>
125+
<ColorSchemeToggle />
126+
<Navigation initialState={initialState}>
127+
<Navigation.Element>
128+
<Placeholder height="100%" label="Lorem" />
129+
</Navigation.Element>
130+
131+
<Navigation.Element>
132+
<Placeholder height="100%" label="Ipsum" />
133+
</Navigation.Element>
134+
</Navigation>
135+
</Header.Element>
136+
</Header>
137+
</>
138+
),
139+
};

src/Navigation/index.scss

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
@use "sass:map";
2+
3+
@use "../sass/mixins";
4+
@use "../sass/tokens";
5+
@use "../sass/functions";
6+
7+
@media screen and (width >= 1024px) {
8+
.navigation {
9+
height: 24px; // Matches the svg-icon height used by the expander in closed mode
10+
padding: 4px; // Matches the svg-icon height used by the expander in closed mode
11+
.navigation__expand {
12+
display: none;
13+
}
14+
15+
.navigation__elements {
16+
height: 24px;
17+
display: flex;
18+
flex-direction: row;
19+
gap: 1em;
20+
}
21+
}
22+
}
23+
24+
@media screen and (width < 1024px) {
25+
.navigation--closed .navigation__elements {
26+
visibility: hidden;
27+
}
28+
.navigation--open .navigation__elements {
29+
visibility: visible;
30+
}
31+
32+
.navigation {
33+
position: relative;
34+
35+
.navigation__expand {
36+
@include mixins.svg-button;
37+
}
38+
39+
.navigation__elements {
40+
display: flex;
41+
flex-direction: column;
42+
gap: 1em;
43+
44+
max-width: 100vu;
45+
46+
position: absolute;
47+
transform-origin: 100% 0;
48+
z-index: 10;
49+
right: 0;
50+
51+
margin-top: 0.5em;
52+
53+
@include mixins.pad(base);
54+
@include mixins.border(base);
55+
56+
background-color: map.get(tokens.$brand, gray);
57+
@include mixins.light-mode {
58+
background-color: map.get(tokens.$brand, light);
59+
}
60+
}
61+
62+
.navigation__element {
63+
width: 320px;
64+
65+
@media screen and (width < 650px) {
66+
width: 268px;
67+
}
68+
}
69+
}
70+
}

src/Navigation/index.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { useState, type FC } from "react";
2+
3+
import "./index.scss";
4+
import React from "react";
5+
import clsx from "clsx";
6+
import { Bars3Icon } from "@heroicons/react/24/outline";
7+
8+
export interface NavigationProps {
9+
initialState?: "open" | "closed";
10+
}
11+
12+
const Root: FC<React.PropsWithChildren<NavigationProps>> = ({
13+
children,
14+
initialState = "closed",
15+
}) => {
16+
let elements: React.ReactElement[] = [];
17+
18+
const [menuState, setMenuState] = useState<boolean>(initialState == "open");
19+
20+
React.Children.forEach(children, (child) => {
21+
if (!React.isValidElement(child)) return;
22+
if (child.type === Navigation.Element) {
23+
elements.push(child);
24+
} else {
25+
console.log("Dropping unknown children of Header.");
26+
}
27+
});
28+
29+
const toggleNavigation = () => {
30+
setMenuState(!menuState);
31+
};
32+
33+
return (
34+
<div
35+
className={clsx(
36+
"navigation",
37+
menuState ? "navigation--open" : "navigation--closed",
38+
)}
39+
>
40+
<button
41+
className="navigation__expand"
42+
aria-label="Open navigation"
43+
onClick={toggleNavigation}
44+
>
45+
<Bars3Icon />
46+
</button>
47+
<div className="navigation__elements">{elements}</div>
48+
</div>
49+
);
50+
};
51+
52+
export interface NavigationElementProps {}
53+
const Element: FC<React.PropsWithChildren<NavigationElementProps>> = ({
54+
children,
55+
}) => {
56+
return <div className="navigation__element">{children}</div>;
57+
};
58+
59+
const Navigation = Object.assign(Root, {
60+
Element,
61+
});
62+
63+
export default Navigation;

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export {
4444
default as GitHubButton,
4545
} from "./GitHubButton";
4646
export { type HeaderProps, default as Header } from "./Header";
47+
export {
48+
type NavigationProps,
49+
type NavigationElementProps,
50+
default as Navigation,
51+
} from "./Navigation";
4752
export {
4853
type PageLayoutProps,
4954
type PageLayoutHeaderProps,

0 commit comments

Comments
 (0)