diff --git a/components/Menu.tsx b/components/Menu.tsx
index 03a2b0a1..ad9c380c 100644
--- a/components/Menu.tsx
+++ b/components/Menu.tsx
@@ -1,15 +1,18 @@
-import React, { useContext, useEffect, useRef } from "react";
+import React, { useContext, useEffect, useReducer, useRef } from "react";
import classNames from "classnames";
import Bars from "./icons/Bars";
import Navigation from "./Navigation";
import MenuContext from "../contexts/MenuContext";
+import SubLinkContext from "../contexts/SubLinkContext";
import useOnClickOutside from "../hooks/useOnClickOutside";
import { useRouter } from "next/router";
+import NavigationFollower, { updateNavReducer } from "./NavigationFollower";
export default function Menu() {
const ref = useRef(null);
const router = useRouter();
const { open, setClose } = useContext(MenuContext);
+ const [activeSubLink, setActiveSubLink] = useReducer(updateNavReducer, "");
const classes = classNames(
[
@@ -47,7 +50,12 @@ export default function Menu() {
onClick={setClose}
className="ml-6 h-7 text-black dark:text-white cursor-pointer md:hidden"
/>
-
+
+
+
+
diff --git a/components/Navigation.tsx b/components/Navigation.tsx
index 348ec8c5..47db50bb 100644
--- a/components/Navigation.tsx
+++ b/components/Navigation.tsx
@@ -1,4 +1,4 @@
-import React, { Fragment, useEffect } from "react";
+import React, { Fragment, useContext, useEffect } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import classNames from "classnames";
@@ -7,6 +7,7 @@ import CaretFill from "./icons/CaretFill";
import useToggle from "../hooks/useToggle";
import Discord from "./icons/Discord";
import ThemeSwitcher from "./ThemeSwitcher";
+import SubLinkContext from "../contexts/SubLinkContext";
interface MenuSelectionProps {
title?: string;
@@ -48,7 +49,6 @@ function NavigationLink({
// TODO: We currently have a bunch of listeners being added here - can this be improved?
useEffect(() => {
const handler = (url: string) => {
- // debugger;
if (url.endsWith(href) && !isOpen) {
toggle();
}
@@ -103,12 +103,21 @@ interface NavigationSubLinkProps {
function NavigationSubLink({ href, children }: NavigationSubLinkProps) {
const router = useRouter();
+ const { active, setActive } = useContext(SubLinkContext);
+ const isActive = active === href;
+
+ useEffect(() => {
+ if (router.asPath === href) {
+ setActive({ type: "direct", payload: href });
+ }
+ }, [router.asPath, href, setActive]);
+
const classes = classNames(
- "group flex items-center ml-6 px-2 py-1 w-full text-sm font-medium rounded-md",
+ "group flex items-center ml-6 px-2 py-1 w-full text-sm font-medium rounded-md motion-safe:duration-200",
{
- "text-dark dark:text-white": router.asPath === href,
+ "text-dark dark:text-white": isActive,
"text-theme-light-sidebar-text hover:text-theme-light-sidebar-hover-text dark:hover:text-white":
- router.asPath !== href,
+ !isActive,
}
);
@@ -116,9 +125,7 @@ function NavigationSubLink({ href, children }: NavigationSubLinkProps) {
- {router.asPath === href ? (
-
- ) : null}
+ {isActive ? : null}
{children}
diff --git a/components/NavigationFollower.tsx b/components/NavigationFollower.tsx
new file mode 100644
index 00000000..765d43b6
--- /dev/null
+++ b/components/NavigationFollower.tsx
@@ -0,0 +1,101 @@
+import { useContext, useEffect } from "react";
+import { useRouter } from "next/router";
+import SubLinkContext from "../contexts/SubLinkContext";
+
+export default function NavigationFollower() {
+ const router = useRouter();
+ const subLinkContext = useContext(SubLinkContext);
+ const dispatch = subLinkContext.setActive;
+
+ useEffect(() => {
+ if (!router.asPath.includes("#")) dispatch({ type: "direct", payload: "" });
+ const activePage = router.basePath + router.pathname;
+ let navPageSublinks = document.querySelectorAll(
+ `nav a[href^="${activePage}#"]`
+ );
+
+ const observers = new Set();
+ let timeout: NodeJS.Timeout | null = null;
+ async function addObservers() {
+ if (navPageSublinks.length === 0) {
+ let time = 100;
+ let waitAdditional = (res: () => void) =>
+ (timeout = setTimeout(() => {
+ navPageSublinks = document.querySelectorAll(
+ `nav a[href^="${activePage}#"]`
+ );
+ if (navPageSublinks.length === 0) {
+ if (time > 2000) res();
+ time *= 2;
+ } else {
+ res();
+ }
+ }, time));
+ await new Promise((res) => waitAdditional(res));
+ if (navPageSublinks.length === 0) return;
+ }
+ const navIds: string[] = [];
+ for (const elem of navPageSublinks) {
+ const href = elem.getAttribute("href");
+ const id = href?.split("#")[1];
+ if (id) navIds.push(id);
+ }
+
+ let currentPageLinks = document.querySelectorAll("main article > a");
+ if (currentPageLinks.length === 0) return;
+ const update = (entries: IntersectionObserverEntry[]) =>
+ dispatch({
+ type: "scroll",
+ payload: { currentPath: router.pathname, navIds, entries },
+ });
+
+ for (const elem of currentPageLinks) {
+ const id = elem.getAttribute("href")?.slice(1);
+ if (!id || !navIds.includes(id)) continue;
+ const observer = new IntersectionObserver(update, {
+ root: document.querySelector("main")?.parentElement,
+ rootMargin: "0px 0px -80% 0px",
+ threshold: 0,
+ });
+ observers.add(observer);
+ observer.observe(elem);
+ }
+ }
+
+ addObservers();
+ return () => {
+ if (timeout) clearTimeout(timeout);
+ for (const observer of observers) {
+ observer.disconnect();
+ }
+ observers.clear();
+ };
+ }, [router.basePath, router.pathname, router.asPath, dispatch]);
+
+ return null;
+}
+
+export function updateNavReducer(state: string, action: any) {
+ if (action.type !== "scroll") {
+ return action.payload;
+ }
+ const { entries, navIds, currentPath } = action.payload;
+ const entry = entries[0];
+ const intersectingHeight = entry.rootBounds!.height;
+ let toBeActive: string | null = null;
+ if (
+ !entry.isIntersecting &&
+ intersectingHeight * 0.9 < entry.boundingClientRect.top &&
+ entry.boundingClientRect.top < intersectingHeight * 1.2
+ ) {
+ const currentIndex = navIds.indexOf(
+ entry.target.getAttribute("href")!.slice(1)
+ );
+ toBeActive = navIds[currentIndex - 1];
+ }
+ if (entry.isIntersecting) {
+ toBeActive = entry.target.getAttribute("href")!.slice(1);
+ }
+ if (!toBeActive) return state;
+ return `${currentPath}#${toBeActive}`;
+}
diff --git a/contexts/SubLinkContext.tsx b/contexts/SubLinkContext.tsx
new file mode 100644
index 00000000..545a1d48
--- /dev/null
+++ b/contexts/SubLinkContext.tsx
@@ -0,0 +1,13 @@
+import { Context, createContext, useContext } from "react";
+
+export interface SubLinkContextInterface {
+ active: string;
+ setActive: (dispatchOptions: { type: string; payload?: unknown }) => void;
+}
+
+const context = createContext({
+ active: "",
+ setActive: () => {},
+});
+
+export default context;
diff --git a/tailwind.config.js b/tailwind.config.js
index 219d44a7..0c013982 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -63,6 +63,7 @@ module.exports = {
variants: {
extend: {
animation: ["motion-safe"],
+ transitionDuration: ["motion-safe"],
},
},
plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],