@ionic/react-router 9.0.8-nightly.20261008 → 9.0.8-nightly.20261009

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/dist/index.js CHANGED
@@ -8,6 +8,30 @@ const IonRouteInner = ({ path, index, caseSensitive, element }) => {
8
8
  return React.createElement(Route, { path: path, index: index, caseSensitive: caseSensitive, element: element });
9
9
  };
10
10
 
11
+ /**
12
+ * Compares two route param objects by value, treating a missing object as empty.
13
+ */
14
+ const areParamsEqual = (a, b) => {
15
+ const paramsA = a || {};
16
+ const paramsB = b || {};
17
+ const keysA = Object.keys(paramsA);
18
+ const keysB = Object.keys(paramsB);
19
+ if (keysA.length !== keysB.length) {
20
+ return false;
21
+ }
22
+ return keysA.every((key) => {
23
+ const valueA = paramsA[key];
24
+ const valueB = paramsB[key];
25
+ if (Array.isArray(valueA) && Array.isArray(valueB)) {
26
+ if (valueA.length !== valueB.length) {
27
+ return false;
28
+ }
29
+ return valueA.every((entry, idx) => entry === valueB[idx]);
30
+ }
31
+ return valueA === valueB;
32
+ });
33
+ };
34
+
11
35
  /**
12
36
  * The matchPath function is used only for matching paths, not rendering components or elements.
13
37
  * @see https://reactrouter.com/v6/utils/match-path
@@ -694,6 +718,40 @@ const resolveIndexRouteMatch = (viewItem, pathname, parentPath) => {
694
718
  const normalizedBase = normalizePathnameForComparison(previousMatch.pathnameBase || previousMatch.pathname || '');
695
719
  return normalizedPathname === normalizedBase ? previousMatch : null;
696
720
  };
721
+ const shallowEqual = (a, b) => {
722
+ const aRecord = a;
723
+ const bRecord = b;
724
+ const aKeys = Object.keys(aRecord);
725
+ return aKeys.length === Object.keys(bRecord).length && aKeys.every((key) => aRecord[key] === bRecord[key]);
726
+ };
727
+ const isSameMatch = (a, b) => a === b ||
728
+ (a.pathname === b.pathname &&
729
+ a.pathnameBase === b.pathnameBase &&
730
+ areParamsEqual(a.params, b.params) &&
731
+ (a.route === b.route || shallowEqual(a.route, b.route)));
732
+ /**
733
+ * The outlet rebuilds a view's `RouteContext` several times per navigation, so
734
+ * this compares by content to tell whether anything actually changed.
735
+ */
736
+ const isSameRouteContext = (a, b) => {
737
+ const { matches: aMatches, ...aRest } = a;
738
+ const { matches: bMatches, ...bRest } = b;
739
+ return (aMatches.length === bMatches.length &&
740
+ shallowEqual(aRest, bRest) &&
741
+ aMatches.every((match, i) => isSameMatch(match, bMatches[i])));
742
+ };
743
+ /**
744
+ * Reuses the last `RouteContext` while its content is the same or `hold` is set,
745
+ * so hidden pages that only call `useParams` don't re-render. It lives in
746
+ * component state because outlets with the same id share view items.
747
+ */
748
+ const ViewRouteContextProvider = ({ value, hold, children }) => {
749
+ const [heldValue, setHeldValue] = React.useState(value);
750
+ if (!hold && heldValue !== value && !isSameRouteContext(heldValue, value)) {
751
+ setHeldValue(value);
752
+ }
753
+ return React.createElement(UNSAFE_RouteContext.Provider, { value: heldValue }, children);
754
+ };
697
755
  class ReactRouterViewStack extends ViewStacks {
698
756
  /**
699
757
  * Stores the computed parent path for each outlet.
@@ -894,6 +952,8 @@ class ReactRouterViewStack extends ViewStacks {
894
952
  viewItem.mount = true;
895
953
  viewItem.routeData.match = match;
896
954
  }
955
+ // A more specific sibling owns the pathname, so this view is hidden even though it matches.
956
+ let hiddenBySpecificMatch = false;
897
957
  // Deactivate wildcard (catch-all) and empty-path (default) routes when a more-specific route matches.
898
958
  // This prevents "Not found" or fallback pages from showing alongside valid routes.
899
959
  if (routePath === '*' || routePath === '/*' || routePath === '') {
@@ -950,6 +1010,7 @@ class ReactRouterViewStack extends ViewStacks {
950
1010
  }
951
1011
  }
952
1012
  if (hasSpecificMatch && !isSwipeRevealed(viewItem)) {
1013
+ hiddenBySpecificMatch = true;
953
1014
  // A splat can be the outlet's container page rather than a "not found" fallback.
954
1015
  // Pushed over it is the page underneath, so unmounting it destroys its state and
955
1016
  // leaves nothing for back to reveal. Hiding it below covers that. A view with no
@@ -972,6 +1033,11 @@ class ReactRouterViewStack extends ViewStacks {
972
1033
  viewItem.routeData.match = match;
973
1034
  }
974
1035
  const routeMatch = shouldSkipForDifferentParam ? viewItem.routeData?.match : match || viewItem.routeData?.match;
1036
+ // Hold the context when a sibling owns the pathname, the view is parked on another param
1037
+ // instance, or it doesn't match. A pathless default only strictly matches at "/", so no match
1038
+ // there doesn't mean it's hidden.
1039
+ const isPathlessDefault = !routePath && !viewItem.routeData?.childProps?.index;
1040
+ const holdRouteContext = hiddenBySpecificMatch || !!shouldSkipForDifferentParam || (!match && !isPathlessDefault);
975
1041
  return (React.createElement(UNSAFE_RouteContext.Consumer, { key: `view-context-${viewItem.id}` }, (parentContext) => {
976
1042
  const parentMatches = (parentContext?.matches ?? []);
977
1043
  // Accumulate params from parent matches, with fallback to other outlets
@@ -987,7 +1053,7 @@ class ReactRouterViewStack extends ViewStacks {
987
1053
  ? { ...parentContext, matches: contextMatches }
988
1054
  : { outlet: null, matches: contextMatches, isDataRoute: false };
989
1055
  return (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) },
990
- React.createElement(UNSAFE_RouteContext.Provider, { value: routeContextValue }, componentElement)));
1056
+ React.createElement(ViewRouteContextProvider, { value: routeContextValue, hold: holdRouteContext }, componentElement)));
991
1057
  }));
992
1058
  };
993
1059
  /**
@@ -3106,26 +3172,6 @@ const checkIsMultiStepBack = (prevInfo, destinationPathname, history) => {
3106
3172
  }
3107
3173
  return false;
3108
3174
  };
3109
- const areParamsEqual = (a, b) => {
3110
- const paramsA = a || {};
3111
- const paramsB = b || {};
3112
- const keysA = Object.keys(paramsA);
3113
- const keysB = Object.keys(paramsB);
3114
- if (keysA.length !== keysB.length) {
3115
- return false;
3116
- }
3117
- return keysA.every((key) => {
3118
- const valueA = paramsA[key];
3119
- const valueB = paramsB[key];
3120
- if (Array.isArray(valueA) && Array.isArray(valueB)) {
3121
- if (valueA.length !== valueB.length) {
3122
- return false;
3123
- }
3124
- return valueA.every((entry, idx) => entry === valueB[idx]);
3125
- }
3126
- return valueA === valueB;
3127
- });
3128
- };
3129
3175
  const IonRouter = ({ children, registerHistoryListener }) => {
3130
3176
  const location = useLocation();
3131
3177
  const navigate = useNavigate();