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

Features: useLinkClickHandler and useLinkPressHandler #7998

Merged
merged 3 commits into from
Sep 3, 2021
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
53 changes: 52 additions & 1 deletion docs/advanced-guides/custom-links.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,54 @@
# Custom Links

TODO
In most cases, the exported `<Link>` component should meet all of your needs as an abstraction of the anchor tag. If you need to return anything other than an anchor element, or override any of `<Link>`'s rendering logic, you can use a few hooks from `react-router-dom` to build your own:

```tsx
import { useHref, useLinkClickHandler } from "react-router-dom";

const StyledLink = styled("a", { color: "fuschia" });

const Link = React.forwardRef(
({ onClick, replace = false, state, target, to, ...rest }, ref) => {
let href = useHref(to);
let handleClick = useLinkClickHandler(to, { replace, state, target });

return (
<StyledLink
{...rest}
href={href}
onClick={event => {
onClick?.(event);
if (!event.defaultPrevented) {
handleClick(event);
}
}}
ref={ref}
target={target}
/>
);
}
);
```

If you're using `react-router-native`, you can create a custom `<Link>` with the `useLinkPressHandler` hook:

```tsx
import { TouchableHighlight } from "react-native";
import { useLinkPressHandler } from "react-router-native";

function Link({ onPress, replace = false, state, to, ...rest }) {
let handlePress = useLinkPressHandler(to, { replace, state });

return (
<TouchableHighlight
{...rest}
onPress={event => {
onPress?.(event);
if (!event.defaultPrevented) {
handlePress(event);
}
}}
/>
);
}
```
97 changes: 97 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ There are a few low-level APIs that we use internally that may also prove useful

- [`useResolvedPath`](#useresolvedpath) - resolves a relative path against the current [location](#location)
- [`useHref`](#usehref) - resolves a relative path suitable for use as a `<a href>`
- [`useLinkClickHandler`](#uselinkclickhandler) - returns an event handler to for navigation when building a custom `<Link>` in `react-router-dom`
- [`useLinkPressHandler`](#uselinkpresshandler) - returns an event handler to for navigation when building a custom `<Link>` in `react-router-native`
- [`resolvePath`](#resolvepath) - resolves a relative path against a given URL pathname

<a name="confirming-navigation"></a>
Expand Down Expand Up @@ -892,6 +894,101 @@ The `useHref` hook returns a URL that may be used to link to the given `to` loca
> component in `react-router-dom` to see how it uses `useHref` internally to
> determine its own `href` value.

<a name="uselinkclickhandler"></a>

### `useLinkClickHandler`

<details>
<summary>Type declaration</summary>

```tsx
declare function useLinkClickHandler<
E extends Element = HTMLAnchorElement,
S extends State = State
>(
to: To,
options?: {
target?: React.HTMLAttributeAnchorTarget;
replace?: boolean;
state?: S;
}
): (event: React.MouseEvent<E, MouseEvent>) => void;
```

</details>

The `useLinkClickHandler` hook returns a click event handler to for navigation when building a custom `<Link>` in `react-router-dom`.

```tsx
import { useHref, useLinkClickHandler } from "react-router-dom";

const StyledLink = styled("a", { color: "fuschia" });

const Link = React.forwardRef(
({ onClick, replace = false, state, target, to, ...rest }, ref) => {
let href = useHref(to);
let handleClick = useLinkClickHandler(to, { replace, state, target });

return (
<StyledLink
{...rest}
href={href}
onClick={event => {
onClick?.(event);
if (!event.defaultPrevented) {
handleClick(event);
}
}}
ref={ref}
target={target}
/>
);
}
);
```

<a name="uselinkpresshandler"></a>

### `useLinkPressHandler`

<details>
<summary>Type declaration</summary>

```tsx
declare function useLinkPressHandler<S extends State = State>(
to: To,
options?: {
replace?: boolean;
state?: S;
}
): (event: GestureResponderEvent) => void;
```

</details>

The `react-router-native` counterpart to `useLinkClickHandler`, `useLinkPressHandler` returns a press event handler for custom `<Link>` navigation.

```tsx
import { TouchableHighlight } from "react-native";
import { useLinkPressHandler } from "react-router-native";

function Link({ onPress, replace = false, state, to, ...rest }) {
let handlePress = useLinkPressHandler(to, { replace, state });

return (
<TouchableHighlight
{...rest}
onPress={event => {
onPress?.(event);
if (!event.defaultPrevented) {
handlePress(event);
}
}}
/>
);
}
```

<a name="useinroutercontext"></a>

### `useInRouterContext`
Expand Down
223 changes: 223 additions & 0 deletions packages/react-router-dom/__tests__/useLinkClickHandler-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { act } from "react-dom/test-utils";
import {
MemoryRouter as Router,
Routes,
Route,
useHref,
useLinkClickHandler
} from "react-router-dom";
import type { LinkProps } from "react-router-dom";

describe("Custom link with useLinkClickHandler", () => {
let node: HTMLDivElement;

function Link({ to, replace, state, target, ...rest }: LinkProps) {
let href = useHref(to);
let handleClick = useLinkClickHandler(to, { target, replace, state });
return (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a {...rest} href={href} onClick={handleClick} target={target} />
);
}

beforeEach(() => {
node = document.createElement("div");
document.body.appendChild(node);
});

afterEach(() => {
document.body.removeChild(node);
node = null!;
});

it("navigates to the new page", () => {
function Home() {
return (
<div>
<h1>Home</h1>
<Link to="../about">About</Link>
</div>
);
}

function About() {
return <h1>About</h1>;
}

act(() => {
ReactDOM.render(
<Router initialEntries={["/home"]}>
<Routes>
<Route path="home" element={<Home />} />
<Route path="about" element={<About />} />
</Routes>
</Router>,
node
);
});

let anchor = node.querySelector("a");
expect(anchor).not.toBeNull();

act(() => {
anchor?.dispatchEvent(
new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true
})
);
});

let h1 = node.querySelector("h1");
expect(h1).not.toBeNull();
expect(h1?.textContent).toEqual("About");
});

describe("with a right click", () => {
it("stays on the same page", () => {
function Home() {
return (
<div>
<h1>Home</h1>
<Link to="../about">About</Link>
</div>
);
}

function About() {
return <h1>About</h1>;
}

act(() => {
ReactDOM.render(
<Router initialEntries={["/home"]}>
<Routes>
<Route path="home" element={<Home />} />
<Route path="about" element={<About />} />
</Routes>
</Router>,
node
);
});

let anchor = node.querySelector("a");
expect(anchor).not.toBeNull();

act(() => {
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
let RightMouseButton = 2;
anchor?.dispatchEvent(
new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
button: RightMouseButton
})
);
});

let h1 = node.querySelector("h1");
expect(h1).not.toBeNull();
expect(h1?.textContent).toEqual("Home");
});
});

describe("when the link is supposed to open in a new window", () => {
it("stays on the same page", () => {
function Home() {
return (
<div>
<h1>Home</h1>
<Link to="../about" target="_blank">
About
</Link>
</div>
);
}

function About() {
return <h1>About</h1>;
}

act(() => {
ReactDOM.render(
<Router initialEntries={["/home"]}>
<Routes>
<Route path="home" element={<Home />} />
<Route path="about" element={<About />} />
</Routes>
</Router>,
node
);
});

let anchor = node.querySelector("a");
expect(anchor).not.toBeNull();

act(() => {
anchor?.dispatchEvent(
new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true
})
);
});

let h1 = node.querySelector("h1");
expect(h1).not.toBeNull();
expect(h1?.textContent).toEqual("Home");
});
});

describe("when the modifier keys are used", () => {
it("stays on the same page", () => {
function Home() {
return (
<div>
<h1>Home</h1>
<Link to="../about">About</Link>
</div>
);
}

function About() {
return <h1>About</h1>;
}

act(() => {
ReactDOM.render(
<Router initialEntries={["/home"]}>
<Routes>
<Route path="home" element={<Home />} />
<Route path="about" element={<About />} />
</Routes>
</Router>,
node
);
});

let anchor = node.querySelector("a");
expect(anchor).not.toBeNull();

act(() => {
anchor?.dispatchEvent(
new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
// The Ctrl key is pressed
ctrlKey: true
})
);
});

let h1 = node.querySelector("h1");
expect(h1).not.toBeNull();
expect(h1?.textContent).toEqual("Home");
});
});
});
Loading