From a13f989ab316b8c5623945d3c27e6b10b58dce0b Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 2 Apr 2021 21:17:44 -0400 Subject: [PATCH] Use history.pushState() for internal links. --- lib/browser-main.tsx | 34 +++++++++++++++++++++++++++++++--- lib/page.tsx | 29 +++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/lib/browser-main.tsx b/lib/browser-main.tsx index 5dbd366..608fb1f 100644 --- a/lib/browser-main.tsx +++ b/lib/browser-main.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import ReactDOM from "react-dom"; import { PageContext, PAGE_QUERY_ARG } from "./page"; import { pageNames, Pages, toPageName, DEFAULT_PAGE } from "./pages"; @@ -11,13 +11,41 @@ if (!appEl) { throw new Error(`Unable to find #${APP_ID}!`); } +function getWindowSearch(): URLSearchParams { + return new URLSearchParams(window.location.search); +} + +/** + * Call the given handler whenever a `popstate` event + * occurs. + * + * Return a function that wraps `window.history.pushState()`; + * the given handler will be called immediately afterwards. + */ +function usePushState(onPushOrPopState: () => void) { + useEffect(() => { + window.addEventListener("popstate", onPushOrPopState); + return () => { + window.removeEventListener("popstate", onPushOrPopState); + }; + }, [onPushOrPopState]); + + return function pushState(href: string) { + window.history.pushState(null, "", href); + onPushOrPopState(); + }; +} + const App: React.FC<{}> = (props) => { - const page = new URLSearchParams(window.location.search); - const currPage = toPageName(page.get(PAGE_QUERY_ARG) || "", DEFAULT_PAGE); + const [search, setSearch] = useState(getWindowSearch()); + const updateSearchFromWindow = () => setSearch(getWindowSearch()); + const currPage = toPageName(search.get(PAGE_QUERY_ARG) || "", DEFAULT_PAGE); const PageComponent = Pages[currPage]; + const pushState = usePushState(updateSearchFromWindow); const ctx: PageContext = { currPage, allPages: pageNames, + pushState, }; return ( diff --git a/lib/page.tsx b/lib/page.tsx index a6dbbb2..4caacfe 100644 --- a/lib/page.tsx +++ b/lib/page.tsx @@ -1,21 +1,42 @@ -import React, { useContext } from "react"; +import React, { MouseEvent, useContext } from "react"; import type { PageName } from "./pages"; export type PageContext = { currPage: PageName; allPages: PageName[]; + pushState: (href: string) => void; }; export const PageContext = React.createContext({ currPage: "vocabulary", allPages: [], + pushState: () => { + throw new Error("No page context is defined!"); + }, }); export const PAGE_QUERY_ARG = "p"; -const PageLink: React.FC<{ page: PageName }> = ({ page }) => ( - {page} -); +function isNormalLinkClick(e: MouseEvent): boolean { + return !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey && e.button === 0; +} + +const PageLink: React.FC<{ page: PageName }> = ({ page }) => { + const href = `?${PAGE_QUERY_ARG}=${encodeURIComponent(page)}`; + const { pushState } = useContext(PageContext); + const handleClick = (e: MouseEvent) => { + if (isNormalLinkClick(e)) { + pushState(href); + e.preventDefault(); + } + }; + + return ( + + {page} + + ); +}; const Navbar: React.FC<{}> = (props) => { const pc = useContext(PageContext);