From 9f5058ba67faa03d42ec5339e28e3adcfd927148 Mon Sep 17 00:00:00 2001 From: ckohen Date: Mon, 6 Sep 2021 03:53:05 -0700 Subject: [PATCH 1/2] Make sublinks highlight as they are reached --- components/Menu.tsx | 12 +++- components/Navigation.tsx | 21 ++++--- components/NavigationFollower.tsx | 101 ++++++++++++++++++++++++++++++ contexts/SubLinkContext.tsx | 13 ++++ 4 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 components/NavigationFollower.tsx create mode 100644 contexts/SubLinkContext.tsx 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..429c5d76 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", { - "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; From f3015da94410d968d0ed8f1933ca07a1bef5072e Mon Sep 17 00:00:00 2001 From: ckohen Date: Sun, 12 Sep 2021 03:10:37 -0700 Subject: [PATCH 2/2] Smooooth --- components/Navigation.tsx | 2 +- tailwind.config.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 429c5d76..47db50bb 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -113,7 +113,7 @@ function NavigationSubLink({ href, children }: NavigationSubLinkProps) { }, [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": isActive, "text-theme-light-sidebar-text hover:text-theme-light-sidebar-hover-text dark:hover:text-white": 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")],