@ionic/react-router 8.7.12-dev.11764777950.1a565a37 → 8.7.12-dev.11764844107.1b9ced55

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
@@ -1,1034 +1,176 @@
1
1
  import { __rest } from 'tslib';
2
- import React, { useRef, useState, useEffect, useCallback } from 'react';
3
- import { Route, matchPath as matchPath$1, Routes, Navigate, UNSAFE_RouteContext, useLocation, useNavigate, BrowserRouter, useNavigationType, HashRouter } from 'react-router-dom';
4
- import { ViewStacks, IonRoute, ViewLifeCycleManager, generateId, StackContext, RouteManagerContext, getConfig, LocationHistory, NavManager } from '@ionic/react';
5
- import { MemoryRouter, useLocation as useLocation$1, useNavigationType as useNavigationType$1 } from 'react-router';
2
+ import { createBrowserHistory, createHashHistory } from 'history';
3
+ import React from 'react';
4
+ import { withRouter, Router } from 'react-router-dom';
5
+ import { ViewStacks, generateId, IonRoute, ViewLifeCycleManager, StackContext, RouteManagerContext, getConfig, LocationHistory, NavManager } from '@ionic/react';
6
+ import { Route, matchPath as matchPath$1, Router as Router$1 } from 'react-router';
6
7
 
7
- const IonRouteInner = ({ path, element }) => {
8
- return React.createElement(Route, { path: path, element: element });
9
- };
10
-
11
- /**
12
- * The matchPath function is used only for matching paths, not rendering components or elements.
13
- * @see https://reactrouter.com/v6/utils/match-path
14
- */
15
- const matchPath = ({ pathname, componentProps }) => {
16
- const { path, index } = componentProps, restProps = __rest(componentProps, ["path", "index"]);
17
- // Handle index routes
18
- if (index && !path) {
19
- // Index routes match when there's no additional path after the parent route
20
- // For example, in a nested outlet at /routing/*, the index route matches
21
- // when the relative path is empty (i.e., we're exactly at /routing)
22
- // If pathname is empty or just "/", it should match the index route
23
- if (pathname === '' || pathname === '/') {
24
- return {
25
- params: {},
26
- pathname: pathname,
27
- pathnameBase: pathname || '/',
28
- pattern: {
29
- path: '',
30
- caseSensitive: false,
31
- end: true,
32
- },
33
- };
34
- }
35
- // Otherwise, index routes don't match when there's additional path
36
- return null;
37
- }
38
- if (!path) {
39
- return null;
40
- }
41
- // For relative paths in nested routes (those that don't start with '/'),
42
- // use React Router's matcher against a normalized path.
43
- if (!path.startsWith('/')) {
44
- const matchOptions = Object.assign({ path: `/${path}` }, restProps);
45
- if ((matchOptions === null || matchOptions === void 0 ? void 0 : matchOptions.end) === undefined) {
46
- matchOptions.end = !path.endsWith('*');
47
- }
48
- const normalizedPathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
49
- const match = matchPath$1(matchOptions, normalizedPathname);
50
- if (match) {
51
- // Adjust the match to remove the leading '/' we added
52
- return Object.assign(Object.assign({}, match), { pathname: pathname, pathnameBase: match.pathnameBase === '/' ? '' : match.pathnameBase.slice(1), pattern: Object.assign(Object.assign({}, match.pattern), { path: path }) });
53
- }
54
- // No match found
55
- return null;
56
- }
57
- // For absolute paths, use React Router's matcher directly.
58
- // React Router v6 routes default to `end: true` unless the pattern
59
- // explicitly opts into wildcards with `*`. Mirror that behaviour so
60
- // matching parity stays aligned with <Route>.
61
- const matchOptions = Object.assign({ path }, restProps);
62
- if ((matchOptions === null || matchOptions === void 0 ? void 0 : matchOptions.end) === undefined) {
63
- matchOptions.end = !path.endsWith('*');
64
- }
65
- return matchPath$1(matchOptions, pathname);
66
- };
67
- /**
68
- * Determines the portion of a pathname that a given route pattern should match against.
69
- * For absolute route patterns we return the full pathname. For relative patterns we
70
- * strip off the already-matched parent segments so React Router receives the remainder.
71
- */
72
- const derivePathnameToMatch = (fullPathname, routePath) => {
73
- var _a;
74
- if (!routePath || routePath === '' || routePath.startsWith('/')) {
75
- return fullPathname;
76
- }
77
- const trimmedPath = fullPathname.startsWith('/') ? fullPathname.slice(1) : fullPathname;
78
- if (!trimmedPath) {
79
- return '';
80
- }
81
- const fullSegments = trimmedPath.split('/').filter(Boolean);
82
- if (fullSegments.length === 0) {
83
- return '';
84
- }
85
- const routeSegments = routePath.split('/').filter(Boolean);
86
- if (routeSegments.length === 0) {
87
- return trimmedPath;
88
- }
89
- const wildcardIndex = routeSegments.findIndex((segment) => segment === '*' || segment === '**');
90
- if (wildcardIndex >= 0) {
91
- const baseSegments = routeSegments.slice(0, wildcardIndex);
92
- if (baseSegments.length === 0) {
93
- return trimmedPath;
94
- }
95
- const startIndex = fullSegments.findIndex((_, idx) => baseSegments.every((seg, segIdx) => {
96
- const target = fullSegments[idx + segIdx];
97
- if (!target) {
98
- return false;
99
- }
100
- if (seg.startsWith(':')) {
101
- return true;
8
+ class IonRouteInner extends React.PureComponent {
9
+ render() {
10
+ return (React.createElement(Route, Object.assign({ path: this.props.path, exact: this.props.exact, render: this.props.render }, (this.props.computedMatch !== undefined
11
+ ? {
12
+ computedMatch: this.props.computedMatch,
102
13
  }
103
- return target === seg;
104
- }));
105
- if (startIndex >= 0) {
106
- return fullSegments.slice(startIndex).join('/');
107
- }
14
+ : {}))));
108
15
  }
109
- if (routeSegments.length <= fullSegments.length) {
110
- return fullSegments.slice(fullSegments.length - routeSegments.length).join('/');
111
- }
112
- return (_a = fullSegments[fullSegments.length - 1]) !== null && _a !== void 0 ? _a : trimmedPath;
113
- };
16
+ }
114
17
 
115
18
  /**
116
- * Finds the longest common prefix among an array of paths.
117
- * Used to determine the scope of an outlet with absolute routes.
118
- *
119
- * @param paths An array of absolute path strings.
120
- * @returns The common prefix shared by all paths.
121
- */
122
- const computeCommonPrefix = (paths) => {
123
- if (paths.length === 0)
124
- return '';
125
- if (paths.length === 1) {
126
- // For a single path, extract the directory-like prefix
127
- // e.g., /dynamic-routes/home -> /dynamic-routes
128
- const segments = paths[0].split('/').filter(Boolean);
129
- if (segments.length > 1) {
130
- return '/' + segments.slice(0, -1).join('/');
131
- }
132
- return '/' + segments[0];
133
- }
134
- // Split all paths into segments
135
- const segmentArrays = paths.map((p) => p.split('/').filter(Boolean));
136
- const minLength = Math.min(...segmentArrays.map((s) => s.length));
137
- const commonSegments = [];
138
- for (let i = 0; i < minLength; i++) {
139
- const segment = segmentArrays[0][i];
140
- // Skip segments with route parameters or wildcards
141
- if (segment.includes(':') || segment.includes('*')) {
142
- break;
143
- }
144
- const allMatch = segmentArrays.every((s) => s[i] === segment);
145
- if (allMatch) {
146
- commonSegments.push(segment);
147
- }
148
- else {
149
- break;
150
- }
151
- }
152
- return commonSegments.length > 0 ? '/' + commonSegments.join('/') : '';
153
- };
154
- /**
155
- * Checks if a route is a specific match (not wildcard or index).
156
- *
157
- * @param route The route element to check.
158
- * @param remainingPath The remaining path to match against.
159
- * @returns True if the route specifically matches the remaining path.
19
+ * @see https://v5.reactrouter.com/web/api/matchPath
160
20
  */
161
- const isSpecificRouteMatch = (route, remainingPath) => {
162
- const routePath = route.props.path;
163
- const isWildcardOnly = routePath === '*' || routePath === '/*';
164
- const isIndex = route.props.index;
165
- // Skip wildcards and index routes
166
- if (isIndex || isWildcardOnly) {
21
+ const matchPath = ({ pathname, componentProps, }) => {
22
+ const { exact, component } = componentProps;
23
+ const path = componentProps.path || componentProps.from;
24
+ /***
25
+ * The props to match against, they are identical
26
+ * to the matching props `Route` accepts. It could also be a string
27
+ * or an array of strings as shortcut for `{ path }`.
28
+ */
29
+ const matchProps = {
30
+ exact,
31
+ path,
32
+ component,
33
+ };
34
+ const match = matchPath$1(pathname, matchProps);
35
+ if (!match) {
167
36
  return false;
168
37
  }
169
- return !!matchPath({
170
- pathname: remainingPath,
171
- componentProps: route.props,
172
- });
173
- };
174
- /**
175
- * Analyzes route children to determine their characteristics.
176
- *
177
- * @param routeChildren The route children to analyze.
178
- * @returns Analysis of the route characteristics.
179
- */
180
- const analyzeRouteChildren = (routeChildren) => {
181
- const hasRelativeRoutes = routeChildren.some((route) => {
182
- const path = route.props.path;
183
- return path && !path.startsWith('/') && path !== '*';
184
- });
185
- const hasIndexRoute = routeChildren.some((route) => route.props.index);
186
- const hasWildcardRoute = routeChildren.some((route) => {
187
- const routePath = route.props.path;
188
- return routePath === '*' || routePath === '/*';
189
- });
190
- return { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute, routeChildren };
191
- };
192
- /**
193
- * Computes the parent path for a nested outlet based on the current pathname
194
- * and the outlet's route configuration.
195
- *
196
- * The algorithm finds the shortest parent path where a route matches the remaining path.
197
- * Priority: specific routes > wildcard routes > index routes (only at mount point)
198
- *
199
- * @param options The options for computing the parent path.
200
- * @returns The computed parent path result.
201
- */
202
- const computeParentPath = (options) => {
203
- const { currentPathname, outletMountPath, routeChildren, hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = options;
204
- // If this outlet previously established a mount path and the current
205
- // pathname is outside of that scope, do not attempt to re-compute a new
206
- // parent path.
207
- if (outletMountPath && !currentPathname.startsWith(outletMountPath)) {
208
- return { parentPath: undefined, outletMountPath };
209
- }
210
- if ((hasRelativeRoutes || hasIndexRoute) && currentPathname.includes('/')) {
211
- const segments = currentPathname.split('/').filter(Boolean);
212
- if (segments.length >= 1) {
213
- // Find matches at each level, keeping track of the FIRST (shortest) match
214
- let firstSpecificMatch = undefined;
215
- let firstWildcardMatch = undefined;
216
- let indexMatchAtMount = undefined;
217
- for (let i = 1; i <= segments.length; i++) {
218
- const parentPath = '/' + segments.slice(0, i).join('/');
219
- const remainingPath = segments.slice(i).join('/');
220
- // Check for specific (non-wildcard, non-index) route matches
221
- const hasSpecificMatch = routeChildren.some((route) => isSpecificRouteMatch(route, remainingPath));
222
- if (hasSpecificMatch && !firstSpecificMatch) {
223
- firstSpecificMatch = parentPath;
224
- // Found a specific match - this is our answer for non-index routes
225
- break;
226
- }
227
- // Check if wildcard would match this remaining path
228
- // Only if remaining is non-empty (wildcard needs something to match)
229
- if (remainingPath !== '' && remainingPath !== '/' && hasWildcardRoute && !firstWildcardMatch) {
230
- // Check if any specific route could plausibly match this remaining path
231
- const remainingFirstSegment = remainingPath.split('/')[0];
232
- const couldAnyRouteMatch = routeChildren.some((route) => {
233
- const routePath = route.props.path;
234
- if (!routePath || routePath === '*' || routePath === '/*')
235
- return false;
236
- if (route.props.index)
237
- return false;
238
- const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, '');
239
- if (!routeFirstSegment)
240
- return false;
241
- // Check for prefix overlap (either direction)
242
- return (routeFirstSegment.startsWith(remainingFirstSegment.slice(0, 3)) ||
243
- remainingFirstSegment.startsWith(routeFirstSegment.slice(0, 3)));
244
- });
245
- // Only save wildcard match if no specific route could match
246
- if (!couldAnyRouteMatch) {
247
- firstWildcardMatch = parentPath;
248
- // Continue looking - might find a specific match at a longer path
249
- }
250
- }
251
- // Check for index route match when remaining path is empty
252
- // BUT only at the outlet's mount path level
253
- if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) {
254
- // Index route matches when current path exactly matches the mount path
255
- // If we already have an outletMountPath, index should only match there
256
- if (outletMountPath) {
257
- if (parentPath === outletMountPath) {
258
- indexMatchAtMount = parentPath;
259
- }
260
- }
261
- else {
262
- // No mount path set yet - index would establish this as mount path
263
- // But only if we haven't found a better match
264
- indexMatchAtMount = parentPath;
265
- }
266
- }
267
- }
268
- // Determine the best parent path:
269
- // 1. Specific match (routes like tabs/*, favorites) - highest priority
270
- // 2. Wildcard match (route path="*") - catches unmatched segments
271
- // 3. Index match - only valid at the outlet's mount point, not deeper
272
- let bestPath = undefined;
273
- if (firstSpecificMatch) {
274
- bestPath = firstSpecificMatch;
275
- }
276
- else if (firstWildcardMatch) {
277
- bestPath = firstWildcardMatch;
278
- }
279
- else if (indexMatchAtMount) {
280
- // Only use index match if no specific or wildcard matched
281
- // This handles the case where pathname exactly matches the mount path
282
- bestPath = indexMatchAtMount;
283
- }
284
- // Store the mount path when we first successfully match a route
285
- let newOutletMountPath = outletMountPath;
286
- if (!outletMountPath && bestPath) {
287
- newOutletMountPath = bestPath;
288
- }
289
- // If we have a mount path, verify the current pathname is within scope
290
- if (newOutletMountPath && !currentPathname.startsWith(newOutletMountPath)) {
291
- return { parentPath: undefined, outletMountPath: newOutletMountPath };
292
- }
293
- return { parentPath: bestPath, outletMountPath: newOutletMountPath };
294
- }
295
- }
296
- // Handle outlets with ONLY absolute routes (no relative routes or index routes)
297
- // Compute the common prefix of all absolute routes to determine the outlet's scope
298
- if (!hasRelativeRoutes && !hasIndexRoute) {
299
- const absolutePathRoutes = routeChildren.filter((route) => {
300
- const path = route.props.path;
301
- return path && path.startsWith('/');
302
- });
303
- if (absolutePathRoutes.length > 0) {
304
- const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
305
- const commonPrefix = computeCommonPrefix(absolutePaths);
306
- if (commonPrefix && commonPrefix !== '/') {
307
- // Set the mount path based on common prefix of absolute routes
308
- const newOutletMountPath = outletMountPath || commonPrefix;
309
- // Check if current pathname is within scope
310
- if (!currentPathname.startsWith(commonPrefix)) {
311
- return { parentPath: undefined, outletMountPath: newOutletMountPath };
312
- }
313
- return { parentPath: commonPrefix, outletMountPath: newOutletMountPath };
314
- }
315
- }
316
- }
317
- return { parentPath: outletMountPath, outletMountPath };
38
+ return match;
318
39
  };
319
40
 
320
- /**
321
- * Ensures the given path has a leading slash.
322
- *
323
- * @param value The path string to normalize.
324
- * @returns The path with a leading slash.
325
- */
326
- const ensureLeadingSlash = (value) => {
327
- if (value === '') {
328
- return '/';
329
- }
330
- return value.startsWith('/') ? value : `/${value}`;
331
- };
332
- /**
333
- * Strips the trailing slash from a path, unless it's the root path.
334
- *
335
- * @param value The path string to normalize.
336
- * @returns The path without a trailing slash.
337
- */
338
- const stripTrailingSlash = (value) => {
339
- return value.length > 1 && value.endsWith('/') ? value.slice(0, -1) : value;
340
- };
341
- /**
342
- * Normalizes a pathname for comparison by ensuring a leading slash
343
- * and removing trailing slashes.
344
- *
345
- * @param value The pathname to normalize, can be undefined.
346
- * @returns A normalized pathname string.
347
- */
348
- const normalizePathnameForComparison = (value) => {
349
- if (!value || value === '') {
350
- return '/';
351
- }
352
- const withLeadingSlash = ensureLeadingSlash(value);
353
- return stripTrailingSlash(withLeadingSlash);
354
- };
355
-
356
- /**
357
- * Extracts the children from a Routes wrapper component.
358
- * The use of `<Routes />` is encouraged with React Router v6.
359
- *
360
- * @param node The React node to extract Routes children from.
361
- * @returns The children of the Routes component, or undefined if not found.
362
- */
363
- const getRoutesChildren = (node) => {
364
- let routesNode;
365
- React.Children.forEach(node, (child) => {
366
- if (child.type === Routes) {
367
- routesNode = child;
368
- }
369
- });
370
- if (routesNode) {
371
- // The children of the `<Routes />` component are most likely
372
- // (and should be) the `<Route />` components.
373
- return routesNode.props.children;
374
- }
375
- return undefined;
376
- };
377
- /**
378
- * Extracts Route children from a node (either directly or from a Routes wrapper).
379
- *
380
- * @param children The children to extract routes from.
381
- * @returns An array of Route elements.
382
- */
383
- const extractRouteChildren = (children) => {
384
- var _a;
385
- const routesChildren = (_a = getRoutesChildren(children)) !== null && _a !== void 0 ? _a : children;
386
- return React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
387
- };
388
- /**
389
- * Checks if a React element is a Navigate component (redirect).
390
- *
391
- * @param element The element to check.
392
- * @returns True if the element is a Navigate component.
393
- */
394
- const isNavigateElement = (element) => {
395
- return (React.isValidElement(element) &&
396
- (element.type === Navigate || (typeof element.type === 'function' && element.type.name === 'Navigate')));
397
- };
398
-
399
- /**
400
- * Sorts view items by route specificity (most specific first).
401
- * - Exact matches (no wildcards/params) come first
402
- * - Among wildcard routes, longer paths are more specific
403
- *
404
- * @param views The view items to sort.
405
- * @returns A new sorted array of view items.
406
- */
407
- const sortViewsBySpecificity = (views) => {
408
- return [...views].sort((a, b) => {
409
- var _a, _b, _c, _d;
410
- const pathA = ((_b = (_a = a.routeData) === null || _a === void 0 ? void 0 : _a.childProps) === null || _b === void 0 ? void 0 : _b.path) || '';
411
- const pathB = ((_d = (_c = b.routeData) === null || _c === void 0 ? void 0 : _c.childProps) === null || _d === void 0 ? void 0 : _d.path) || '';
412
- // Exact matches (no wildcards/params) come first
413
- const aHasWildcard = pathA.includes('*') || pathA.includes(':');
414
- const bHasWildcard = pathB.includes('*') || pathB.includes(':');
415
- if (!aHasWildcard && bHasWildcard)
416
- return -1;
417
- if (aHasWildcard && !bHasWildcard)
418
- return 1;
419
- // Among wildcard routes, longer paths are more specific
420
- return pathB.length - pathA.length;
421
- });
422
- };
423
-
424
- /**
425
- * `ReactRouterViewStack` is a custom navigation manager used in Ionic React
426
- * apps to map React Router route elements (such as `<IonRoute>`) to "view
427
- * items" that Ionic can manage in a view stack. This is critical to maintain
428
- * Ionic’s animation, lifecycle, and history behavior across views.
429
- */
430
- /**
431
- * Delay in milliseconds before removing a Navigate view item after a redirect.
432
- * This ensures the redirect navigation completes before the view is removed.
433
- */
434
- const NAVIGATE_REDIRECT_DELAY_MS = 100;
435
- /**
436
- * Delay in milliseconds before cleaning up a view without an IonPage element.
437
- * This double-checks that the view is truly not needed before removal.
438
- */
439
- const VIEW_CLEANUP_DELAY_MS = 200;
440
- const createDefaultMatch = (fullPathname, routeProps) => {
441
- var _a, _b;
442
- const isIndexRoute = !!routeProps.index;
443
- const patternPath = (_a = routeProps.path) !== null && _a !== void 0 ? _a : '';
444
- const pathnameBase = fullPathname === '' ? '/' : fullPathname;
445
- const computedEnd = routeProps.end !== undefined ? routeProps.end : patternPath !== '' ? !patternPath.endsWith('*') : true;
446
- return {
447
- params: {},
448
- pathname: isIndexRoute ? '' : fullPathname,
449
- pathnameBase,
450
- pattern: {
451
- path: patternPath,
452
- caseSensitive: (_b = routeProps.caseSensitive) !== null && _b !== void 0 ? _b : false,
453
- end: isIndexRoute ? true : computedEnd,
454
- },
455
- };
456
- };
457
- const computeRelativeToParent = (pathname, parentPath) => {
458
- if (!parentPath)
459
- return null;
460
- const normalizedParent = normalizePathnameForComparison(parentPath);
461
- const normalizedPathname = normalizePathnameForComparison(pathname);
462
- if (normalizedPathname === normalizedParent) {
463
- return '';
464
- }
465
- const withSlash = normalizedParent === '/' ? '/' : normalizedParent + '/';
466
- if (normalizedPathname.startsWith(withSlash)) {
467
- return normalizedPathname.slice(withSlash.length);
468
- }
469
- return null;
470
- };
471
- const resolveIndexRouteMatch = (viewItem, pathname, parentPath) => {
472
- var _a, _b, _c;
473
- if (!((_b = (_a = viewItem.routeData) === null || _a === void 0 ? void 0 : _a.childProps) === null || _b === void 0 ? void 0 : _b.index)) {
474
- return null;
475
- }
476
- // Prefer computing against the parent path when available to align with RRv6 semantics
477
- const relative = computeRelativeToParent(pathname, parentPath);
478
- if (relative !== null) {
479
- // Index routes match only when there is no remaining path
480
- if (relative === '' || relative === '/') {
481
- return createDefaultMatch(parentPath || pathname, viewItem.routeData.childProps);
482
- }
483
- return null;
484
- }
485
- // Fallback: use previously computed match base for equality check
486
- const previousMatch = (_c = viewItem.routeData) === null || _c === void 0 ? void 0 : _c.match;
487
- if (!previousMatch) {
488
- return null;
489
- }
490
- const normalizedPathname = normalizePathnameForComparison(pathname);
491
- const normalizedBase = normalizePathnameForComparison(previousMatch.pathnameBase || previousMatch.pathname || '');
492
- return normalizedPathname === normalizedBase ? previousMatch : null;
493
- };
494
41
  class ReactRouterViewStack extends ViewStacks {
495
42
  constructor() {
496
43
  super();
497
- this.viewItemCounter = 0;
498
- /**
499
- * Creates a new view item for the given outlet and react route element.
500
- * Associates route props with the matched route path for further lookups.
501
- */
502
- this.createViewItem = (outletId, reactElement, routeInfo, page) => {
503
- var _a, _b;
504
- const routePath = reactElement.props.path || '';
505
- // Check if we already have a view item for this exact route that we can reuse
506
- // Include wildcard routes like tabs/* since they should be reused
507
- // Also check unmounted items that might have been preserved for browser navigation
508
- const existingViewItem = this.getViewItemsForOutlet(outletId).find((v) => {
509
- var _a, _b, _c, _d, _e, _f;
510
- const existingRouteProps = (_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) !== null && _b !== void 0 ? _b : {};
511
- const existingPath = existingRouteProps.path || '';
512
- const existingElement = existingRouteProps.element;
513
- const newElement = reactElement.props.element;
514
- const existingIsIndexRoute = !!existingRouteProps.index;
515
- const newIsIndexRoute = !!reactElement.props.index;
516
- // For Navigate components, match by destination
517
- const existingIsNavigate = React.isValidElement(existingElement) && existingElement.type === Navigate;
518
- const newIsNavigate = React.isValidElement(newElement) && newElement.type === Navigate;
519
- if (existingIsNavigate && newIsNavigate) {
520
- const existingTo = (_c = existingElement.props) === null || _c === void 0 ? void 0 : _c.to;
521
- const newTo = (_d = newElement.props) === null || _d === void 0 ? void 0 : _d.to;
522
- if (existingTo === newTo) {
523
- return true;
524
- }
525
- }
526
- if (existingIsIndexRoute && newIsIndexRoute) {
527
- return true;
528
- }
529
- // Reuse view items with the same path
530
- // Special case: reuse tabs/* and other specific wildcard routes
531
- // Don't reuse index routes (empty path) or generic catch-all wildcards (*)
532
- if (existingPath === routePath && existingPath !== '' && existingPath !== '*') {
533
- // For parameterized routes (containing :param), only reuse if the ACTUAL pathname matches
534
- // This ensures /details/1 and /details/2 get separate view items and component instances
535
- const hasParams = routePath.includes(':');
536
- if (hasParams) {
537
- // Check if the existing view item's pathname matches the new pathname
538
- const existingPathname = (_f = (_e = v.routeData) === null || _e === void 0 ? void 0 : _e.match) === null || _f === void 0 ? void 0 : _f.pathname;
539
- if (existingPathname !== routeInfo.pathname) {
540
- return false; // Different param values, don't reuse
541
- }
542
- }
543
- return true;
544
- }
545
- // Also reuse specific wildcard routes like tabs/*
546
- if (existingPath === routePath && existingPath.endsWith('/*') && existingPath !== '/*') {
547
- return true;
548
- }
549
- return false;
550
- });
551
- if (existingViewItem) {
552
- // Update and ensure the existing view item is properly configured
553
- existingViewItem.reactElement = reactElement;
554
- existingViewItem.mount = true;
555
- existingViewItem.ionPageElement = page || existingViewItem.ionPageElement;
556
- const updatedMatch = matchComponent$1(reactElement, routeInfo.pathname, false) ||
557
- ((_a = existingViewItem.routeData) === null || _a === void 0 ? void 0 : _a.match) ||
558
- createDefaultMatch(routeInfo.pathname, reactElement.props);
559
- existingViewItem.routeData = {
560
- match: updatedMatch,
561
- childProps: reactElement.props,
562
- lastPathname: (_b = existingViewItem.routeData) === null || _b === void 0 ? void 0 : _b.lastPathname, // Preserve navigation history
563
- };
564
- return existingViewItem;
565
- }
566
- this.viewItemCounter++;
567
- const id = `${outletId}-${this.viewItemCounter}`;
568
- const viewItem = {
569
- id,
570
- outletId,
571
- ionPageElement: page,
572
- reactElement,
573
- mount: true,
574
- ionRoute: true,
575
- };
576
- if (reactElement.type === IonRoute) {
577
- viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
578
- }
579
- const initialMatch = matchComponent$1(reactElement, routeInfo.pathname, true) ||
580
- createDefaultMatch(routeInfo.pathname, reactElement.props);
581
- viewItem.routeData = {
582
- match: initialMatch,
583
- childProps: reactElement.props,
584
- };
585
- this.add(viewItem);
586
- return viewItem;
44
+ this.createViewItem = this.createViewItem.bind(this);
45
+ this.findViewItemByRouteInfo = this.findViewItemByRouteInfo.bind(this);
46
+ this.findLeavingViewItemByRouteInfo = this.findLeavingViewItemByRouteInfo.bind(this);
47
+ this.getChildrenToRender = this.getChildrenToRender.bind(this);
48
+ this.findViewItemByPathname = this.findViewItemByPathname.bind(this);
49
+ }
50
+ createViewItem(outletId, reactElement, routeInfo, page) {
51
+ const viewItem = {
52
+ id: generateId('viewItem'),
53
+ outletId,
54
+ ionPageElement: page,
55
+ reactElement,
56
+ mount: true,
57
+ ionRoute: false,
587
58
  };
588
- /**
589
- * Renders a ViewLifeCycleManager for the given view item.
590
- * Handles cleanup if the view no longer matches.
591
- *
592
- * - Deactivates view if it no longer matches the current route
593
- * - Wraps the route element in <Routes> to support nested routing and ensure remounting
594
- * - Adds a unique key to <Routes> so React Router remounts routes when switching
595
- */
596
- this.renderViewItem = (viewItem, routeInfo, parentPath) => {
597
- var _a, _b, _c, _d, _e, _f, _g, _h;
598
- const routePath = viewItem.reactElement.props.path || '';
599
- let match = matchComponent$1(viewItem.reactElement, routeInfo.pathname);
600
- if (!match) {
601
- const indexMatch = resolveIndexRouteMatch(viewItem, routeInfo.pathname, parentPath);
602
- if (indexMatch) {
603
- match = indexMatch;
604
- }
605
- }
606
- // For parameterized routes, check if this is a navigation to a different path instance
607
- // In that case, we should NOT reuse this view - a new view should be created
608
- const isParameterRoute = routePath.includes(':');
609
- const previousMatch = (_a = viewItem.routeData) === null || _a === void 0 ? void 0 : _a.match;
610
- const isSamePath = (match === null || match === void 0 ? void 0 : match.pathname) === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathname);
611
- // Flag to indicate this view should not be reused for this different parameterized path
612
- const shouldSkipForDifferentParam = isParameterRoute && match && previousMatch && !isSamePath;
613
- // Don't deactivate views automatically - let the StackManager handle view lifecycle
614
- // This preserves views in the stack for navigation history like native apps
615
- // Views will be hidden/shown by the StackManager's transition logic instead of being unmounted
616
- // Special handling for Navigate components - they should unmount after redirecting
617
- const elementComponent = (_c = (_b = viewItem.reactElement) === null || _b === void 0 ? void 0 : _b.props) === null || _c === void 0 ? void 0 : _c.element;
618
- const isNavigateComponent = isNavigateElement(elementComponent);
619
- if (isNavigateComponent) {
620
- // Navigate components should only be mounted when they match
621
- // Once they redirect (no longer match), they should be removed completely
622
- // IMPORTANT: For index routes, we need to check indexMatch too since matchComponent
623
- // may not properly match index routes without explicit parent path context
624
- const indexMatch = ((_e = (_d = viewItem.routeData) === null || _d === void 0 ? void 0 : _d.childProps) === null || _e === void 0 ? void 0 : _e.index)
625
- ? resolveIndexRouteMatch(viewItem, routeInfo.pathname, parentPath)
626
- : null;
627
- const hasValidMatch = match || indexMatch;
628
- if (!hasValidMatch && viewItem.mount) {
629
- viewItem.mount = false;
630
- // Schedule removal of the Navigate view item after a short delay
631
- // This ensures the redirect completes before removal
632
- setTimeout(() => {
633
- this.remove(viewItem);
634
- }, NAVIGATE_REDIRECT_DELAY_MS);
635
- }
636
- }
637
- // Components that don't have IonPage elements and no longer match should be cleaned up
638
- // BUT we need to be careful not to remove them if they're part of browser navigation history
639
- // This handles components that perform immediate actions like programmatic navigation
640
- // EXCEPTION: Navigate components should ALWAYS remain mounted until they redirect
641
- // since they need to be rendered to trigger the navigation
642
- if (!match && viewItem.mount && !viewItem.ionPageElement && !isNavigateComponent) {
643
- // Check if this view item should be preserved for browser navigation
644
- // We'll keep it if it was recently active (within the last navigation)
645
- const shouldPreserve = viewItem.routeData.lastPathname === routeInfo.pathname ||
646
- ((_f = viewItem.routeData.match) === null || _f === void 0 ? void 0 : _f.pathname) === routeInfo.lastPathname;
647
- if (!shouldPreserve) {
648
- // This view item doesn't match and doesn't have an IonPage
649
- // It's likely a utility component that performs an action and navigates away
650
- viewItem.mount = false;
651
- // Schedule removal to allow it to be recreated on next navigation
652
- setTimeout(() => {
653
- // Double-check before removing - the view might be needed again
654
- const stillNotNeeded = !viewItem.mount && !viewItem.ionPageElement;
655
- if (stillNotNeeded) {
656
- this.remove(viewItem);
657
- }
658
- }, VIEW_CLEANUP_DELAY_MS);
659
- }
660
- else {
661
- // Preserve it but unmount it for now
662
- viewItem.mount = false;
663
- }
664
- }
665
- // Reactivate view if it matches but was previously deactivated
666
- // Don't reactivate if this is a parameterized route navigating to a different path instance
667
- if (match && !viewItem.mount && !shouldSkipForDifferentParam) {
668
- viewItem.mount = true;
669
- viewItem.routeData.match = match;
670
- }
671
- // Deactivate wildcard routes and catch-all routes (empty path) when we have specific route matches
672
- // This prevents "Not found" or fallback pages from showing alongside valid routes
673
- if (routePath === '*' || routePath === '') {
674
- // Check if any other view in this outlet has a match for the current route
675
- const hasSpecificMatch = this.getViewItemsForOutlet(viewItem.outletId).some((v) => {
676
- var _a, _b;
677
- if (v.id === viewItem.id)
678
- return false; // Skip self
679
- const vRoutePath = ((_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path) || '';
680
- if (vRoutePath === '*' || vRoutePath === '')
681
- return false; // Skip other wildcard/empty routes
682
- // Check if this view item would match the current route
683
- const vMatch = v.reactElement ? matchComponent$1(v.reactElement, routeInfo.pathname) : null;
684
- return !!vMatch;
685
- });
686
- if (hasSpecificMatch) {
687
- viewItem.mount = false;
688
- // Also hide the ion-page element immediately to prevent visual overlap
689
- if (viewItem.ionPageElement) {
690
- viewItem.ionPageElement.classList.add('ion-page-hidden');
691
- viewItem.ionPageElement.setAttribute('aria-hidden', 'true');
692
- }
693
- }
694
- }
695
- const routeElement = React.cloneElement(viewItem.reactElement);
696
- const componentElement = routeElement.props.element;
697
- // Don't update match for parameterized routes navigating to different path instances
698
- // This preserves the original match so that findViewItemByPath can correctly skip this view
699
- if (match && viewItem.routeData.match !== match && !shouldSkipForDifferentParam) {
700
- viewItem.routeData.match = match;
701
- }
702
- const routeMatch = shouldSkipForDifferentParam ? (_g = viewItem.routeData) === null || _g === void 0 ? void 0 : _g.match : match || ((_h = viewItem.routeData) === null || _h === void 0 ? void 0 : _h.match);
703
- return (React.createElement(UNSAFE_RouteContext.Consumer, { key: `view-context-${viewItem.id}` }, (parentContext) => {
704
- var _a, _b;
705
- const parentMatches = (_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.matches) !== null && _a !== void 0 ? _a : [];
706
- const accumulatedParentParams = parentMatches.reduce((acc, match) => {
707
- return Object.assign(Object.assign({}, acc), match.params);
708
- }, {});
709
- const combinedParams = Object.assign(Object.assign({}, accumulatedParentParams), ((_b = routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.params) !== null && _b !== void 0 ? _b : {}));
710
- // For relative route paths, we need to compute an absolute pathnameBase
711
- // by combining the parent's pathnameBase with the matched portion
712
- let absolutePathnameBase = (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase) || routeInfo.pathname;
713
- const routePath = routeElement.props.path;
714
- const isRelativePath = routePath && !routePath.startsWith('/');
715
- const isIndexRoute = !!routeElement.props.index;
716
- if (isRelativePath || isIndexRoute) {
717
- // Get the parent's pathnameBase to build the absolute path
718
- const parentPathnameBase = parentMatches.length > 0 ? parentMatches[parentMatches.length - 1].pathnameBase : '/';
719
- // For relative paths, the matchPath returns a relative pathnameBase
720
- // We need to make it absolute by prepending the parent's base
721
- if ((routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase) && isRelativePath) {
722
- // Strip leading slash if present in the relative match
723
- const relativeBase = routeMatch.pathnameBase.startsWith('/')
724
- ? routeMatch.pathnameBase.slice(1)
725
- : routeMatch.pathnameBase;
726
- absolutePathnameBase =
727
- parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
728
- }
729
- else if (isIndexRoute) {
730
- // Index routes should use the parent's base as their base
731
- absolutePathnameBase = parentPathnameBase;
732
- }
733
- }
734
- const contextMatches = [
735
- ...parentMatches,
736
- {
737
- params: combinedParams,
738
- pathname: (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathname) || routeInfo.pathname,
739
- pathnameBase: absolutePathnameBase,
740
- route: {
741
- id: viewItem.id,
742
- path: routeElement.props.path,
743
- element: componentElement,
744
- index: !!routeElement.props.index,
745
- caseSensitive: routeElement.props.caseSensitive,
746
- hasErrorBoundary: false,
747
- },
748
- },
749
- ];
750
- const routeContextValue = parentContext
751
- ? Object.assign(Object.assign({}, parentContext), { matches: contextMatches }) : {
752
- outlet: null,
753
- matches: contextMatches,
754
- isDataRoute: false,
755
- };
756
- return (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) },
757
- React.createElement(UNSAFE_RouteContext.Provider, { value: routeContextValue }, componentElement)));
758
- }));
59
+ if (reactElement.type === IonRoute) {
60
+ viewItem.ionRoute = true;
61
+ viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
62
+ }
63
+ viewItem.routeData = {
64
+ match: matchPath({
65
+ pathname: routeInfo.pathname,
66
+ componentProps: reactElement.props,
67
+ }),
68
+ childProps: reactElement.props,
759
69
  };
760
- /**
761
- * Re-renders all active view items for the specified outlet.
762
- * Ensures React elements are updated with the latest match.
763
- *
764
- * 1. Iterates through children of IonRouterOutlet
765
- * 2. Updates each matching viewItem with the current child React element
766
- * (important for updating props or changes to elements)
767
- * 3. Returns a list of React components that will be rendered inside the outlet
768
- * Each view is wrapped in <ViewLifeCycleManager> to manage lifecycle and rendering
769
- */
770
- this.getChildrenToRender = (outletId, ionRouterOutlet, routeInfo) => {
771
- const viewItems = this.getViewItemsForOutlet(outletId);
772
- // Determine parentPath for nested outlets to properly evaluate index routes
773
- let parentPath = undefined;
774
- try {
775
- // Only attempt parent path computation for non-root outlets
776
- if (outletId !== 'routerOutlet') {
777
- const routeChildren = extractRouteChildren(ionRouterOutlet.props.children);
778
- const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
779
- if (hasRelativeRoutes || hasIndexRoute) {
780
- const result = computeParentPath({
781
- currentPathname: routeInfo.pathname,
782
- outletMountPath: undefined,
783
- routeChildren,
784
- hasRelativeRoutes,
785
- hasIndexRoute,
786
- hasWildcardRoute,
787
- });
788
- parentPath = result.parentPath;
789
- }
790
- }
791
- }
792
- catch (e) {
793
- // Non-fatal: if we fail to compute parentPath, fall back to previous behavior
794
- }
795
- // Sync child elements with stored viewItems (e.g. to reflect new props)
796
- React.Children.forEach(ionRouterOutlet.props.children, (child) => {
797
- // Ensure the child is a valid React element since we
798
- // might have whitespace strings or other non-element children
799
- if (React.isValidElement(child)) {
800
- // Find view item by exact path match to avoid wildcard routes overwriting specific routes
801
- const childPath = child.props.path;
802
- const viewItem = viewItems.find((v) => {
803
- var _a, _b;
804
- const viewItemPath = (_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
805
- // Only update if paths match exactly (prevents wildcard routes from overwriting specific routes)
806
- return viewItemPath === childPath;
807
- });
808
- if (viewItem) {
809
- viewItem.reactElement = child;
810
- }
811
- }
812
- });
813
- // Filter out duplicate view items by ID (but keep all mounted items)
814
- const uniqueViewItems = viewItems.filter((viewItem, index, array) => {
815
- // Remove duplicates by ID (keep first occurrence)
816
- const isFirstOccurrence = array.findIndex((v) => v.id === viewItem.id) === index;
817
- return isFirstOccurrence;
818
- });
819
- // Filter out unmounted Navigate components to prevent them from being rendered
820
- // and triggering unwanted redirects
821
- const renderableViewItems = uniqueViewItems.filter((viewItem) => {
822
- var _a, _b, _c, _d;
823
- const elementComponent = (_b = (_a = viewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.element;
824
- const isNavigateComponent = isNavigateElement(elementComponent);
825
- // Exclude unmounted Navigate components from rendering
826
- if (isNavigateComponent && !viewItem.mount) {
827
- return false;
828
- }
829
- // Filter out views that are unmounted, have no ionPageElement, and don't match the current route.
830
- // These are "stale" views from previous routes that should not be rendered.
831
- // Views WITH ionPageElement are handled by the normal lifecycle events.
832
- // Views that MATCH the current route should be kept (they might be transitioning).
833
- if (!viewItem.mount && !viewItem.ionPageElement) {
834
- // Check if this view's route path matches the current pathname
835
- const viewRoutePath = (_d = (_c = viewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
836
- if (viewRoutePath) {
837
- // First try exact match using matchComponent
838
- const routeMatch = matchComponent$1(viewItem.reactElement, routeInfo.pathname);
839
- if (routeMatch) {
840
- // View matches current route, keep it
841
- return true;
842
- }
843
- // For parent routes (like /multiple-tabs or /routing), check if current pathname
844
- // starts with this route's path. This handles views with IonSplitPane/IonTabs
845
- // that don't have IonPage but should remain mounted while navigating within their children.
846
- const normalizedViewPath = normalizePathnameForComparison(viewRoutePath.replace(/\/?\*$/, '')); // Remove trailing wildcard
847
- const normalizedCurrentPath = normalizePathnameForComparison(routeInfo.pathname);
848
- // Check if current pathname is within this view's route hierarchy
849
- const isWithinRouteHierarchy = normalizedCurrentPath === normalizedViewPath || normalizedCurrentPath.startsWith(normalizedViewPath + '/');
850
- if (!isWithinRouteHierarchy) {
851
- // View is outside current route hierarchy, remove it
852
- setTimeout(() => {
853
- this.remove(viewItem);
854
- }, 0);
855
- return false;
856
- }
857
- }
858
- }
859
- return true;
70
+ return viewItem;
71
+ }
72
+ getChildrenToRender(outletId, ionRouterOutlet, routeInfo) {
73
+ const viewItems = this.getViewItemsForOutlet(outletId);
74
+ // Sync latest routes with viewItems
75
+ React.Children.forEach(ionRouterOutlet.props.children, (child) => {
76
+ const viewItem = viewItems.find((v) => {
77
+ return matchComponent$1(child, v.routeData.childProps.path || v.routeData.childProps.from);
860
78
  });
861
- const renderedItems = renderableViewItems.map((viewItem) => this.renderViewItem(viewItem, routeInfo, parentPath));
862
- return renderedItems;
863
- };
864
- /**
865
- * Finds a view item matching the current route, optionally updating its match state.
866
- */
867
- this.findViewItemByRouteInfo = (routeInfo, outletId, updateMatch) => {
868
- const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
869
- const shouldUpdateMatch = updateMatch === undefined || updateMatch === true;
870
- if (shouldUpdateMatch && viewItem && match) {
871
- viewItem.routeData.match = match;
79
+ if (viewItem) {
80
+ viewItem.reactElement = child;
872
81
  }
873
- return viewItem;
874
- };
875
- /**
876
- * Finds the view item that was previously active before a route change.
877
- */
878
- this.findLeavingViewItemByRouteInfo = (routeInfo, outletId, mustBeIonRoute = true) => {
879
- // If the lastPathname is not set, we cannot find a leaving view item
880
- if (!routeInfo.lastPathname) {
881
- return undefined;
882
- }
883
- const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute);
884
- return viewItem;
885
- };
886
- /**
887
- * Finds a view item by pathname only, used in simpler queries.
888
- */
889
- this.findViewItemByPathname = (pathname, outletId) => {
890
- const { viewItem } = this.findViewItemByPath(pathname, outletId);
891
- return viewItem;
892
- };
893
- /**
894
- * Clean up old, unmounted view items to prevent memory leaks
895
- */
896
- this.cleanupStaleViewItems = (outletId) => {
897
- const viewItems = this.getViewItemsForOutlet(outletId);
898
- // Keep only the most recent mounted views and a few unmounted ones for history
899
- const maxUnmountedItems = 3;
900
- const unmountedItems = viewItems.filter((v) => !v.mount);
901
- if (unmountedItems.length > maxUnmountedItems) {
902
- // Remove oldest unmounted items
903
- const itemsToRemove = unmountedItems.slice(0, unmountedItems.length - maxUnmountedItems);
904
- itemsToRemove.forEach((item) => {
905
- this.remove(item);
906
- });
82
+ });
83
+ const children = viewItems.map((viewItem) => {
84
+ let clonedChild;
85
+ if (viewItem.ionRoute && !viewItem.disableIonPageManagement) {
86
+ clonedChild = (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) }, React.cloneElement(viewItem.reactElement, {
87
+ computedMatch: viewItem.routeData.match,
88
+ })));
907
89
  }
908
- };
909
- /**
910
- * Override add to prevent duplicate view items with the same ID in the same outlet
911
- * But allow multiple view items for the same route path (for navigation history)
912
- */
913
- this.add = (viewItem) => {
914
- const existingViewItem = this.getViewItemsForOutlet(viewItem.outletId).find((v) => v.id === viewItem.id);
915
- if (existingViewItem) {
916
- return;
90
+ else {
91
+ const match = matchComponent$1(viewItem.reactElement, routeInfo.pathname);
92
+ clonedChild = (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) }, React.cloneElement(viewItem.reactElement, {
93
+ computedMatch: viewItem.routeData.match,
94
+ })));
95
+ if (!match && viewItem.routeData.match) {
96
+ viewItem.routeData.match = undefined;
97
+ viewItem.mount = false;
98
+ }
917
99
  }
918
- super.add(viewItem);
919
- this.cleanupStaleViewItems(viewItem.outletId);
920
- };
921
- /**
922
- * Override remove
923
- */
924
- this.remove = (viewItem) => {
925
- super.remove(viewItem);
926
- };
100
+ return clonedChild;
101
+ });
102
+ return children;
103
+ }
104
+ findViewItemByRouteInfo(routeInfo, outletId, updateMatch) {
105
+ const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
106
+ const shouldUpdateMatch = updateMatch === undefined || updateMatch === true;
107
+ if (shouldUpdateMatch && viewItem && match) {
108
+ viewItem.routeData.match = match;
109
+ }
110
+ return viewItem;
111
+ }
112
+ findLeavingViewItemByRouteInfo(routeInfo, outletId, mustBeIonRoute = true) {
113
+ const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute);
114
+ return viewItem;
115
+ }
116
+ findViewItemByPathname(pathname, outletId) {
117
+ const { viewItem } = this.findViewItemByPath(pathname, outletId);
118
+ return viewItem;
927
119
  }
928
120
  /**
929
- * Core function that matches a given pathname against all view items.
930
- * Returns both the matched view item and match metadata.
121
+ * Returns the matching view item and the match result for a given pathname.
931
122
  */
932
- findViewItemByPath(pathname, outletId, mustBeIonRoute, allowDefaultMatch = true) {
123
+ findViewItemByPath(pathname, outletId, mustBeIonRoute) {
933
124
  let viewItem;
934
- let match = null;
125
+ let match;
935
126
  let viewStack;
936
127
  if (outletId) {
937
- viewStack = sortViewsBySpecificity(this.getViewItemsForOutlet(outletId));
128
+ viewStack = this.getViewItemsForOutlet(outletId);
938
129
  viewStack.some(matchView);
939
- if (!viewItem && allowDefaultMatch)
130
+ if (!viewItem) {
940
131
  viewStack.some(matchDefaultRoute);
132
+ }
941
133
  }
942
134
  else {
943
- const viewItems = sortViewsBySpecificity(this.getAllViewItems());
135
+ const viewItems = this.getAllViewItems();
944
136
  viewItems.some(matchView);
945
- if (!viewItem && allowDefaultMatch)
137
+ if (!viewItem) {
946
138
  viewItems.some(matchDefaultRoute);
139
+ }
947
140
  }
948
- // If we still have not found a view item for this outlet, try to find a matching
949
- // view item across all outlets and adopt it into the current outlet. This helps
950
- // recover when an outlet remounts and receives a new id, leaving views associated
951
- // with the previous outlet id.
952
- // Do not adopt across outlets; if we didn't find a view for this outlet,
953
- // defer to route matching to create a new one.
954
141
  return { viewItem, match };
955
- /**
956
- * Matches a route path with dynamic parameters (e.g. /tabs/:id)
957
- */
958
142
  function matchView(v) {
959
- var _a;
960
- if (mustBeIonRoute && !v.ionRoute)
143
+ var _a, _b;
144
+ if (mustBeIonRoute && !v.ionRoute) {
961
145
  return false;
962
- const viewItemPath = v.routeData.childProps.path || '';
963
- const isIndexRoute = !!v.routeData.childProps.index;
964
- const previousMatch = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match;
965
- const result = v.reactElement ? matchComponent$1(v.reactElement, pathname) : null;
966
- if (!result) {
967
- const indexMatch = resolveIndexRouteMatch(v, pathname, undefined);
968
- if (indexMatch) {
969
- match = indexMatch;
970
- viewItem = v;
971
- return true;
972
- }
973
146
  }
974
- if (result) {
975
- const hasParams = result.params && Object.keys(result.params).length > 0;
976
- const isSamePath = result.pathname === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathname);
977
- // Don't allow view items with undefined paths to match specific routes
978
- // This prevents broken index route view items from interfering with navigation
979
- if (!viewItemPath && !isIndexRoute && pathname !== '/' && pathname !== '') {
980
- return false;
981
- }
982
- // For parameterized routes, never reuse if the pathname is different
983
- // This ensures /details/1 and /details/2 get separate view items
984
- const isParameterRoute = viewItemPath.includes(':');
985
- if (isParameterRoute && !isSamePath) {
986
- return false;
987
- }
988
- // For routes without params, or when navigating to the exact same path,
989
- // or when there's no previous match, reuse the view item
990
- if (!hasParams || isSamePath || !previousMatch) {
991
- match = result;
992
- viewItem = v;
993
- return true;
994
- }
995
- // For wildcard routes, only reuse if the pathname exactly matches
996
- const isWildcardRoute = viewItemPath.includes('*');
997
- if (isWildcardRoute && isSamePath) {
998
- match = result;
147
+ match = matchPath({
148
+ pathname,
149
+ componentProps: v.routeData.childProps,
150
+ });
151
+ if (match) {
152
+ /**
153
+ * Even though we have a match from react-router, we do not know if the match
154
+ * is for this specific view item.
155
+ *
156
+ * To validate this, we need to check if the path and url match the view item's route data.
157
+ */
158
+ const hasParameter = match.path.includes(':');
159
+ if (!hasParameter || (hasParameter && match.url === ((_b = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match) === null || _b === void 0 ? void 0 : _b.url))) {
999
160
  viewItem = v;
1000
161
  return true;
1001
162
  }
1002
163
  }
1003
164
  return false;
1004
165
  }
1005
- /**
1006
- * Matches a view with no path prop (default fallback route) or index route.
1007
- */
1008
166
  function matchDefaultRoute(v) {
1009
- var _a;
1010
- const childProps = v.routeData.childProps;
1011
- const isDefaultRoute = childProps.path === undefined || childProps.path === '';
1012
- const isIndexRoute = !!childProps.index;
1013
- if (isIndexRoute) {
1014
- const indexMatch = resolveIndexRouteMatch(v, pathname, undefined);
1015
- if (indexMatch) {
1016
- match = indexMatch;
1017
- viewItem = v;
1018
- return true;
1019
- }
1020
- return false;
1021
- }
1022
- if (isDefaultRoute) {
167
+ // try to find a route that doesn't have a path or from prop, that will be our default route
168
+ if (!v.routeData.childProps.path && !v.routeData.childProps.from) {
1023
169
  match = {
170
+ path: pathname,
171
+ url: pathname,
172
+ isExact: true,
1024
173
  params: {},
1025
- pathname,
1026
- pathnameBase: pathname === '' ? '/' : pathname,
1027
- pattern: {
1028
- path: '',
1029
- caseSensitive: (_a = childProps.caseSensitive) !== null && _a !== void 0 ? _a : false,
1030
- end: true,
1031
- },
1032
174
  };
1033
175
  viewItem = v;
1034
176
  return true;
@@ -1037,29 +179,11 @@ class ReactRouterViewStack extends ViewStacks {
1037
179
  }
1038
180
  }
1039
181
  }
1040
- /**
1041
- * Utility to apply matchPath to a React element and return its match state.
1042
- */
1043
- function matchComponent$1(node, pathname, allowFallback = false) {
1044
- var _a;
1045
- const routeProps = (_a = node === null || node === void 0 ? void 0 : node.props) !== null && _a !== void 0 ? _a : {};
1046
- const routePath = routeProps.path;
1047
- const pathnameToMatch = derivePathnameToMatch(pathname, routePath);
1048
- const match = matchPath({
1049
- pathname: pathnameToMatch,
1050
- componentProps: routeProps,
182
+ function matchComponent$1(node, pathname) {
183
+ return matchPath({
184
+ pathname,
185
+ componentProps: node.props,
1051
186
  });
1052
- if (match || !allowFallback) {
1053
- return match;
1054
- }
1055
- const isIndexRoute = !!routeProps.index;
1056
- if (isIndexRoute) {
1057
- return createDefaultMatch(pathname, routeProps);
1058
- }
1059
- if (!routePath || routePath === '') {
1060
- return createDefaultMatch(pathname, routeProps);
1061
- }
1062
- return null;
1063
187
  }
1064
188
 
1065
189
  function clonePageElement(leavingViewHtml) {
@@ -1084,40 +208,7 @@ function clonePageElement(leavingViewHtml) {
1084
208
  return undefined;
1085
209
  }
1086
210
 
1087
- /**
1088
- * `StackManager` is responsible for managing page transitions, keeping track
1089
- * of views (pages), and ensuring that navigation behaves like native apps —
1090
- * particularly with animations and swipe gestures.
1091
- */
1092
- /**
1093
- * Delay in milliseconds before unmounting a view after a transition completes.
1094
- * This ensures the page transition animation finishes before the view is removed.
1095
- */
1096
- const VIEW_UNMOUNT_DELAY_MS = 250;
1097
- /**
1098
- * Delay in milliseconds to wait for an IonPage element to be mounted before
1099
- * proceeding with a page transition.
1100
- */
1101
- const ION_PAGE_WAIT_TIMEOUT_MS = 50;
1102
211
  const isViewVisible = (el) => !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden');
1103
- /**
1104
- * Hides an ion-page element by adding hidden class and aria attribute.
1105
- */
1106
- const hideIonPageElement = (element) => {
1107
- if (element) {
1108
- element.classList.add('ion-page-hidden');
1109
- element.setAttribute('aria-hidden', 'true');
1110
- }
1111
- };
1112
- /**
1113
- * Shows an ion-page element by removing hidden class and aria attribute.
1114
- */
1115
- const showIonPageElement = (element) => {
1116
- if (element) {
1117
- element.classList.remove('ion-page-hidden');
1118
- element.removeAttribute('aria-hidden');
1119
- }
1120
- };
1121
212
  class StackManager extends React.PureComponent {
1122
213
  constructor(props) {
1123
214
  super(props);
@@ -1126,314 +217,12 @@ class StackManager extends React.PureComponent {
1126
217
  isInOutlet: () => true,
1127
218
  };
1128
219
  this.pendingPageTransition = false;
1129
- this.waitingForIonPage = false;
1130
- this.outletMountPath = undefined;
1131
220
  this.registerIonPage = this.registerIonPage.bind(this);
1132
221
  this.transitionPage = this.transitionPage.bind(this);
1133
222
  this.handlePageTransition = this.handlePageTransition.bind(this);
1134
- this.id = props.id || `routerOutlet-${generateId('routerOutlet')}`;
1135
- this.prevProps = undefined;
1136
- this.skipTransition = false;
1137
- }
1138
- /**
1139
- * Determines the parent path that was matched to reach this outlet.
1140
- * This helps with nested routing in React Router 6.
1141
- *
1142
- * The algorithm finds the shortest parent path where a route matches the remaining path.
1143
- * Priority: specific routes > wildcard routes > index routes (only at mount point)
1144
- */
1145
- getParentPath() {
1146
- const currentPathname = this.props.routeInfo.pathname;
1147
- // If this outlet previously established a mount path and the current
1148
- // pathname is outside of that scope, do not attempt to re-compute a new
1149
- // parent path. This prevents out-of-scope outlets from "adopting"
1150
- // unrelated routes (e.g., matching their index route under /overlays).
1151
- if (this.outletMountPath && !currentPathname.startsWith(this.outletMountPath)) {
1152
- return undefined;
1153
- }
1154
- // If this is a nested outlet (has an explicit ID like "main"),
1155
- // we need to figure out what part of the path was already matched
1156
- if (this.id !== 'routerOutlet' && this.ionRouterOutlet) {
1157
- const routeChildren = extractRouteChildren(this.ionRouterOutlet.props.children);
1158
- const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
1159
- const result = computeParentPath({
1160
- currentPathname,
1161
- outletMountPath: this.outletMountPath,
1162
- routeChildren,
1163
- hasRelativeRoutes,
1164
- hasIndexRoute,
1165
- hasWildcardRoute,
1166
- });
1167
- // Update the outlet mount path if it was set
1168
- if (result.outletMountPath && !this.outletMountPath) {
1169
- this.outletMountPath = result.outletMountPath;
1170
- }
1171
- return result.parentPath;
1172
- }
1173
- return this.outletMountPath;
1174
- }
1175
- /**
1176
- * Finds the entering and leaving view items for a route transition,
1177
- * handling special redirect cases.
1178
- */
1179
- findViewItems(routeInfo) {
1180
- const enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
1181
- let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
1182
- // If we don't have a leaving view item, but the route info indicates
1183
- // that the user has routed from a previous path, then the leaving view
1184
- // can be found by the last known pathname.
1185
- if (!leavingViewItem && routeInfo.prevRouteLastPathname) {
1186
- leavingViewItem = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
1187
- }
1188
- // Special case for redirects: When a redirect happens inside a nested route,
1189
- // the entering and leaving view might be the same (the container route like tabs/*).
1190
- // In this case, we need to look at prevRouteLastPathname to find the actual
1191
- // view we're transitioning away from.
1192
- if (enteringViewItem &&
1193
- leavingViewItem &&
1194
- enteringViewItem === leavingViewItem &&
1195
- routeInfo.routeAction === 'replace' &&
1196
- routeInfo.prevRouteLastPathname) {
1197
- const actualLeavingView = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
1198
- if (actualLeavingView && actualLeavingView !== enteringViewItem) {
1199
- leavingViewItem = actualLeavingView;
1200
- }
1201
- }
1202
- // Also check if we're in a redirect scenario where entering and leaving are different
1203
- // but we still need to handle the actual previous view.
1204
- if (enteringViewItem &&
1205
- !leavingViewItem &&
1206
- routeInfo.routeAction === 'replace' &&
1207
- routeInfo.prevRouteLastPathname) {
1208
- const actualLeavingView = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
1209
- if (actualLeavingView && actualLeavingView !== enteringViewItem) {
1210
- leavingViewItem = actualLeavingView;
1211
- }
1212
- }
1213
- return { enteringViewItem, leavingViewItem };
1214
- }
1215
- /**
1216
- * Determines if the leaving view item should be unmounted after a transition.
1217
- */
1218
- shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem) {
1219
- if (!leavingViewItem) {
1220
- return false;
1221
- }
1222
- if (routeInfo.routeAction === 'replace') {
1223
- return true;
1224
- }
1225
- const isForwardPush = routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward';
1226
- if (!isForwardPush && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
1227
- return true;
1228
- }
1229
- return false;
1230
- }
1231
- /**
1232
- * Handles the case when the outlet is out of scope (current route is outside mount path).
1233
- * Returns true if the transition should be aborted.
1234
- */
1235
- handleOutOfScopeOutlet(routeInfo) {
1236
- if (!this.outletMountPath || routeInfo.pathname.startsWith(this.outletMountPath)) {
1237
- return false;
1238
- }
1239
- // Clear any pending unmount timeout to avoid conflicts
1240
- if (this.outOfScopeUnmountTimeout) {
1241
- clearTimeout(this.outOfScopeUnmountTimeout);
1242
- this.outOfScopeUnmountTimeout = undefined;
1243
- }
1244
- // When an outlet is out of scope, unmount its views immediately
1245
- const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
1246
- // Unmount and remove all views in this outlet immediately to avoid leftover content
1247
- allViewsInOutlet.forEach((viewItem) => {
1248
- hideIonPageElement(viewItem.ionPageElement);
1249
- this.context.unMountViewItem(viewItem);
1250
- });
1251
- this.forceUpdate();
1252
- return true;
1253
- }
1254
- /**
1255
- * Handles the case when this is a nested outlet with relative routes but no valid parent path.
1256
- * Returns true if the transition should be aborted.
1257
- */
1258
- handleOutOfContextNestedOutlet(parentPath, leavingViewItem) {
1259
- var _a;
1260
- if (this.id === 'routerOutlet' || parentPath !== undefined || !this.ionRouterOutlet) {
1261
- return false;
1262
- }
1263
- const routesChildren = (_a = getRoutesChildren(this.ionRouterOutlet.props.children)) !== null && _a !== void 0 ? _a : this.ionRouterOutlet.props.children;
1264
- const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
1265
- const hasRelativeRoutes = routeChildren.some((route) => {
1266
- const path = route.props.path;
1267
- return path && !path.startsWith('/') && path !== '*';
1268
- });
1269
- if (hasRelativeRoutes) {
1270
- // Hide any visible views in this outlet since it's out of scope
1271
- hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
1272
- if (leavingViewItem) {
1273
- leavingViewItem.mount = false;
1274
- }
1275
- this.forceUpdate();
1276
- return true;
1277
- }
1278
- return false;
1279
- }
1280
- /**
1281
- * Handles the case when a nested outlet has no matching route.
1282
- * Returns true if the transition should be aborted.
1283
- */
1284
- handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem) {
1285
- if (this.id === 'routerOutlet' || enteringRoute || enteringViewItem) {
1286
- return false;
1287
- }
1288
- // Hide any visible views in this outlet since it has no matching route
1289
- hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
1290
- if (leavingViewItem) {
1291
- leavingViewItem.mount = false;
1292
- }
1293
- this.forceUpdate();
1294
- return true;
1295
- }
1296
- /**
1297
- * Handles the transition when entering view item has an ion-page element ready.
1298
- */
1299
- handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem) {
1300
- var _a, _b;
1301
- // Ensure the entering view is not hidden from previous navigations
1302
- showIonPageElement(enteringViewItem.ionPageElement);
1303
- // Handle same view item case (e.g., parameterized route changes)
1304
- if (enteringViewItem === leavingViewItem) {
1305
- const routePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1306
- const isParameterizedRoute = routePath ? routePath.includes(':') : false;
1307
- if (isParameterizedRoute) {
1308
- // Refresh match metadata so the component receives updated params
1309
- const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true);
1310
- if (updatedMatch) {
1311
- enteringViewItem.routeData.match = updatedMatch;
1312
- }
1313
- const enteringEl = enteringViewItem.ionPageElement;
1314
- if (enteringEl) {
1315
- enteringEl.classList.remove('ion-page-hidden', 'ion-page-invisible');
1316
- enteringEl.removeAttribute('aria-hidden');
1317
- }
1318
- this.forceUpdate();
1319
- return;
1320
- }
1321
- }
1322
- // Try to find leaving view using prev route info if still not found
1323
- if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
1324
- leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
1325
- }
1326
- // Skip transition if entering view is visible and leaving view is not
1327
- if (enteringViewItem.ionPageElement &&
1328
- isViewVisible(enteringViewItem.ionPageElement) &&
1329
- leavingViewItem !== undefined &&
1330
- leavingViewItem.ionPageElement &&
1331
- !isViewVisible(leavingViewItem.ionPageElement)) {
1332
- return;
1333
- }
1334
- // Check for duplicate transition
1335
- const currentTransition = {
1336
- enteringId: enteringViewItem.id,
1337
- leavingId: leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.id,
1338
- };
1339
- if (leavingViewItem &&
1340
- this.lastTransition &&
1341
- this.lastTransition.leavingId &&
1342
- this.lastTransition.enteringId === currentTransition.enteringId &&
1343
- this.lastTransition.leavingId === currentTransition.leavingId) {
1344
- return;
1345
- }
1346
- this.lastTransition = currentTransition;
1347
- this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
1348
- // Handle unmounting the leaving view
1349
- if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
1350
- leavingViewItem.mount = false;
1351
- this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
1352
- }
1353
- }
1354
- /**
1355
- * Handles the delayed unmount of the leaving view item after a replace action.
1356
- */
1357
- handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem) {
1358
- var _a, _b, _c, _d, _e, _f;
1359
- if (routeInfo.routeAction !== 'replace' || !leavingViewItem.ionPageElement) {
1360
- return;
1361
- }
1362
- // Check if we should skip removal for nested outlet redirects
1363
- const enteringRoutePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1364
- const leavingRoutePath = (_d = (_c = leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1365
- const isEnteringContainerRoute = enteringRoutePath && enteringRoutePath.endsWith('/*');
1366
- const isLeavingSpecificRoute = leavingRoutePath &&
1367
- leavingRoutePath !== '' &&
1368
- leavingRoutePath !== '*' &&
1369
- !leavingRoutePath.endsWith('/*') &&
1370
- !((_f = (_e = leavingViewItem.reactElement) === null || _e === void 0 ? void 0 : _e.props) === null || _f === void 0 ? void 0 : _f.index);
1371
- // Skip removal only for container-to-container transitions
1372
- if (isEnteringContainerRoute && !isLeavingSpecificRoute) {
1373
- return;
1374
- }
1375
- const viewToUnmount = leavingViewItem;
1376
- setTimeout(() => {
1377
- this.context.unMountViewItem(viewToUnmount);
1378
- }, VIEW_UNMOUNT_DELAY_MS);
1379
- }
1380
- /**
1381
- * Handles the case when entering view has no ion-page element yet (waiting for render).
1382
- */
1383
- handleWaitingForIonPage(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem) {
1384
- var _a, _b;
1385
- const enteringRouteElement = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.element;
1386
- // Handle Navigate components (they never render an IonPage)
1387
- if (isNavigateElement(enteringRouteElement)) {
1388
- this.waitingForIonPage = false;
1389
- if (this.ionPageWaitTimeout) {
1390
- clearTimeout(this.ionPageWaitTimeout);
1391
- this.ionPageWaitTimeout = undefined;
1392
- }
1393
- this.pendingPageTransition = false;
1394
- // Hide the leaving view immediately for Navigate redirects
1395
- hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
1396
- // Don't unmount if entering and leaving are the same view item
1397
- if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
1398
- leavingViewItem.mount = false;
1399
- }
1400
- this.forceUpdate();
1401
- return;
1402
- }
1403
- // Hide leaving view while we wait for the entering view's IonPage to mount
1404
- hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
1405
- this.waitingForIonPage = true;
1406
- if (this.ionPageWaitTimeout) {
1407
- clearTimeout(this.ionPageWaitTimeout);
1408
- }
1409
- this.ionPageWaitTimeout = setTimeout(() => {
1410
- var _a, _b;
1411
- this.ionPageWaitTimeout = undefined;
1412
- if (!this.waitingForIonPage) {
1413
- return;
1414
- }
1415
- this.waitingForIonPage = false;
1416
- const latestEnteringView = (_a = this.context.findViewItemByRouteInfo(routeInfo, this.id)) !== null && _a !== void 0 ? _a : enteringViewItem;
1417
- const latestLeavingView = (_b = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id)) !== null && _b !== void 0 ? _b : leavingViewItem;
1418
- if (latestEnteringView === null || latestEnteringView === void 0 ? void 0 : latestEnteringView.ionPageElement) {
1419
- this.transitionPage(routeInfo, latestEnteringView, latestLeavingView !== null && latestLeavingView !== void 0 ? latestLeavingView : undefined);
1420
- if (shouldUnmountLeavingViewItem && latestLeavingView && latestEnteringView !== latestLeavingView) {
1421
- latestLeavingView.mount = false;
1422
- }
1423
- this.forceUpdate();
1424
- }
1425
- }, ION_PAGE_WAIT_TIMEOUT_MS);
1426
- this.forceUpdate();
1427
- }
1428
- /**
1429
- * Gets the route info to use for finding views during swipe-to-go-back gestures.
1430
- * This pattern is used in multiple places in setupRouterOutlet.
1431
- */
1432
- getSwipeBackRouteInfo() {
1433
- const { routeInfo } = this.props;
1434
- return this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
1435
- ? this.prevProps.routeInfo
1436
- : { pathname: routeInfo.pushedByRoute || '' };
223
+ this.id = generateId('routerOutlet');
224
+ this.prevProps = undefined;
225
+ this.skipTransition = false;
1437
226
  }
1438
227
  componentDidMount() {
1439
228
  if (this.clearOutletTimeout) {
@@ -1466,123 +255,114 @@ class StackManager extends React.PureComponent {
1466
255
  }
1467
256
  }
1468
257
  componentWillUnmount() {
1469
- if (this.ionPageWaitTimeout) {
1470
- clearTimeout(this.ionPageWaitTimeout);
1471
- this.ionPageWaitTimeout = undefined;
1472
- }
1473
- if (this.outOfScopeUnmountTimeout) {
1474
- clearTimeout(this.outOfScopeUnmountTimeout);
1475
- this.outOfScopeUnmountTimeout = undefined;
1476
- }
1477
- this.waitingForIonPage = false;
1478
- // Hide all views in this outlet before clearing.
1479
- // This is critical for nested outlets - when the parent component unmounts,
1480
- // the nested outlet's componentDidUpdate won't be called, so we must hide
1481
- // the ion-page elements here to prevent them from remaining visible on top
1482
- // of other content after navigation to a different route.
1483
- const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
1484
- allViewsInOutlet.forEach((viewItem) => {
1485
- hideIonPageElement(viewItem.ionPageElement);
1486
- });
1487
258
  this.clearOutletTimeout = this.context.clearOutlet(this.id);
1488
259
  }
1489
- /**
1490
- * Sets the transition between pages within this router outlet.
1491
- * This function determines the entering and leaving views based on the
1492
- * provided route information and triggers the appropriate animation.
1493
- * It also handles scenarios like initial loads, back navigation, and
1494
- * navigation to the same view with different parameters.
1495
- *
1496
- * @param routeInfo It contains info about the current route,
1497
- * the previous route, and the action taken (e.g., push, replace).
1498
- *
1499
- * @returns A promise that resolves when the transition is complete.
1500
- * If no transition is needed or if the router outlet isn't ready,
1501
- * the Promise may resolve immediately.
1502
- */
1503
260
  async handlePageTransition(routeInfo) {
1504
- var _a;
1505
- // Wait for router outlet to mount
261
+ var _a, _b;
1506
262
  if (!this.routerOutletElement || !this.routerOutletElement.commit) {
263
+ /**
264
+ * The route outlet has not mounted yet. We need to wait for it to render
265
+ * before we can transition the page.
266
+ *
267
+ * Set a flag to indicate that we should transition the page after
268
+ * the component has updated.
269
+ */
1507
270
  this.pendingPageTransition = true;
1508
- return;
1509
- }
1510
- // Find entering and leaving view items
1511
- const viewItems = this.findViewItems(routeInfo);
1512
- let enteringViewItem = viewItems.enteringViewItem;
1513
- const leavingViewItem = viewItems.leavingViewItem;
1514
- const shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
1515
- // Get parent path for nested outlets
1516
- const parentPath = this.getParentPath();
1517
- // Handle out-of-scope outlet (route outside mount path)
1518
- if (this.handleOutOfScopeOutlet(routeInfo)) {
1519
- return;
1520
- }
1521
- // Clear any pending out-of-scope unmount timeout
1522
- if (this.outOfScopeUnmountTimeout) {
1523
- clearTimeout(this.outOfScopeUnmountTimeout);
1524
- this.outOfScopeUnmountTimeout = undefined;
1525
- }
1526
- // Handle nested outlet with relative routes but no valid parent path
1527
- if (this.handleOutOfContextNestedOutlet(parentPath, leavingViewItem)) {
1528
- return;
1529
- }
1530
- // Find the matching route element
1531
- const enteringRoute = findRouteByRouteInfo((_a = this.ionRouterOutlet) === null || _a === void 0 ? void 0 : _a.props.children, routeInfo, parentPath);
1532
- // Handle nested outlet with no matching route
1533
- if (this.handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem)) {
1534
- return;
1535
- }
1536
- // Create or update the entering view item
1537
- if (enteringViewItem && enteringRoute) {
1538
- enteringViewItem.reactElement = enteringRoute;
1539
271
  }
1540
- else if (enteringRoute) {
1541
- enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
1542
- this.context.addViewItem(enteringViewItem);
1543
- }
1544
- // Handle transition based on ion-page element availability
1545
- if (enteringViewItem && enteringViewItem.ionPageElement) {
1546
- // Clear waiting state
1547
- if (this.waitingForIonPage) {
1548
- this.waitingForIonPage = false;
1549
- }
1550
- if (this.ionPageWaitTimeout) {
1551
- clearTimeout(this.ionPageWaitTimeout);
1552
- this.ionPageWaitTimeout = undefined;
272
+ else {
273
+ let enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
274
+ let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
275
+ if (!leavingViewItem && routeInfo.prevRouteLastPathname) {
276
+ leavingViewItem = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
1553
277
  }
1554
- this.handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem);
1555
- }
1556
- else if (enteringViewItem && !enteringViewItem.ionPageElement) {
1557
- // Wait for ion-page to mount
1558
- this.handleWaitingForIonPage(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem);
1559
- return;
1560
- }
1561
- else if (!enteringViewItem && !enteringRoute) {
1562
- // No view or route found - likely leaving to another outlet
278
+ // Check if leavingViewItem should be unmounted
1563
279
  if (leavingViewItem) {
1564
- hideIonPageElement(leavingViewItem.ionPageElement);
1565
- if (shouldUnmountLeavingViewItem) {
280
+ if (routeInfo.routeAction === 'replace') {
281
+ leavingViewItem.mount = false;
282
+ }
283
+ else if (!(routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward')) {
284
+ if (routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
285
+ leavingViewItem.mount = false;
286
+ }
287
+ }
288
+ else if ((_a = routeInfo.routeOptions) === null || _a === void 0 ? void 0 : _a.unmount) {
1566
289
  leavingViewItem.mount = false;
1567
290
  }
1568
291
  }
292
+ const enteringRoute = matchRoute((_b = this.ionRouterOutlet) === null || _b === void 0 ? void 0 : _b.props.children, routeInfo);
293
+ if (enteringViewItem) {
294
+ enteringViewItem.reactElement = enteringRoute;
295
+ }
296
+ else if (enteringRoute) {
297
+ enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
298
+ this.context.addViewItem(enteringViewItem);
299
+ }
300
+ if (enteringViewItem && enteringViewItem.ionPageElement) {
301
+ /**
302
+ * If the entering view item is the same as the leaving view item,
303
+ * then we don't need to transition.
304
+ */
305
+ if (enteringViewItem === leavingViewItem) {
306
+ /**
307
+ * If the entering view item is the same as the leaving view item,
308
+ * we are either transitioning using parameterized routes to the same view
309
+ * or a parent router outlet is re-rendering as a result of React props changing.
310
+ *
311
+ * If the route data does not match the current path, the parent router outlet
312
+ * is attempting to transition and we cancel the operation.
313
+ */
314
+ if (enteringViewItem.routeData.match.url !== routeInfo.pathname) {
315
+ return;
316
+ }
317
+ }
318
+ /**
319
+ * If there isn't a leaving view item, but the route info indicates
320
+ * that the user has routed from a previous path, then we need
321
+ * to find the leaving view item to transition between.
322
+ */
323
+ if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
324
+ leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
325
+ }
326
+ /**
327
+ * If the entering view is already visible and the leaving view is not, the transition does not need to occur.
328
+ */
329
+ if (isViewVisible(enteringViewItem.ionPageElement) &&
330
+ leavingViewItem !== undefined &&
331
+ !isViewVisible(leavingViewItem.ionPageElement)) {
332
+ return;
333
+ }
334
+ /**
335
+ * The view should only be transitioned in the following cases:
336
+ * 1. Performing a replace or pop action, such as a swipe to go back gesture
337
+ * to animation the leaving view off the screen.
338
+ *
339
+ * 2. Navigating between top-level router outlets, such as /page-1 to /page-2;
340
+ * or navigating within a nested outlet, such as /tabs/tab-1 to /tabs/tab-2.
341
+ *
342
+ * 3. The entering view is an ion-router-outlet containing a page
343
+ * matching the current route and that hasn't already transitioned in.
344
+ *
345
+ * This should only happen when navigating directly to a nested router outlet
346
+ * route or on an initial page load (i.e. refreshing). In cases when loading
347
+ * /tabs/tab-1, we need to transition the /tabs page element into the view.
348
+ */
349
+ this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
350
+ }
351
+ else if (leavingViewItem && !enteringRoute && !enteringViewItem) {
352
+ // If we have a leavingView but no entering view/route, we are probably leaving to
353
+ // another outlet, so hide this leavingView. We do it in a timeout to give time for a
354
+ // transition to finish.
355
+ // setTimeout(() => {
356
+ if (leavingViewItem.ionPageElement) {
357
+ leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
358
+ leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
359
+ }
360
+ // }, 250);
361
+ }
362
+ this.forceUpdate();
1569
363
  }
1570
- this.forceUpdate();
1571
364
  }
1572
- /**
1573
- * Registers an `<IonPage>` DOM element with the `StackManager`.
1574
- * This is called when `<IonPage>` has been mounted.
1575
- *
1576
- * @param page The element of the rendered `<IonPage>`.
1577
- * @param routeInfo The route information that associates with `<IonPage>`.
1578
- */
1579
365
  registerIonPage(page, routeInfo) {
1580
- this.waitingForIonPage = false;
1581
- if (this.ionPageWaitTimeout) {
1582
- clearTimeout(this.ionPageWaitTimeout);
1583
- this.ionPageWaitTimeout = undefined;
1584
- }
1585
- this.pendingPageTransition = false;
1586
366
  const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
1587
367
  if (foundView) {
1588
368
  const oldPageElement = foundView.ionPageElement;
@@ -1599,38 +379,48 @@ class StackManager extends React.PureComponent {
1599
379
  }
1600
380
  this.handlePageTransition(routeInfo);
1601
381
  }
1602
- /**
1603
- * Configures the router outlet for the swipe-to-go-back gesture.
1604
- *
1605
- * @param routerOutlet The Ionic router outlet component: `<IonRouterOutlet>`.
1606
- */
1607
382
  async setupRouterOutlet(routerOutlet) {
1608
383
  const canStart = () => {
1609
384
  const config = getConfig();
1610
- // Check if swipe back is enabled in config (default to true for iOS mode)
1611
385
  const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios');
1612
386
  if (!swipeEnabled) {
1613
387
  return false;
1614
388
  }
1615
389
  const { routeInfo } = this.props;
1616
- const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1617
- const enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1618
- const canStartSwipe = !!enteringViewItem &&
1619
- // The root url '/' is treated as the first view item (but is never mounted),
1620
- // so we do not want to swipe back to the root url.
390
+ const propsToUse = this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
391
+ ? this.prevProps.routeInfo
392
+ : { pathname: routeInfo.pushedByRoute || '' };
393
+ const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
394
+ return (!!enteringViewItem &&
395
+ /**
396
+ * The root url '/' is treated as
397
+ * the first view item (but is never mounted),
398
+ * so we do not want to swipe back to the
399
+ * root url.
400
+ */
1621
401
  enteringViewItem.mount &&
1622
- // When on the first page it is possible for findViewItemByRouteInfo to
1623
- // return the exact same view you are currently on.
1624
- // Make sure that we are not swiping back to the same instances of a view.
1625
- enteringViewItem.routeData.match.pattern.path !== routeInfo.pathname;
1626
- return canStartSwipe;
402
+ /**
403
+ * When on the first page (whatever view
404
+ * you land on after the root url) it
405
+ * is possible for findViewItemByRouteInfo to
406
+ * return the exact same view you are currently on.
407
+ * Make sure that we are not swiping back to the same
408
+ * instances of a view.
409
+ */
410
+ enteringViewItem.routeData.match.path !== routeInfo.pathname);
1627
411
  };
1628
412
  const onStart = async () => {
1629
413
  const { routeInfo } = this.props;
1630
- const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1631
- const enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
414
+ const propsToUse = this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
415
+ ? this.prevProps.routeInfo
416
+ : { pathname: routeInfo.pushedByRoute || '' };
417
+ const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
1632
418
  const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
1633
- // When the gesture starts, kick off a transition controlled via swipe gesture
419
+ /**
420
+ * When the gesture starts, kick off
421
+ * a transition that is controlled
422
+ * via a swipe gesture.
423
+ */
1634
424
  if (enteringViewItem && leavingViewItem) {
1635
425
  await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
1636
426
  }
@@ -1638,19 +428,34 @@ class StackManager extends React.PureComponent {
1638
428
  };
1639
429
  const onEnd = (shouldContinue) => {
1640
430
  if (shouldContinue) {
1641
- // User finished the swipe gesture, so complete the back navigation
1642
431
  this.skipTransition = true;
1643
432
  this.context.goBack();
1644
433
  }
1645
434
  else {
1646
- // Swipe gesture was aborted - re-hide the page that was going to enter
435
+ /**
436
+ * In the event that the swipe
437
+ * gesture was aborted, we should
438
+ * re-hide the page that was going to enter.
439
+ */
1647
440
  const { routeInfo } = this.props;
1648
- const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1649
- const enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
441
+ const propsToUse = this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
442
+ ? this.prevProps.routeInfo
443
+ : { pathname: routeInfo.pushedByRoute || '' };
444
+ const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
1650
445
  const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
1651
- // Don't hide if entering and leaving are the same (parameterized route edge case)
446
+ /**
447
+ * Ionic React has a design defect where it
448
+ * a) Unmounts the leaving view item when using parameterized routes
449
+ * b) Considers the current view to be the entering view when using
450
+ * parameterized routes
451
+ *
452
+ * As a result, we should not hide the view item here
453
+ * as it will cause the current view to be hidden.
454
+ */
1652
455
  if (enteringViewItem !== leavingViewItem && (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) !== undefined) {
1653
- hideIonPageElement(enteringViewItem.ionPageElement);
456
+ const { ionPageElement } = enteringViewItem;
457
+ ionPageElement.setAttribute('aria-hidden', 'true');
458
+ ionPageElement.classList.add('ion-page-hidden');
1654
459
  }
1655
460
  }
1656
461
  };
@@ -1660,18 +465,6 @@ class StackManager extends React.PureComponent {
1660
465
  onEnd,
1661
466
  };
1662
467
  }
1663
- /**
1664
- * Animates the transition between the entering and leaving pages within the
1665
- * router outlet.
1666
- *
1667
- * @param routeInfo Info about the current route.
1668
- * @param enteringViewItem The view item that is entering.
1669
- * @param leavingViewItem The view item that is leaving.
1670
- * @param direction The direction of the transition.
1671
- * @param progressAnimation Indicates if the transition is part of a
1672
- * gesture controlled animation (e.g., swipe to go back).
1673
- * Defaults to `false`.
1674
- */
1675
468
  async transitionPage(routeInfo, enteringViewItem, leavingViewItem, direction, progressAnimation = false) {
1676
469
  const runCommit = async (enteringEl, leavingEl) => {
1677
470
  const skipTransition = this.skipTransition;
@@ -1717,8 +510,7 @@ class StackManager extends React.PureComponent {
1717
510
  if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) {
1718
511
  // If a page is transitioning to another version of itself
1719
512
  // we clone it so we can have an animation to show
1720
- // (e.g., `/user/1` → `/user/2`)
1721
- const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname);
513
+ const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, true);
1722
514
  if (match) {
1723
515
  const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
1724
516
  if (newLeavingElement) {
@@ -1728,15 +520,6 @@ class StackManager extends React.PureComponent {
1728
520
  }
1729
521
  }
1730
522
  else {
1731
- /**
1732
- * The route no longer matches the component type of the leaving view.
1733
- * (e.g., `/user/1` → `/settings`)
1734
- *
1735
- * This can also occur in edge cases like rapid navigation
1736
- * or during parent component re-renders that briefly cause
1737
- * the view items to be the same instance before the final
1738
- * route component is determined.
1739
- */
1740
523
  await runCommit(enteringViewItem.ionPageElement, undefined);
1741
524
  }
1742
525
  }
@@ -1752,25 +535,20 @@ class StackManager extends React.PureComponent {
1752
535
  render() {
1753
536
  const { children } = this.props;
1754
537
  const ionRouterOutlet = React.Children.only(children);
1755
- // Store reference for use in getParentPath() and handlePageTransition()
1756
538
  this.ionRouterOutlet = ionRouterOutlet;
1757
539
  const components = this.context.getChildrenToRender(this.id, this.ionRouterOutlet, this.props.routeInfo, () => {
1758
- // Callback triggers re-render when view items are modified during getChildrenToRender
1759
540
  this.forceUpdate();
1760
541
  });
1761
542
  return (React.createElement(StackContext.Provider, { value: this.stackContextValue }, React.cloneElement(ionRouterOutlet, {
1762
543
  ref: (node) => {
1763
544
  if (ionRouterOutlet.props.setRef) {
1764
- // Needed to handle external refs from devs.
1765
545
  ionRouterOutlet.props.setRef(node);
1766
546
  }
1767
547
  if (ionRouterOutlet.props.forwardedRef) {
1768
- // Needed to handle external refs from devs.
1769
548
  ionRouterOutlet.props.forwardedRef.current = node;
1770
549
  }
1771
550
  this.routerOutletElement = node;
1772
551
  const { ref } = ionRouterOutlet;
1773
- // Check for legacy refs.
1774
552
  if (typeof ref === 'function') {
1775
553
  ref(node);
1776
554
  }
@@ -1781,351 +559,169 @@ class StackManager extends React.PureComponent {
1781
559
  return RouteManagerContext;
1782
560
  }
1783
561
  }
1784
- /**
1785
- * Finds the `<Route />` node matching the current route info.
1786
- * If no `<Route />` can be matched, a fallback node is returned.
1787
- * Routes are prioritized by specificity (most specific first).
1788
- *
1789
- * @param node The root node to search for `<Route />` nodes.
1790
- * @param routeInfo The route information to match against.
1791
- * @param parentPath The parent path that was matched by the parent outlet (for nested routing)
1792
- */
1793
- function findRouteByRouteInfo(node, routeInfo, parentPath) {
1794
- var _a;
562
+ function matchRoute(node, routeInfo) {
1795
563
  let matchedNode;
1796
- let fallbackNode;
1797
- // `<Route />` nodes are rendered inside of a <Routes /> node
1798
- const routesChildren = (_a = getRoutesChildren(node)) !== null && _a !== void 0 ? _a : node;
1799
- // Collect all route children
1800
- const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
1801
- // Sort routes by specificity (most specific first)
1802
- const sortedRoutes = routeChildren.sort((a, b) => {
1803
- const pathA = a.props.path || '';
1804
- const pathB = b.props.path || '';
1805
- // Index routes come first
1806
- if (a.props.index && !b.props.index)
1807
- return -1;
1808
- if (!a.props.index && b.props.index)
1809
- return 1;
1810
- // Wildcard-only routes (*) should come LAST
1811
- const aIsWildcardOnly = pathA === '*';
1812
- const bIsWildcardOnly = pathB === '*';
1813
- if (!aIsWildcardOnly && bIsWildcardOnly)
1814
- return -1;
1815
- if (aIsWildcardOnly && !bIsWildcardOnly)
1816
- return 1;
1817
- // Exact matches (no wildcards/params) come before wildcard/param routes
1818
- const aHasWildcard = pathA.includes('*') || pathA.includes(':');
1819
- const bHasWildcard = pathB.includes('*') || pathB.includes(':');
1820
- if (!aHasWildcard && bHasWildcard)
1821
- return -1;
1822
- if (aHasWildcard && !bHasWildcard)
1823
- return 1;
1824
- // Among routes with same wildcard status, longer paths are more specific
1825
- if (pathA.length !== pathB.length) {
1826
- return pathB.length - pathA.length;
1827
- }
1828
- return 0;
1829
- });
1830
- // For nested routes in React Router 6, we need to extract the relative path
1831
- // that this outlet should be responsible for matching
1832
- let pathnameToMatch = routeInfo.pathname;
1833
- // Check if we have relative routes (routes that don't start with '/')
1834
- const hasRelativeRoutes = sortedRoutes.some((r) => r.props.path && !r.props.path.startsWith('/'));
1835
- const hasIndexRoute = sortedRoutes.some((r) => r.props.index);
1836
- // SIMPLIFIED: Trust React Router 6's matching more, compute relative path when parent is known
1837
- if ((hasRelativeRoutes || hasIndexRoute) && parentPath) {
1838
- const parentPrefix = parentPath.replace('/*', '');
1839
- const normalizedParent = stripTrailingSlash(parentPrefix);
1840
- const normalizedPathname = stripTrailingSlash(routeInfo.pathname);
1841
- // Only compute relative path if pathname is within parent scope
1842
- if (normalizedPathname.startsWith(normalizedParent + '/') || normalizedPathname === normalizedParent) {
1843
- const pathSegments = routeInfo.pathname.split('/').filter(Boolean);
1844
- const parentSegments = normalizedParent.split('/').filter(Boolean);
1845
- const relativeSegments = pathSegments.slice(parentSegments.length);
1846
- pathnameToMatch = relativeSegments.join('/'); // Empty string is valid for index routes
1847
- }
1848
- }
1849
- // Find the first matching route
1850
- for (const child of sortedRoutes) {
564
+ React.Children.forEach(node, (child) => {
1851
565
  const match = matchPath({
1852
- pathname: pathnameToMatch,
566
+ pathname: routeInfo.pathname,
1853
567
  componentProps: child.props,
1854
568
  });
1855
569
  if (match) {
1856
570
  matchedNode = child;
1857
- break;
1858
571
  }
1859
- }
572
+ });
1860
573
  if (matchedNode) {
1861
574
  return matchedNode;
1862
575
  }
1863
- // If we haven't found a node, try to find one that doesn't have a path prop (fallback route)
1864
- // BUT only return the fallback if the current pathname is within the outlet's scope.
1865
- // For outlets with absolute paths, compute the common prefix to determine scope.
1866
- const absolutePathRoutes = routeChildren.filter((r) => r.props.path && r.props.path.startsWith('/'));
1867
- // Determine if pathname is within scope before returning fallback
1868
- let isPathnameInScope = true;
1869
- if (absolutePathRoutes.length > 0) {
1870
- // Find common prefix of all absolute paths to determine outlet scope
1871
- const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
1872
- const commonPrefix = computeCommonPrefix(absolutePaths);
1873
- // If we have a common prefix, check if the current pathname is within that scope
1874
- if (commonPrefix && commonPrefix !== '/') {
1875
- isPathnameInScope = routeInfo.pathname.startsWith(commonPrefix);
1876
- }
1877
- }
1878
- // Only look for fallback route if pathname is within scope
1879
- if (isPathnameInScope) {
1880
- for (const child of routeChildren) {
1881
- if (!child.props.path) {
1882
- fallbackNode = child;
1883
- break;
1884
- }
576
+ // If we haven't found a node
577
+ // try to find one that doesn't have a path or from prop, that will be our not found route
578
+ React.Children.forEach(node, (child) => {
579
+ if (!(child.props.path || child.props.from)) {
580
+ matchedNode = child;
1885
581
  }
1886
- }
1887
- return matchedNode !== null && matchedNode !== void 0 ? matchedNode : fallbackNode;
582
+ });
583
+ return matchedNode;
1888
584
  }
1889
585
  function matchComponent(node, pathname, forceExact) {
1890
- var _a;
1891
- const routePath = (_a = node === null || node === void 0 ? void 0 : node.props) === null || _a === void 0 ? void 0 : _a.path;
1892
- const pathnameToMatch = derivePathnameToMatch(pathname, routePath);
1893
586
  return matchPath({
1894
- pathname: pathnameToMatch,
1895
- componentProps: Object.assign(Object.assign({}, node.props), { end: forceExact }),
587
+ pathname,
588
+ componentProps: Object.assign(Object.assign({}, node.props), { exact: forceExact }),
1896
589
  });
1897
590
  }
1898
591
 
1899
- /**
1900
- * `IonRouter` is responsible for managing the application's navigation
1901
- * state, tracking the history of visited routes, and coordinating
1902
- * transitions between different views. It intercepts route changes from
1903
- * React Router and translates them into actions that Ionic can understand
1904
- * and animate.
1905
- */
1906
- const filterUndefinedParams = (params) => {
1907
- const result = {};
1908
- for (const key of Object.keys(params)) {
1909
- const value = params[key];
1910
- if (value !== undefined) {
1911
- result[key] = value;
1912
- }
1913
- }
1914
- return result;
1915
- };
1916
- const areParamsEqual = (a, b) => {
1917
- const paramsA = a || {};
1918
- const paramsB = b || {};
1919
- const keysA = Object.keys(paramsA);
1920
- const keysB = Object.keys(paramsB);
1921
- if (keysA.length !== keysB.length) {
1922
- return false;
592
+ class IonRouterInner extends React.PureComponent {
593
+ constructor(props) {
594
+ super(props);
595
+ this.exitViewFromOtherOutletHandlers = [];
596
+ this.locationHistory = new LocationHistory();
597
+ this.viewStack = new ReactRouterViewStack();
598
+ this.routeMangerContextState = {
599
+ canGoBack: () => this.locationHistory.canGoBack(),
600
+ clearOutlet: this.viewStack.clear,
601
+ findViewItemByPathname: this.viewStack.findViewItemByPathname,
602
+ getChildrenToRender: this.viewStack.getChildrenToRender,
603
+ goBack: () => this.handleNavigateBack(),
604
+ createViewItem: this.viewStack.createViewItem,
605
+ findViewItemByRouteInfo: this.viewStack.findViewItemByRouteInfo,
606
+ findLeavingViewItemByRouteInfo: this.viewStack.findLeavingViewItemByRouteInfo,
607
+ addViewItem: this.viewStack.add,
608
+ unMountViewItem: this.viewStack.remove,
609
+ };
610
+ const routeInfo = {
611
+ id: generateId('routeInfo'),
612
+ pathname: this.props.location.pathname,
613
+ search: this.props.location.search,
614
+ };
615
+ this.locationHistory.add(routeInfo);
616
+ this.handleChangeTab = this.handleChangeTab.bind(this);
617
+ this.handleResetTab = this.handleResetTab.bind(this);
618
+ this.handleNativeBack = this.handleNativeBack.bind(this);
619
+ this.handleNavigate = this.handleNavigate.bind(this);
620
+ this.handleNavigateBack = this.handleNavigateBack.bind(this);
621
+ this.props.registerHistoryListener(this.handleHistoryChange.bind(this));
622
+ this.handleSetCurrentTab = this.handleSetCurrentTab.bind(this);
623
+ this.state = {
624
+ routeInfo,
625
+ };
1923
626
  }
1924
- return keysA.every((key) => {
1925
- const valueA = paramsA[key];
1926
- const valueB = paramsB[key];
1927
- if (Array.isArray(valueA) && Array.isArray(valueB)) {
1928
- if (valueA.length !== valueB.length) {
1929
- return false;
1930
- }
1931
- return valueA.every((entry, idx) => entry === valueB[idx]);
1932
- }
1933
- return valueA === valueB;
1934
- });
1935
- };
1936
- const IonRouter = ({ children, registerHistoryListener }) => {
1937
- const location = useLocation();
1938
- const navigate = useNavigate();
1939
- const didMountRef = useRef(false);
1940
- const locationHistory = useRef(new LocationHistory());
1941
- const currentTab = useRef(undefined);
1942
- const viewStack = useRef(new ReactRouterViewStack());
1943
- const incomingRouteParams = useRef(null);
1944
- const [routeInfo, setRouteInfo] = useState({
1945
- id: generateId('routeInfo'),
1946
- pathname: location.pathname,
1947
- search: location.search,
1948
- params: {},
1949
- });
1950
- useEffect(() => {
1951
- if (didMountRef.current) {
627
+ handleChangeTab(tab, path, routeOptions) {
628
+ if (!path) {
1952
629
  return;
1953
630
  }
1954
- // Seed the history stack with the initial location and begin listening
1955
- // for future navigations once React has committed the mount. This avoids
1956
- // duplicate entries when React StrictMode runs an extra render pre-commit.
1957
- locationHistory.current.add(routeInfo);
1958
- registerHistoryListener(handleHistoryChange);
1959
- didMountRef.current = true;
1960
- }, []);
1961
- useEffect(() => {
1962
- var _a;
1963
- const activeView = viewStack.current.findViewItemByRouteInfo(routeInfo, undefined, true);
1964
- const matchedParams = (_a = activeView === null || activeView === void 0 ? void 0 : activeView.routeData.match) === null || _a === void 0 ? void 0 : _a.params;
1965
- if (matchedParams) {
1966
- const paramsCopy = filterUndefinedParams(Object.assign({}, matchedParams));
1967
- if (areParamsEqual(routeInfo.params, paramsCopy)) {
1968
- return;
631
+ const routeInfo = this.locationHistory.getCurrentRouteInfoForTab(tab);
632
+ const [pathname, search] = path.split('?');
633
+ if (routeInfo) {
634
+ this.incomingRouteParams = Object.assign(Object.assign({}, routeInfo), { routeAction: 'push', routeDirection: 'none' });
635
+ if (routeInfo.pathname === pathname) {
636
+ this.incomingRouteParams.routeOptions = routeOptions;
637
+ this.props.history.push(routeInfo.pathname + (routeInfo.search || ''));
638
+ }
639
+ else {
640
+ this.incomingRouteParams.pathname = pathname;
641
+ this.incomingRouteParams.search = search ? '?' + search : undefined;
642
+ this.incomingRouteParams.routeOptions = routeOptions;
643
+ this.props.history.push(pathname + (search ? '?' + search : ''));
1969
644
  }
1970
- const updatedRouteInfo = Object.assign(Object.assign({}, routeInfo), { params: paramsCopy });
1971
- locationHistory.current.update(updatedRouteInfo);
1972
- setRouteInfo(updatedRouteInfo);
1973
645
  }
1974
- }, [routeInfo]);
1975
- /**
1976
- * Triggered whenever the history changes, either through user navigation
1977
- * or programmatic changes. It transforms the raw browser history changes
1978
- * into `RouteInfo` objects, which are needed Ionic's animations and
1979
- * navigation patterns.
1980
- *
1981
- * @param location The current location object from the history.
1982
- * @param action The action that triggered the history change.
1983
- */
1984
- const handleHistoryChange = (location, action) => {
1985
- var _a, _b, _c, _d, _e;
646
+ else {
647
+ this.handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
648
+ }
649
+ }
650
+ handleHistoryChange(location, action) {
651
+ var _a, _b, _c;
1986
652
  let leavingLocationInfo;
1987
- /**
1988
- * A programmatic navigation was triggered.
1989
- * e.g., `<Redirect />`, `history.push()`, or `handleNavigate()`
1990
- */
1991
- if (incomingRouteParams.current) {
1992
- /**
1993
- * The current history entry is overwritten, so the previous entry
1994
- * is the one we are leaving.
1995
- */
1996
- if (((_a = incomingRouteParams.current) === null || _a === void 0 ? void 0 : _a.routeAction) === 'replace') {
1997
- leavingLocationInfo = locationHistory.current.previous();
653
+ if (this.incomingRouteParams) {
654
+ if (this.incomingRouteParams.routeAction === 'replace') {
655
+ leavingLocationInfo = this.locationHistory.previous();
1998
656
  }
1999
657
  else {
2000
- // If the action is 'push' or 'pop', we want to use the current route.
2001
- leavingLocationInfo = locationHistory.current.current();
658
+ leavingLocationInfo = this.locationHistory.current();
2002
659
  }
2003
660
  }
2004
661
  else {
2005
- /**
2006
- * An external navigation was triggered
2007
- * e.g., browser back/forward button or direct link
2008
- *
2009
- * The leaving location is the current route.
2010
- */
2011
- leavingLocationInfo = locationHistory.current.current();
662
+ leavingLocationInfo = this.locationHistory.current();
2012
663
  }
2013
664
  const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
2014
665
  if (leavingUrl !== location.pathname) {
2015
- if (!incomingRouteParams.current) {
2016
- // Determine if the destination is a tab route by checking if it matches
2017
- // the pattern of tab routes (containing /tabs/ in the path)
2018
- const isTabRoute = /\/tabs(\/|$)/.test(location.pathname);
2019
- const tabToUse = isTabRoute ? currentTab.current : undefined;
2020
- // If we're leaving tabs entirely, clear the current tab
2021
- if (!isTabRoute && currentTab.current) {
2022
- currentTab.current = undefined;
2023
- }
2024
- /**
2025
- * A `REPLACE` action can be triggered by React Router's
2026
- * `<Redirect />` component.
2027
- */
666
+ if (!this.incomingRouteParams) {
2028
667
  if (action === 'REPLACE') {
2029
- incomingRouteParams.current = {
668
+ this.incomingRouteParams = {
2030
669
  routeAction: 'replace',
2031
670
  routeDirection: 'none',
2032
- tab: tabToUse,
671
+ tab: this.currentTab,
2033
672
  };
2034
673
  }
2035
- /**
2036
- * A `POP` action can be triggered by the browser's back/forward
2037
- * button.
2038
- */
2039
674
  if (action === 'POP') {
2040
- const currentRoute = locationHistory.current.current();
2041
- /**
2042
- * Check if the current route was "pushed" by a previous route
2043
- * (indicates a linear history path).
2044
- */
675
+ const currentRoute = this.locationHistory.current();
2045
676
  if (currentRoute && currentRoute.pushedByRoute) {
2046
- const prevInfo = locationHistory.current.findLastLocation(currentRoute);
2047
- incomingRouteParams.current = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
2048
- // It's a non-linear history path like a direct link.
677
+ const prevInfo = this.locationHistory.findLastLocation(currentRoute);
678
+ this.incomingRouteParams = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
2049
679
  }
2050
680
  else {
2051
- incomingRouteParams.current = {
681
+ this.incomingRouteParams = {
2052
682
  routeAction: 'pop',
2053
683
  routeDirection: 'none',
2054
- tab: tabToUse,
684
+ tab: this.currentTab,
2055
685
  };
2056
686
  }
2057
687
  }
2058
- if (!incomingRouteParams.current) {
2059
- const state = location.state;
2060
- incomingRouteParams.current = {
688
+ if (!this.incomingRouteParams) {
689
+ this.incomingRouteParams = {
2061
690
  routeAction: 'push',
2062
- routeDirection: (state === null || state === void 0 ? void 0 : state.direction) || 'forward',
2063
- routeOptions: state === null || state === void 0 ? void 0 : state.routerOptions,
2064
- tab: tabToUse,
691
+ routeDirection: ((_a = location.state) === null || _a === void 0 ? void 0 : _a.direction) || 'forward',
692
+ routeOptions: (_b = location.state) === null || _b === void 0 ? void 0 : _b.routerOptions,
693
+ tab: this.currentTab,
2065
694
  };
2066
695
  }
2067
696
  }
2068
697
  let routeInfo;
2069
- // If we're navigating away from tabs to a non-tab route, clear the current tab
2070
- if (!/\/tabs(\/|$)/.test(location.pathname) && currentTab.current) {
2071
- currentTab.current = undefined;
2072
- }
2073
- /**
2074
- * An existing id indicates that it's re-activating an existing route.
2075
- * e.g., tab switching or navigating back to a previous route
2076
- */
2077
- if ((_b = incomingRouteParams.current) === null || _b === void 0 ? void 0 : _b.id) {
2078
- routeInfo = Object.assign(Object.assign({}, incomingRouteParams.current), { lastPathname: leavingLocationInfo.pathname });
2079
- locationHistory.current.add(routeInfo);
2080
- /**
2081
- * A new route is being created since it's not re-activating
2082
- * an existing route.
2083
- */
698
+ if ((_c = this.incomingRouteParams) === null || _c === void 0 ? void 0 : _c.id) {
699
+ routeInfo = Object.assign(Object.assign({}, this.incomingRouteParams), { lastPathname: leavingLocationInfo.pathname });
700
+ this.locationHistory.add(routeInfo);
2084
701
  }
2085
702
  else {
2086
- const isPushed = ((_c = incomingRouteParams.current) === null || _c === void 0 ? void 0 : _c.routeAction) === 'push' &&
2087
- incomingRouteParams.current.routeDirection === 'forward';
2088
- routeInfo = Object.assign(Object.assign({ id: generateId('routeInfo') }, incomingRouteParams.current), { lastPathname: leavingLocationInfo.pathname, pathname: location.pathname, search: location.search, params: ((_d = incomingRouteParams.current) === null || _d === void 0 ? void 0 : _d.params)
2089
- ? filterUndefinedParams(incomingRouteParams.current.params)
2090
- : {}, prevRouteLastPathname: leavingLocationInfo.lastPathname });
703
+ const isPushed = this.incomingRouteParams.routeAction === 'push' && this.incomingRouteParams.routeDirection === 'forward';
704
+ routeInfo = Object.assign(Object.assign({ id: generateId('routeInfo') }, this.incomingRouteParams), { lastPathname: leavingLocationInfo.pathname, pathname: location.pathname, search: location.search, params: this.props.match.params, prevRouteLastPathname: leavingLocationInfo.lastPathname });
2091
705
  if (isPushed) {
2092
- // Only inherit tab from leaving route if we don't already have one.
2093
- // This preserves tab context for same-tab navigation while allowing cross-tab navigation.
2094
- routeInfo.tab = routeInfo.tab || leavingLocationInfo.tab;
706
+ routeInfo.tab = leavingLocationInfo.tab;
2095
707
  routeInfo.pushedByRoute = leavingLocationInfo.pathname;
2096
- // Triggered by a browser back button or handleNavigateBack.
2097
708
  }
2098
709
  else if (routeInfo.routeAction === 'pop') {
2099
- // Find the route that pushed this one.
2100
- const r = locationHistory.current.findLastLocation(routeInfo);
710
+ const r = this.locationHistory.findLastLocation(routeInfo);
2101
711
  routeInfo.pushedByRoute = r === null || r === void 0 ? void 0 : r.pushedByRoute;
2102
- // Navigating to a new tab.
2103
712
  }
2104
713
  else if (routeInfo.routeAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
2105
- /**
2106
- * If we are switching tabs grab the last route info for the
2107
- * tab and use its `pushedByRoute`.
2108
- */
2109
- const lastRoute = locationHistory.current.getCurrentRouteInfoForTab(routeInfo.tab);
2110
- // This helps maintain correct back stack behavior within tabs.
2111
- // If this is the first time entering this tab from a different context,
2112
- // use the leaving route's pathname as the pushedByRoute to maintain the back stack.
2113
- routeInfo.pushedByRoute = (_e = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute) !== null && _e !== void 0 ? _e : leavingLocationInfo.pathname;
2114
- // Triggered by `history.replace()` or a `<Redirect />` component, etc.
714
+ // If we are switching tabs grab the last route info for the tab and use its pushedByRoute
715
+ const lastRoute = this.locationHistory.getCurrentRouteInfoForTab(routeInfo.tab);
716
+ routeInfo.pushedByRoute = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute;
2115
717
  }
2116
718
  else if (routeInfo.routeAction === 'replace') {
719
+ // Make sure to set the lastPathname, etc.. to the current route so the page transitions out
720
+ const currentRouteInfo = this.locationHistory.current();
2117
721
  /**
2118
- * Make sure to set the `lastPathname`, etc.. to the current route
2119
- * so the page transitions out.
2120
- */
2121
- const currentRouteInfo = locationHistory.current.current();
2122
- /**
2123
- * Special handling for `replace` to ensure correct `pushedByRoute`
2124
- * and `lastPathname`.
2125
- *
2126
- * If going from `/home` to `/child`, then replacing from
2127
- * `/child` to `/home`, we don't want the route info to
2128
- * say that `/home` was pushed by `/home` which is not correct.
722
+ * If going from /home to /child, then replacing from
723
+ * /child to /home, we don't want the route info to
724
+ * say that /home was pushed by /home which is not correct.
2129
725
  */
2130
726
  const currentPushedBy = currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.pushedByRoute;
2131
727
  const pushedByRoute = currentPushedBy !== undefined && currentPushedBy !== routeInfo.pathname
@@ -2143,107 +739,46 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2143
739
  routeInfo.routeDirection = routeInfo.routeDirection || (currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.routeDirection);
2144
740
  routeInfo.routeAnimation = routeInfo.routeAnimation || (currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.routeAnimation);
2145
741
  }
2146
- locationHistory.current.add(routeInfo);
742
+ this.locationHistory.add(routeInfo);
2147
743
  }
2148
- setRouteInfo(routeInfo);
2149
- }
2150
- incomingRouteParams.current = null;
2151
- };
2152
- /**
2153
- * Resets the specified tab to its initial, root route.
2154
- *
2155
- * @param tab The tab to reset.
2156
- * @param originalHref The original href for the tab.
2157
- * @param originalRouteOptions The original route options for the tab.
2158
- */
2159
- const handleResetTab = (tab, originalHref, originalRouteOptions) => {
2160
- const routeInfo = locationHistory.current.getFirstRouteInfoForTab(tab);
2161
- if (routeInfo) {
2162
- const newRouteInfo = Object.assign({}, routeInfo);
2163
- newRouteInfo.pathname = originalHref;
2164
- newRouteInfo.routeOptions = originalRouteOptions;
2165
- incomingRouteParams.current = Object.assign(Object.assign({}, newRouteInfo), { routeAction: 'pop', routeDirection: 'back' });
2166
- navigate(newRouteInfo.pathname + (newRouteInfo.search || ''));
744
+ this.setState({
745
+ routeInfo,
746
+ });
2167
747
  }
2168
- };
748
+ this.incomingRouteParams = undefined;
749
+ }
2169
750
  /**
2170
- * Handles tab changes.
2171
- *
2172
- * @param tab The tab to switch to.
2173
- * @param path The new path for the tab.
2174
- * @param routeOptions Additional route options.
751
+ * history@4.x uses goBack(), history@5.x uses back()
752
+ * TODO: If support for React Router <=5 is dropped
753
+ * this logic is no longer needed. We can just
754
+ * assume back() is available.
2175
755
  */
2176
- const handleChangeTab = (tab, path, routeOptions) => {
2177
- if (!path) {
2178
- return;
2179
- }
2180
- const routeInfo = locationHistory.current.getCurrentRouteInfoForTab(tab);
2181
- const [pathname, search] = path.split('?');
2182
- // User has navigated to the current tab before.
2183
- if (routeInfo) {
2184
- const routeParams = Object.assign(Object.assign({}, routeInfo), { routeAction: 'push', routeDirection: 'none' });
2185
- /**
2186
- * User is navigating to the same tab.
2187
- * e.g., `/tabs/home` → `/tabs/home`
2188
- */
2189
- if (routeInfo.pathname === pathname) {
2190
- incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { routeOptions });
2191
- navigate(routeInfo.pathname + (routeInfo.search || ''));
2192
- /**
2193
- * User is navigating to a different tab.
2194
- * e.g., `/tabs/home` → `/tabs/settings`
2195
- */
2196
- }
2197
- else {
2198
- incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { pathname, search: search ? '?' + search : undefined, routeOptions });
2199
- navigate(pathname + (search ? '?' + search : ''));
2200
- }
2201
- // User has not navigated to this tab before.
756
+ handleNativeBack() {
757
+ const history = this.props.history;
758
+ const goBack = history.goBack || history.back;
759
+ goBack();
760
+ }
761
+ handleNavigate(path, routeAction, routeDirection, routeAnimation, routeOptions, tab) {
762
+ this.incomingRouteParams = Object.assign(this.incomingRouteParams || {}, {
763
+ routeAction,
764
+ routeDirection,
765
+ routeOptions,
766
+ routeAnimation,
767
+ tab,
768
+ });
769
+ if (routeAction === 'push') {
770
+ this.props.history.push(path);
2202
771
  }
2203
772
  else {
2204
- handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
2205
- }
2206
- };
2207
- /**
2208
- * Set the current active tab in `locationHistory`.
2209
- * This is crucial for maintaining tab history since each tab has
2210
- * its own navigation stack.
2211
- *
2212
- * @param tab The tab to set as active.
2213
- */
2214
- const handleSetCurrentTab = (tab) => {
2215
- currentTab.current = tab;
2216
- const ri = Object.assign({}, locationHistory.current.current());
2217
- if (ri.tab !== tab) {
2218
- ri.tab = tab;
2219
- locationHistory.current.update(ri);
773
+ this.props.history.replace(path);
2220
774
  }
2221
- };
2222
- /**
2223
- * Handles the native back button press.
2224
- * It's usually called when a user presses the platform-native back action.
2225
- */
2226
- const handleNativeBack = () => {
2227
- navigate(-1);
2228
- };
2229
- /**
2230
- * Used to manage the back navigation within the Ionic React's routing
2231
- * system. It's deeply integrated with Ionic's view lifecycle, animations,
2232
- * and its custom history tracking (`locationHistory`) to provide a
2233
- * native-like transition and maintain correct application state.
2234
- *
2235
- * @param defaultHref The fallback URL to navigate to if there's no
2236
- * previous entry in the `locationHistory` stack.
2237
- * @param routeAnimation A custom animation builder to override the
2238
- * default "back" animation.
2239
- */
2240
- const handleNavigateBack = (defaultHref = '/', routeAnimation) => {
775
+ }
776
+ handleNavigateBack(defaultHref = '/', routeAnimation) {
2241
777
  const config = getConfig();
2242
778
  defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref');
2243
- const routeInfo = locationHistory.current.current();
2244
- // It's a linear navigation.
779
+ const routeInfo = this.locationHistory.current();
2245
780
  if (routeInfo && routeInfo.pushedByRoute) {
2246
- const prevInfo = locationHistory.current.findLastLocation(routeInfo);
781
+ const prevInfo = this.locationHistory.findLastLocation(routeInfo);
2247
782
  if (prevInfo) {
2248
783
  /**
2249
784
  * This needs to be passed to handleNavigate
@@ -2251,232 +786,160 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2251
786
  * will be overridden.
2252
787
  */
2253
788
  const incomingAnimation = routeAnimation || routeInfo.routeAnimation;
2254
- incomingRouteParams.current = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back', routeAnimation: incomingAnimation });
2255
- /**
2256
- * Check if it's a simple linear back navigation (not tabbed).
2257
- * e.g., `/home` `/settings` back to `/home`
2258
- */
2259
- const condition1 = routeInfo.lastPathname === routeInfo.pushedByRoute;
2260
- const condition2 = prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '';
2261
- if (condition1 || condition2) {
2262
- navigate(-1);
2263
- }
2264
- else {
789
+ this.incomingRouteParams = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back', routeAnimation: incomingAnimation });
790
+ if (routeInfo.lastPathname === routeInfo.pushedByRoute ||
791
+ /**
792
+ * We need to exclude tab switches/tab
793
+ * context changes here because tabbed
794
+ * navigation is not linear, but router.back()
795
+ * will go back in a linear fashion.
796
+ */
797
+ (prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '')) {
2265
798
  /**
2266
- * It's a non-linear back navigation.
2267
- * e.g., direct link or tab switch or nested navigation with redirects
799
+ * history@4.x uses goBack(), history@5.x uses back()
800
+ * TODO: If support for React Router <=5 is dropped
801
+ * this logic is no longer needed. We can just
802
+ * assume back() is available.
2268
803
  */
2269
- handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
804
+ const history = this.props.history;
805
+ const goBack = history.goBack || history.back;
806
+ goBack();
807
+ }
808
+ else {
809
+ this.handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
2270
810
  }
2271
- /**
2272
- * `pushedByRoute` exists, but no corresponding previous entry in
2273
- * the history stack.
2274
- */
2275
811
  }
2276
812
  else {
2277
- handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
813
+ this.handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
2278
814
  }
2279
- /**
2280
- * No `pushedByRoute`
2281
- * e.g., initial page load
2282
- */
2283
815
  }
2284
816
  else {
2285
- handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
817
+ this.handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
2286
818
  }
2287
- };
2288
- /**
2289
- * Used to programmatically navigate through the app.
2290
- *
2291
- * @param path The path to navigate to.
2292
- * @param routeAction The action to take (push, replace, etc.).
2293
- * @param routeDirection The direction of the navigation (forward,
2294
- * back, etc.).
2295
- * @param routeAnimation The animation to use for the transition.
2296
- * @param routeOptions Additional options for the route.
2297
- * @param tab The tab to navigate to, if applicable.
2298
- */
2299
- const handleNavigate = (path, routeAction, routeDirection, routeAnimation, routeOptions, tab) => {
2300
- var _a;
2301
- const normalizedRouteDirection = routeAction === 'push' && routeDirection === undefined ? 'forward' : routeDirection;
2302
- // When navigating from tabs context, we need to determine if the destination
2303
- // is also within tabs. If not, we should clear the tab context.
2304
- let navigationTab = tab;
2305
- // If no explicit tab is provided and we're in a tab context,
2306
- // check if the destination path is outside of the current tab context
2307
- if (!tab && currentTab.current && path) {
2308
- // Get the current route info to understand where we are
2309
- const currentRoute = locationHistory.current.current();
2310
- // If we're navigating from a tab route to a completely different path structure,
2311
- // we should clear the tab context. This is a simplified check that assumes
2312
- // tab routes share a common parent path.
2313
- if (currentRoute && currentRoute.pathname) {
2314
- // Extract the base tab path (e.g., /routing/tabs from /routing/tabs/home)
2315
- const tabBaseMatch = currentRoute.pathname.match(/^(.*\/tabs)/);
2316
- if (tabBaseMatch) {
2317
- const tabBasePath = tabBaseMatch[1];
2318
- // If the new path doesn't start with the tab base path, we're leaving tabs
2319
- if (!path.startsWith(tabBasePath)) {
2320
- currentTab.current = undefined;
2321
- navigationTab = undefined;
2322
- }
2323
- else {
2324
- // Still within tabs, preserve the tab context
2325
- navigationTab = currentTab.current;
2326
- }
2327
- }
2328
- }
819
+ }
820
+ handleResetTab(tab, originalHref, originalRouteOptions) {
821
+ const routeInfo = this.locationHistory.getFirstRouteInfoForTab(tab);
822
+ if (routeInfo) {
823
+ const newRouteInfo = Object.assign({}, routeInfo);
824
+ newRouteInfo.pathname = originalHref;
825
+ newRouteInfo.routeOptions = originalRouteOptions;
826
+ this.incomingRouteParams = Object.assign(Object.assign({}, newRouteInfo), { routeAction: 'pop', routeDirection: 'back' });
827
+ this.props.history.push(newRouteInfo.pathname + (newRouteInfo.search || ''));
2329
828
  }
2330
- const baseParams = (_a = incomingRouteParams.current) !== null && _a !== void 0 ? _a : {};
2331
- incomingRouteParams.current = Object.assign(Object.assign({}, baseParams), { routeAction, routeDirection: normalizedRouteDirection, routeOptions,
2332
- routeAnimation, tab: navigationTab });
2333
- navigate(path, { replace: routeAction !== 'push' });
2334
- };
2335
- const routeMangerContextValue = {
2336
- canGoBack: () => locationHistory.current.canGoBack(),
2337
- clearOutlet: viewStack.current.clear,
2338
- findViewItemByPathname: viewStack.current.findViewItemByPathname,
2339
- getChildrenToRender: viewStack.current.getChildrenToRender,
2340
- getViewItemsForOutlet: viewStack.current.getViewItemsForOutlet.bind(viewStack.current),
2341
- goBack: () => handleNavigateBack(),
2342
- createViewItem: viewStack.current.createViewItem,
2343
- findViewItemByRouteInfo: viewStack.current.findViewItemByRouteInfo,
2344
- findLeavingViewItemByRouteInfo: viewStack.current.findLeavingViewItemByRouteInfo,
2345
- addViewItem: viewStack.current.add,
2346
- unMountViewItem: viewStack.current.remove,
2347
- };
2348
- return (React.createElement(RouteManagerContext.Provider, { value: routeMangerContextValue },
2349
- React.createElement(NavManager, { ionRoute: IonRouteInner, ionRedirect: {}, stackManager: StackManager, routeInfo: routeInfo, onNativeBack: handleNativeBack, onNavigateBack: handleNavigateBack, onNavigate: handleNavigate, onSetCurrentTab: handleSetCurrentTab, onChangeTab: handleChangeTab, onResetTab: handleResetTab, locationHistory: locationHistory.current }, children)));
2350
- };
829
+ }
830
+ handleSetCurrentTab(tab) {
831
+ this.currentTab = tab;
832
+ const ri = Object.assign({}, this.locationHistory.current());
833
+ if (ri.tab !== tab) {
834
+ ri.tab = tab;
835
+ this.locationHistory.update(ri);
836
+ }
837
+ }
838
+ render() {
839
+ return (React.createElement(RouteManagerContext.Provider, { value: this.routeMangerContextState },
840
+ React.createElement(NavManager, { ionRoute: IonRouteInner, ionRedirect: {}, stackManager: StackManager, routeInfo: this.state.routeInfo, onNativeBack: this.handleNativeBack, onNavigateBack: this.handleNavigateBack, onNavigate: this.handleNavigate, onSetCurrentTab: this.handleSetCurrentTab, onChangeTab: this.handleChangeTab, onResetTab: this.handleResetTab, locationHistory: this.locationHistory }, this.props.children)));
841
+ }
842
+ }
843
+ const IonRouter = withRouter(IonRouterInner);
2351
844
  IonRouter.displayName = 'IonRouter';
2352
845
 
2353
- /**
2354
- * `IonReactRouter` facilitates the integration of Ionic's specific
2355
- * navigation and UI management with the standard React Router mechanisms,
2356
- * allowing an inner Ionic-specific router (`IonRouter`) to react to
2357
- * navigation events.
2358
- */
2359
- /**
2360
- * This component acts as a bridge to ensure React Router hooks like
2361
- * `useLocation` and `useNavigationType` are called within the valid
2362
- * context of a `<BrowserRouter>`.
2363
- *
2364
- * It was split from `IonReactRouter` because these hooks must be
2365
- * descendants of a `<Router>` component, which `BrowserRouter` provides.
2366
- */
2367
- const RouterContent$2 = ({ children }) => {
2368
- const location = useLocation();
2369
- const navigationType = useNavigationType();
2370
- const historyListenHandler = useRef();
2371
- const registerHistoryListener = useCallback((cb) => {
2372
- historyListenHandler.current = cb;
2373
- }, []);
846
+ class IonReactRouter extends React.Component {
847
+ constructor(props) {
848
+ super(props);
849
+ const { history } = props, rest = __rest(props, ["history"]);
850
+ this.history = history || createBrowserHistory(rest);
851
+ this.history.listen(this.handleHistoryChange.bind(this));
852
+ this.registerHistoryListener = this.registerHistoryListener.bind(this);
853
+ }
2374
854
  /**
2375
- * Processes navigation changes within the application.
2376
- *
2377
- * Its purpose is to relay the current `location` and the associated
2378
- * `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
2379
- * primarily for `IonRouter` to manage Ionic-specific UI updates and
2380
- * navigation stack behavior.
2381
- *
2382
- * @param loc The current browser history location object.
2383
- * @param act The type of navigation action ('PUSH', 'POP', or
2384
- * 'REPLACE').
855
+ * history@4.x passes separate location and action
856
+ * params. history@5.x passes location and action
857
+ * together as a single object.
858
+ * TODO: If support for React Router <=5 is dropped
859
+ * this logic is no longer needed. We can just assume
860
+ * a single object with both location and action.
2385
861
  */
2386
- const handleHistoryChange = useCallback((loc, act) => {
2387
- if (historyListenHandler.current) {
2388
- historyListenHandler.current(loc, act);
862
+ handleHistoryChange(location, action) {
863
+ const locationValue = location.location || location;
864
+ const actionValue = location.action || action;
865
+ if (this.historyListenHandler) {
866
+ this.historyListenHandler(locationValue, actionValue);
2389
867
  }
2390
- }, []);
2391
- useEffect(() => {
2392
- handleHistoryChange(location, navigationType);
2393
- }, [location, navigationType, handleHistoryChange]);
2394
- return React.createElement(IonRouter, { registerHistoryListener: registerHistoryListener }, children);
2395
- };
2396
- const IonReactRouter = (_a) => {
2397
- var { children } = _a, browserRouterProps = __rest(_a, ["children"]);
2398
- return (React.createElement(BrowserRouter, Object.assign({}, browserRouterProps),
2399
- React.createElement(RouterContent$2, null, children)));
2400
- };
868
+ }
869
+ registerHistoryListener(cb) {
870
+ this.historyListenHandler = cb;
871
+ }
872
+ render() {
873
+ const _a = this.props, { children } = _a, props = __rest(_a, ["children"]);
874
+ return (React.createElement(Router, Object.assign({ history: this.history }, props),
875
+ React.createElement(IonRouter, { registerHistoryListener: this.registerHistoryListener }, children)));
876
+ }
877
+ }
2401
878
 
2402
- /**
2403
- * `IonReactMemoryRouter` provides a way to use `react-router` in
2404
- * environments where a traditional browser history (like `BrowserRouter`)
2405
- * isn't available or desirable.
2406
- */
2407
- const RouterContent$1 = ({ children }) => {
2408
- const location = useLocation$1();
2409
- const navigationType = useNavigationType$1();
2410
- const historyListenHandler = useRef();
2411
- const registerHistoryListener = (cb) => {
2412
- historyListenHandler.current = cb;
2413
- };
879
+ class IonReactMemoryRouter extends React.Component {
880
+ constructor(props) {
881
+ super(props);
882
+ this.history = props.history;
883
+ this.history.listen(this.handleHistoryChange.bind(this));
884
+ this.registerHistoryListener = this.registerHistoryListener.bind(this);
885
+ }
2414
886
  /**
2415
- * Processes navigation changes within the application.
2416
- *
2417
- * Its purpose is to relay the current `location` and the associated
2418
- * `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
2419
- * primarily for `IonRouter` to manage Ionic-specific UI updates and
2420
- * navigation stack behavior.
2421
- *
2422
- * @param location The current browser history location object.
2423
- * @param action The type of navigation action ('PUSH', 'POP', or
2424
- * 'REPLACE').
887
+ * history@4.x passes separate location and action
888
+ * params. history@5.x passes location and action
889
+ * together as a single object.
890
+ * TODO: If support for React Router <=5 is dropped
891
+ * this logic is no longer needed. We can just assume
892
+ * a single object with both location and action.
2425
893
  */
2426
- const handleHistoryChange = (location, action) => {
2427
- if (historyListenHandler.current) {
2428
- historyListenHandler.current(location, action);
894
+ handleHistoryChange(location, action) {
895
+ const locationValue = location.location || location;
896
+ const actionValue = location.action || action;
897
+ if (this.historyListenHandler) {
898
+ this.historyListenHandler(locationValue, actionValue);
2429
899
  }
2430
- };
2431
- useEffect(() => {
2432
- handleHistoryChange(location, navigationType);
2433
- }, [location, navigationType]);
2434
- return React.createElement(IonRouter, { registerHistoryListener: registerHistoryListener }, children);
2435
- };
2436
- const IonReactMemoryRouter = (_a) => {
2437
- var { children } = _a, routerProps = __rest(_a, ["children"]);
2438
- return (React.createElement(MemoryRouter, Object.assign({}, routerProps),
2439
- React.createElement(RouterContent$1, null, children)));
2440
- };
900
+ }
901
+ registerHistoryListener(cb) {
902
+ this.historyListenHandler = cb;
903
+ }
904
+ render() {
905
+ const _a = this.props, { children } = _a, props = __rest(_a, ["children"]);
906
+ return (React.createElement(Router$1, Object.assign({}, props),
907
+ React.createElement(IonRouter, { registerHistoryListener: this.registerHistoryListener }, children)));
908
+ }
909
+ }
2441
910
 
2442
- /**
2443
- * `IonReactHashRouter` provides a way to use hash-based routing in Ionic
2444
- * React applications.
2445
- */
2446
- const RouterContent = ({ children }) => {
2447
- const location = useLocation();
2448
- const navigationType = useNavigationType();
2449
- const historyListenHandler = useRef();
2450
- const registerHistoryListener = (cb) => {
2451
- historyListenHandler.current = cb;
2452
- };
911
+ class IonReactHashRouter extends React.Component {
912
+ constructor(props) {
913
+ super(props);
914
+ const { history } = props, rest = __rest(props, ["history"]);
915
+ this.history = history || createHashHistory(rest);
916
+ this.history.listen(this.handleHistoryChange.bind(this));
917
+ this.registerHistoryListener = this.registerHistoryListener.bind(this);
918
+ }
2453
919
  /**
2454
- * Processes navigation changes within the application.
2455
- *
2456
- * Its purpose is to relay the current `location` and the associated
2457
- * `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
2458
- * primarily for `IonRouter` to manage Ionic-specific UI updates and
2459
- * navigation stack behavior.
2460
- *
2461
- * @param location The current browser history location object.
2462
- * @param action The type of navigation action ('PUSH', 'POP', or
2463
- * 'REPLACE').
920
+ * history@4.x passes separate location and action
921
+ * params. history@5.x passes location and action
922
+ * together as a single object.
923
+ * TODO: If support for React Router <=5 is dropped
924
+ * this logic is no longer needed. We can just assume
925
+ * a single object with both location and action.
2464
926
  */
2465
- const handleHistoryChange = (location, action) => {
2466
- if (historyListenHandler.current) {
2467
- historyListenHandler.current(location, action);
927
+ handleHistoryChange(location, action) {
928
+ const locationValue = location.location || location;
929
+ const actionValue = location.action || action;
930
+ if (this.historyListenHandler) {
931
+ this.historyListenHandler(locationValue, actionValue);
2468
932
  }
2469
- };
2470
- useEffect(() => {
2471
- handleHistoryChange(location, navigationType);
2472
- }, [location, navigationType]);
2473
- return React.createElement(IonRouter, { registerHistoryListener: registerHistoryListener }, children);
2474
- };
2475
- const IonReactHashRouter = (_a) => {
2476
- var { children } = _a, routerProps = __rest(_a, ["children"]);
2477
- return (React.createElement(HashRouter, Object.assign({}, routerProps),
2478
- React.createElement(RouterContent, null, children)));
2479
- };
933
+ }
934
+ registerHistoryListener(cb) {
935
+ this.historyListenHandler = cb;
936
+ }
937
+ render() {
938
+ const _a = this.props, { children } = _a, props = __rest(_a, ["children"]);
939
+ return (React.createElement(Router, Object.assign({ history: this.history }, props),
940
+ React.createElement(IonRouter, { registerHistoryListener: this.registerHistoryListener }, children)));
941
+ }
942
+ }
2480
943
 
2481
944
  export { IonReactHashRouter, IonReactMemoryRouter, IonReactRouter };
2482
945
  //# sourceMappingURL=index.js.map