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

v3: link with as child pattern #378

Merged
merged 18 commits into from
Nov 15, 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
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
},
{
"path": "packages/wouter-preact/esm/index.js",
"limit": "2000 B",
"limit": "2500 B",
"ignore": [
"preact",
"preact/hooks"
Expand Down Expand Up @@ -135,6 +135,7 @@
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-replace": "^5.0.2",
"@size-limit/preset-small-lib": "^10.0.1",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.0.0",
"@types/babel__core": "^7.20.2",
"@types/react": "^18.2.0",
Expand All @@ -157,7 +158,7 @@
"rimraf": "^3.0.2",
"rollup": "^3.7.4",
"size-limit": "^10.0.1",
"typescript": "5.0.4",
"vitest": "^0.34.3"
"typescript": "5.2.2",
"vitest": "^0.34.6"
}
}
2 changes: 1 addition & 1 deletion packages/wouter-preact/test/preact.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe("Preact support", () => {
<Link href="/albums/all" onClick={fn} data-testid="index-link">
The Best Albums Ever
</Link>
<Link to="/albums/london-calling">
<Link to="/albums/london-calling" asChild>
<a data-testid="featured-link">
Featured Now: London Calling, Clash
</a>
Expand Down
11 changes: 6 additions & 5 deletions packages/wouter-preact/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,12 @@ export type NavigationalProps<
> = ({ to: Path; href?: never } | { href: Path; to?: never }) &
HookNavigationOptions<H>;

export type LinkProps<H extends BaseLocationHook = BrowserLocationHook> = Omit<
JSX.HTMLAttributes,
"href"
> &
NavigationalProps<H>;
type AsChildProps<ComponentProps, DefaultElementProps> =
| ({ asChild?: false } & ComponentProps & DefaultElementProps)
| ({ asChild: true; children: ComponentChildren } & ComponentProps);

export type LinkProps<H extends BaseLocationHook = BrowserLocationHook> =
AsChildProps<NavigationalProps<H>, JSX.HTMLAttributes>;

export type RedirectProps<H extends BaseLocationHook = BrowserLocationHook> =
NavigationalProps<H> & {
Expand Down
1 change: 1 addition & 0 deletions packages/wouter/setup-vitest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "@testing-library/jest-dom/vitest";
38 changes: 22 additions & 16 deletions packages/wouter/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,20 @@ export const Link = forwardRef((props, ref) => {
const router = useRouter();
const [, navigate] = useLocationFromRouter(router);

const { to, href = to, children, onClick } = props;

const handleClick = useEvent((event) => {
const {
to,
href: _href = to,
onClick: _onClick,
asChild,
children,
/* eslint-disable no-unused-vars */
replace /* ignore nav props */,
state /* ignore nav props */,
/* eslint-enable no-unused-vars */
...restProps
} = props;

const onClick = useEvent((event) => {
// ignores the navigation when clicked using right mouse button or
// by holding a special modifier key: ctrl, command, win, alt, shift
if (
Expand All @@ -197,24 +208,19 @@ export const Link = forwardRef((props, ref) => {
)
return;

onClick && onClick(event);
_onClick && _onClick(event); // TODO: is it safe to use _onClick?.(event)
if (!event.defaultPrevented) {
event.preventDefault();
navigate(to || href, props);
navigate(_href, props);
}
});

// wraps children in `a` if needed
const extraProps = {
// handle nested routers and absolute paths
href: href[0] === "~" ? href.slice(1) : router.base + href,
onClick: handleClick,
to: null,
ref,
};
const jsx = isValidElement(children) ? children : h("a", props);

return cloneElement(jsx, extraProps);
// handle nested routers and absolute paths
const href = _href[0] === "~" ? _href.slice(1) : router.base + _href;

return asChild && isValidElement(children)
? cloneElement(children, { href, onClick })
: h("a", { ...restProps, href, onClick, children, ref });
});

const flattenChildren = (children) => {
Expand Down
168 changes: 131 additions & 37 deletions packages/wouter/test/link.test-d.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,83 @@
import { describe, it, assertType } from "vitest";
import { Link } from "wouter";
import { describe, it } from "vitest";
import { Link, type Path } from "wouter";
import * as React from "react";

describe("Link types", () => {
type NetworkLocationHook = () => [
Path,
(path: string, options: { host: string; retries?: number }) => void
];

describe("<Link /> types", () => {
it("should have required prop href", () => {
// @ts-expect-error
assertType(<Link>test</Link>);
assertType(<Link href="/">test</Link>);
<Link>test</Link>;
<Link href="/">test</Link>;
});

it("does not allow `to` and `href` props to be used at the same time", () => {
// @ts-expect-error
<Link to="/hello" href="/world">
Hello
</Link>;
});

it("should inherit props from `HTMLAnchorElement`", () => {
<Link to="/hello" className="hello">
Hello
</Link>;

<Link to="/hello" style={{}}>
Hello
</Link>;

<Link to="/hello" target="_blank">
Hello
</Link>;

<Link to="/hello" download ping="he-he">
Hello
</Link>;
});

it("should support other navigation params", () => {
<Link href="/" state={{ a: "foo" }}>
test
</Link>;

<Link href="/" replace>
test
</Link>;
jeetiss marked this conversation as resolved.
Show resolved Hide resolved

// @ts-expect-error
<Link to="/" replace={{ nope: 1 }}>
Hello
</Link>;

<Link href="/" state={undefined}>
test
</Link>;
});
jeetiss marked this conversation as resolved.
Show resolved Hide resolved

it("should support state prop", () => {
assertType(
<Link href="/" state={{ a: "foo" }}>
test
</Link>
);
assertType(
<Link href="/" state={null}>
test
</Link>
);
assertType(
<Link href="/" state={undefined}>
test
</Link>
);
assertType(
<Link href="/" state="string">
test
</Link>
);
it("should work with generic type", () => {
<Link<NetworkLocationHook> href="/" host="wouter.com">
test
</Link>;

// @ts-expect-error
<Link<NetworkLocationHook> href="/">test</Link>;

<Link<NetworkLocationHook> href="/" host="wouter.com" retries={4}>
test
</Link>;
});
});

describe("Link's ref", () => {
describe("<Link /> with ref", () => {
it("should work", () => {
const ref = React.useRef<HTMLAnchorElement>(null);

<Link to="/" ref={ref}>
HELLO
Hello
</Link>;
});

Expand All @@ -47,7 +86,7 @@ describe("Link's ref", () => {

// @ts-expect-error
<Link to="/" ref={ref}>
HELLO
Hello
</Link>;
});

Expand All @@ -56,19 +95,74 @@ describe("Link's ref", () => {

// @ts-expect-error
<Link to="/" ref={ref}>
HELLO
Hello
</Link>;
});
});

it("should work with composed components", () => {
const ref = React.useRef<React.ElementRef<typeof Component>>(null);
describe("<Link /> with `asChild` prop", () => {
it("should work", () => {
<Link to="/" asChild>
<a>Hello</a>
</Link>;
molefrog marked this conversation as resolved.
Show resolved Hide resolved
});

<Link to="/">
<Component ref={ref}></Component>
it("does not allow `to` and `href` props to be used at the same time", () => {
// @ts-expect-error
<Link to="/hello" href="/world" asChild>
<a>Hello</a>
</Link>;
});

it("does not allow other props", () => {
// @ts-expect-error
<Link to="/" asChild className="">
<a>Hello</a>
</Link>;

// @ts-expect-error
<Link to="/" asChild style={{}}>
<a>Hello</a>
</Link>;

// @ts-expect-error
<Link to="/" asChild unknown={10}>
<a>Hello</a>
</Link>;

// @ts-expect-error
<Link to="/" asChild ref={null}>
<a>Hello</a>
</Link>;
});
});

const Component = React.forwardRef<{ hello: "world" }>((props, ref) => {
return <>test</>;
it("should support other navigation params", () => {
<Link to="/" asChild replace>
<a>Hello</a>
</Link>;

// @ts-expect-error
<Link to="/" asChild replace={12}>
<a>Hello</a>
</Link>;

<Link to="/" asChild state={{ hello: "world" }}>
<a>Hello</a>
</Link>;
});

it("should work with generic type", () => {
<Link<NetworkLocationHook> asChild to="/" host="wouter.com">
<div>test</div>
</Link>;

// @ts-expect-error
<Link<NetworkLocationHook> asChild to="/">
<div>test</div>
</Link>;

<Link<NetworkLocationHook> asChild to="/" host="wouter.com" retries={4}>
<div>test</div>
</Link>;
});
});
Loading