use simple script for popover menu

This commit is contained in:
MapleLeaf
2022-01-03 04:06:55 -06:00
committed by Darius
parent 1f9adc5593
commit 557fb4f8dc
6 changed files with 102 additions and 58 deletions

View File

@@ -1,14 +1,9 @@
import React from "react"
import { guideLinks } from "../data/guide-links"
import { mainLinks } from "../data/main-links"
import { createHydrater } from "../helpers/hydration"
import { linkClass } from "../styles/components"
import { AppLink } from "./app-link"
import type { MainNavigationMobileMenuData } from "./main-navigation-mobile-menu"
const MenuHydrater = await createHydrater<MainNavigationMobileMenuData>(
new URL("./main-navigation-mobile-menu.tsx", import.meta.url).pathname,
)
import { PopoverMenu } from "./popover-menu"
export function MainNavigation() {
return (
@@ -22,7 +17,23 @@ export function MainNavigation() {
))}
</div>
<div className="md:hidden" id="main-navigation-popover">
<MenuHydrater data={{ guideLinks }} />
<PopoverMenu>
{mainLinks.map((link) => (
<AppLink
{...link}
key={link.to}
className={PopoverMenu.itemClass}
/>
))}
<hr className="border-0 h-[2px] bg-black/50" />
{guideLinks.map((link) => (
<AppLink
{...link}
key={link.to}
className={PopoverMenu.itemClass}
/>
))}
</PopoverMenu>
</div>
</nav>
)

View File

@@ -0,0 +1,38 @@
import clsx from "clsx"
const menus = document.querySelectorAll("[data-popover]")
for (const menu of menus) {
const button = menu.querySelector<HTMLButtonElement>("[data-popover-button]")!
const panel = menu.querySelector<HTMLDivElement>("[data-popover-panel]")!
const panelClasses = clsx`${panel.className} transition-all`
const visibleClass = clsx`${panelClasses} visible opacity-100 translate-y-0`
const hiddenClass = clsx`${panelClasses} invisible opacity-0 translate-y-2`
let visible = false
const setVisible = (newVisible: boolean) => {
visible = newVisible
panel.className = visible ? visibleClass : hiddenClass
if (!visible) return
requestAnimationFrame(() => {
const handleClose = (event: MouseEvent) => {
if (panel.contains(event.target as Node)) return
setVisible(false)
window.removeEventListener("click", handleClose)
}
window.addEventListener("click", handleClose)
})
}
const toggleVisible = () => setVisible(!visible)
button.addEventListener("click", toggleVisible)
setVisible(false)
panel.hidden = false
}

View File

@@ -1,63 +1,22 @@
import { MenuAlt4Icon } from "@heroicons/react/outline"
import { useRect } from "@reach/rect"
import clsx from "clsx"
import React, { useRef, useState } from "react"
import { FocusOn } from "react-focus-on"
import React from "react"
import { linkClass } from "../styles/components"
// todo: remove useRect usage and rely on css absolute positioning instead
export function PopoverMenu({ children }: { children: React.ReactNode }) {
const [visible, setVisible] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const buttonRect = useRect(buttonRef)
const panelRef = useRef<HTMLDivElement>(null)
const panelRect = useRect(panelRef)
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
return (
<>
<button
title="Menu"
className={linkClass}
onClick={() => setVisible(!visible)}
ref={buttonRef}
>
<div data-popover className="relative">
<button data-popover-button title="Menu" className={linkClass}>
<MenuAlt4Icon className="w-6" />
</button>
<FocusOn
enabled={visible}
onClickOutside={() => setVisible(false)}
onEscapeKey={() => setVisible(false)}
<div
data-popover-panel
hidden
className="absolute w-48 bg-slate-800 rounded-lg shadow overflow-hidden max-h-[calc(100vh-4rem)] overflow-y-auto right-0 top-[calc(100%+8px)]"
>
<div
className="fixed"
style={{
left: (buttonRect?.right ?? 0) - (panelRect?.width ?? 0),
top: (buttonRect?.bottom ?? 0) + 8,
}}
onClick={() => setVisible(false)}
>
<div
className={clsx(
"transition-all",
visible
? "opacity-100 visible"
: "translate-y-2 opacity-0 invisible",
)}
>
<div ref={panelRef}>
<div className="w-48 bg-slate-800 rounded-lg shadow overflow-hidden max-h-[calc(100vh-4rem)] overflow-y-auto">
{children}
</div>
</div>
</div>
</div>
</FocusOn>
</>
{children}
</div>
</div>
)
}

View File

@@ -0,0 +1,26 @@
import { build } from "esbuild"
import type { RequestHandler } from "express"
import { readFile } from "node:fs/promises"
import { dirname } from "node:path"
export async function serveCompiledScript(
scriptFilePath: string,
): Promise<RequestHandler> {
const scriptBuild = await build({
bundle: true,
stdin: {
contents: await readFile(scriptFilePath, "utf-8"),
sourcefile: scriptFilePath,
loader: "tsx",
resolveDir: dirname(scriptFilePath),
},
target: ["chrome89", "firefox89"],
format: "esm",
write: false,
})
return (req, res) => {
res.setHeader("Content-Type", "application/javascript")
res.end(scriptBuild.outputFiles[0]!.contents)
}
}

View File

@@ -31,6 +31,8 @@ export function Html({
<link href="/tailwind.css" rel="stylesheet" />
<link href="/prism-theme.css" rel="stylesheet" />
<script type="module" src="/popover-menu.client.js" />
<title>{title}</title>
</head>
<body>{children}</body>

View File

@@ -7,6 +7,7 @@ import pinoHttp from "pino-http"
import * as React from "react"
import { renderMarkdownFile } from "./helpers/markdown"
import { sendJsx } from "./helpers/send-jsx"
import { serveCompiledScript } from "./helpers/serve-compiled-script"
import { serveFile } from "./helpers/serve-file"
import { serveTailwindCss } from "./helpers/tailwind"
import DocsPage from "./pages/docs"
@@ -25,6 +26,13 @@ const app = express()
serveFile(new URL("./styles/prism-theme.css", import.meta.url).pathname),
)
.get(
"/popover-menu.client.js",
await serveCompiledScript(
new URL("./components/popover-menu.client.tsx", import.meta.url).pathname,
),
)
.get("/docs/*", async (req: Request<{ 0: string }>, res) => {
const { html, data } = await renderMarkdownFile(
`src/docs/${req.params[0]}.md`,