clear-react-router 1.9.3 → 1.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,6 +107,8 @@ Component for client-side navigation with prefetch support, active state detecti
107
107
  | Prop | Type | Default | Description |
108
108
  |------|------|---------|-------------|
109
109
  | `to` | `string` | required | Target path |
110
+ | `as` | `(props: ElementProps<T>) => ReactElement` | renders `<a>` | Render prop for using a custom element/component instead of the default `<a>`. Receives the computed isActive and isPending values, event handlers, and ref to attach to your own element |
111
+ | `exact` | `boolean` | `false` | When `false`, the link is also considered active if the current URL starts with `to` (useful for nested routes) |
110
112
  | `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `Router` config | Override the global prefetch strategy |
111
113
  | `hoverPrefetchDelay` | `number` | `Router` config | Override the global hover delay |
112
114
  | `children` | `ReactNode` | required | Content to render inside the link |
@@ -120,7 +122,7 @@ Component for client-side navigation with prefetch support, active state detecti
120
122
 
121
123
  | State | Type | Description |
122
124
  |-------|------|-------------|
123
- | `isActive` | `boolean` | `true` when the link's `to` matches the current URL |
125
+ | `isActive` | `boolean` | `true` when the link's `to` matches the current URL considering `exact` value |
124
126
  | `isPending` | `boolean` | `true` when the target route is currently loading (loader is running) |
125
127
 
126
128
  ### Prefetch Strategies
@@ -137,6 +139,22 @@ Component for client-side navigation with prefetch support, active state detecti
137
139
  ```tsx
138
140
  import { Router, Link } from 'clear-react-router';
139
141
 
142
+ // Render a custom element/component via `as`. The function receives ref, event handlers, isActive/isPending and must render them itself
143
+ import { Button } from '@mui/material';
144
+
145
+ <Link
146
+ to="/dashboard"
147
+ as={({ isActive, isPending, ...props }) => (
148
+ <Button
149
+ {...props}
150
+ variant={isActive ? 'contained' : 'outlined'}
151
+ sx={{ opacity: isPending ? 0.5 : 1 }}
152
+ />
153
+ )}
154
+ >
155
+ Dashboard
156
+ </Link>
157
+
140
158
  // Global prefetch: hover with 100ms delay
141
159
  <Router routes={routes} prefetch="hover" hoverPrefetchDelay={100} />
142
160
 
@@ -169,6 +187,12 @@ import { Router, Link } from 'clear-react-router';
169
187
  <Link to="/details" beforeNavigate={saveDashboardData}>
170
188
  Admin Panel
171
189
  </Link>
190
+
191
+ // `exact={false}` — active for nested routes too
192
+ // e.g. active when current URL is "/settings" or "/settings/profile"
193
+ <Link to="/settings" exact={false}>
194
+ Settings
195
+ </Link>
172
196
  ```
173
197
  **Important**: prefetch="render" should be used sparingly, as it preloads data immediately when the link is rendered, which may cause unnecessary network requests.
174
198
 
@@ -1,19 +1,33 @@
1
- import { type CSSProperties, ReactNode, ComponentPropsWithoutRef } from 'react';
1
+ import { type CSSProperties, ReactNode, MouseEvent, ReactElement, Ref } from 'react';
2
2
  import { RouterProps } from '../types';
3
3
  type States = {
4
4
  isActive: boolean;
5
5
  isPending: boolean;
6
6
  };
7
- type LinkProps = Omit<ComponentPropsWithoutRef<'a'>, 'href' | 'className' | 'style'> & {
7
+ type ElementProps<T extends HTMLElement = HTMLElement> = {
8
+ ref: Ref<T>;
9
+ href: string;
10
+ isActive: boolean;
11
+ isPending: boolean;
12
+ onClick(event: MouseEvent): void;
13
+ onMouseEnter(event: MouseEvent): void;
14
+ onMouseLeave(event: MouseEvent): void;
15
+ className?: string;
16
+ style?: CSSProperties;
17
+ children?: ReactNode;
18
+ };
19
+ type LinkProps<T extends HTMLElement = HTMLAnchorElement> = {
8
20
  to: string;
9
- children: ReactNode;
21
+ children?: ReactNode;
22
+ as?: (props: ElementProps<T>) => ReactElement;
10
23
  prefetch?: RouterProps['prefetch'];
11
24
  hoverPrefetchDelay?: number;
12
- style?: CSSProperties | ((arg: States) => CSSProperties);
13
25
  className?: string | ((arg: States) => string);
14
26
  activeClassName?: string;
15
27
  pendingClassName?: string;
16
28
  beforeNavigate?(): Promise<void>;
29
+ style?: CSSProperties | ((arg: States) => CSSProperties);
30
+ exact?: boolean;
17
31
  };
18
- export declare const Link: ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style, beforeNavigate, activeClassName, pendingClassName, }: LinkProps) => import("react/jsx-runtime").JSX.Element;
32
+ export declare const Link: <T extends HTMLElement = HTMLAnchorElement>({ children, to, as, prefetch: prefetchLink, hoverPrefetchDelay, className, style, beforeNavigate, exact, activeClassName, pendingClassName, }: LinkProps<T>) => ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
19
33
  export {};
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ var createCommitState = ({ routeItemDataState, pendingState }) => (nextLocation,
44
44
  //#region constants.ts
45
45
  var emptyLoaderState = {};
46
46
  //#endregion
47
- //#region \0@oxc-project+runtime@0.133.0/helpers/esm/typeof.js
47
+ //#region \0@oxc-project+runtime@0.143.0/helpers/esm/typeof.js
48
48
  function _typeof(o) {
49
49
  "@babel/helpers - typeof";
50
50
  return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
@@ -54,7 +54,7 @@ function _typeof(o) {
54
54
  }, _typeof(o);
55
55
  }
56
56
  //#endregion
57
- //#region \0@oxc-project+runtime@0.133.0/helpers/esm/toPrimitive.js
57
+ //#region \0@oxc-project+runtime@0.143.0/helpers/esm/toPrimitive.js
58
58
  function toPrimitive(t, r) {
59
59
  if ("object" != _typeof(t) || !t) return t;
60
60
  var e = t[Symbol.toPrimitive];
@@ -66,13 +66,13 @@ function toPrimitive(t, r) {
66
66
  return ("string" === r ? String : Number)(t);
67
67
  }
68
68
  //#endregion
69
- //#region \0@oxc-project+runtime@0.133.0/helpers/esm/toPropertyKey.js
69
+ //#region \0@oxc-project+runtime@0.143.0/helpers/esm/toPropertyKey.js
70
70
  function toPropertyKey(t) {
71
71
  var i = toPrimitive(t, "string");
72
72
  return "symbol" == _typeof(i) ? i : i + "";
73
73
  }
74
74
  //#endregion
75
- //#region \0@oxc-project+runtime@0.133.0/helpers/esm/defineProperty.js
75
+ //#region \0@oxc-project+runtime@0.143.0/helpers/esm/defineProperty.js
76
76
  function _defineProperty(e, r, t) {
77
77
  return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
78
78
  value: t,
@@ -133,7 +133,8 @@ var createIsCacheItemFresh = (loaderMap) => ({ routeItem, pathname }) => {
133
133
  * LICENSE file in the root directory of this source tree.
134
134
  */
135
135
  var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((exports) => {
136
- var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
136
+ var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element");
137
+ var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
137
138
  function jsxProd(type, config, maybeKey) {
138
139
  var key = null;
139
140
  void 0 !== maybeKey && (key = "" + maybeKey);
@@ -756,12 +757,13 @@ var useLocation = () => {
756
757
  };
757
758
  //#endregion
758
759
  //#region components/Link.tsx
759
- var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style, beforeNavigate, activeClassName = "active-link", pendingClassName = "pending-link" }) => {
760
+ var defaultAs = ({ isActive, isPending, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { ...props });
761
+ var Link = ({ children, to, as = defaultAs, prefetch: prefetchLink, hoverPrefetchDelay, className, style, beforeNavigate, exact = false, activeClassName = "active-link", pendingClassName = "pending-link" }) => {
760
762
  const isPending = useIsRoutePending(to);
761
763
  const { pathname } = useLocation();
762
764
  const navigate = useNavigate();
763
765
  const timeout = useRef(0);
764
- const ref = useRef(null);
766
+ const elementRef = useRef(null);
765
767
  const { prefetch: configPrefetch, hoverPrefetchDelay: configPrefetchDelay } = routerConfig;
766
768
  const prefetch = prefetchLink || configPrefetch;
767
769
  const prefetchDelay = hoverPrefetchDelay ?? configPrefetchDelay;
@@ -789,17 +791,20 @@ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, classNam
789
791
  }, [prefetch, to]);
790
792
  useEffect(() => {
791
793
  if (prefetch !== "viewport") return;
792
- const element = ref.current;
793
- const observer = new IntersectionObserver(async () => {
794
+ const element = elementRef.current;
795
+ if (!element) return;
796
+ const observer = new IntersectionObserver(async ([entry]) => {
797
+ if (!entry.isIntersecting) return;
794
798
  await router.runtime.prefetch(to);
795
799
  observer.disconnect();
796
800
  });
797
- if (element) observer.observe(element);
798
- return () => {
799
- if (element) observer.disconnect();
800
- };
801
+ observer.observe(element);
802
+ return () => observer.disconnect();
801
803
  }, [prefetch, to]);
802
- const isActive = to === pathname;
804
+ useEffect(() => () => {
805
+ if (timeout.current) clearTimeout(timeout.current);
806
+ }, []);
807
+ const isActive = to === "/" ? pathname === "/" : exact ? pathname === to : pathname === to || pathname?.startsWith(`${to}/`);
803
808
  const normalizedClassName = typeof className === "function" ? className({
804
809
  isActive,
805
810
  isPending
@@ -814,18 +819,21 @@ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, classNam
814
819
  normalizedClassName
815
820
  ].filter(Boolean).join(" ");
816
821
  const clickHandler = async (event) => {
822
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
817
823
  event.preventDefault();
818
824
  await beforeNavigate?.();
819
825
  await navigate(to);
820
826
  };
821
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
827
+ return as({
828
+ ref: elementRef,
822
829
  href: to,
823
- ref,
824
830
  style: normalizedStyle,
825
831
  className: resultClassName,
826
832
  onClick: clickHandler,
827
833
  onMouseEnter,
828
834
  onMouseLeave,
835
+ isActive,
836
+ isPending,
829
837
  children
830
838
  });
831
839
  };
@@ -984,8 +992,10 @@ var useSearchParams = () => {
984
992
  currentParams.delete(param);
985
993
  (Array.isArray(value) ? value : [value]).forEach((v) => currentParams.append(param, v));
986
994
  navigateWithSearchParams(currentParams);
987
- } else if (typeof param === "function") navigateWithSearchParams(param(currentParams));
988
- else throw new Error("useSearchParams first argument must be either function or string");
995
+ } else if (typeof param === "function") {
996
+ const newParams = param(currentParams);
997
+ navigateWithSearchParams(newParams);
998
+ } else throw new Error("useSearchParams first argument must be either function or string");
989
999
  }, [navigateWithSearchParams, search])
990
1000
  };
991
1001
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.9.3",
3
+ "version": "1.9.5",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {