use simple script for popover menu
This commit is contained in:
@@ -1,14 +1,9 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { guideLinks } from "../data/guide-links"
|
import { guideLinks } from "../data/guide-links"
|
||||||
import { mainLinks } from "../data/main-links"
|
import { mainLinks } from "../data/main-links"
|
||||||
import { createHydrater } from "../helpers/hydration"
|
|
||||||
import { linkClass } from "../styles/components"
|
import { linkClass } from "../styles/components"
|
||||||
import { AppLink } from "./app-link"
|
import { AppLink } from "./app-link"
|
||||||
import type { MainNavigationMobileMenuData } from "./main-navigation-mobile-menu"
|
import { PopoverMenu } from "./popover-menu"
|
||||||
|
|
||||||
const MenuHydrater = await createHydrater<MainNavigationMobileMenuData>(
|
|
||||||
new URL("./main-navigation-mobile-menu.tsx", import.meta.url).pathname,
|
|
||||||
)
|
|
||||||
|
|
||||||
export function MainNavigation() {
|
export function MainNavigation() {
|
||||||
return (
|
return (
|
||||||
@@ -22,7 +17,23 @@ export function MainNavigation() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="md:hidden" id="main-navigation-popover">
|
<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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
|||||||
38
packages/docs-new/src/components/popover-menu.client.tsx
Normal file
38
packages/docs-new/src/components/popover-menu.client.tsx
Normal 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
|
||||||
|
}
|
||||||
@@ -1,63 +1,22 @@
|
|||||||
import { MenuAlt4Icon } from "@heroicons/react/outline"
|
import { MenuAlt4Icon } from "@heroicons/react/outline"
|
||||||
import { useRect } from "@reach/rect"
|
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import React, { useRef, useState } from "react"
|
import React from "react"
|
||||||
import { FocusOn } from "react-focus-on"
|
|
||||||
import { linkClass } from "../styles/components"
|
import { linkClass } from "../styles/components"
|
||||||
|
|
||||||
// todo: remove useRect usage and rely on css absolute positioning instead
|
|
||||||
export function PopoverMenu({ children }: { children: React.ReactNode }) {
|
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 (
|
return (
|
||||||
<>
|
<div data-popover className="relative">
|
||||||
<button
|
<button data-popover-button title="Menu" className={linkClass}>
|
||||||
title="Menu"
|
|
||||||
className={linkClass}
|
|
||||||
onClick={() => setVisible(!visible)}
|
|
||||||
ref={buttonRef}
|
|
||||||
>
|
|
||||||
<MenuAlt4Icon className="w-6" />
|
<MenuAlt4Icon className="w-6" />
|
||||||
</button>
|
</button>
|
||||||
|
<div
|
||||||
<FocusOn
|
data-popover-panel
|
||||||
enabled={visible}
|
hidden
|
||||||
onClickOutside={() => setVisible(false)}
|
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)]"
|
||||||
onEscapeKey={() => setVisible(false)}
|
|
||||||
>
|
>
|
||||||
<div
|
{children}
|
||||||
className="fixed"
|
</div>
|
||||||
style={{
|
</div>
|
||||||
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>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
packages/docs-new/src/helpers/serve-compiled-script.ts
Normal file
26
packages/docs-new/src/helpers/serve-compiled-script.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@ export function Html({
|
|||||||
<link href="/tailwind.css" rel="stylesheet" />
|
<link href="/tailwind.css" rel="stylesheet" />
|
||||||
<link href="/prism-theme.css" rel="stylesheet" />
|
<link href="/prism-theme.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<script type="module" src="/popover-menu.client.js" />
|
||||||
|
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>{children}</body>
|
<body>{children}</body>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import pinoHttp from "pino-http"
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { renderMarkdownFile } from "./helpers/markdown"
|
import { renderMarkdownFile } from "./helpers/markdown"
|
||||||
import { sendJsx } from "./helpers/send-jsx"
|
import { sendJsx } from "./helpers/send-jsx"
|
||||||
|
import { serveCompiledScript } from "./helpers/serve-compiled-script"
|
||||||
import { serveFile } from "./helpers/serve-file"
|
import { serveFile } from "./helpers/serve-file"
|
||||||
import { serveTailwindCss } from "./helpers/tailwind"
|
import { serveTailwindCss } from "./helpers/tailwind"
|
||||||
import DocsPage from "./pages/docs"
|
import DocsPage from "./pages/docs"
|
||||||
@@ -25,6 +26,13 @@ const app = express()
|
|||||||
serveFile(new URL("./styles/prism-theme.css", import.meta.url).pathname),
|
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) => {
|
.get("/docs/*", async (req: Request<{ 0: string }>, res) => {
|
||||||
const { html, data } = await renderMarkdownFile(
|
const { html, data } = await renderMarkdownFile(
|
||||||
`src/docs/${req.params[0]}.md`,
|
`src/docs/${req.params[0]}.md`,
|
||||||
|
|||||||
Reference in New Issue
Block a user