NavLink.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import React from "react";
  2. import { __RouterContext as RouterContext, matchPath } from "react-router";
  3. import PropTypes from "prop-types";
  4. import invariant from "tiny-invariant";
  5. import Link from "./Link";
  6. import { resolveToLocation, normalizeToLocation } from "./utils/locationUtils";
  7. // React 15 compat
  8. const forwardRefShim = C => C;
  9. let { forwardRef } = React;
  10. if (typeof forwardRef === "undefined") {
  11. forwardRef = forwardRefShim;
  12. }
  13. function joinClassnames(...classnames) {
  14. return classnames.filter(i => i).join(" ");
  15. }
  16. /**
  17. * A <Link> wrapper that knows if it's "active" or not.
  18. */
  19. const NavLink = forwardRef(
  20. (
  21. {
  22. "aria-current": ariaCurrent = "page",
  23. activeClassName = "active",
  24. activeStyle,
  25. className: classNameProp,
  26. exact,
  27. isActive: isActiveProp,
  28. location: locationProp,
  29. strict,
  30. style: styleProp,
  31. to,
  32. innerRef, // TODO: deprecate
  33. ...rest
  34. },
  35. forwardedRef
  36. ) => {
  37. return (
  38. <RouterContext.Consumer>
  39. {context => {
  40. invariant(context, "You should not use <NavLink> outside a <Router>");
  41. const currentLocation = locationProp || context.location;
  42. const toLocation = normalizeToLocation(
  43. resolveToLocation(to, currentLocation),
  44. currentLocation
  45. );
  46. const { pathname: path } = toLocation;
  47. // Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
  48. const escapedPath =
  49. path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
  50. const match = escapedPath
  51. ? matchPath(currentLocation.pathname, {
  52. path: escapedPath,
  53. exact,
  54. strict
  55. })
  56. : null;
  57. const isActive = !!(isActiveProp
  58. ? isActiveProp(match, currentLocation)
  59. : match);
  60. const className = isActive
  61. ? joinClassnames(classNameProp, activeClassName)
  62. : classNameProp;
  63. const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;
  64. const props = {
  65. "aria-current": (isActive && ariaCurrent) || null,
  66. className,
  67. style,
  68. to: toLocation,
  69. ...rest
  70. };
  71. // React 15 compat
  72. if (forwardRefShim !== forwardRef) {
  73. props.ref = forwardedRef || innerRef;
  74. } else {
  75. props.innerRef = innerRef;
  76. }
  77. return <Link {...props} />;
  78. }}
  79. </RouterContext.Consumer>
  80. );
  81. }
  82. );
  83. if (__DEV__) {
  84. NavLink.displayName = "NavLink";
  85. const ariaCurrentType = PropTypes.oneOf([
  86. "page",
  87. "step",
  88. "location",
  89. "date",
  90. "time",
  91. "true"
  92. ]);
  93. NavLink.propTypes = {
  94. ...Link.propTypes,
  95. "aria-current": ariaCurrentType,
  96. activeClassName: PropTypes.string,
  97. activeStyle: PropTypes.object,
  98. className: PropTypes.string,
  99. exact: PropTypes.bool,
  100. isActive: PropTypes.func,
  101. location: PropTypes.object,
  102. strict: PropTypes.bool,
  103. style: PropTypes.object
  104. };
  105. }
  106. export default NavLink;