Skip to content

Commit 65d3044

Browse files
committed
nav
1 parent 9569b27 commit 65d3044

File tree

3 files changed

+259
-114
lines changed

3 files changed

+259
-114
lines changed

src/components/Header.astro

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
2-
import { promises as fs } from "fs";
3-
import { NavItems } from "@components/nav-items";
2+
import NavItems from "@components/NavItems.astro";
43
import HeaderActions from "@components/header/header-actions.astro";
54
import HeaderLogo from "@components/header/header-logo.astro";
65
import Search from "@components/Search.astro";

src/components/NavItems.astro

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
---
2+
import Icon from "@ui/Icon.astro";
3+
import clsx from "clsx";
4+
5+
type Item = {
6+
name: string;
7+
path?: string;
8+
items?: Item[];
9+
};
10+
11+
interface Props {
12+
items: Item[];
13+
inverted?: boolean;
14+
level?: number;
15+
}
16+
17+
const { items, inverted = false, level = 1 } = Astro.props;
18+
---
19+
20+
<ul
21+
class={clsx(
22+
level === 1 ? "flex flex-col xl:flex-row" : "",
23+
level === 2 ? "mb-3 xl:bg-secondary xl:mt-2 xl:rounded-[30px] xl:min-w-[220px]" : "",
24+
level === 3 ? "bg-[#1c294b] xl:rounded-[20px] xl:my-1 xl:mx-2 xl:min-w-[200px]" : "",
25+
level > 1 ? "shadow-lg" : ""
26+
)}
27+
role={level === 1 ? "menubar" : "menu"}
28+
aria-label={level === 1 ? "Main navigation" : level === 2 ? "Submenu" : "Sub-submenu"}
29+
>
30+
{items.map((item) => (
31+
<li
32+
class={clsx(
33+
"relative",
34+
level === 1 ? "border-b border-border-secondary-dark xl:border-0 group" : "",
35+
level === 2 ? "bg-secondary text-white block w-full font-bold text-center xl:text-left xl:hover:bg-secondary-light xl:first:rounded-t-[30px] xl:first:pt-2 xl:last:rounded-b-[30px] xl:last:pb-2 group/level2" : "",
36+
level === 3 ? "text-white xl:hover:bg-secondary-light xl:first:rounded-t-[20px] xl:last:rounded-b-[20px]" : "",
37+
{
38+
"text-white": inverted,
39+
"text-primary": !inverted && level === 1,
40+
}
41+
)}
42+
role="none"
43+
>
44+
{!item.items ? (
45+
<a
46+
href={item.path || "#"}
47+
class={clsx(
48+
"flex items-center justify-between",
49+
level === 1 ? "font-bold inline-block w-full text-3xl xl:text-base p-5 text-center xl:text-left xl:p-2 xl:px-5 xl:group-hover:bg-secondary xl:group-hover:rounded-[30px] xl:group-hover:text-white xl:group-focus:bg-secondary xl:group-focus:rounded-[30px] xl:group-focus:text-white" : "",
50+
level === 2 ? "block w-full font-bold text-center xl:text-left mb-[2px] p-2 xl:px-5" : "",
51+
level === 3 ? "block w-full p-2 xl:px-4 text-sm xl:text-left" : ""
52+
)}
53+
rel={item.path?.startsWith("http") ? "nofollow noopener" : undefined}
54+
target={item.path?.startsWith("http") ? "_blank" : undefined}
55+
role="menuitem"
56+
>
57+
<span>{item.name}</span>
58+
{item.path?.startsWith("http") && (
59+
<span class="ml-1" aria-hidden="true">↗</span>
60+
)}
61+
</a>
62+
) : (
63+
<button
64+
type="button"
65+
aria-haspopup="true"
66+
aria-expanded="false"
67+
class={clsx(
68+
"flex items-center justify-between",
69+
level === 1 ? "font-bold inline-block w-full text-3xl xl:text-base p-5 text-center xl:text-left xl:p-2 xl:px-5 xl:group-hover:bg-secondary xl:group-hover:rounded-[30px] xl:group-hover:text-white xl:group-focus:bg-secondary xl:group-focus:rounded-[30px] xl:group-focus:text-white" : "",
70+
level === 2 ? "block w-full font-bold text-center xl:text-left mb-[2px] p-2 xl:px-5" : "",
71+
level === 3 ? "block w-full p-2 xl:px-4 text-sm xl:text-left" : ""
72+
)}
73+
role="menuitem"
74+
>
75+
<span>{item.name}</span>
76+
{level === 2 && (
77+
<span aria-hidden="true">
78+
<Icon name="caret-right" />
79+
</span>
80+
)}
81+
</button>
82+
)}
83+
84+
{item.items && (
85+
<div
86+
class={clsx(
87+
level === 1 ? "xl:hidden xl:group-hover:block xl:absolute z-50 xl:top-full xl:left-0 xl:group-focus-within:block" : "",
88+
level === 2 ? "xl:hidden xl:group-hover/level2:block xl:absolute z-50 xl:left-full xl:top-0 xl:-mt-2 xl:group-focus-within:block" : ""
89+
)}
90+
>
91+
{level === 1 && (
92+
<Astro.self items={item.items} level={2} />
93+
)}
94+
95+
{level === 2 && (
96+
<Astro.self items={item.items} level={3} />
97+
)}
98+
</div>
99+
)}
100+
</li>
101+
))}
102+
</ul>
103+
104+
<script>
105+
// Add keyboard navigation support
106+
document.addEventListener('DOMContentLoaded', () => {
107+
const navButtons = document.querySelectorAll('[role="menuitem"][aria-haspopup="true"]');
108+
109+
navButtons.forEach(button => {
110+
button.addEventListener('keydown', (e) => {
111+
const key = e.key;
112+
113+
if (key === 'Enter' || key === ' ' || key === 'ArrowDown') {
114+
e.preventDefault();
115+
toggleSubmenu(button, true);
116+
} else if (key === 'Escape') {
117+
e.preventDefault();
118+
toggleSubmenu(button, false);
119+
}
120+
});
121+
122+
// Close submenu when focus leaves
123+
button.addEventListener('blur', (e) => {
124+
if (!e.currentTarget.contains(e.relatedTarget)) {
125+
toggleSubmenu(button, false);
126+
}
127+
});
128+
});
129+
130+
function toggleSubmenu(button, show) {
131+
const expanded = button.getAttribute('aria-expanded') === 'true';
132+
133+
if (show && !expanded) {
134+
button.setAttribute('aria-expanded', 'true');
135+
// Add focus to first link in submenu
136+
const submenu = button.nextElementSibling;
137+
if (submenu) {
138+
const firstLink = submenu.querySelector('[role="menuitem"]');
139+
if (firstLink) setTimeout(() => firstLink.focus(), 10);
140+
}
141+
} else if (!show && expanded) {
142+
button.setAttribute('aria-expanded', 'false');
143+
}
144+
}
145+
146+
// Add arrow key navigation within same level
147+
const menuLists = document.querySelectorAll('[role="menu"], [role="menubar"]');
148+
menuLists.forEach(menu => {
149+
const menuItems = menu.querySelectorAll('[role="menuitem"]');
150+
151+
menuItems.forEach((item, index) => {
152+
item.addEventListener('keydown', (e) => {
153+
if (e.key === 'ArrowRight' && index < menuItems.length - 1) {
154+
e.preventDefault();
155+
menuItems[index + 1].focus();
156+
} else if (e.key === 'ArrowLeft' && index > 0) {
157+
e.preventDefault();
158+
menuItems[index - 1].focus();
159+
} else if (e.key === 'Home') {
160+
e.preventDefault();
161+
menuItems[0].focus();
162+
} else if (e.key === 'End') {
163+
e.preventDefault();
164+
menuItems[menuItems.length - 1].focus();
165+
}
166+
});
167+
});
168+
});
169+
});
170+
</script>
171+
172+
<style>
173+
/* Add transition for smoother hover effects */
174+
li {
175+
transition: all 0.2s ease-in-out;
176+
}
177+
178+
/* Custom styling for desktop third level menu positioning */
179+
@media (min-width: 1280px) {
180+
li.group\/level2:hover > div {
181+
display: block;
182+
}
183+
}
184+
185+
/* Focus styles for keyboard navigation */
186+
[role="menuitem"]:focus-visible {
187+
outline: 2px solid white;
188+
outline-offset: -2px;
189+
border-radius: 4px;
190+
}
191+
192+
/* Hide outline for mouse users, but keep for keyboard */
193+
[role="menuitem"]:focus:not(:focus-visible) {
194+
outline: none;
195+
}
196+
</style>

0 commit comments

Comments
 (0)