Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use wouter for routing #969

Merged
merged 6 commits into from
Apr 19, 2023
Merged
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"main": "./lib/index.js",
"browserslist": "chrome 70, firefox 70, safari 11.1",
"dependencies": {
"highlight.js": "^11.6.0"
"highlight.js": "^11.6.0",
"wouter-preact": "^2.10.0-alpha.1"
}
}
26 changes: 24 additions & 2 deletions src/pattern-library/components/Library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import classnames from 'classnames';
import { toChildArray } from 'preact';
import type { ComponentChildren, JSX } from 'preact';
import { useMemo, useState } from 'preact/hooks';
import { Link as RouteLink } from 'wouter-preact';

import { Scroll, ScrollContainer } from '../../';
import { Link as UILink, Scroll, ScrollContainer } from '../../';
import { jsxToHTML } from '../util/jsx-to-string';

/**
Expand Down Expand Up @@ -48,7 +49,10 @@ export type LibraryPageProps = {
function Page({ children, intro, title }: LibraryPageProps) {
return (
<section className="max-w-6xl pb-16 space-y-8 text-slate-7">
<div className="sticky top-0 z-4 h-16 flex items-center bg-slate-0 border-b">
<div
className="sticky top-0 z-4 h-16 flex items-center bg-slate-0 border-b"
id="page-header"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the page's sticky header. Just giving it an ID so I can query for it in the PlaygroundApp's navigation/scroll handling useEffect hook.

>
<h1 className="px-4 text-4xl font-light">{title}</h1>
</div>
{intro && (
Expand Down Expand Up @@ -365,12 +369,30 @@ function Usage({ componentName, size = 'md' }: LibraryUsageProps) {
);
}

export type LinkProps = {
children: ComponentChildren;
href: string;
};

/**
* Render an internal link to another pattern-library page.
* TODO: Support external links
*/
function Link({ children, href }: LinkProps) {
return (
<RouteLink href={href}>
<UILink underline="always">{children}</UILink>
</RouteLink>
);
}
Comment on lines +381 to +387
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So many things named Link, heh.

Copy link
Contributor

@acelaya acelaya Apr 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should call this component LinkLink 😝


export default {
Changelog,
ChangelogItem,
Code,
Demo,
Example,
Link,
Page,
Pattern,
Section,
Expand Down
285 changes: 176 additions & 109 deletions src/pattern-library/components/PlaygroundApp.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,85 @@
import classnames from 'classnames';
import type { ComponentChildren } from 'preact';
import { useEffect } from 'preact/hooks';
import {
Route,
Router,
Switch,
Link as RouteLink,
useLocation,
useRoute,
} from 'wouter-preact';

import type { PlaygroundAppProps } from '../';
import { Link, LogoIcon } from '../../';
import { useRoute } from '../router';
import { componentGroups, getRoutes } from '../routes';
import type { PlaygroundRoute } from '../routes';
import Library from './Library';

/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No meaningful changes to NavHeader, NavSection or NavList: just extracting them so that they are not inside of the PlaygroundApp component function.

* Header for a primary section of nav
*/
function NavHeader({ children }: { children: ComponentChildren }) {
return <h2 className="border-b px-2 py-1 font-light text-lg">{children}</h2>;
}

/**
* Secondary section of navigation, with header
*/
function NavSection({
title,
children,
}: {
title: string;
children: ComponentChildren;
}) {
return (
<div className="space-y-4">
<h3 className="mx-2 text-slate-7 font-semibold text-sm">{title}</h3>
{children}
</div>
);
}

/**
* Render a list of navigation items
*/
function NavList({ children }: { children: ComponentChildren }) {
return (
<ul className="ml-2 space-y-2 border-l-2 border-slate-0">{children}</ul>
);
}

/**
* A single navigation link
*/
function NavLink({ route }: { route: PlaygroundRoute }) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But there are (obviously) changes here to use the wouter API.

const [isActive] = useRoute(route.route ?? '');
return (
<li className="-ml-[2px]">
{route.route && (
<RouteLink href={route.route ?? ''}>
<Link
classes={classnames(
'pl-4 w-full border-l-2 hover:border-l-brand',

{
'border-l-2 border-brand font-semibold': isActive,
'border-transparent': !isActive,
}
)}
>
{route.title}
</Link>
</RouteLink>
)}
{!route.route && (
<div className="pl-4 w-full text-slate-5 font-light">{route.title}</div>
)}
</li>
);
}

/**
* Render web content for the playground application. This includes the "frame"
* around the page and a navigation channel, as well as the content rendered
Expand All @@ -19,130 +91,125 @@ export default function PlaygroundApp({
extraRoutesTitle = 'Playground',
}: PlaygroundAppProps) {
const routes = getRoutes();
const [pathname] = useLocation();

useEffect(
/**
* Support hash-based navigation and reset scroll when `wouter` path
* changes.
* - For locations without hash, reset scroll to top of page
* - For locations with hash, scroll to top of fragment-indicated element,
* and ensure it's not obscured by the sticky `#page-header` element.
*/
() => {
const hash = window.location.hash.replace(/^#/, '');
if (hash) {
const fragElement = document.getElementById(hash);
if (fragElement) {
fragElement.scrollIntoView();
const pageHeaderElement = document.getElementById('page-header');
// Height taken up by sticky header on page. Add 8 pixels to give
// some visual padding.
const headerOffset = pageHeaderElement
? pageHeaderElement.getBoundingClientRect().height + 8
: 0;
const fragTop = fragElement.getBoundingClientRect().top;
if (fragTop <= headerOffset) {
// Adjustment to accommodate sticky header (only if fragment is at or
// near top of viewport)
window.scrollBy({ top: -1 * (headerOffset - fragTop) });
}
}
} else {
window.scrollTo(0, 0);
}
},
[pathname]
);

// Put all of the custom routes into the "custom" group
const customRoutes = extraRoutes.map((route): PlaygroundRoute => {
return { ...route, group: 'custom' };
});
const allRoutes = routes.concat(customRoutes);
const [activeRoute] = useRoute(baseURL, allRoutes);
const content =
activeRoute && activeRoute.component ? (
<activeRoute.component />
) : (
<Library.Page title=":( Sorry">
<h1 className="text-2xl">Page not found</h1>
</Library.Page>
);

/**
* Header for a primary section of nav
*/
function NavHeader({ children }: { children: ComponentChildren }) {
return (
<h2 className="border-b px-2 py-1 font-light text-lg">{children}</h2>
);
}

/**
* A single navigation link
*/
function NavLink({ route }: { route: PlaygroundRoute }) {
return (
<li className="-ml-[2px]">
{route.route && (
<Link
classes={classnames('pl-4 w-full border-l-2 hover:border-l-brand', {
'border-l-2 border-brand font-semibold':
activeRoute?.route === route.route,
'border-transparent': activeRoute?.route !== route.route,
})}
href={`${baseURL}${route.route.toString()}`}
>
{route.title}
</Link>
)}
{!route.route && (
<div className="pl-4 w-full text-slate-5 font-light">
{route.title}
</div>
)}
</li>
);
}

/**
* Render a list of navigation items
*/
function NavList({ routes }: { routes: PlaygroundRoute[] }) {
return (
<ul className="ml-2 space-y-2 border-l-2 border-slate-0">
{routes.map(route => (
<NavLink key={route.title} route={route} />
const pageRoutes = (
<>
{allRoutes
.filter(route => !!route.route)
.map(aRoute => (
<Route key={aRoute.title} path={aRoute.route}>
{aRoute.component ?? aRoute.title}
</Route>
))}
</ul>
);
}
</>
);

/**
* Secondary section of navigation, with header
*/
function NavSection({
title,
children,
}: {
title: string;
children: ComponentChildren;
}) {
return (
<div className="space-y-4">
<h3 className="mx-2 text-slate-7 font-semibold text-sm">{title}</h3>
{children}
</div>
);
}
const groupKeys = Object.keys(componentGroups) as Array<
keyof typeof componentGroups
>;
return (
<main className="max-w-[1200px] mx-auto">
<div className="md:grid md:grid-cols-[2fr_5fr]">
<div className="md:h-screen md:sticky md:top-0 overflow-scroll">
<div className="md:sticky md:top-0 h-16 px-6 flex items-center bg-slate-0 border-b">
<h1 className="text-lg">
<Link href={baseURL + '/'} classes="grow flex gap-x-2">
<LogoIcon />
Component Library
</Link>
</h1>
</div>
<nav id="nav" className="pt-8 pb-16 space-y-4 mr-4">
<NavHeader>Foundations</NavHeader>
<NavList routes={getRoutes('foundations')} />
<Router base={baseURL}>
Copy link
Contributor Author

@lyzadanger lyzadanger Apr 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB: Except for wrapping the whole thing in a Router, there are no wouter-specific changes here except where noted.

<main className="max-w-[1200px] mx-auto">
<div className="md:grid md:grid-cols-[2fr_5fr]">
<div className="md:h-screen md:sticky md:top-0 overflow-scroll">
<div className="md:sticky md:top-0 h-16 px-6 flex items-center bg-slate-0 border-b">
<h1 className="text-lg">
<Link href={baseURL + '/'} classes="grow flex gap-x-2">
<LogoIcon />
Component Library
</Link>
</h1>
</div>
<nav id="nav" className="pt-8 pb-16 space-y-4 mr-4">
<NavHeader>Foundations</NavHeader>
<NavList>
{getRoutes('foundations').map(route => (
<NavLink key={route.title} route={route} />
))}
</NavList>

<NavHeader>Components</NavHeader>
<NavHeader>Components</NavHeader>

{groupKeys.map(group => {
return (
<NavSection
key={group}
title={componentGroups[group] as string}
>
<NavList routes={getRoutes(group)} />
</NavSection>
);
})}
{groupKeys.map(group => {
return (
<NavSection
key={group}
title={componentGroups[group] as string}
>
<NavList>
{getRoutes(group).map(route => (
<NavLink key={route.title} route={route} />
))}
</NavList>
</NavSection>
);
})}

{extraRoutes.length > 0 && (
<>
<NavHeader>{extraRoutesTitle}</NavHeader>
<NavList routes={customRoutes} />
</>
)}
</nav>
{extraRoutes.length > 0 && (
<>
<NavHeader>{extraRoutesTitle}</NavHeader>
<NavList>
{customRoutes.map(route => (
<NavLink key={route.title} route={route} />
))}
</NavList>
</>
)}
</nav>
</div>
<div className="bg-white pb-16">
<Switch>
{pageRoutes}
<Route>
<Library.Page title=":( Sorry">
<h1 className="text-2xl">Page not found</h1>
</Library.Page>
</Route>
</Switch>
Comment on lines +202 to +209
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<Switch> is a wouter component that will render (only) the first matching <Route> inside of it.

</div>
</div>
<div className="bg-white pb-16">{content}</div>
</div>
</main>
</main>
</Router>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export default function DialogPage() {
}
>
<Library.Section
id="dialog"
title="Dialog"
intro={
<p>
Expand Down Expand Up @@ -391,6 +392,7 @@ export default function DialogPage() {
</Library.Section>

<Library.Section
id="modaldialog"
title="ModalDialog"
intro={
<p>
Expand Down
Loading