NavLink.js 3.5 KB

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