Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/app/customer-login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export default async function CustomerLoginPage() {
)}
</article>

<CustomerLoginForm />
<div className="page__form">
<CustomerLoginForm />
</div>
</div>
</main>
);
Expand Down
79 changes: 42 additions & 37 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import "@/styles/main.scss";
import { RootLayoutQuery } from "@graphql";
import { fetchGraphql } from "@/lib/graphql";
import ActiveLink from "@/components/ActiveLink";
import NavMenu from "@/components/NavMenu";
import PreloadWebfonts from "@/components/PreloadWebfonts";
import FontdueHTML from "@/components/FontdueHTML";

Expand Down Expand Up @@ -86,48 +87,52 @@ export default async function RootLayout(props: LayoutProps) {
},
}}
>
<nav className="nav" data-border="true">
<div className="nav__links">
<div className="nav__item" data-label="home">
{viewer.logo ? (
<nav className="nav" data-border="true" aria-label="Main">
<div className="nav__item" data-label="home">
{viewer.logo ? (
<ActiveLink href="/" className="nav__link">
<Image
src={viewer.logo.url}
alt="Logo"
width={(viewer.logo.meta.width ?? 100) / 2}
height={(viewer.logo.meta.height ?? 100) / 2}
priority
/>
</ActiveLink>
) : (
<h1>
<ActiveLink href="/" className="nav__link">
<Image
src={viewer.logo.url}
alt="Logo"
width={(viewer.logo.meta.width ?? 100) / 2}
height={(viewer.logo.meta.height ?? 100) / 2}
priority
/>
{viewer.settings?.title}
</ActiveLink>
) : (
<h1>
<ActiveLink href="/" className="nav__link">
{viewer.settings?.title}
</h1>
)}
</div>

<NavMenu>
<div className="nav__links">
<ActiveLink className="nav__link" href="/">
{moreThanOneCollection ? "Fonts" : "Home"}
</ActiveLink>
{pages
?.filter((node) => node.slug?.name !== "customer-login")
.map((node) => (
<ActiveLink
href={`/${node.slug?.name}`}
className="nav__link"
key={node.id}
>
{node.title}
</ActiveLink>
</h1>
)}
))}
</div>
<ActiveLink className="nav__link" href="/">
{moreThanOneCollection ? "Fonts" : "Home"}
</ActiveLink>
{pages
?.filter((node) => node.slug?.name !== "customer-login")
.map((node) => (
<ActiveLink
href={`/${node.slug?.name}`}
className="nav__link"
key={node.id}
>
{node.title}
</ActiveLink>
))}
</div>

<div className="nav__item" data-label="login">
<ActiveLink className="nav__link" href="/customer-login">
Log in
</ActiveLink>
</div>
<div className="nav__item" data-label="login">
<ActiveLink className="nav__link" href="/customer-login">
Log in
</ActiveLink>
</div>
</NavMenu>

<div className="nav__item" data-label="cart">
<CartButton buttonStyle="icon" />
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/app/test-fonts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export default async function TestFontsPage() {
{page?.text ? <FontdueHTML html={page.text} /> : null}
</article>

<TestFontsForm />
<div className="page__form">
<TestFontsForm />
</div>
</div>
</main>
);
Expand Down
10 changes: 6 additions & 4 deletions src/components/FontDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,12 @@ async function CollectionHeader({
view it.
</p>
</article>
<NodePasswordForm
collectionSlug={collectionSlug}
unlockEndpoint="/api/unlock"
/>
<div className="page__form">
<NodePasswordForm
collectionSlug={collectionSlug}
unlockEndpoint="/api/unlock"
/>
</div>
</div>
</main>
);
Expand Down
28 changes: 27 additions & 1 deletion src/components/FontdueHTML.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useMemo } from "react";
import parse from "html-react-parser";
import parse, { attributesToProps, Text } from "html-react-parser";
import { kebabToCamel } from "../lib/utils";
import TestFontsForm from "fontdue-js/TestFontsForm";
import NewsletterSignup from "fontdue-js/NewsletterSignup";
Expand Down Expand Up @@ -32,9 +32,35 @@ interface FontdueHTML_props {

export default function FontdueHTML({ html }: FontdueHTML_props) {
const content = useMemo(() => {
// Body images come through as bare <img> tags. The first one is often the
// only one above the fold, so it keeps loading eagerly and the rest wait
// until they are scrolled near, instead of every visitor fetching all of
// them up front.
let imageCount = 0;
return parse(html ?? "", {
replace: (domNode) => {
// Body copy sometimes arrives as a run of text with no paragraph
// around it. The reading measure is set on the children of the
// article, and a bare run of text is not one of them, so it is
// wrapped to keep it from running the full width of the column.
if (
domNode instanceof Text &&
domNode.parent === null &&
domNode.data.trim() !== ""
) {
return <div>{domNode.data}</div>;
}
if ("name" in domNode && "attribs" in domNode) {
if (domNode.name === "img") {
const isFirstImage = imageCount++ === 0;
return (
<img
{...attributesToProps(domNode.attribs)}
loading={isFirstImage ? "eager" : "lazy"}
decoding="async"
/>
);
}
const props = attrsToProps(domNode.attribs);
if (domNode.name === "fontdue-test-fonts-form") {
return <TestFontsForm {...props} />;
Expand Down
119 changes: 119 additions & 0 deletions src/components/NavMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { usePathname } from "next/navigation";

// The part of the header that folds away on narrow screens: on wide ones a row
// of links next to the wordmark, on narrow ones a panel behind a toggle.
//
// Which of the two you get is decided entirely in CSS, so this component only
// tracks whether the menu is open and there is no breakpoint duplicated here
// that could drift from the stylesheet. The panel is a sibling of the button
// rather than nested inside it so that on wide screens it can be dissolved
// with `display: contents`, letting the groups it holds sit directly in the
// header grid.
export default function NavMenu({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
const pathname = usePathname();
const toggleRef = useRef<HTMLButtonElement>(null);
const panelRef = useRef<HTMLDivElement>(null);

// Navigating does not unmount the panel, so without this it would stay open
// over the page you just moved to. Also covers the browser's back button,
// which no click handler would see.
useEffect(() => setOpen(false), [pathname]);

useEffect(() => {
if (!open) return;

const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setOpen(false);
toggleRef.current?.focus();
}
};
document.addEventListener("keydown", onKeyDown);

// Past the width the stylesheet stops drawing a panel, the menu is part of
// the bar again and none of what follows belongs to it. A window dragged
// wider or a phone turned on its side therefore has to close it, or the
// page keeps a scroll lock and an unreachable body with nothing on screen
// to explain either. Read the answer off the panel itself so the width
// that decides it stays in the stylesheet.
const onResize = () => {
const panel = panelRef.current;
if (panel && getComputedStyle(panel).display === "contents") {
setOpen(false);
}
};
window.addEventListener("resize", onResize);

// The menu covers the page, so the page behind it should neither scroll
// nor be reachable by keyboard while it does.
const { body } = document;
const previousOverflow = body.style.overflow;
const previousPadding = body.style.paddingRight;
// Locking the page takes its scrollbar away with it, and on the desktop
// browsers that reserve room for one that would shift the whole page
// sideways as the menu opens. Handing the width back as padding holds it
// still. Nothing to do where scrollbars are drawn over the page, which is
// every touch device.
const scrollbar = window.innerWidth - document.documentElement.clientWidth;
body.style.overflow = "hidden";
if (scrollbar > 0) {
const padding = parseFloat(getComputedStyle(body).paddingRight) || 0;
body.style.paddingRight = `${padding + scrollbar}px`;
}

const concealed = Array.from(
document.querySelectorAll<HTMLElement>(".main, .footer"),
);
concealed.forEach((element) => element.setAttribute("inert", ""));

// Move into the menu so the first thing after the toggle is a destination,
// not whatever happened to follow it in the document.
panelRef.current?.querySelector<HTMLElement>("a, button")?.focus();

return () => {
document.removeEventListener("keydown", onKeyDown);
window.removeEventListener("resize", onResize);
body.style.overflow = previousOverflow;
body.style.paddingRight = previousPadding;
concealed.forEach((element) => element.removeAttribute("inert"));
};
}, [open]);

return (
<>
<button
ref={toggleRef}
type="button"
className="nav__toggle"
// Doubles as the styling hook for the bars becoming a cross, so the
// open state is never stored in two places.
aria-expanded={open}
aria-controls="nav-menu"
onClick={() => setOpen((wasOpen) => !wasOpen)}
>
<span className="visually-hidden">{open ? "Close menu" : "Menu"}</span>
<span className="nav__burger" aria-hidden="true" />
</button>

<div
ref={panelRef}
className="nav__panel"
id="nav-menu"
data-open={open}
// Anything actionable in here navigates, and leaving the menu up over
// the page it just moved to is wrong.
onClick={(event) => {
if ((event.target as HTMLElement).closest("a, button")) {
setOpen(false);
}
}}
>
{children}
</div>
</>
);
}
Loading
Loading