Skip to content

Commit

Permalink
feat: combined hamburger and navbar navigation (#120)
Browse files Browse the repository at this point in the history
  • Loading branch information
niccofyren authored Dec 4, 2024
1 parent 77469e7 commit a5e5ce2
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 111 deletions.
115 changes: 43 additions & 72 deletions src/components/Header.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
---
import NavItem from "./navigation/NavItem.astro";
import type { NavItem } from "../types";
import HeaderNavigation from "./HeaderNavigation.astro";
const NAVIGATION: NavItem[] = [
{
title: "Om TG",
path: "/about",
items: [
{ title: "Bli utstiller", path: "/about/expo" },
{ title: "Bli sponsor", path: "/about/sponsor" },
],
},
{
title: "Billetter",
path: "/tickets",
items: [
{ title: "Vilkår", path: "/tickets/terms-and-conditions" },
{ title: "Arrangementsregler", path: "/event/rules" },
{ title: "Konstruksjonsregler", path: "/event/construction-rules" },
],
},
{
title: "Konkurranser",
items: [
{ title: "Kreative", path: "/competitions/creative" },
{ title: "Esport", path: "/competitions/esport" },
],
},
{ title: "Kontakt oss", path: "/contact" },
];
---

<script>
Expand Down Expand Up @@ -32,7 +61,7 @@ import NavItem from "./navigation/NavItem.astro";
class="flex justify-between items-center py-1 px-5 max-w-7xl mb-8 justify-self-center w-full mx-auto bg-backgroundSecondary sm:rounded-xl sm:mx-5 sm:mt-2 sm:mb-8 sm:w-auto xl:mx-auto xl:w-full"
>
<nav class="w-full">
<div class="relative h-16 flex items-center">
<div class="relative h-16 flex items-center justify-between">
<a href="/" class="sm:flex gap-x-4 items-center hidden">
<img
src="/images/tglogo-bw.png"
Expand All @@ -49,42 +78,13 @@ import NavItem from "./navigation/NavItem.astro";
class="aspect-thumbnail object-cover"
/>
</a>
<ul class="hidden sm:flex w-full space-x-6 space-y-0 justify-end">
<NavItem
title="Om TG"
path="/about"
subItems={[
{ title: "Bli utstiller", path: "/about/expo" },
{ title: "Bli sponsor", path: "/about/sponsor" },
]}
/>
<NavItem
title="Konkurranser"
subItems={[
{ title: "Kreative", path: "/competitions/creative" },
{ title: "Esport", path: "/competitions/esport" },
]}
/>
<NavItem title="Kontakt oss" path="/contact" />
<NavItem
title="Billetter"
path="/tickets"
subAlignment="right"
subItems={[
{ title: "Vilkår", path: "/tickets/terms-and-conditions" },
{
title: "Arrangementsregler",
path: "/event/rules",
},
{
title: "Konstruksjonsregler",
path: "/event/construction-rules",
},
]}
/>
</ul>

<div class="absolute inset-y-0 right-0 flex items-center sm:hidden">
<HeaderNavigation
navigationItems={NAVIGATION}
class="hidden sm:flex w-full space-x-6 space-y-0 justify-end"
prioritized
/>
<div class="flex items-center">
<button
type="button"
data-part="mobile-menu-trigger"
Expand Down Expand Up @@ -126,41 +126,12 @@ import NavItem from "./navigation/NavItem.astro";
</div>
</div>
<!-- Mobile menu, show/hide based on menu state. -->
<div class="hidden md:hidden" id="mobile-menu" data-part="mobile-menu">
<ul class="space-y-3 px-2 pb-3 pt-2 flex flex-col">
<NavItem
title="Om TG"
path="/about"
subItems={[
{ title: "Bli utstiller", path: "/about/expo" },
{ title: "Bli sponsor", path: "/about/sponsor" },
]}
/>
<NavItem
title="Konkurranser"
subItems={[
{ title: "Kreative", path: "/competitions/creative" },
{ title: "Esport", path: "/competitions/esport" },
]}
/>
<NavItem title="Kontakt oss" path="/contact" />
<NavItem
title="Billetter"
path="/tickets"
subAlignment="right"
subItems={[
{ title: "Vilkår", path: "/tickets/terms-and-conditions" },
{
title: "Arrangementsregler",
path: "/event/rules",
},
{
title: "Konstruksjonsregler",
path: "/event/construction-rules",
},
]}
/>
</ul>
<div class="hidden" id="mobile-menu" data-part="mobile-menu">
<HeaderNavigation
navigationItems={NAVIGATION}
class="space-y-3 px-2 pb-3 pt-2 flex flex-col"
hamburger
/>
</div>
</nav>
</div>
Expand Down
79 changes: 79 additions & 0 deletions src/components/HeaderNavigation.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
import type { NavItem } from "../types";
import NavItemComponent from "./navigation/NavItem.astro";
interface Props {
navigationItems: NavItem[];
hamburger?: boolean;
prioritized?: boolean;
class?: string;
}
const {
navigationItems,
class: classNames = "",
prioritized = false,
hamburger = false,
} = Astro.props;
const lastItemIndex = navigationItems.length - 1;
---

<script>
const prioritizedMenus = Array.from(
document.querySelectorAll('ul[data-prioritized="true"]'),
);

function updateVisibilityFactory(menu: Element | null) {
const items = Array.from(
menu?.querySelectorAll('[data-component="nav-item"]') || [],
);

if (!menu || !items) {
return;
}

return () => {
const menuWidth = menu.clientWidth;
// Adjust values to add preferred extra margins
const margin = 24;
let usedWidth = 50;
items.forEach((item) => {
item.classList.remove("hidden");
usedWidth += margin + (item?.scrollWidth || 0);

if (usedWidth >= menuWidth) {
item.classList.add("hidden");
}
});
};
}

for (const menu of prioritizedMenus) {
const updateVisibility = updateVisibilityFactory(menu);
if (!updateVisibility) {
continue;
}
window.addEventListener("resize", updateVisibility);
window.addEventListener("load", updateVisibility);
}
</script>

<ul
class={classNames}
data-component="header-navigation"
data-prioritized={prioritized ? "true" : "false"}
>
{
navigationItems.map(({ title, path, items }, index) => (
<NavItemComponent
title={title}
path={path}
items={items}
hamburger={hamburger}
interactionMode={hamburger ? "click" : "hover"}
subAlignment={index === lastItemIndex ? "right" : "left"}
/>
))
}
</ul>
89 changes: 50 additions & 39 deletions src/components/navigation/NavItem.astro
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
---
export interface Props {
title: string;
path?: string;
subAlignment?: "left" | "right";
subItems?: SubItem[];
import type { NavItem } from "../../types";
export interface Props extends NavItem {
class?: string;
}
interface SubItem {
title: string;
path: string;
subAlignment?: "left" | "right";
interactionMode?: "click" | "hover";
hamburger?: boolean;
}
const {
title,
path,
subItems = [],
items = [],
subAlignment = "left",
class: className = "",
interactionMode = "click",
hamburger = false,
} = Astro.props;
const key = `${title.toLowerCase().replace(" ", "-")}-${path || ""}`;
const clickable = !!path;
const expandable = !!subItems.length;
const expandable = !!items.length;
const TextContainer = clickable ? "a" : "span";
const isActive =
path === Astro.url.pathname ||
subItems.some((item) => item.path === Astro.url.pathname);
items.some((item) => item.path === Astro.url.pathname);
const subItemsWithParent: Array<
SubItem & {
const itemsWithParent: Array<
NavItem & {
touchOnly?: boolean;
}
> = path ? [{ path, title, touchOnly: true }, ...subItems] : subItems;
> = path ? [{ path, title, touchOnly: true }, ...items] : items;
---

<script>
Expand All @@ -42,9 +41,6 @@ const subItemsWithParent: Array<
navItem: HTMLElement;
}

const isTouchDevice =
"ontouchstart" in window || navigator.maxTouchPoints > 0;

const navItems: SubNavComponents[] = [
...document.querySelectorAll("[data-component='nav-item']"),
]
Expand Down Expand Up @@ -76,43 +72,52 @@ const subItemsWithParent: Array<
});
}

const isTouchDevice =
"ontouchstart" in window ||
navigator.maxTouchPoints > 0 ||
// @ts-ignore
navigator.msMaxTouchPoints > 0;

navItems.forEach(({ mainLink, navItem, trigger, subMenu }, index: number) => {
navItem.classList.toggle("touch-device", isTouchDevice);
const interactionMode = navItem.getAttribute("data-mode") || "click";
navItem.classList.add(`interaction-mode-${interactionMode}`);

if (isTouchDevice) {
subMenu.querySelectorAll(".hidden").forEach((subMenu) => {
subMenu.classList.remove("hidden");
});
[mainLink, trigger].forEach((item) =>
item?.addEventListener("click", (event) => {
event?.preventDefault();
const expanded = !(
trigger.getAttribute("aria-expanded") === "true" || false
);
closeAll();
expand(index, expanded);
}),
);
} else {
if (interactionMode === "hover" && !isTouchDevice) {
navItem.addEventListener("mouseenter", () => {
closeAll();
expand(index, true);
});
navItem.addEventListener("mouseleave", () => {
expand(index, false);
});
return;
}

subMenu.querySelectorAll(".hidden").forEach((subMenu) => {
subMenu.classList.remove("hidden");
});
[mainLink, trigger].forEach((item) =>
item?.addEventListener("click", (event) => {
event?.preventDefault();
const expanded = !(
trigger.getAttribute("aria-expanded") === "true" || false
);
closeAll();
expand(index, expanded);
}),
);
});
</script>

<li
class={`relative group bg-backgroundSecondary ${className}`}
data-component="nav-item"
data-mode={interactionMode}
>
<div
class={`block ${isActive ? "text-orange-500" : "text-white"} ${clickable ? "group-[.expanded]:text-orange-500" : "group-[.expanded]:text-neutral-400"} flex z-10 relative ${clickable ? "cursor-pointer" : "cursor-default"} items-center h-6`}
class={`block ${isActive ? "text-orange-500" : "text-white"} ${clickable ? "group-[.expanded]:text-orange-500" : "group-[.expanded]:text-neutral-400"} flex ${interactionMode === "hover" ? "z-20" : ""} relative ${clickable ? "cursor-pointer" : "cursor-default"} items-center h-6`}
>
<TextContainer href={path} data-part="main-link" class="mr-3 w-full">
<TextContainer href={path} data-part="main-link" class="mr-3 w-max">
{title}
</TextContainer>
{
Expand All @@ -122,7 +127,7 @@ const subItemsWithParent: Array<
data-part="trigger"
aria-controls={`nav-item-sub-${key}`}
aria-expanded="false"
class="block p-1 ml-auto rounded group-[.touch-device]:bg-neutral-800"
class="block p-1 ml-auto rounded group-[.interaction-mode-click]:bg-neutral-800"
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand Down Expand Up @@ -150,12 +155,18 @@ const subItemsWithParent: Array<
{
expandable ? (
<ul
class={`hidden min-w-max flex-col bg-neutral-800 md:bg-backgroundSecondary space-y-1 px-2 py-3 mt-3 z-0 md:mt-0 md:pt-10 md:rounded-b-md md:absolute md:top-0 ${subAlignment === "left" ? "md:left-0" : "md:right-0"}`}
class:list={[
"hidden min-w-max w-full flex-col space-y-1 px-2 py-3",
hamburger
? "mt-3 bg-neutral-800"
: "z-10 mt-0 pt-10 rounded-b-md absolute top-0 bg-backgroundSecondary",
subAlignment === "left" ? "left-0" : "right-0",
]}
id={`nav-item-sub-${key}`}
aria-role="menu"
data-part="sub-menu"
>
{subItemsWithParent.map((subItem) => (
{itemsWithParent.map((subItem) => (
<li class={`${subItem.touchOnly ? "hidden" : ""}`}>
<a
href={subItem.path}
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./articles";
export * from "./images";
export * from "./navigation";
7 changes: 7 additions & 0 deletions src/types/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface MinimalNavItem {
title: string;
path?: string;
}
export interface NavItem extends MinimalNavItem {
items?: MinimalNavItem[];
}

0 comments on commit a5e5ce2

Please sign in to comment.