Skip to content

Commit b5528ed

Browse files
ryanflorencemjackson
authored andcommittedSep 13, 2019
Use forwardRef on Link, NavLink (#6914)
This allows components like @reach/menu-button to manage focus or do anything else they need to with the underlying DOM element. While `ref` is preferred, the `innerRef` prop will still work.
1 parent 5abf91d commit b5528ed

File tree

4 files changed

+208
-102
lines changed

4 files changed

+208
-102
lines changed
 

‎packages/react-router-dom/modules/Link.js

+71-51
Original file line numberDiff line numberDiff line change
@@ -4,71 +4,89 @@ import PropTypes from "prop-types";
44
import invariant from "tiny-invariant";
55
import { resolveToLocation, normalizeToLocation } from "./utils/locationUtils";
66

7+
// React 15 compat
8+
let { forwardRef } = React;
9+
if (typeof forwardRef === "undefined") {
10+
forwardRef = C => C;
11+
}
12+
713
function isModifiedEvent(event) {
814
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
915
}
1016

11-
function LinkAnchor({ innerRef, navigate, onClick, ...rest }) {
12-
const { target } = rest;
13-
14-
return (
15-
<a
16-
{...rest}
17-
ref={innerRef} // TODO: Use forwardRef instead
18-
onClick={event => {
19-
try {
20-
if (onClick) onClick(event);
21-
} catch (ex) {
22-
event.preventDefault();
23-
throw ex;
24-
}
25-
26-
if (
27-
!event.defaultPrevented && // onClick prevented default
28-
event.button === 0 && // ignore everything but left clicks
29-
(!target || target === "_self") && // let browser handle "target=_blank" etc.
30-
!isModifiedEvent(event) // ignore clicks with modifier keys
31-
) {
32-
event.preventDefault();
33-
navigate();
34-
}
35-
}}
36-
/>
37-
);
17+
const LinkAnchor = forwardRef(
18+
({ innerRef, navigate, onClick, ...rest }, forwardedRef) => {
19+
const { target } = rest;
20+
21+
return (
22+
<a
23+
{...rest}
24+
ref={forwardedRef || innerRef}
25+
onClick={event => {
26+
try {
27+
if (onClick) onClick(event);
28+
} catch (ex) {
29+
event.preventDefault();
30+
throw ex;
31+
}
32+
33+
if (
34+
!event.defaultPrevented && // onClick prevented default
35+
event.button === 0 && // ignore everything but left clicks
36+
(!target || target === "_self") && // let browser handle "target=_blank" etc.
37+
!isModifiedEvent(event) // ignore clicks with modifier keys
38+
) {
39+
event.preventDefault();
40+
navigate();
41+
}
42+
}}
43+
/>
44+
);
45+
}
46+
);
47+
48+
if (__DEV__) {
49+
LinkAnchor.displayName = "LinkAnchor";
3850
}
3951

4052
/**
4153
* The public API for rendering a history-aware <a>.
4254
*/
43-
function Link({ component = LinkAnchor, replace, to, ...rest }) {
44-
return (
45-
<RouterContext.Consumer>
46-
{context => {
47-
invariant(context, "You should not use <Link> outside a <Router>");
55+
const Link = forwardRef(
56+
(
57+
{ component = LinkAnchor, replace, to, innerRef, ...rest },
58+
forwardedRef
59+
) => {
60+
return (
61+
<RouterContext.Consumer>
62+
{context => {
63+
invariant(context, "You should not use <Link> outside a <Router>");
4864

49-
const { history } = context;
65+
const { history } = context;
5066

51-
const location = normalizeToLocation(
52-
resolveToLocation(to, context.location),
53-
context.location
54-
);
67+
const location = normalizeToLocation(
68+
resolveToLocation(to, context.location),
69+
context.location
70+
);
5571

56-
const href = location ? history.createHref(location) : "";
72+
const href = location ? history.createHref(location) : "";
5773

58-
return React.createElement(component, {
59-
...rest,
60-
href,
61-
navigate() {
62-
const location = resolveToLocation(to, context.location);
63-
const method = replace ? history.replace : history.push;
74+
return React.createElement(component, {
75+
...rest,
76+
ref: forwardedRef || innerRef,
77+
href,
78+
navigate() {
79+
const location = resolveToLocation(to, context.location);
80+
const method = replace ? history.replace : history.push;
6481

65-
method(location);
66-
}
67-
});
68-
}}
69-
</RouterContext.Consumer>
70-
);
71-
}
82+
method(location);
83+
}
84+
});
85+
}}
86+
</RouterContext.Consumer>
87+
);
88+
}
89+
);
7290

7391
if (__DEV__) {
7492
const toType = PropTypes.oneOfType([
@@ -82,6 +100,8 @@ if (__DEV__) {
82100
PropTypes.shape({ current: PropTypes.any })
83101
]);
84102

103+
Link.displayName = "Link";
104+
85105
Link.propTypes = {
86106
innerRef: refType,
87107
onClick: PropTypes.func,

‎packages/react-router-dom/modules/NavLink.js

+68-49
Original file line numberDiff line numberDiff line change
@@ -5,68 +5,87 @@ import invariant from "tiny-invariant";
55
import Link from "./Link";
66
import { resolveToLocation, normalizeToLocation } from "./utils/locationUtils";
77

8+
// React 15 compat
9+
let { forwardRef } = React;
10+
if (typeof forwardRef === "undefined") {
11+
forwardRef = C => C;
12+
}
13+
814
function joinClassnames(...classnames) {
915
return classnames.filter(i => i).join(" ");
1016
}
1117

1218
/**
1319
* A <Link> wrapper that knows if it's "active" or not.
1420
*/
15-
function NavLink({
16-
"aria-current": ariaCurrent = "page",
17-
activeClassName = "active",
18-
activeStyle,
19-
className: classNameProp,
20-
exact,
21-
isActive: isActiveProp,
22-
location: locationProp,
23-
strict,
24-
style: styleProp,
25-
to,
26-
...rest
27-
}) {
28-
return (
29-
<RouterContext.Consumer>
30-
{context => {
31-
invariant(context, "You should not use <NavLink> outside a <Router>");
21+
const NavLink = forwardRef(
22+
(
23+
{
24+
"aria-current": ariaCurrent = "page",
25+
activeClassName = "active",
26+
activeStyle,
27+
className: classNameProp,
28+
exact,
29+
isActive: isActiveProp,
30+
location: locationProp,
31+
strict,
32+
style: styleProp,
33+
to,
34+
innerRef,
35+
...rest
36+
},
37+
forwardedRef
38+
) => {
39+
return (
40+
<RouterContext.Consumer>
41+
{context => {
42+
invariant(context, "You should not use <NavLink> outside a <Router>");
3243

33-
const currentLocation = locationProp || context.location;
34-
const toLocation = normalizeToLocation(
35-
resolveToLocation(to, currentLocation),
36-
currentLocation
37-
);
38-
const { pathname: path } = toLocation;
39-
// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
40-
const escapedPath =
41-
path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
44+
const currentLocation = locationProp || context.location;
45+
const toLocation = normalizeToLocation(
46+
resolveToLocation(to, currentLocation),
47+
currentLocation
48+
);
49+
const { pathname: path } = toLocation;
50+
// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
51+
const escapedPath =
52+
path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
4253

43-
const match = escapedPath
44-
? matchPath(currentLocation.pathname, { path: escapedPath, exact, strict })
45-
: null;
46-
const isActive = !!(isActiveProp
47-
? isActiveProp(match, currentLocation)
48-
: match);
54+
const match = escapedPath
55+
? matchPath(currentLocation.pathname, {
56+
path: escapedPath,
57+
exact,
58+
strict
59+
})
60+
: null;
61+
const isActive = !!(isActiveProp
62+
? isActiveProp(match, currentLocation)
63+
: match);
4964

50-
const className = isActive
51-
? joinClassnames(classNameProp, activeClassName)
52-
: classNameProp;
53-
const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;
65+
const className = isActive
66+
? joinClassnames(classNameProp, activeClassName)
67+
: classNameProp;
68+
const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;
5469

55-
return (
56-
<Link
57-
aria-current={(isActive && ariaCurrent) || null}
58-
className={className}
59-
style={style}
60-
to={toLocation}
61-
{...rest}
62-
/>
63-
);
64-
}}
65-
</RouterContext.Consumer>
66-
);
67-
}
70+
return (
71+
<Link
72+
ref={forwardedRef || innerRef}
73+
aria-current={(isActive && ariaCurrent) || null}
74+
className={className}
75+
style={style}
76+
to={toLocation}
77+
{...rest}
78+
/>
79+
);
80+
}}
81+
</RouterContext.Consumer>
82+
);
83+
}
84+
);
6885

6986
if (__DEV__) {
87+
NavLink.displayName = "NavLink";
88+
7089
const ariaCurrentType = PropTypes.oneOf([
7190
"page",
7291
"step",

‎packages/react-router-dom/modules/__tests__/Link-test.js

+45-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,26 @@ describe("A <Link>", () => {
107107
});
108108
});
109109

110-
it("exposes its ref via an innerRef callbar prop", () => {
110+
it("forwards a ref", () => {
111+
let refNode;
112+
function refCallback(n) {
113+
refNode = n;
114+
}
115+
116+
renderStrict(
117+
<MemoryRouter>
118+
<Link to="/" ref={refCallback}>
119+
link
120+
</Link>
121+
</MemoryRouter>,
122+
node
123+
);
124+
125+
expect(refNode).not.toBe(undefined);
126+
expect(refNode.tagName).toEqual("A");
127+
});
128+
129+
it("exposes its ref via an innerRef callback prop", () => {
111130
let refNode;
112131
function refCallback(n) {
113132
refNode = n;
@@ -126,6 +145,31 @@ describe("A <Link>", () => {
126145
expect(refNode.tagName).toEqual("A");
127146
});
128147

148+
it("prefers forwardRef over innerRef", () => {
149+
let refNode;
150+
function refCallback(n) {
151+
refNode = n;
152+
}
153+
154+
renderStrict(
155+
<MemoryRouter>
156+
<Link
157+
to="/"
158+
ref={refCallback}
159+
innerRef={() => {
160+
throw new Error("wrong ref, champ");
161+
}}
162+
>
163+
link
164+
</Link>
165+
</MemoryRouter>,
166+
node
167+
);
168+
169+
expect(refNode).not.toBe(undefined);
170+
expect(refNode.tagName).toEqual("A");
171+
});
172+
129173
it("uses a custom component prop", () => {
130174
let linkProps;
131175
function MyComponent(p) {

‎packages/react-router-dom/modules/__tests__/NavLink-test.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@ describe("A <NavLink>", () => {
1111
ReactDOM.unmountComponentAtNode(node);
1212
});
1313

14+
it("forwards a ref", () => {
15+
let refNode;
16+
function refCallback(n) {
17+
refNode = n;
18+
}
19+
20+
renderStrict(
21+
<MemoryRouter>
22+
<NavLink to="/" ref={refCallback}>
23+
link
24+
</NavLink>
25+
</MemoryRouter>,
26+
node
27+
);
28+
29+
expect(refNode).not.toBe(undefined);
30+
expect(refNode.tagName).toEqual("A");
31+
});
32+
1433
describe("when active", () => {
1534
it("applies its default activeClassName", () => {
1635
renderStrict(
@@ -490,7 +509,11 @@ describe("A <NavLink>", () => {
490509
it("overrides the current location for isActive", () => {
491510
renderStrict(
492511
<MemoryRouter initialEntries={["/pizza"]}>
493-
<NavLink to="/pasta" isActive={(_, location) => location.pathname === '/pasta'} location={{ pathname: "/pasta" }}>
512+
<NavLink
513+
to="/pasta"
514+
isActive={(_, location) => location.pathname === "/pasta"}
515+
location={{ pathname: "/pasta" }}
516+
>
494517
Pasta!
495518
</NavLink>
496519
</MemoryRouter>,

0 commit comments

Comments
 (0)
Please sign in to comment.