@ionic/react-router 8.8.1-dev.11773168858.1f9c0eb8 → 8.8.1-dev.11773242897.1966d6b2

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,1130 +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';
2
+ import { createBrowserHistory, createHashHistory } from 'history';
3
+ import React from 'react';
4
+ import { withRouter, Router } from 'react-router-dom';
4
5
  import { ViewStacks, generateId, IonRoute, ViewLifeCycleManager, StackContext, RouteManagerContext, getConfig, LocationHistory, NavManager } from '@ionic/react';
5
- import { MemoryRouter, useLocation as useLocation$1, useNavigationType as useNavigationType$1 } from 'react-router';
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
- var _a, _b;
17
- const { path, index } = componentProps, restProps = __rest(componentProps, ["path", "index"]);
18
- // Handle index routes - they match when pathname is empty or just "/"
19
- if (index && !path) {
20
- if (pathname === '' || pathname === '/') {
21
- return {
22
- params: {},
23
- pathname: pathname,
24
- pathnameBase: pathname || '/',
25
- pattern: {
26
- path: '',
27
- caseSensitive: false,
28
- end: true,
29
- },
30
- };
31
- }
32
- return null;
33
- }
34
- // Handle empty path routes - they match when pathname is also empty or just "/"
35
- if (path === '' || path === undefined) {
36
- if (pathname === '' || pathname === '/') {
37
- return {
38
- params: {},
39
- pathname: pathname,
40
- pathnameBase: pathname || '/',
41
- pattern: {
42
- path: '',
43
- caseSensitive: (_a = restProps.caseSensitive) !== null && _a !== void 0 ? _a : false,
44
- end: (_b = restProps.end) !== null && _b !== void 0 ? _b : true,
45
- },
46
- };
47
- }
48
- return null;
49
- }
50
- // For relative paths (don't start with '/'), normalize both path and pathname for matching
51
- if (!path.startsWith('/')) {
52
- const matchOptions = Object.assign({ path: `/${path}` }, restProps);
53
- if ((matchOptions === null || matchOptions === void 0 ? void 0 : matchOptions.end) === undefined) {
54
- matchOptions.end = !path.endsWith('*');
55
- }
56
- const normalizedPathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
57
- const match = matchPath$1(matchOptions, normalizedPathname);
58
- if (match) {
59
- // Adjust the match to remove the leading '/' we added
60
- return Object.assign(Object.assign({}, match), { pathname: pathname, pathnameBase: match.pathnameBase === '/' ? '' : match.pathnameBase.slice(1), pattern: Object.assign(Object.assign({}, match.pattern), { path: path }) });
61
- }
62
- return null;
63
- }
64
- // For absolute paths, use React Router's matcher directly.
65
- // React Router v6 routes default to `end: true` unless the pattern
66
- // explicitly opts into wildcards with `*`. Mirror that behaviour so
67
- // matching parity stays aligned with <Route>.
68
- const matchOptions = Object.assign({ path }, restProps);
69
- if ((matchOptions === null || matchOptions === void 0 ? void 0 : matchOptions.end) === undefined) {
70
- matchOptions.end = !path.endsWith('*');
71
- }
72
- return matchPath$1(matchOptions, pathname);
73
- };
74
- /**
75
- * Determines the portion of a pathname that a given route pattern should match against.
76
- * For absolute route patterns we return the full pathname. For relative patterns we
77
- * strip off the already-matched parent segments so React Router receives the remainder.
78
- */
79
- const derivePathnameToMatch = (fullPathname, routePath) => {
80
- var _a;
81
- // For absolute or empty routes, use the full pathname as-is
82
- if (!routePath || routePath === '' || routePath.startsWith('/')) {
83
- return fullPathname;
84
- }
85
- const trimmedPath = fullPathname.startsWith('/') ? fullPathname.slice(1) : fullPathname;
86
- if (!trimmedPath) {
87
- // For root-level relative routes (pathname is "/" and routePath is relative),
88
- // return the full pathname so matchPath can normalize both.
89
- // This allows routes like <Route path="foo/*" .../> at root level to work correctly.
90
- return fullPathname;
91
- }
92
- const fullSegments = trimmedPath.split('/').filter(Boolean);
93
- if (fullSegments.length === 0) {
94
- return '';
95
- }
96
- const routeSegments = routePath.split('/').filter(Boolean);
97
- if (routeSegments.length === 0) {
98
- return trimmedPath;
99
- }
100
- const wildcardIndex = routeSegments.findIndex((segment) => segment === '*' || segment === '**');
101
- if (wildcardIndex >= 0) {
102
- const baseSegments = routeSegments.slice(0, wildcardIndex);
103
- if (baseSegments.length === 0) {
104
- return trimmedPath;
105
- }
106
- const startIndex = fullSegments.findIndex((_, idx) => baseSegments.every((seg, segIdx) => {
107
- const target = fullSegments[idx + segIdx];
108
- if (!target) {
109
- return false;
110
- }
111
- if (seg.startsWith(':')) {
112
- 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,
113
13
  }
114
- return target === seg;
115
- }));
116
- if (startIndex >= 0) {
117
- return fullSegments.slice(startIndex).join('/');
118
- }
119
- }
120
- if (routeSegments.length <= fullSegments.length) {
121
- return fullSegments.slice(fullSegments.length - routeSegments.length).join('/');
14
+ : {}))));
122
15
  }
123
- return (_a = fullSegments[fullSegments.length - 1]) !== null && _a !== void 0 ? _a : trimmedPath;
124
- };
16
+ }
125
17
 
126
18
  /**
127
- * Finds the longest common prefix among an array of paths.
128
- * Used to determine the scope of an outlet with absolute routes.
129
- *
130
- * @param paths An array of absolute path strings.
131
- * @returns The common prefix shared by all paths.
132
- */
133
- const computeCommonPrefix = (paths) => {
134
- if (paths.length === 0)
135
- return '';
136
- if (paths.length === 1) {
137
- // For a single path, extract the directory-like prefix
138
- // e.g., /dynamic-routes/home -> /dynamic-routes
139
- const segments = paths[0].split('/').filter(Boolean);
140
- if (segments.length > 1) {
141
- return '/' + segments.slice(0, -1).join('/');
142
- }
143
- return '/' + segments[0];
144
- }
145
- // Split all paths into segments
146
- const segmentArrays = paths.map((p) => p.split('/').filter(Boolean));
147
- const minLength = Math.min(...segmentArrays.map((s) => s.length));
148
- const commonSegments = [];
149
- for (let i = 0; i < minLength; i++) {
150
- const segment = segmentArrays[0][i];
151
- // Skip segments with route parameters or wildcards
152
- if (segment.includes(':') || segment.includes('*')) {
153
- break;
154
- }
155
- const allMatch = segmentArrays.every((s) => s[i] === segment);
156
- if (allMatch) {
157
- commonSegments.push(segment);
158
- }
159
- else {
160
- break;
161
- }
162
- }
163
- return commonSegments.length > 0 ? '/' + commonSegments.join('/') : '';
164
- };
165
- /**
166
- * Checks if a route path is a "splat-only" route (just `*` or `/*`).
167
- */
168
- const isSplatOnlyRoute = (routePath) => {
169
- return routePath === '*' || routePath === '/*';
170
- };
171
- /**
172
- * Checks if a route has an embedded wildcard (e.g., "tab1/*" but not "*" or "/*").
19
+ * @see https://v5.reactrouter.com/web/api/matchPath
173
20
  */
174
- const hasEmbeddedWildcard = (routePath) => {
175
- return !!routePath && routePath.includes('*') && !isSplatOnlyRoute(routePath);
176
- };
177
- /**
178
- * Checks if a route with an embedded wildcard matches a pathname.
179
- */
180
- const matchesEmbeddedWildcardRoute = (route, pathname) => {
181
- const routePath = route.props.path;
182
- if (!hasEmbeddedWildcard(routePath)) {
183
- return false;
184
- }
185
- return !!matchPath({ pathname, componentProps: route.props });
186
- };
187
- /**
188
- * Checks if a route is a specific match (not wildcard-only or index).
189
- */
190
- const isSpecificRouteMatch = (route, remainingPath) => {
191
- const routePath = route.props.path;
192
- if (route.props.index || isSplatOnlyRoute(routePath)) {
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) {
193
36
  return false;
194
37
  }
195
- return !!matchPath({ pathname: remainingPath, componentProps: route.props });
196
- };
197
- /**
198
- * Analyzes route children to determine their characteristics.
199
- *
200
- * @param routeChildren The route children to analyze.
201
- * @returns Analysis of the route characteristics.
202
- */
203
- const analyzeRouteChildren = (routeChildren) => {
204
- const hasRelativeRoutes = routeChildren.some((route) => {
205
- const path = route.props.path;
206
- return path && !path.startsWith('/') && path !== '*';
207
- });
208
- const hasIndexRoute = routeChildren.some((route) => route.props.index);
209
- const hasWildcardRoute = routeChildren.some((route) => {
210
- const routePath = route.props.path;
211
- return routePath === '*' || routePath === '/*';
212
- });
213
- return { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute, routeChildren };
214
- };
215
- /**
216
- * Checks if any route matches as a specific (non-wildcard, non-index) route.
217
- */
218
- const findSpecificMatch = (routeChildren, remainingPath) => {
219
- return routeChildren.some((route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath));
220
- };
221
- /**
222
- * Checks if any specific route could plausibly match the remaining path.
223
- * Used to determine if we should fall back to a wildcard match.
224
- */
225
- const couldSpecificRouteMatch = (routeChildren, remainingPath) => {
226
- const remainingFirstSegment = remainingPath.split('/')[0];
227
- return routeChildren.some((route) => {
228
- const routePath = route.props.path;
229
- if (!routePath || routePath === '*' || routePath === '/*')
230
- return false;
231
- if (route.props.index)
232
- return false;
233
- const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, '');
234
- if (!routeFirstSegment)
235
- return false;
236
- // Check for prefix overlap (either direction)
237
- return (routeFirstSegment.startsWith(remainingFirstSegment.slice(0, 3)) ||
238
- remainingFirstSegment.startsWith(routeFirstSegment.slice(0, 3)));
239
- });
240
- };
241
- /**
242
- * Checks for index route match when remaining path is empty.
243
- * Index routes only match at the outlet's mount path level.
244
- */
245
- const checkIndexMatch = (parentPath, remainingPath, hasIndexRoute, outletMountPath) => {
246
- if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) {
247
- if (outletMountPath) {
248
- // Index should only match at the existing mount path
249
- return parentPath === outletMountPath ? parentPath : undefined;
250
- }
251
- // No mount path yet - this would establish it
252
- return parentPath;
253
- }
254
- return undefined;
255
- };
256
- /**
257
- * Determines the best parent path from the available matches.
258
- * Priority: specific > wildcard > index
259
- */
260
- const selectBestMatch = (specificMatch, wildcardMatch, indexMatch) => {
261
- var _a;
262
- return (_a = specificMatch !== null && specificMatch !== void 0 ? specificMatch : wildcardMatch) !== null && _a !== void 0 ? _a : indexMatch;
263
- };
264
- /**
265
- * Handles outlets with only absolute routes by computing their common prefix.
266
- */
267
- const computeAbsoluteRoutesParentPath = (routeChildren, currentPathname, outletMountPath) => {
268
- const absolutePathRoutes = routeChildren.filter((route) => {
269
- const path = route.props.path;
270
- return path && path.startsWith('/');
271
- });
272
- if (absolutePathRoutes.length === 0) {
273
- return undefined;
274
- }
275
- const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
276
- const commonPrefix = computeCommonPrefix(absolutePaths);
277
- if (!commonPrefix || commonPrefix === '/') {
278
- return undefined;
279
- }
280
- const newOutletMountPath = outletMountPath || commonPrefix;
281
- if (!currentPathname.startsWith(commonPrefix)) {
282
- return { parentPath: undefined, outletMountPath: newOutletMountPath };
283
- }
284
- return { parentPath: commonPrefix, outletMountPath: newOutletMountPath };
285
- };
286
- /**
287
- * Computes the parent path for a nested outlet based on the current pathname
288
- * and the outlet's route configuration.
289
- *
290
- * The algorithm finds the shortest parent path where a route matches the remaining path.
291
- * Priority: specific routes > wildcard routes > index routes (only at mount point)
292
- *
293
- * @param options The options for computing the parent path.
294
- * @returns The computed parent path result.
295
- */
296
- const computeParentPath = (options) => {
297
- const { currentPathname, outletMountPath, routeChildren, hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = options;
298
- // If pathname is outside the established mount path scope, skip computation
299
- if (outletMountPath && !currentPathname.startsWith(outletMountPath)) {
300
- return { parentPath: undefined, outletMountPath };
301
- }
302
- if ((hasRelativeRoutes || hasIndexRoute) && currentPathname.includes('/')) {
303
- const segments = currentPathname.split('/').filter(Boolean);
304
- if (segments.length >= 1) {
305
- let firstSpecificMatch;
306
- let firstWildcardMatch;
307
- let indexMatchAtMount;
308
- // Iterate through path segments to find the shortest matching parent path
309
- for (let i = 1; i <= segments.length; i++) {
310
- const parentPath = '/' + segments.slice(0, i).join('/');
311
- const remainingPath = segments.slice(i).join('/');
312
- // Check for specific route match (highest priority)
313
- if (!firstSpecificMatch && findSpecificMatch(routeChildren, remainingPath)) {
314
- firstSpecificMatch = parentPath;
315
- break;
316
- }
317
- // Check for wildcard match (only if remaining path is non-empty)
318
- const hasNonEmptyRemaining = remainingPath !== '' && remainingPath !== '/';
319
- if (!firstWildcardMatch && hasNonEmptyRemaining && hasWildcardRoute) {
320
- if (!couldSpecificRouteMatch(routeChildren, remainingPath)) {
321
- firstWildcardMatch = parentPath;
322
- }
323
- }
324
- // Check for index route match
325
- const indexMatch = checkIndexMatch(parentPath, remainingPath, hasIndexRoute, outletMountPath);
326
- if (indexMatch) {
327
- indexMatchAtMount = indexMatch;
328
- }
329
- }
330
- // Fallback: check root level for embedded wildcard routes (e.g., "tab1/*")
331
- if (!firstSpecificMatch) {
332
- const fullRemainingPath = segments.join('/');
333
- if (routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath))) {
334
- firstSpecificMatch = '/';
335
- }
336
- }
337
- const bestPath = selectBestMatch(firstSpecificMatch, firstWildcardMatch, indexMatchAtMount);
338
- // Establish mount path on first successful match
339
- const newOutletMountPath = outletMountPath || bestPath;
340
- if (newOutletMountPath && !currentPathname.startsWith(newOutletMountPath)) {
341
- return { parentPath: undefined, outletMountPath: newOutletMountPath };
342
- }
343
- return { parentPath: bestPath, outletMountPath: newOutletMountPath };
344
- }
345
- }
346
- // Handle outlets with only absolute routes
347
- if (!hasRelativeRoutes && !hasIndexRoute) {
348
- const result = computeAbsoluteRoutesParentPath(routeChildren, currentPathname, outletMountPath);
349
- if (result) {
350
- return result;
351
- }
352
- }
353
- return { parentPath: outletMountPath, outletMountPath };
354
- };
355
-
356
- /**
357
- * Ensures the given path has a leading slash.
358
- *
359
- * @param value The path string to normalize.
360
- * @returns The path with a leading slash.
361
- */
362
- const ensureLeadingSlash = (value) => {
363
- if (value === '') {
364
- return '/';
365
- }
366
- return value.startsWith('/') ? value : `/${value}`;
367
- };
368
- /**
369
- * Strips the trailing slash from a path, unless it's the root path.
370
- *
371
- * @param value The path string to normalize.
372
- * @returns The path without a trailing slash.
373
- */
374
- const stripTrailingSlash = (value) => {
375
- return value.length > 1 && value.endsWith('/') ? value.slice(0, -1) : value;
376
- };
377
- /**
378
- * Normalizes a pathname for comparison by ensuring a leading slash
379
- * and removing trailing slashes.
380
- *
381
- * @param value The pathname to normalize, can be undefined.
382
- * @returns A normalized pathname string.
383
- */
384
- const normalizePathnameForComparison = (value) => {
385
- if (!value || value === '') {
386
- return '/';
387
- }
388
- const withLeadingSlash = ensureLeadingSlash(value);
389
- return stripTrailingSlash(withLeadingSlash);
390
- };
391
-
392
- /**
393
- * Extracts the children from a Routes wrapper component.
394
- * The use of `<Routes />` is encouraged with React Router v6.
395
- *
396
- * @param node The React node to extract Routes children from.
397
- * @returns The children of the Routes component, or undefined if not found.
398
- */
399
- const getRoutesChildren = (node) => {
400
- let routesNode;
401
- React.Children.forEach(node, (child) => {
402
- if (child.type === Routes) {
403
- routesNode = child;
404
- }
405
- });
406
- if (routesNode) {
407
- // The children of the `<Routes />` component are most likely
408
- // (and should be) the `<Route />` components.
409
- return routesNode.props.children;
410
- }
411
- return undefined;
412
- };
413
- /**
414
- * Extracts Route children from a node (either directly or from a Routes wrapper).
415
- *
416
- * @param children The children to extract routes from.
417
- * @returns An array of Route elements.
418
- */
419
- const extractRouteChildren = (children) => {
420
- var _a;
421
- const routesChildren = (_a = getRoutesChildren(children)) !== null && _a !== void 0 ? _a : children;
422
- return React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
423
- };
424
- /**
425
- * Checks if a React element is a Navigate component (redirect).
426
- *
427
- * @param element The element to check.
428
- * @returns True if the element is a Navigate component.
429
- */
430
- const isNavigateElement = (element) => {
431
- return (React.isValidElement(element) &&
432
- (element.type === Navigate || (typeof element.type === 'function' && element.type.name === 'Navigate')));
433
- };
434
-
435
- /**
436
- * Sorts view items by route specificity (most specific first).
437
- * - Exact matches (no wildcards/params) come first
438
- * - Among wildcard routes, longer paths are more specific
439
- *
440
- * @param views The view items to sort.
441
- * @returns A new sorted array of view items.
442
- */
443
- const sortViewsBySpecificity = (views) => {
444
- return [...views].sort((a, b) => {
445
- var _a, _b, _c, _d;
446
- const pathA = ((_b = (_a = a.routeData) === null || _a === void 0 ? void 0 : _a.childProps) === null || _b === void 0 ? void 0 : _b.path) || '';
447
- const pathB = ((_d = (_c = b.routeData) === null || _c === void 0 ? void 0 : _c.childProps) === null || _d === void 0 ? void 0 : _d.path) || '';
448
- // Exact matches (no wildcards/params) come first
449
- const aHasWildcard = pathA.includes('*') || pathA.includes(':');
450
- const bHasWildcard = pathB.includes('*') || pathB.includes(':');
451
- if (!aHasWildcard && bHasWildcard)
452
- return -1;
453
- if (aHasWildcard && !bHasWildcard)
454
- return 1;
455
- // Among wildcard routes, longer paths are more specific
456
- return pathB.length - pathA.length;
457
- });
38
+ return match;
458
39
  };
459
40
 
460
- /**
461
- * `ReactRouterViewStack` is a custom navigation manager used in Ionic React
462
- * apps to map React Router route elements (such as `<IonRoute>`) to "view
463
- * items" that Ionic can manage in a view stack. This is critical to maintain
464
- * Ionic’s animation, lifecycle, and history behavior across views.
465
- */
466
- /**
467
- * Delay in milliseconds before removing a Navigate view item after a redirect.
468
- * This ensures the redirect navigation completes before the view is removed.
469
- */
470
- const NAVIGATE_REDIRECT_DELAY_MS = 100;
471
- /**
472
- * Delay in milliseconds before cleaning up a view without an IonPage element.
473
- * This double-checks that the view is truly not needed before removal.
474
- */
475
- const VIEW_CLEANUP_DELAY_MS = 200;
476
- /**
477
- * Computes the absolute pathnameBase for a route element based on its type.
478
- * Handles relative paths, index routes, and splat routes differently.
479
- */
480
- const computeAbsolutePathnameBase = (routeElement, routeMatch, parentPathnameBase, routeInfoPathname) => {
481
- const routePath = routeElement.props.path;
482
- const isRelativePath = routePath && !routePath.startsWith('/');
483
- const isIndexRoute = !!routeElement.props.index;
484
- const isSplatOnlyRoute = routePath === '*' || routePath === '/*';
485
- if (isSplatOnlyRoute) {
486
- // Splat routes should NOT contribute their matched portion to pathnameBase
487
- // This aligns with React Router v7's v7_relativeSplatPath behavior
488
- return parentPathnameBase;
489
- }
490
- if (isRelativePath && (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase)) {
491
- const relativeBase = routeMatch.pathnameBase.startsWith('/')
492
- ? routeMatch.pathnameBase.slice(1)
493
- : routeMatch.pathnameBase;
494
- return parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
495
- }
496
- if (isIndexRoute) {
497
- return parentPathnameBase;
498
- }
499
- return (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase) || routeInfoPathname;
500
- };
501
- /**
502
- * Gets fallback params from view items in other outlets when parent context is empty.
503
- * This handles cases where React context propagation doesn't work as expected.
504
- */
505
- const getFallbackParamsFromViewItems = (allViewItems, currentOutletId, currentPathname) => {
506
- var _a;
507
- const params = {};
508
- for (const otherViewItem of allViewItems) {
509
- if (otherViewItem.outletId === currentOutletId)
510
- continue;
511
- const otherMatch = (_a = otherViewItem.routeData) === null || _a === void 0 ? void 0 : _a.match;
512
- if ((otherMatch === null || otherMatch === void 0 ? void 0 : otherMatch.params) && Object.keys(otherMatch.params).length > 0) {
513
- const matchedPathname = otherMatch.pathnameBase || otherMatch.pathname;
514
- if (matchedPathname && currentPathname.startsWith(matchedPathname)) {
515
- Object.assign(params, otherMatch.params);
516
- }
517
- }
518
- }
519
- return params;
520
- };
521
- /**
522
- * Builds the matches array for RouteContext.
523
- */
524
- const buildContextMatches = (parentMatches, combinedParams, routeMatch, routeInfoPathname, absolutePathnameBase, viewItem, routeElement, componentElement) => {
525
- return [
526
- ...parentMatches,
527
- {
528
- params: combinedParams,
529
- pathname: (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathname) || routeInfoPathname,
530
- pathnameBase: absolutePathnameBase,
531
- route: {
532
- id: viewItem.id,
533
- path: routeElement.props.path,
534
- element: componentElement,
535
- index: !!routeElement.props.index,
536
- caseSensitive: routeElement.props.caseSensitive,
537
- hasErrorBoundary: false,
538
- },
539
- },
540
- ];
541
- };
542
- const createDefaultMatch = (fullPathname, routeProps) => {
543
- var _a, _b;
544
- const isIndexRoute = !!routeProps.index;
545
- const patternPath = (_a = routeProps.path) !== null && _a !== void 0 ? _a : '';
546
- const pathnameBase = fullPathname === '' ? '/' : fullPathname;
547
- const computedEnd = routeProps.end !== undefined ? routeProps.end : patternPath !== '' ? !patternPath.endsWith('*') : true;
548
- return {
549
- params: {},
550
- pathname: isIndexRoute ? '' : fullPathname,
551
- pathnameBase,
552
- pattern: {
553
- path: patternPath,
554
- caseSensitive: (_b = routeProps.caseSensitive) !== null && _b !== void 0 ? _b : false,
555
- end: isIndexRoute ? true : computedEnd,
556
- },
557
- };
558
- };
559
- const computeRelativeToParent = (pathname, parentPath) => {
560
- if (!parentPath)
561
- return null;
562
- const normalizedParent = normalizePathnameForComparison(parentPath);
563
- const normalizedPathname = normalizePathnameForComparison(pathname);
564
- if (normalizedPathname === normalizedParent) {
565
- return '';
566
- }
567
- const withSlash = normalizedParent === '/' ? '/' : normalizedParent + '/';
568
- if (normalizedPathname.startsWith(withSlash)) {
569
- return normalizedPathname.slice(withSlash.length);
570
- }
571
- return null;
572
- };
573
- const resolveIndexRouteMatch = (viewItem, pathname, parentPath) => {
574
- var _a, _b, _c;
575
- if (!((_b = (_a = viewItem.routeData) === null || _a === void 0 ? void 0 : _a.childProps) === null || _b === void 0 ? void 0 : _b.index)) {
576
- return null;
577
- }
578
- // Prefer computing against the parent path when available to align with RRv6 semantics
579
- const relative = computeRelativeToParent(pathname, parentPath);
580
- if (relative !== null) {
581
- // Index routes match only when there is no remaining path
582
- if (relative === '' || relative === '/') {
583
- return createDefaultMatch(parentPath || pathname, viewItem.routeData.childProps);
584
- }
585
- return null;
586
- }
587
- // Fallback: use previously computed match base for equality check
588
- const previousMatch = (_c = viewItem.routeData) === null || _c === void 0 ? void 0 : _c.match;
589
- if (!previousMatch) {
590
- return null;
591
- }
592
- const normalizedPathname = normalizePathnameForComparison(pathname);
593
- const normalizedBase = normalizePathnameForComparison(previousMatch.pathnameBase || previousMatch.pathname || '');
594
- return normalizedPathname === normalizedBase ? previousMatch : null;
595
- };
596
41
  class ReactRouterViewStack extends ViewStacks {
597
42
  constructor() {
598
43
  super();
599
- /**
600
- * Creates a new view item for the given outlet and react route element.
601
- * Associates route props with the matched route path for further lookups.
602
- */
603
- this.createViewItem = (outletId, reactElement, routeInfo, page) => {
604
- var _a, _b;
605
- const routePath = reactElement.props.path || '';
606
- // Check if we already have a view item for this exact route that we can reuse
607
- // Include wildcard routes like tabs/* since they should be reused
608
- // Also check unmounted items that might have been preserved for browser navigation
609
- const existingViewItem = this.getViewItemsForOutlet(outletId).find((v) => {
610
- var _a, _b, _c, _d, _e, _f, _g, _h;
611
- const existingRouteProps = (_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) !== null && _b !== void 0 ? _b : {};
612
- const existingPath = existingRouteProps.path || '';
613
- const existingElement = existingRouteProps.element;
614
- const newElement = reactElement.props.element;
615
- const existingIsIndexRoute = !!existingRouteProps.index;
616
- const newIsIndexRoute = !!reactElement.props.index;
617
- // For Navigate components, match by destination
618
- const existingIsNavigate = React.isValidElement(existingElement) && existingElement.type === Navigate;
619
- const newIsNavigate = React.isValidElement(newElement) && newElement.type === Navigate;
620
- if (existingIsNavigate && newIsNavigate) {
621
- const existingTo = (_c = existingElement.props) === null || _c === void 0 ? void 0 : _c.to;
622
- const newTo = (_d = newElement.props) === null || _d === void 0 ? void 0 : _d.to;
623
- if (existingTo === newTo) {
624
- return true;
625
- }
626
- }
627
- if (existingIsIndexRoute && newIsIndexRoute) {
628
- return true;
629
- }
630
- // Reuse view items with the same path
631
- // Special case: reuse tabs/* and other specific wildcard routes
632
- // Don't reuse index routes (empty path) or generic catch-all wildcards (*)
633
- if (existingPath === routePath && existingPath !== '' && existingPath !== '*') {
634
- // Parameterized routes need pathname matching to ensure /details/1 and /details/2
635
- // get separate view items. For wildcard routes (e.g., user/:userId/*), compare
636
- // pathnameBase to allow child path changes while preserving the parent view.
637
- const hasParams = routePath.includes(':');
638
- const isWildcard = routePath.includes('*');
639
- if (hasParams) {
640
- if (isWildcard) {
641
- const existingPathnameBase = (_f = (_e = v.routeData) === null || _e === void 0 ? void 0 : _e.match) === null || _f === void 0 ? void 0 : _f.pathnameBase;
642
- const newMatch = matchComponent$1(reactElement, routeInfo.pathname, false);
643
- const newPathnameBase = newMatch === null || newMatch === void 0 ? void 0 : newMatch.pathnameBase;
644
- if (existingPathnameBase !== newPathnameBase) {
645
- return false;
646
- }
647
- }
648
- else {
649
- const existingPathname = (_h = (_g = v.routeData) === null || _g === void 0 ? void 0 : _g.match) === null || _h === void 0 ? void 0 : _h.pathname;
650
- if (existingPathname !== routeInfo.pathname) {
651
- return false;
652
- }
653
- }
654
- }
655
- return true;
656
- }
657
- // Also reuse specific wildcard routes like tabs/*
658
- if (existingPath === routePath && existingPath.endsWith('/*') && existingPath !== '/*') {
659
- return true;
660
- }
661
- return false;
662
- });
663
- if (existingViewItem) {
664
- // Update and ensure the existing view item is properly configured
665
- existingViewItem.reactElement = reactElement;
666
- existingViewItem.mount = true;
667
- existingViewItem.ionPageElement = page || existingViewItem.ionPageElement;
668
- const updatedMatch = matchComponent$1(reactElement, routeInfo.pathname, false) ||
669
- ((_a = existingViewItem.routeData) === null || _a === void 0 ? void 0 : _a.match) ||
670
- createDefaultMatch(routeInfo.pathname, reactElement.props);
671
- existingViewItem.routeData = {
672
- match: updatedMatch,
673
- childProps: reactElement.props,
674
- lastPathname: (_b = existingViewItem.routeData) === null || _b === void 0 ? void 0 : _b.lastPathname, // Preserve navigation history
675
- };
676
- return existingViewItem;
677
- }
678
- const id = `${outletId}-${generateId(outletId)}`;
679
- const viewItem = {
680
- id,
681
- outletId,
682
- ionPageElement: page,
683
- reactElement,
684
- mount: true,
685
- ionRoute: true,
686
- };
687
- if (reactElement.type === IonRoute) {
688
- viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
689
- }
690
- const initialMatch = matchComponent$1(reactElement, routeInfo.pathname, true) ||
691
- createDefaultMatch(routeInfo.pathname, reactElement.props);
692
- viewItem.routeData = {
693
- match: initialMatch,
694
- childProps: reactElement.props,
695
- };
696
- this.add(viewItem);
697
- 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,
698
58
  };
699
- /**
700
- * Renders a ViewLifeCycleManager for the given view item.
701
- * Handles cleanup if the view no longer matches.
702
- *
703
- * - Deactivates view if it no longer matches the current route
704
- * - Wraps the route element in <Routes> to support nested routing and ensure remounting
705
- * - Adds a unique key to <Routes> so React Router remounts routes when switching
706
- */
707
- this.renderViewItem = (viewItem, routeInfo, parentPath) => {
708
- var _a, _b, _c, _d, _e, _f, _g, _h;
709
- const routePath = viewItem.reactElement.props.path || '';
710
- let match = matchComponent$1(viewItem.reactElement, routeInfo.pathname);
711
- if (!match) {
712
- const indexMatch = resolveIndexRouteMatch(viewItem, routeInfo.pathname, parentPath);
713
- if (indexMatch) {
714
- match = indexMatch;
715
- }
716
- }
717
- // For parameterized routes, check if this is a navigation to a different path instance
718
- // In that case, we should NOT reuse this view - a new view should be created
719
- const isParameterRoute = routePath.includes(':');
720
- const previousMatch = (_a = viewItem.routeData) === null || _a === void 0 ? void 0 : _a.match;
721
- const isSamePath = (match === null || match === void 0 ? void 0 : match.pathname) === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathname);
722
- // Flag to indicate this view should not be reused for this different parameterized path
723
- const shouldSkipForDifferentParam = isParameterRoute && match && previousMatch && !isSamePath;
724
- // Don't deactivate views automatically - let the StackManager handle view lifecycle
725
- // This preserves views in the stack for navigation history like native apps
726
- // Views will be hidden/shown by the StackManager's transition logic instead of being unmounted
727
- // Special handling for Navigate components - they should unmount after redirecting
728
- const elementComponent = (_c = (_b = viewItem.reactElement) === null || _b === void 0 ? void 0 : _b.props) === null || _c === void 0 ? void 0 : _c.element;
729
- const isNavigateComponent = isNavigateElement(elementComponent);
730
- if (isNavigateComponent) {
731
- // Navigate components should only be mounted when they match
732
- // Once they redirect (no longer match), they should be removed completely
733
- // IMPORTANT: For index routes, we need to check indexMatch too since matchComponent
734
- // may not properly match index routes without explicit parent path context
735
- const indexMatch = ((_e = (_d = viewItem.routeData) === null || _d === void 0 ? void 0 : _d.childProps) === null || _e === void 0 ? void 0 : _e.index)
736
- ? resolveIndexRouteMatch(viewItem, routeInfo.pathname, parentPath)
737
- : null;
738
- const hasValidMatch = match || indexMatch;
739
- if (!hasValidMatch && viewItem.mount) {
740
- viewItem.mount = false;
741
- // Schedule removal of the Navigate view item after a short delay
742
- // This ensures the redirect completes before removal
743
- setTimeout(() => {
744
- this.remove(viewItem);
745
- }, NAVIGATE_REDIRECT_DELAY_MS);
746
- }
747
- }
748
- // Components that don't have IonPage elements and no longer match should be cleaned up
749
- // BUT we need to be careful not to remove them if they're part of browser navigation history
750
- // This handles components that perform immediate actions like programmatic navigation
751
- // EXCEPTION: Navigate components should ALWAYS remain mounted until they redirect
752
- // since they need to be rendered to trigger the navigation
753
- if (!match && viewItem.mount && !viewItem.ionPageElement && !isNavigateComponent) {
754
- // Check if this view item should be preserved for browser navigation
755
- // We'll keep it if it was recently active (within the last navigation)
756
- const shouldPreserve = viewItem.routeData.lastPathname === routeInfo.pathname ||
757
- ((_f = viewItem.routeData.match) === null || _f === void 0 ? void 0 : _f.pathname) === routeInfo.lastPathname;
758
- if (!shouldPreserve) {
759
- // This view item doesn't match and doesn't have an IonPage
760
- // It's likely a utility component that performs an action and navigates away
761
- viewItem.mount = false;
762
- // Schedule removal to allow it to be recreated on next navigation
763
- setTimeout(() => {
764
- // Double-check before removing - the view might be needed again
765
- const stillNotNeeded = !viewItem.mount && !viewItem.ionPageElement;
766
- if (stillNotNeeded) {
767
- this.remove(viewItem);
768
- }
769
- }, VIEW_CLEANUP_DELAY_MS);
770
- }
771
- else {
772
- // Preserve it but unmount it for now
773
- viewItem.mount = false;
774
- }
775
- }
776
- // Reactivate view if it matches but was previously deactivated
777
- // Don't reactivate if this is a parameterized route navigating to a different path instance
778
- if (match && !viewItem.mount && !shouldSkipForDifferentParam) {
779
- viewItem.mount = true;
780
- viewItem.routeData.match = match;
781
- }
782
- // Deactivate wildcard routes and catch-all routes (empty path) when we have specific route matches
783
- // This prevents "Not found" or fallback pages from showing alongside valid routes
784
- if (routePath === '*' || routePath === '') {
785
- // Check if any other view in this outlet has a match for the current route
786
- const hasSpecificMatch = this.getViewItemsForOutlet(viewItem.outletId).some((v) => {
787
- var _a, _b;
788
- if (v.id === viewItem.id)
789
- return false; // Skip self
790
- const vRoutePath = ((_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path) || '';
791
- if (vRoutePath === '*' || vRoutePath === '')
792
- return false; // Skip other wildcard/empty routes
793
- // Check if this view item would match the current route
794
- const vMatch = v.reactElement ? matchComponent$1(v.reactElement, routeInfo.pathname) : null;
795
- return !!vMatch;
796
- });
797
- if (hasSpecificMatch) {
798
- viewItem.mount = false;
799
- if (viewItem.ionPageElement) {
800
- viewItem.ionPageElement.classList.add('ion-page-hidden');
801
- viewItem.ionPageElement.setAttribute('aria-hidden', 'true');
802
- }
803
- }
804
- }
805
- const routeElement = React.cloneElement(viewItem.reactElement);
806
- const componentElement = routeElement.props.element;
807
- // Don't update match for parameterized routes navigating to different path instances
808
- // This preserves the original match so that findViewItemByPath can correctly skip this view
809
- if (match && viewItem.routeData.match !== match && !shouldSkipForDifferentParam) {
810
- viewItem.routeData.match = match;
811
- }
812
- 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);
813
- return (React.createElement(UNSAFE_RouteContext.Consumer, { key: `view-context-${viewItem.id}` }, (parentContext) => {
814
- var _a, _b;
815
- const parentMatches = ((_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.matches) !== null && _a !== void 0 ? _a : []);
816
- // Accumulate params from parent matches, with fallback to other outlets
817
- let accumulatedParentParams = parentMatches.reduce((acc, m) => (Object.assign(Object.assign({}, acc), m.params)), {});
818
- if (parentMatches.length === 0 && Object.keys(accumulatedParentParams).length === 0) {
819
- accumulatedParentParams = getFallbackParamsFromViewItems(this.getAllViewItems(), viewItem.outletId, routeInfo.pathname);
820
- }
821
- const combinedParams = Object.assign(Object.assign({}, accumulatedParentParams), ((_b = routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.params) !== null && _b !== void 0 ? _b : {}));
822
- const parentPathnameBase = parentMatches.length > 0 ? parentMatches[parentMatches.length - 1].pathnameBase : '/';
823
- const absolutePathnameBase = computeAbsolutePathnameBase(routeElement, routeMatch, parentPathnameBase, routeInfo.pathname);
824
- const contextMatches = buildContextMatches(parentMatches, combinedParams, routeMatch, routeInfo.pathname, absolutePathnameBase, viewItem, routeElement, componentElement);
825
- const routeContextValue = parentContext
826
- ? Object.assign(Object.assign({}, parentContext), { matches: contextMatches }) : { outlet: null, matches: contextMatches, isDataRoute: false };
827
- return (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) },
828
- React.createElement(UNSAFE_RouteContext.Provider, { value: routeContextValue }, componentElement)));
829
- }));
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,
830
69
  };
831
- /**
832
- * Re-renders all active view items for the specified outlet.
833
- * Ensures React elements are updated with the latest match.
834
- *
835
- * 1. Iterates through children of IonRouterOutlet
836
- * 2. Updates each matching viewItem with the current child React element
837
- * (important for updating props or changes to elements)
838
- * 3. Returns a list of React components that will be rendered inside the outlet
839
- * Each view is wrapped in <ViewLifeCycleManager> to manage lifecycle and rendering
840
- */
841
- this.getChildrenToRender = (outletId, ionRouterOutlet, routeInfo) => {
842
- const viewItems = this.getViewItemsForOutlet(outletId);
843
- // Determine parentPath for nested outlets to properly evaluate index routes
844
- let parentPath = undefined;
845
- try {
846
- // Only attempt parent path computation for non-root outlets
847
- // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
848
- const isRootOutlet = outletId.startsWith('routerOutlet');
849
- if (!isRootOutlet) {
850
- const routeChildren = extractRouteChildren(ionRouterOutlet.props.children);
851
- const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
852
- if (hasRelativeRoutes || hasIndexRoute) {
853
- const result = computeParentPath({
854
- currentPathname: routeInfo.pathname,
855
- outletMountPath: undefined,
856
- routeChildren,
857
- hasRelativeRoutes,
858
- hasIndexRoute,
859
- hasWildcardRoute,
860
- });
861
- parentPath = result.parentPath;
862
- }
863
- }
864
- }
865
- catch (e) {
866
- // Non-fatal: if we fail to compute parentPath, fall back to previous behavior
867
- }
868
- // Sync child elements with stored viewItems (e.g. to reflect new props)
869
- React.Children.forEach(ionRouterOutlet.props.children, (child) => {
870
- // Ensure the child is a valid React element since we
871
- // might have whitespace strings or other non-element children
872
- if (React.isValidElement(child)) {
873
- // Find view item by exact path match to avoid wildcard routes overwriting specific routes
874
- const childPath = child.props.path;
875
- const viewItem = viewItems.find((v) => {
876
- var _a, _b;
877
- const viewItemPath = (_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
878
- // Only update if paths match exactly (prevents wildcard routes from overwriting specific routes)
879
- return viewItemPath === childPath;
880
- });
881
- if (viewItem) {
882
- viewItem.reactElement = child;
883
- }
884
- }
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);
885
78
  });
886
- // Filter out duplicate view items by ID (but keep all mounted items)
887
- const uniqueViewItems = viewItems.filter((viewItem, index, array) => {
888
- // Remove duplicates by ID (keep first occurrence)
889
- const isFirstOccurrence = array.findIndex((v) => v.id === viewItem.id) === index;
890
- return isFirstOccurrence;
891
- });
892
- // Filter out unmounted Navigate components to prevent them from being rendered
893
- // and triggering unwanted redirects
894
- const renderableViewItems = uniqueViewItems.filter((viewItem) => {
895
- var _a, _b, _c, _d;
896
- const elementComponent = (_b = (_a = viewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.element;
897
- const isNavigateComponent = isNavigateElement(elementComponent);
898
- // Exclude unmounted Navigate components from rendering
899
- if (isNavigateComponent && !viewItem.mount) {
900
- return false;
901
- }
902
- // Filter out views that are unmounted, have no ionPageElement, and don't match the current route.
903
- // These are "stale" views from previous routes that should not be rendered.
904
- // Views WITH ionPageElement are handled by the normal lifecycle events.
905
- // Views that MATCH the current route should be kept (they might be transitioning).
906
- if (!viewItem.mount && !viewItem.ionPageElement) {
907
- // Check if this view's route path matches the current pathname
908
- const viewRoutePath = (_d = (_c = viewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
909
- if (viewRoutePath) {
910
- // First try exact match using matchComponent
911
- const routeMatch = matchComponent$1(viewItem.reactElement, routeInfo.pathname);
912
- if (routeMatch) {
913
- // View matches current route, keep it
914
- return true;
915
- }
916
- // For parent routes (like /multiple-tabs or /routing), check if current pathname
917
- // starts with this route's path. This handles views with IonSplitPane/IonTabs
918
- // that don't have IonPage but should remain mounted while navigating within their children.
919
- const normalizedViewPath = normalizePathnameForComparison(viewRoutePath.replace(/\/?\*$/, '')); // Remove trailing wildcard
920
- const normalizedCurrentPath = normalizePathnameForComparison(routeInfo.pathname);
921
- // Check if current pathname is within this view's route hierarchy
922
- const isWithinRouteHierarchy = normalizedCurrentPath === normalizedViewPath || normalizedCurrentPath.startsWith(normalizedViewPath + '/');
923
- if (!isWithinRouteHierarchy) {
924
- // View is outside current route hierarchy, remove it
925
- setTimeout(() => {
926
- this.remove(viewItem);
927
- }, 0);
928
- return false;
929
- }
930
- }
931
- }
932
- return true;
933
- });
934
- const renderedItems = renderableViewItems.map((viewItem) => this.renderViewItem(viewItem, routeInfo, parentPath));
935
- return renderedItems;
936
- };
937
- /**
938
- * Finds a view item matching the current route, optionally updating its match state.
939
- */
940
- this.findViewItemByRouteInfo = (routeInfo, outletId, updateMatch) => {
941
- const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
942
- const shouldUpdateMatch = updateMatch === undefined || updateMatch === true;
943
- if (shouldUpdateMatch && viewItem && match) {
944
- viewItem.routeData.match = match;
945
- }
946
- return viewItem;
947
- };
948
- /**
949
- * Finds the view item that was previously active before a route change.
950
- */
951
- this.findLeavingViewItemByRouteInfo = (routeInfo, outletId, mustBeIonRoute = true) => {
952
- // If the lastPathname is not set, we cannot find a leaving view item
953
- if (!routeInfo.lastPathname) {
954
- return undefined;
79
+ if (viewItem) {
80
+ viewItem.reactElement = child;
955
81
  }
956
- const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute);
957
- return viewItem;
958
- };
959
- /**
960
- * Finds a view item by pathname only, used in simpler queries.
961
- */
962
- this.findViewItemByPathname = (pathname, outletId) => {
963
- const { viewItem } = this.findViewItemByPath(pathname, outletId);
964
- return viewItem;
965
- };
966
- /**
967
- * Clean up old, unmounted view items to prevent memory leaks
968
- */
969
- this.cleanupStaleViewItems = (outletId) => {
970
- const viewItems = this.getViewItemsForOutlet(outletId);
971
- // Keep only the most recent mounted views and a few unmounted ones for history
972
- const maxUnmountedItems = 3;
973
- const unmountedItems = viewItems.filter((v) => !v.mount);
974
- if (unmountedItems.length > maxUnmountedItems) {
975
- // Remove oldest unmounted items
976
- const itemsToRemove = unmountedItems.slice(0, unmountedItems.length - maxUnmountedItems);
977
- itemsToRemove.forEach((item) => {
978
- this.remove(item);
979
- });
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
+ })));
980
89
  }
981
- };
982
- /**
983
- * Override add to prevent duplicate view items with the same ID in the same outlet
984
- * But allow multiple view items for the same route path (for navigation history)
985
- */
986
- this.add = (viewItem) => {
987
- const existingViewItem = this.getViewItemsForOutlet(viewItem.outletId).find((v) => v.id === viewItem.id);
988
- if (existingViewItem) {
989
- 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
+ }
990
99
  }
991
- super.add(viewItem);
992
- this.cleanupStaleViewItems(viewItem.outletId);
993
- };
994
- /**
995
- * Override remove
996
- */
997
- this.remove = (viewItem) => {
998
- super.remove(viewItem);
999
- };
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;
1000
119
  }
1001
120
  /**
1002
- * Core function that matches a given pathname against all view items.
1003
- * Returns both the matched view item and match metadata.
121
+ * Returns the matching view item and the match result for a given pathname.
1004
122
  */
1005
- findViewItemByPath(pathname, outletId, mustBeIonRoute, allowDefaultMatch = true) {
123
+ findViewItemByPath(pathname, outletId, mustBeIonRoute) {
1006
124
  let viewItem;
1007
- let match = null;
125
+ let match;
1008
126
  let viewStack;
1009
127
  if (outletId) {
1010
- viewStack = sortViewsBySpecificity(this.getViewItemsForOutlet(outletId));
128
+ viewStack = this.getViewItemsForOutlet(outletId);
1011
129
  viewStack.some(matchView);
1012
- if (!viewItem && allowDefaultMatch)
130
+ if (!viewItem) {
1013
131
  viewStack.some(matchDefaultRoute);
132
+ }
1014
133
  }
1015
134
  else {
1016
- const viewItems = sortViewsBySpecificity(this.getAllViewItems());
135
+ const viewItems = this.getAllViewItems();
1017
136
  viewItems.some(matchView);
1018
- if (!viewItem && allowDefaultMatch)
137
+ if (!viewItem) {
1019
138
  viewItems.some(matchDefaultRoute);
139
+ }
1020
140
  }
1021
- // If we still have not found a view item for this outlet, try to find a matching
1022
- // view item across all outlets and adopt it into the current outlet. This helps
1023
- // recover when an outlet remounts and receives a new id, leaving views associated
1024
- // with the previous outlet id.
1025
- // Do not adopt across outlets; if we didn't find a view for this outlet,
1026
- // defer to route matching to create a new one.
1027
141
  return { viewItem, match };
1028
- /**
1029
- * Matches a route path with dynamic parameters (e.g. /tabs/:id)
1030
- */
1031
142
  function matchView(v) {
1032
- var _a;
1033
- if (mustBeIonRoute && !v.ionRoute)
143
+ var _a, _b;
144
+ if (mustBeIonRoute && !v.ionRoute) {
1034
145
  return false;
1035
- const viewItemPath = v.routeData.childProps.path || '';
1036
- const isIndexRoute = !!v.routeData.childProps.index;
1037
- const previousMatch = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match;
1038
- const result = v.reactElement ? matchComponent$1(v.reactElement, pathname) : null;
1039
- if (!result) {
1040
- const indexMatch = resolveIndexRouteMatch(v, pathname, undefined);
1041
- if (indexMatch) {
1042
- match = indexMatch;
1043
- viewItem = v;
1044
- return true;
1045
- }
1046
146
  }
1047
- if (result) {
1048
- const hasParams = result.params && Object.keys(result.params).length > 0;
1049
- const isSamePath = result.pathname === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathname);
1050
- const isWildcardRoute = viewItemPath.includes('*');
1051
- const isParameterRoute = viewItemPath.includes(':');
1052
- // Don't allow view items with undefined paths to match specific routes
1053
- // This prevents broken index route view items from interfering with navigation
1054
- if (!viewItemPath && !isIndexRoute && pathname !== '/' && pathname !== '') {
1055
- return false;
1056
- }
1057
- // For parameterized routes, check if we should reuse the view item.
1058
- // Wildcard routes (e.g., user/:userId/*) compare pathnameBase to allow
1059
- // child path changes while preserving the parent view.
1060
- if (isParameterRoute && !isSamePath) {
1061
- if (isWildcardRoute) {
1062
- const isSameBase = result.pathnameBase === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathnameBase);
1063
- if (isSameBase) {
1064
- match = result;
1065
- viewItem = v;
1066
- return true;
1067
- }
1068
- }
1069
- return false;
1070
- }
1071
- // For routes without params, or when navigating to the exact same path,
1072
- // or when there's no previous match, reuse the view item
1073
- if (!hasParams || isSamePath || !previousMatch) {
1074
- 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))) {
1075
160
  viewItem = v;
1076
161
  return true;
1077
162
  }
1078
- // For pure wildcard routes (without : params), compare pathnameBase to allow
1079
- // child path changes while preserving the parent view. This handles container
1080
- // routes like /tabs/* where switching between /tabs/tab1 and /tabs/tab2
1081
- // should reuse the same ViewItem.
1082
- if (isWildcardRoute && !isParameterRoute) {
1083
- const isSameBase = result.pathnameBase === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathnameBase);
1084
- if (isSameBase) {
1085
- match = result;
1086
- viewItem = v;
1087
- return true;
1088
- }
1089
- }
1090
163
  }
1091
164
  return false;
1092
165
  }
1093
- /**
1094
- * Matches a view with no path prop (default fallback route) or index route.
1095
- */
1096
166
  function matchDefaultRoute(v) {
1097
- var _a, _b, _c;
1098
- const childProps = v.routeData.childProps;
1099
- const isDefaultRoute = childProps.path === undefined || childProps.path === '';
1100
- const isIndexRoute = !!childProps.index;
1101
- if (isIndexRoute) {
1102
- const indexMatch = resolveIndexRouteMatch(v, pathname, undefined);
1103
- if (indexMatch) {
1104
- match = indexMatch;
1105
- viewItem = v;
1106
- return true;
1107
- }
1108
- return false;
1109
- }
1110
- // For empty path routes, only match if we're at the same level as when the view was created.
1111
- // This prevents an empty path view item from being reused for different routes.
1112
- if (isDefaultRoute) {
1113
- const previousPathnameBase = ((_b = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match) === null || _b === void 0 ? void 0 : _b.pathnameBase) || '';
1114
- const normalizedBase = normalizePathnameForComparison(previousPathnameBase);
1115
- const normalizedPathname = normalizePathnameForComparison(pathname);
1116
- if (normalizedPathname !== normalizedBase) {
1117
- return false;
1118
- }
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) {
1119
169
  match = {
170
+ path: pathname,
171
+ url: pathname,
172
+ isExact: true,
1120
173
  params: {},
1121
- pathname,
1122
- pathnameBase: pathname === '' ? '/' : pathname,
1123
- pattern: {
1124
- path: '',
1125
- caseSensitive: (_c = childProps.caseSensitive) !== null && _c !== void 0 ? _c : false,
1126
- end: true,
1127
- },
1128
174
  };
1129
175
  viewItem = v;
1130
176
  return true;
@@ -1133,29 +179,11 @@ class ReactRouterViewStack extends ViewStacks {
1133
179
  }
1134
180
  }
1135
181
  }
1136
- /**
1137
- * Utility to apply matchPath to a React element and return its match state.
1138
- */
1139
- function matchComponent$1(node, pathname, allowFallback = false) {
1140
- var _a;
1141
- const routeProps = (_a = node === null || node === void 0 ? void 0 : node.props) !== null && _a !== void 0 ? _a : {};
1142
- const routePath = routeProps.path;
1143
- const pathnameToMatch = derivePathnameToMatch(pathname, routePath);
1144
- const match = matchPath({
1145
- pathname: pathnameToMatch,
1146
- componentProps: routeProps,
182
+ function matchComponent$1(node, pathname) {
183
+ return matchPath({
184
+ pathname,
185
+ componentProps: node.props,
1147
186
  });
1148
- if (match || !allowFallback) {
1149
- return match;
1150
- }
1151
- const isIndexRoute = !!routeProps.index;
1152
- if (isIndexRoute) {
1153
- return createDefaultMatch(pathname, routeProps);
1154
- }
1155
- if (!routePath || routePath === '') {
1156
- return createDefaultMatch(pathname, routeProps);
1157
- }
1158
- return null;
1159
187
  }
1160
188
 
1161
189
  function clonePageElement(leavingViewHtml) {
@@ -1180,35 +208,7 @@ function clonePageElement(leavingViewHtml) {
1180
208
  return undefined;
1181
209
  }
1182
210
 
1183
- /**
1184
- * `StackManager` is responsible for managing page transitions, keeping track
1185
- * of views (pages), and ensuring that navigation behaves like native apps —
1186
- * particularly with animations and swipe gestures.
1187
- */
1188
- /**
1189
- * Delay in milliseconds before unmounting a view after a transition completes.
1190
- * This ensures the page transition animation finishes before the view is removed.
1191
- */
1192
- const VIEW_UNMOUNT_DELAY_MS = 250;
1193
- /**
1194
- * Delay in milliseconds to wait for an IonPage element to be mounted before
1195
- * proceeding with a page transition.
1196
- */
1197
- const ION_PAGE_WAIT_TIMEOUT_MS = 50;
1198
- const isViewVisible = (el) => !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden') && el.style.display !== 'none';
1199
- const hideIonPageElement = (element) => {
1200
- if (element) {
1201
- element.classList.add('ion-page-hidden');
1202
- element.setAttribute('aria-hidden', 'true');
1203
- }
1204
- };
1205
- const showIonPageElement = (element) => {
1206
- if (element) {
1207
- element.style.removeProperty('display');
1208
- element.classList.remove('ion-page-hidden');
1209
- element.removeAttribute('aria-hidden');
1210
- }
1211
- };
211
+ const isViewVisible = (el) => !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden');
1212
212
  class StackManager extends React.PureComponent {
1213
213
  constructor(props) {
1214
214
  super(props);
@@ -1217,479 +217,13 @@ class StackManager extends React.PureComponent {
1217
217
  isInOutlet: () => true,
1218
218
  };
1219
219
  this.pendingPageTransition = false;
1220
- this.waitingForIonPage = false;
1221
- this.outletMountPath = undefined;
1222
220
  this.registerIonPage = this.registerIonPage.bind(this);
1223
221
  this.transitionPage = this.transitionPage.bind(this);
1224
222
  this.handlePageTransition = this.handlePageTransition.bind(this);
1225
- this.id = props.id || `routerOutlet-${generateId('routerOutlet')}`;
223
+ this.id = generateId('routerOutlet');
1226
224
  this.prevProps = undefined;
1227
225
  this.skipTransition = false;
1228
226
  }
1229
- /**
1230
- * Determines the parent path for nested routing in React Router 6.
1231
- * Priority: specific routes > wildcard routes > index routes (only at mount point)
1232
- */
1233
- getParentPath() {
1234
- const currentPathname = this.props.routeInfo.pathname;
1235
- // Prevent out-of-scope outlets from adopting unrelated routes
1236
- if (this.outletMountPath && !currentPathname.startsWith(this.outletMountPath)) {
1237
- return undefined;
1238
- }
1239
- if (this.ionRouterOutlet) {
1240
- const routeChildren = extractRouteChildren(this.ionRouterOutlet.props.children);
1241
- const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
1242
- const isRootOutlet = this.id.startsWith('routerOutlet');
1243
- const needsParentPath = !isRootOutlet || hasRelativeRoutes || hasIndexRoute;
1244
- if (needsParentPath) {
1245
- const result = computeParentPath({
1246
- currentPathname,
1247
- outletMountPath: this.outletMountPath,
1248
- routeChildren,
1249
- hasRelativeRoutes,
1250
- hasIndexRoute,
1251
- hasWildcardRoute,
1252
- });
1253
- if (result.outletMountPath && !this.outletMountPath) {
1254
- this.outletMountPath = result.outletMountPath;
1255
- }
1256
- return result.parentPath;
1257
- }
1258
- }
1259
- return this.outletMountPath;
1260
- }
1261
- /**
1262
- * Finds the entering and leaving view items, handling redirect cases.
1263
- */
1264
- findViewItems(routeInfo) {
1265
- const enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
1266
- let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
1267
- // Try to find leaving view by previous pathname
1268
- if (!leavingViewItem && routeInfo.prevRouteLastPathname) {
1269
- leavingViewItem = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
1270
- }
1271
- // For redirects where entering === leaving, find the actual previous view
1272
- if (enteringViewItem &&
1273
- leavingViewItem &&
1274
- enteringViewItem === leavingViewItem &&
1275
- routeInfo.routeAction === 'replace' &&
1276
- routeInfo.prevRouteLastPathname) {
1277
- const actualLeavingView = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
1278
- if (actualLeavingView && actualLeavingView !== enteringViewItem) {
1279
- leavingViewItem = actualLeavingView;
1280
- }
1281
- }
1282
- // Handle redirect scenario with no leaving view
1283
- if (enteringViewItem &&
1284
- !leavingViewItem &&
1285
- routeInfo.routeAction === 'replace' &&
1286
- routeInfo.prevRouteLastPathname) {
1287
- const actualLeavingView = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
1288
- if (actualLeavingView && actualLeavingView !== enteringViewItem) {
1289
- leavingViewItem = actualLeavingView;
1290
- }
1291
- }
1292
- return { enteringViewItem, leavingViewItem };
1293
- }
1294
- shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem) {
1295
- var _a, _b, _c, _d;
1296
- if (!leavingViewItem) {
1297
- return false;
1298
- }
1299
- if (routeInfo.routeAction === 'replace') {
1300
- const enteringRoutePath = (_b = (_a = enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1301
- const leavingRoutePath = (_d = (_c = leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1302
- // Never unmount root path - needed for back navigation
1303
- if (leavingRoutePath === '/' || leavingRoutePath === '') {
1304
- return false;
1305
- }
1306
- if (enteringRoutePath && leavingRoutePath) {
1307
- const getParentPath = (path) => {
1308
- const normalized = path.replace(/\/\*$/, '');
1309
- const lastSlash = normalized.lastIndexOf('/');
1310
- return lastSlash > 0 ? normalized.substring(0, lastSlash) : '/';
1311
- };
1312
- const enteringParent = getParentPath(enteringRoutePath);
1313
- const leavingParent = getParentPath(leavingRoutePath);
1314
- // Unmount if routes are siblings or entering is a child of leaving (redirect)
1315
- const areSiblings = enteringParent === leavingParent && enteringParent !== '/';
1316
- const isChildRedirect = enteringRoutePath.startsWith(leavingRoutePath) ||
1317
- (leavingRoutePath.endsWith('/*') && enteringRoutePath.startsWith(leavingRoutePath.slice(0, -2)));
1318
- return areSiblings || isChildRedirect;
1319
- }
1320
- return false;
1321
- }
1322
- // For non-replace actions, only unmount for back navigation
1323
- const isForwardPush = routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward';
1324
- if (!isForwardPush && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
1325
- return true;
1326
- }
1327
- return false;
1328
- }
1329
- /**
1330
- * Handles out-of-scope outlet. Returns true if transition should be aborted.
1331
- */
1332
- handleOutOfScopeOutlet(routeInfo) {
1333
- if (!this.outletMountPath || routeInfo.pathname.startsWith(this.outletMountPath)) {
1334
- return false;
1335
- }
1336
- if (this.outOfScopeUnmountTimeout) {
1337
- clearTimeout(this.outOfScopeUnmountTimeout);
1338
- this.outOfScopeUnmountTimeout = undefined;
1339
- }
1340
- const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
1341
- allViewsInOutlet.forEach((viewItem) => {
1342
- hideIonPageElement(viewItem.ionPageElement);
1343
- this.context.unMountViewItem(viewItem);
1344
- });
1345
- this.forceUpdate();
1346
- return true;
1347
- }
1348
- /**
1349
- * Handles nested outlet with relative routes but no parent path. Returns true to abort.
1350
- */
1351
- handleOutOfContextNestedOutlet(parentPath, leavingViewItem) {
1352
- var _a;
1353
- const isRootOutlet = this.id.startsWith('routerOutlet');
1354
- if (isRootOutlet || parentPath !== undefined || !this.ionRouterOutlet) {
1355
- return false;
1356
- }
1357
- const routesChildren = (_a = getRoutesChildren(this.ionRouterOutlet.props.children)) !== null && _a !== void 0 ? _a : this.ionRouterOutlet.props.children;
1358
- const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
1359
- const hasRelativeRoutes = routeChildren.some((route) => {
1360
- const path = route.props.path;
1361
- return path && !path.startsWith('/') && path !== '*';
1362
- });
1363
- if (hasRelativeRoutes) {
1364
- hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
1365
- if (leavingViewItem) {
1366
- leavingViewItem.mount = false;
1367
- }
1368
- this.forceUpdate();
1369
- return true;
1370
- }
1371
- return false;
1372
- }
1373
- /**
1374
- * Handles nested outlet with no matching route. Returns true to abort.
1375
- */
1376
- handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem) {
1377
- const isRootOutlet = this.id.startsWith('routerOutlet');
1378
- if (isRootOutlet || enteringRoute || enteringViewItem) {
1379
- return false;
1380
- }
1381
- hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
1382
- if (leavingViewItem) {
1383
- leavingViewItem.mount = false;
1384
- }
1385
- this.forceUpdate();
1386
- return true;
1387
- }
1388
- /**
1389
- * Handles transition when entering view has ion-page element ready.
1390
- */
1391
- handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem) {
1392
- var _a, _b;
1393
- const routePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1394
- const isParameterizedRoute = routePath ? routePath.includes(':') : false;
1395
- const isWildcardContainerRoute = routePath ? routePath.endsWith('/*') : false;
1396
- // Handle same-view transitions (parameterized routes like /user/:id or container routes like /tabs/*)
1397
- // When entering === leaving, the view is already visible - skip transition to prevent flash
1398
- if (enteringViewItem === leavingViewItem) {
1399
- if (isParameterizedRoute || isWildcardContainerRoute) {
1400
- const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true);
1401
- if (updatedMatch) {
1402
- enteringViewItem.routeData.match = updatedMatch;
1403
- }
1404
- const enteringEl = enteringViewItem.ionPageElement;
1405
- if (enteringEl) {
1406
- showIonPageElement(enteringEl);
1407
- enteringEl.classList.remove('ion-page-invisible');
1408
- }
1409
- this.forceUpdate();
1410
- return;
1411
- }
1412
- }
1413
- // For wildcard container routes, check if we're navigating within the same container.
1414
- // If both the current pathname and the previous pathname match the same container route,
1415
- // skip the transition - the nested outlet will handle the actual page change.
1416
- // This handles cases where leavingViewItem lookup fails (e.g., no IonPage wrapper).
1417
- if (isWildcardContainerRoute && routeInfo.lastPathname) {
1418
- // routePath is guaranteed to exist since isWildcardContainerRoute checks routePath?.endsWith('/*')
1419
- const containerBase = routePath.replace(/\/\*$/, '');
1420
- const currentInContainer = routeInfo.pathname.startsWith(containerBase + '/') || routeInfo.pathname === containerBase;
1421
- const previousInContainer = routeInfo.lastPathname.startsWith(containerBase + '/') || routeInfo.lastPathname === containerBase;
1422
- if (currentInContainer && previousInContainer) {
1423
- const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true);
1424
- if (updatedMatch) {
1425
- enteringViewItem.routeData.match = updatedMatch;
1426
- }
1427
- this.forceUpdate();
1428
- return;
1429
- }
1430
- }
1431
- if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
1432
- leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
1433
- }
1434
- // Re-mount views that were previously unmounted (e.g., navigating back to home)
1435
- if (!enteringViewItem.mount) {
1436
- enteringViewItem.mount = true;
1437
- }
1438
- // Check visibility state BEFORE showing entering view
1439
- const enteringWasVisible = enteringViewItem.ionPageElement && isViewVisible(enteringViewItem.ionPageElement);
1440
- const leavingIsHidden = leavingViewItem !== undefined && leavingViewItem.ionPageElement && !isViewVisible(leavingViewItem.ionPageElement);
1441
- const currentTransition = {
1442
- enteringId: enteringViewItem.id,
1443
- leavingId: leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.id,
1444
- };
1445
- const isDuplicateTransition = leavingViewItem &&
1446
- this.lastTransition &&
1447
- this.lastTransition.leavingId &&
1448
- this.lastTransition.enteringId === currentTransition.enteringId &&
1449
- this.lastTransition.leavingId === currentTransition.leavingId;
1450
- // Skip if transition already performed (e.g., via swipe gesture)
1451
- if (enteringWasVisible && leavingIsHidden && isDuplicateTransition) {
1452
- if (this.skipTransition &&
1453
- shouldUnmountLeavingViewItem &&
1454
- leavingViewItem &&
1455
- enteringViewItem !== leavingViewItem) {
1456
- leavingViewItem.mount = false;
1457
- // Trigger ionViewDidLeave lifecycle for ViewLifeCycleManager cleanup
1458
- this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
1459
- }
1460
- this.skipTransition = false;
1461
- this.forceUpdate();
1462
- return;
1463
- }
1464
- showIonPageElement(enteringViewItem.ionPageElement);
1465
- // Handle duplicate transition or swipe gesture completion
1466
- if (isDuplicateTransition || this.skipTransition) {
1467
- if (this.skipTransition &&
1468
- shouldUnmountLeavingViewItem &&
1469
- leavingViewItem &&
1470
- enteringViewItem !== leavingViewItem) {
1471
- leavingViewItem.mount = false;
1472
- // Re-fire ionViewDidLeave since gesture completed before mount=false was set
1473
- this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
1474
- }
1475
- this.skipTransition = false;
1476
- this.forceUpdate();
1477
- return;
1478
- }
1479
- this.lastTransition = currentTransition;
1480
- const shouldSkipAnimation = this.applySkipAnimationIfNeeded(enteringViewItem, leavingViewItem);
1481
- this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, undefined, false, shouldSkipAnimation);
1482
- if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
1483
- leavingViewItem.mount = false;
1484
- this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
1485
- }
1486
- // Clean up orphaned sibling views after replace actions (redirects)
1487
- this.cleanupOrphanedSiblingViews(routeInfo, enteringViewItem, leavingViewItem);
1488
- }
1489
- /**
1490
- * Handles leaving view unmount for replace actions.
1491
- */
1492
- handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem) {
1493
- var _a, _b, _c, _d, _e, _f;
1494
- if (!leavingViewItem.ionPageElement) {
1495
- return;
1496
- }
1497
- // Only replace actions unmount views; push/pop cache for navigation history
1498
- if (routeInfo.routeAction !== 'replace') {
1499
- return;
1500
- }
1501
- const enteringRoutePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1502
- const leavingRoutePath = (_d = (_c = leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1503
- const isEnteringContainerRoute = enteringRoutePath && enteringRoutePath.endsWith('/*');
1504
- const isLeavingSpecificRoute = leavingRoutePath &&
1505
- leavingRoutePath !== '' &&
1506
- leavingRoutePath !== '*' &&
1507
- !leavingRoutePath.endsWith('/*') &&
1508
- !((_f = (_e = leavingViewItem.reactElement) === null || _e === void 0 ? void 0 : _e.props) === null || _f === void 0 ? void 0 : _f.index);
1509
- // Skip removal for container-to-container transitions (e.g., /tabs/* → /settings/*).
1510
- // These routes manage their own nested outlets; unmounting would disrupt child views.
1511
- if (isEnteringContainerRoute && !isLeavingSpecificRoute) {
1512
- return;
1513
- }
1514
- const viewToUnmount = leavingViewItem;
1515
- setTimeout(() => {
1516
- this.context.unMountViewItem(viewToUnmount);
1517
- this.forceUpdate();
1518
- }, VIEW_UNMOUNT_DELAY_MS);
1519
- }
1520
- /**
1521
- * Cleans up orphaned sibling views after replace actions or push-to-container navigations.
1522
- */
1523
- cleanupOrphanedSiblingViews(routeInfo, enteringViewItem, leavingViewItem) {
1524
- var _a, _b, _c, _d, _e, _f, _g;
1525
- const enteringRoutePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1526
- if (!enteringRoutePath) {
1527
- return;
1528
- }
1529
- const leavingRoutePath = (_d = (_c = leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1530
- const isContainerRoute = (path) => path === null || path === void 0 ? void 0 : path.endsWith('/*');
1531
- const isReplaceAction = routeInfo.routeAction === 'replace';
1532
- const isPushToContainer = routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'none' && isContainerRoute(enteringRoutePath);
1533
- if (!isReplaceAction && !isPushToContainer) {
1534
- return;
1535
- }
1536
- // Skip cleanup for tab switches
1537
- const isSameView = enteringViewItem === leavingViewItem;
1538
- const isSameContainerRoute = isContainerRoute(enteringRoutePath) && leavingRoutePath === enteringRoutePath;
1539
- const isNavigatingWithinContainer = isPushToContainer &&
1540
- !leavingViewItem &&
1541
- ((_e = routeInfo.prevRouteLastPathname) === null || _e === void 0 ? void 0 : _e.startsWith(enteringRoutePath.replace(/\/\*$/, '')));
1542
- if (isSameView || isSameContainerRoute || isNavigatingWithinContainer) {
1543
- return;
1544
- }
1545
- const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
1546
- const areSiblingRoutes = (path1, path2) => {
1547
- const path1IsRelative = !path1.startsWith('/');
1548
- const path2IsRelative = !path2.startsWith('/');
1549
- if (path1IsRelative && path2IsRelative) {
1550
- const path1Depth = path1.replace(/\/\*$/, '').split('/').filter(Boolean).length;
1551
- const path2Depth = path2.replace(/\/\*$/, '').split('/').filter(Boolean).length;
1552
- return path1Depth === path2Depth && path1Depth <= 1;
1553
- }
1554
- const getParent = (path) => {
1555
- const normalized = path.replace(/\/\*$/, '');
1556
- const lastSlash = normalized.lastIndexOf('/');
1557
- return lastSlash > 0 ? normalized.substring(0, lastSlash) : '/';
1558
- };
1559
- return getParent(path1) === getParent(path2);
1560
- };
1561
- for (const viewItem of allViewsInOutlet) {
1562
- const viewRoutePath = (_g = (_f = viewItem.reactElement) === null || _f === void 0 ? void 0 : _f.props) === null || _g === void 0 ? void 0 : _g.path;
1563
- const shouldSkip = viewItem.id === enteringViewItem.id ||
1564
- (leavingViewItem && viewItem.id === leavingViewItem.id) ||
1565
- !viewItem.mount ||
1566
- !viewRoutePath ||
1567
- (viewRoutePath.endsWith('/*') && enteringRoutePath.endsWith('/*'));
1568
- if (shouldSkip) {
1569
- continue;
1570
- }
1571
- if (areSiblingRoutes(enteringRoutePath, viewRoutePath)) {
1572
- hideIonPageElement(viewItem.ionPageElement);
1573
- viewItem.mount = false;
1574
- const viewToRemove = viewItem;
1575
- setTimeout(() => {
1576
- this.context.unMountViewItem(viewToRemove);
1577
- this.forceUpdate();
1578
- }, VIEW_UNMOUNT_DELAY_MS);
1579
- }
1580
- }
1581
- }
1582
- /**
1583
- * Determines whether to skip the transition animation and, if so, immediately
1584
- * hides the leaving view with inline `display:none`.
1585
- *
1586
- * Skips transitions in outlets nested inside a parent IonPage. These outlets
1587
- * render pages inside a parent page's content area. The MD animation shows
1588
- * both entering and leaving pages simultaneously, causing text overlap and
1589
- * nested scrollbars (each page has its own IonContent). Top-level outlets
1590
- * are unaffected and animate normally.
1591
- *
1592
- * Uses inline display:none rather than ion-page-hidden class because core's
1593
- * beforeTransition() removes ion-page-hidden via setPageHidden().
1594
- * Inline display:none survives that removal, keeping the page hidden
1595
- * until React unmounts it after ionViewDidLeave fires.
1596
- */
1597
- applySkipAnimationIfNeeded(enteringViewItem, leavingViewItem) {
1598
- var _a;
1599
- const isNestedOutlet = !!((_a = this.routerOutletElement) === null || _a === void 0 ? void 0 : _a.closest('.ion-page'));
1600
- const shouldSkip = isNestedOutlet && !!leavingViewItem && enteringViewItem !== leavingViewItem;
1601
- if (shouldSkip && (leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement)) {
1602
- leavingViewItem.ionPageElement.style.setProperty('display', 'none');
1603
- leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
1604
- }
1605
- return shouldSkip;
1606
- }
1607
- /**
1608
- * Handles entering view with no ion-page element yet (waiting for render).
1609
- */
1610
- handleWaitingForIonPage(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem) {
1611
- var _a, _b;
1612
- const enteringRouteElement = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.element;
1613
- // Handle Navigate components (they never render an IonPage)
1614
- if (isNavigateElement(enteringRouteElement)) {
1615
- this.waitingForIonPage = false;
1616
- if (this.ionPageWaitTimeout) {
1617
- clearTimeout(this.ionPageWaitTimeout);
1618
- this.ionPageWaitTimeout = undefined;
1619
- }
1620
- this.pendingPageTransition = false;
1621
- // Hide ALL other visible views in this outlet for Navigate redirects.
1622
- // Same rationale as the timeout path: intermediate redirects can shift
1623
- // the leaving view reference, leaving the original page visible.
1624
- const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
1625
- allViewsInOutlet.forEach((viewItem) => {
1626
- if (viewItem.id !== enteringViewItem.id && viewItem.ionPageElement) {
1627
- hideIonPageElement(viewItem.ionPageElement);
1628
- }
1629
- });
1630
- // Don't unmount if entering and leaving are the same view item
1631
- if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
1632
- leavingViewItem.mount = false;
1633
- }
1634
- this.forceUpdate();
1635
- return;
1636
- }
1637
- // Do not hide the leaving view here - wait until the entering view is ready.
1638
- // Hiding the leaving view while the entering view is still mounting causes a flash
1639
- // where both views are hidden/invisible simultaneously.
1640
- // The leaving view will be hidden in transitionPage() after the entering view is visible.
1641
- this.waitingForIonPage = true;
1642
- if (this.ionPageWaitTimeout) {
1643
- clearTimeout(this.ionPageWaitTimeout);
1644
- }
1645
- this.ionPageWaitTimeout = setTimeout(() => {
1646
- var _a, _b;
1647
- this.ionPageWaitTimeout = undefined;
1648
- if (!this.waitingForIonPage) {
1649
- return;
1650
- }
1651
- this.waitingForIonPage = false;
1652
- const latestEnteringView = (_a = this.context.findViewItemByRouteInfo(routeInfo, this.id)) !== null && _a !== void 0 ? _a : enteringViewItem;
1653
- const latestLeavingView = (_b = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id)) !== null && _b !== void 0 ? _b : leavingViewItem;
1654
- if (latestEnteringView === null || latestEnteringView === void 0 ? void 0 : latestEnteringView.ionPageElement) {
1655
- const shouldSkipAnimation = this.applySkipAnimationIfNeeded(latestEnteringView, latestLeavingView !== null && latestLeavingView !== void 0 ? latestLeavingView : undefined);
1656
- this.transitionPage(routeInfo, latestEnteringView, latestLeavingView !== null && latestLeavingView !== void 0 ? latestLeavingView : undefined, undefined, false, shouldSkipAnimation);
1657
- if (shouldUnmountLeavingViewItem && latestLeavingView && latestEnteringView !== latestLeavingView) {
1658
- latestLeavingView.mount = false;
1659
- // Call handleLeavingViewUnmount to ensure the view is properly removed
1660
- this.handleLeavingViewUnmount(routeInfo, latestEnteringView, latestLeavingView);
1661
- }
1662
- this.forceUpdate();
1663
- }
1664
- else {
1665
- /**
1666
- * Timeout fired and entering view still has no ionPageElement.
1667
- * This happens for container routes that render nested outlets without a direct IonPage.
1668
- * Hide ALL other visible views in this outlet, not just the computed leaving view.
1669
- * This handles cases where intermediate redirects (e.g., Navigate in nested routes)
1670
- * change the leaving view reference, leaving the original page still visible.
1671
- */
1672
- const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
1673
- allViewsInOutlet.forEach((viewItem) => {
1674
- if (viewItem.id !== latestEnteringView.id && viewItem.ionPageElement) {
1675
- hideIonPageElement(viewItem.ionPageElement);
1676
- }
1677
- });
1678
- this.forceUpdate();
1679
- }
1680
- }, ION_PAGE_WAIT_TIMEOUT_MS);
1681
- this.forceUpdate();
1682
- }
1683
- /**
1684
- * Gets the route info to use for finding views during swipe-to-go-back gestures.
1685
- * This pattern is used in multiple places in setupRouterOutlet.
1686
- */
1687
- getSwipeBackRouteInfo() {
1688
- const { routeInfo } = this.props;
1689
- return this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
1690
- ? this.prevProps.routeInfo
1691
- : { pathname: routeInfo.pushedByRoute || '' };
1692
- }
1693
227
  componentDidMount() {
1694
228
  if (this.clearOutletTimeout) {
1695
229
  /**
@@ -1721,175 +255,117 @@ class StackManager extends React.PureComponent {
1721
255
  }
1722
256
  }
1723
257
  componentWillUnmount() {
1724
- if (this.ionPageWaitTimeout) {
1725
- clearTimeout(this.ionPageWaitTimeout);
1726
- this.ionPageWaitTimeout = undefined;
1727
- }
1728
- if (this.outOfScopeUnmountTimeout) {
1729
- clearTimeout(this.outOfScopeUnmountTimeout);
1730
- this.outOfScopeUnmountTimeout = undefined;
1731
- }
1732
- this.waitingForIonPage = false;
1733
- // Hide all views in this outlet before clearing.
1734
- // This is critical for nested outlets - when the parent component unmounts,
1735
- // the nested outlet's componentDidUpdate won't be called, so we must hide
1736
- // the ion-page elements here to prevent them from remaining visible on top
1737
- // of other content after navigation to a different route.
1738
- const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
1739
- allViewsInOutlet.forEach((viewItem) => {
1740
- hideIonPageElement(viewItem.ionPageElement);
1741
- });
1742
258
  this.clearOutletTimeout = this.context.clearOutlet(this.id);
1743
259
  }
1744
- /**
1745
- * Sets the transition between pages within this router outlet.
1746
- * This function determines the entering and leaving views based on the
1747
- * provided route information and triggers the appropriate animation.
1748
- * It also handles scenarios like initial loads, back navigation, and
1749
- * navigation to the same view with different parameters.
1750
- *
1751
- * @param routeInfo It contains info about the current route,
1752
- * the previous route, and the action taken (e.g., push, replace).
1753
- *
1754
- * @returns A promise that resolves when the transition is complete.
1755
- * If no transition is needed or if the router outlet isn't ready,
1756
- * the Promise may resolve immediately.
1757
- */
1758
260
  async handlePageTransition(routeInfo) {
1759
- var _a;
1760
- // Wait for router outlet to mount
261
+ var _a, _b;
1761
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
+ */
1762
270
  this.pendingPageTransition = true;
1763
- return;
1764
- }
1765
- // Find entering and leaving view items
1766
- const viewItems = this.findViewItems(routeInfo);
1767
- let enteringViewItem = viewItems.enteringViewItem;
1768
- const leavingViewItem = viewItems.leavingViewItem;
1769
- const shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
1770
- // Get parent path for nested outlets
1771
- const parentPath = this.getParentPath();
1772
- // Handle out-of-scope outlet (route outside mount path)
1773
- if (this.handleOutOfScopeOutlet(routeInfo)) {
1774
- return;
1775
- }
1776
- // Clear any pending out-of-scope unmount timeout
1777
- if (this.outOfScopeUnmountTimeout) {
1778
- clearTimeout(this.outOfScopeUnmountTimeout);
1779
- this.outOfScopeUnmountTimeout = undefined;
1780
- }
1781
- // Handle nested outlet with relative routes but no valid parent path
1782
- if (this.handleOutOfContextNestedOutlet(parentPath, leavingViewItem)) {
1783
- return;
1784
271
  }
1785
- // Find the matching route element
1786
- const enteringRoute = findRouteByRouteInfo((_a = this.ionRouterOutlet) === null || _a === void 0 ? void 0 : _a.props.children, routeInfo, parentPath);
1787
- // Handle nested outlet with no matching route
1788
- if (this.handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem)) {
1789
- return;
1790
- }
1791
- // Create or update the entering view item
1792
- if (enteringViewItem && enteringRoute) {
1793
- enteringViewItem.reactElement = enteringRoute;
1794
- }
1795
- else if (enteringRoute) {
1796
- enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
1797
- this.context.addViewItem(enteringViewItem);
1798
- }
1799
- // Handle transition based on ion-page element availability
1800
- // Check if the ionPageElement is still in the document.
1801
- // If the view was previously unmounted (mount=false), the ViewLifeCycleManager
1802
- // removes the React component from the tree, which removes the IonPage from the DOM.
1803
- // The ionPageElement reference becomes stale and we need to wait for a new one.
1804
- const ionPageIsInDocument = (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) && document.body.contains(enteringViewItem.ionPageElement);
1805
- if (enteringViewItem && ionPageIsInDocument) {
1806
- // Clear waiting state
1807
- if (this.waitingForIonPage) {
1808
- this.waitingForIonPage = false;
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);
1809
277
  }
1810
- if (this.ionPageWaitTimeout) {
1811
- clearTimeout(this.ionPageWaitTimeout);
1812
- this.ionPageWaitTimeout = undefined;
278
+ // Check if leavingViewItem should be unmounted
279
+ if (leavingViewItem) {
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) {
289
+ leavingViewItem.mount = false;
290
+ }
1813
291
  }
1814
- this.handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem);
1815
- }
1816
- else if (enteringViewItem && !ionPageIsInDocument) {
1817
- // Wait for ion-page to mount
1818
- // This handles both: no ionPageElement, or stale ionPageElement (not in document)
1819
- // Clear stale reference if the element is no longer in the document
1820
- if (enteringViewItem.ionPageElement && !document.body.contains(enteringViewItem.ionPageElement)) {
1821
- enteringViewItem.ionPageElement = undefined;
292
+ const enteringRoute = matchRoute((_b = this.ionRouterOutlet) === null || _b === void 0 ? void 0 : _b.props.children, routeInfo);
293
+ if (enteringViewItem) {
294
+ enteringViewItem.reactElement = enteringRoute;
1822
295
  }
1823
- // Ensure the view is marked as mounted so ViewLifeCycleManager renders the IonPage
1824
- if (!enteringViewItem.mount) {
1825
- enteringViewItem.mount = true;
296
+ else if (enteringRoute) {
297
+ enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
298
+ this.context.addViewItem(enteringViewItem);
1826
299
  }
1827
- this.handleWaitingForIonPage(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem);
1828
- return;
1829
- }
1830
- else if (!enteringViewItem && !enteringRoute) {
1831
- // No view or route found - likely leaving to another outlet
1832
- if (leavingViewItem) {
1833
- hideIonPageElement(leavingViewItem.ionPageElement);
1834
- if (shouldUnmountLeavingViewItem) {
1835
- leavingViewItem.mount = false;
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
+ }
1836
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);
1837
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();
1838
363
  }
1839
- this.forceUpdate();
1840
364
  }
1841
- /**
1842
- * Registers an `<IonPage>` DOM element with the `StackManager`.
1843
- * This is called when `<IonPage>` has been mounted.
1844
- *
1845
- * @param page The element of the rendered `<IonPage>`.
1846
- * @param routeInfo The route information that associates with `<IonPage>`.
1847
- */
1848
365
  registerIonPage(page, routeInfo) {
1849
- /**
1850
- * DO NOT remove ion-page-invisible here.
1851
- *
1852
- * PageManager's ref callback adds ion-page-invisible synchronously to prevent flash.
1853
- * At this point, the <IonPage> div exists but its CHILDREN (header, toolbar, menu-button)
1854
- * have NOT rendered yet. If we remove ion-page-invisible now, the page becomes visible
1855
- * with empty/incomplete content, causing a flicker (especially for ion-menu-button which
1856
- * starts with menu-button-hidden class).
1857
- *
1858
- * Instead, let transitionPage handle visibility AFTER waiting for components to be ready.
1859
- * This ensures the page only becomes visible when its content is fully rendered.
1860
- */
1861
- this.waitingForIonPage = false;
1862
- if (this.ionPageWaitTimeout) {
1863
- clearTimeout(this.ionPageWaitTimeout);
1864
- this.ionPageWaitTimeout = undefined;
1865
- }
1866
- this.pendingPageTransition = false;
1867
366
  const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
1868
367
  if (foundView) {
1869
368
  const oldPageElement = foundView.ionPageElement;
1870
- /**
1871
- * FIX for issue #28878: Reject orphaned IonPage registrations.
1872
- *
1873
- * When a component conditionally renders different IonPages (e.g., list vs empty state)
1874
- * using React keys, and state changes simultaneously with navigation, the new IonPage
1875
- * tries to register for a route we're navigating away from. This creates a stale view.
1876
- *
1877
- * Only reject if both pageIds exist and differ, to allow nested outlet registrations.
1878
- */
1879
- if (this.shouldRejectOrphanedPage(page, oldPageElement, routeInfo)) {
1880
- this.hideAndRemoveOrphanedPage(page);
1881
- return;
1882
- }
1883
- /**
1884
- * Don't let a nested element (e.g., ion-router-outlet with ionPage prop)
1885
- * override an existing IonPage registration when the existing element is
1886
- * an ancestor of the new one. This ensures ionPageElement always points
1887
- * to the outermost IonPage, which is needed to properly hide the entire
1888
- * page during back navigation (not just the inner outlet).
1889
- */
1890
- if (oldPageElement && oldPageElement !== page && oldPageElement.isConnected && oldPageElement.contains(page)) {
1891
- return;
1892
- }
1893
369
  foundView.ionPageElement = page;
1894
370
  foundView.ionRoute = true;
1895
371
  /**
@@ -1903,32 +379,6 @@ class StackManager extends React.PureComponent {
1903
379
  }
1904
380
  this.handlePageTransition(routeInfo);
1905
381
  }
1906
- /**
1907
- * Checks if a new IonPage should be rejected (component re-rendered while navigating away).
1908
- */
1909
- shouldRejectOrphanedPage(newPage, oldPageElement, routeInfo) {
1910
- if (!oldPageElement || oldPageElement === newPage) {
1911
- return false;
1912
- }
1913
- const newPageId = newPage.getAttribute('data-pageid');
1914
- const oldPageId = oldPageElement.getAttribute('data-pageid');
1915
- if (!newPageId || !oldPageId || newPageId === oldPageId) {
1916
- return false;
1917
- }
1918
- return this.props.routeInfo.pathname !== routeInfo.pathname;
1919
- }
1920
- hideAndRemoveOrphanedPage(page) {
1921
- page.classList.add('ion-page-hidden');
1922
- page.setAttribute('aria-hidden', 'true');
1923
- setTimeout(() => {
1924
- if (page.parentElement) {
1925
- page.remove();
1926
- }
1927
- }, VIEW_UNMOUNT_DELAY_MS);
1928
- }
1929
- /**
1930
- * Configures swipe-to-go-back gesture for the router outlet.
1931
- */
1932
382
  async setupRouterOutlet(routerOutlet) {
1933
383
  const canStart = () => {
1934
384
  const config = getConfig();
@@ -1937,34 +387,40 @@ class StackManager extends React.PureComponent {
1937
387
  return false;
1938
388
  }
1939
389
  const { routeInfo } = this.props;
1940
- const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1941
- let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1942
- if (!enteringViewItem) {
1943
- enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
1944
- }
1945
- // View might have mount=false but ionPageElement still in DOM
1946
- const ionPageInDocument = Boolean((enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) && document.body.contains(enteringViewItem.ionPageElement));
1947
- const canStartSwipe = !!enteringViewItem &&
1948
- (enteringViewItem.mount || ionPageInDocument) &&
1949
- enteringViewItem.routeData.match.pattern.path !== routeInfo.pathname;
1950
- return canStartSwipe;
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
+ */
401
+ enteringViewItem.mount &&
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);
1951
411
  };
1952
412
  const onStart = async () => {
1953
413
  const { routeInfo } = this.props;
1954
- const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1955
- // First try to find the view in the current outlet, then search all outlets
1956
- let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1957
- if (!enteringViewItem) {
1958
- enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
1959
- }
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);
1960
418
  const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
1961
- // Ensure the entering view is mounted so React keeps rendering it during the gesture.
1962
- // This is important when the view was previously marked for unmount but its
1963
- // ionPageElement is still in the DOM.
1964
- if (enteringViewItem && !enteringViewItem.mount) {
1965
- enteringViewItem.mount = true;
1966
- }
1967
- // 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
+ */
1968
424
  if (enteringViewItem && leavingViewItem) {
1969
425
  await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
1970
426
  }
@@ -1972,23 +428,34 @@ class StackManager extends React.PureComponent {
1972
428
  };
1973
429
  const onEnd = (shouldContinue) => {
1974
430
  if (shouldContinue) {
1975
- // User finished the swipe gesture, so complete the back navigation
1976
431
  this.skipTransition = true;
1977
432
  this.context.goBack();
1978
433
  }
1979
434
  else {
1980
- // 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
+ */
1981
440
  const { routeInfo } = this.props;
1982
- const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1983
- // First try to find the view in the current outlet, then search all outlets
1984
- let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1985
- if (!enteringViewItem) {
1986
- enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
1987
- }
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);
1988
445
  const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
1989
- // 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
+ */
1990
455
  if (enteringViewItem !== leavingViewItem && (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) !== undefined) {
1991
- hideIonPageElement(enteringViewItem.ionPageElement);
456
+ const { ionPageElement } = enteringViewItem;
457
+ ionPageElement.setAttribute('aria-hidden', 'true');
458
+ ionPageElement.classList.add('ion-page-hidden');
1992
459
  }
1993
460
  }
1994
461
  };
@@ -1998,23 +465,7 @@ class StackManager extends React.PureComponent {
1998
465
  onEnd,
1999
466
  };
2000
467
  }
2001
- /**
2002
- * Animates the transition between the entering and leaving pages within the
2003
- * router outlet.
2004
- *
2005
- * @param routeInfo Info about the current route.
2006
- * @param enteringViewItem The view item that is entering.
2007
- * @param leavingViewItem The view item that is leaving.
2008
- * @param direction The direction of the transition.
2009
- * @param progressAnimation Indicates if the transition is part of a
2010
- * gesture controlled animation (e.g., swipe to go back).
2011
- * Defaults to `false`.
2012
- * @param skipAnimation When true, forces `duration: 0` so the page
2013
- * swap is instant (no visible animation). Used for ionPage outlets
2014
- * and back navigations that unmount the leaving view to prevent
2015
- * overlapping content during the transition. Defaults to `false`.
2016
- */
2017
- async transitionPage(routeInfo, enteringViewItem, leavingViewItem, direction, progressAnimation = false, skipAnimation = false) {
468
+ async transitionPage(routeInfo, enteringViewItem, leavingViewItem, direction, progressAnimation = false) {
2018
469
  const runCommit = async (enteringEl, leavingEl) => {
2019
470
  const skipTransition = this.skipTransition;
2020
471
  /**
@@ -2042,18 +493,10 @@ class StackManager extends React.PureComponent {
2042
493
  }
2043
494
  else {
2044
495
  enteringEl.classList.add('ion-page');
2045
- /**
2046
- * Only add ion-page-invisible if the element is not already visible.
2047
- * During tab switches, the container page (e.g., TabContext wrapper) is
2048
- * already visible and should remain so. Adding ion-page-invisible would
2049
- * cause a flash where the visible page briefly becomes invisible.
2050
- */
2051
- if (!isViewVisible(enteringEl)) {
2052
- enteringEl.classList.add('ion-page-invisible');
2053
- }
496
+ enteringEl.classList.add('ion-page-invisible');
2054
497
  }
2055
498
  await routerOutlet.commit(enteringEl, leavingEl, {
2056
- duration: skipTransition || skipAnimation || directionToUse === undefined ? 0 : undefined,
499
+ duration: skipTransition || directionToUse === undefined ? 0 : undefined,
2057
500
  direction: directionToUse,
2058
501
  showGoBack: !!routeInfo.pushedByRoute,
2059
502
  progressAnimation,
@@ -2065,8 +508,9 @@ class StackManager extends React.PureComponent {
2065
508
  const directionToUse = direction !== null && direction !== void 0 ? direction : routeInfoFallbackDirection;
2066
509
  if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
2067
510
  if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) {
2068
- // Clone page for same-view transitions (e.g., /user/1 /user/2)
2069
- const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname);
511
+ // If a page is transitioning to another version of itself
512
+ // we clone it so we can have an animation to show
513
+ const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, true);
2070
514
  if (match) {
2071
515
  const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
2072
516
  if (newLeavingElement) {
@@ -2076,99 +520,14 @@ class StackManager extends React.PureComponent {
2076
520
  }
2077
521
  }
2078
522
  else {
2079
- // Route no longer matches (e.g., /user/1 → /settings)
2080
523
  await runCommit(enteringViewItem.ionPageElement, undefined);
2081
524
  }
2082
525
  }
2083
526
  else {
2084
- const leavingEl = leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement;
2085
- // For non-animated transitions, don't pass leaving element to commit() to avoid
2086
- // flicker caused by commit() briefly unhiding the leaving page
2087
- const isNonAnimatedTransition = directionToUse === undefined && !progressAnimation;
2088
- if (isNonAnimatedTransition && leavingEl) {
2089
- /**
2090
- * Flicker prevention for non-animated transitions:
2091
- * Skip commit() entirely for simple visibility swaps (like tab switches).
2092
- * commit() runs animation logic that can cause intermediate paints even with
2093
- * duration: 0. Instead, we directly swap visibility classes and wait for
2094
- * components to be ready before showing the entering element.
2095
- */
2096
- const enteringEl = enteringViewItem.ionPageElement;
2097
- // Ensure entering element has proper base classes
2098
- enteringEl.classList.add('ion-page');
2099
- // Only add ion-page-invisible if not already visible (e.g., tab switches)
2100
- if (!isViewVisible(enteringEl)) {
2101
- enteringEl.classList.add('ion-page-invisible');
2102
- }
2103
- enteringEl.classList.remove('ion-page-hidden');
2104
- enteringEl.removeAttribute('aria-hidden');
2105
- // Handle can-go-back class since we're skipping commit() which normally sets this
2106
- if (routeInfo.pushedByRoute) {
2107
- enteringEl.classList.add('can-go-back');
2108
- }
2109
- else {
2110
- enteringEl.classList.remove('can-go-back');
2111
- }
2112
- /**
2113
- * Wait for components to be ready. Menu buttons start hidden (menu-button-hidden)
2114
- * and become visible after componentDidLoad. Wait for hydration and visibility.
2115
- */
2116
- const waitForComponentsReady = () => {
2117
- return new Promise((resolve) => {
2118
- const checkReady = () => {
2119
- const ionicComponents = enteringEl.querySelectorAll('ion-header, ion-toolbar, ion-buttons, ion-menu-button, ion-title, ion-content');
2120
- const allHydrated = Array.from(ionicComponents).every((el) => el.classList.contains('hydrated'));
2121
- const menuButtons = enteringEl.querySelectorAll('ion-menu-button');
2122
- const menuButtonsReady = Array.from(menuButtons).every((el) => !el.classList.contains('menu-button-hidden'));
2123
- return allHydrated && menuButtonsReady;
2124
- };
2125
- if (checkReady()) {
2126
- resolve();
2127
- return;
2128
- }
2129
- let resolved = false;
2130
- const observer = new MutationObserver(() => {
2131
- if (!resolved && checkReady()) {
2132
- resolved = true;
2133
- observer.disconnect();
2134
- resolve();
2135
- }
2136
- });
2137
- observer.observe(enteringEl, {
2138
- subtree: true,
2139
- attributes: true,
2140
- attributeFilter: ['class'],
2141
- });
2142
- setTimeout(() => {
2143
- if (!resolved) {
2144
- resolved = true;
2145
- observer.disconnect();
2146
- resolve();
2147
- }
2148
- }, 100);
2149
- });
2150
- };
2151
- await waitForComponentsReady();
2152
- // Swap visibility in sync with browser's render cycle
2153
- await new Promise((resolve) => {
2154
- requestAnimationFrame(() => {
2155
- enteringEl.classList.remove('ion-page-invisible');
2156
- // Second rAF ensures entering is painted before hiding leaving
2157
- requestAnimationFrame(() => {
2158
- leavingEl.classList.add('ion-page-hidden');
2159
- leavingEl.setAttribute('aria-hidden', 'true');
2160
- resolve();
2161
- });
2162
- });
2163
- });
2164
- }
2165
- else {
2166
- await runCommit(enteringViewItem.ionPageElement, leavingEl);
2167
- // For animated transitions, hide leaving element after commit completes
2168
- if (leavingEl && !progressAnimation) {
2169
- leavingEl.classList.add('ion-page-hidden');
2170
- leavingEl.setAttribute('aria-hidden', 'true');
2171
- }
527
+ await runCommit(enteringViewItem.ionPageElement, leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
528
+ if (leavingViewItem && leavingViewItem.ionPageElement && !progressAnimation) {
529
+ leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
530
+ leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
2172
531
  }
2173
532
  }
2174
533
  }
@@ -2176,25 +535,20 @@ class StackManager extends React.PureComponent {
2176
535
  render() {
2177
536
  const { children } = this.props;
2178
537
  const ionRouterOutlet = React.Children.only(children);
2179
- // Store reference for use in getParentPath() and handlePageTransition()
2180
538
  this.ionRouterOutlet = ionRouterOutlet;
2181
539
  const components = this.context.getChildrenToRender(this.id, this.ionRouterOutlet, this.props.routeInfo, () => {
2182
- // Callback triggers re-render when view items are modified during getChildrenToRender
2183
540
  this.forceUpdate();
2184
541
  });
2185
542
  return (React.createElement(StackContext.Provider, { value: this.stackContextValue }, React.cloneElement(ionRouterOutlet, {
2186
543
  ref: (node) => {
2187
544
  if (ionRouterOutlet.props.setRef) {
2188
- // Needed to handle external refs from devs.
2189
545
  ionRouterOutlet.props.setRef(node);
2190
546
  }
2191
547
  if (ionRouterOutlet.props.forwardedRef) {
2192
- // Needed to handle external refs from devs.
2193
548
  ionRouterOutlet.props.forwardedRef.current = node;
2194
549
  }
2195
550
  this.routerOutletElement = node;
2196
551
  const { ref } = ionRouterOutlet;
2197
- // Check for legacy refs.
2198
552
  if (typeof ref === 'function') {
2199
553
  ref(node);
2200
554
  }
@@ -2205,422 +559,169 @@ class StackManager extends React.PureComponent {
2205
559
  return RouteManagerContext;
2206
560
  }
2207
561
  }
2208
- /**
2209
- * Finds the `<Route />` node matching the current route info.
2210
- * If no `<Route />` can be matched, a fallback node is returned.
2211
- * Routes are prioritized by specificity (most specific first).
2212
- *
2213
- * @param node The root node to search for `<Route />` nodes.
2214
- * @param routeInfo The route information to match against.
2215
- * @param parentPath The parent path that was matched by the parent outlet (for nested routing)
2216
- */
2217
- function findRouteByRouteInfo(node, routeInfo, parentPath) {
2218
- var _a;
562
+ function matchRoute(node, routeInfo) {
2219
563
  let matchedNode;
2220
- let fallbackNode;
2221
- // `<Route />` nodes are rendered inside of a <Routes /> node
2222
- const routesChildren = (_a = getRoutesChildren(node)) !== null && _a !== void 0 ? _a : node;
2223
- // Collect all route children
2224
- const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
2225
- // Sort routes by specificity (most specific first)
2226
- const sortedRoutes = routeChildren.sort((a, b) => {
2227
- const pathA = a.props.path || '';
2228
- const pathB = b.props.path || '';
2229
- // Index routes come first
2230
- if (a.props.index && !b.props.index)
2231
- return -1;
2232
- if (!a.props.index && b.props.index)
2233
- return 1;
2234
- // Wildcard-only routes (*) should come LAST
2235
- const aIsWildcardOnly = pathA === '*';
2236
- const bIsWildcardOnly = pathB === '*';
2237
- if (!aIsWildcardOnly && bIsWildcardOnly)
2238
- return -1;
2239
- if (aIsWildcardOnly && !bIsWildcardOnly)
2240
- return 1;
2241
- // Exact matches (no wildcards/params) come before wildcard/param routes
2242
- const aHasWildcard = pathA.includes('*') || pathA.includes(':');
2243
- const bHasWildcard = pathB.includes('*') || pathB.includes(':');
2244
- if (!aHasWildcard && bHasWildcard)
2245
- return -1;
2246
- if (aHasWildcard && !bHasWildcard)
2247
- return 1;
2248
- // Among routes with same wildcard status, longer paths are more specific
2249
- if (pathA.length !== pathB.length) {
2250
- return pathB.length - pathA.length;
2251
- }
2252
- return 0;
2253
- });
2254
- // For nested routes in React Router 6, we need to extract the relative path
2255
- // that this outlet should be responsible for matching
2256
- const originalPathname = routeInfo.pathname;
2257
- let relativePathnameToMatch = routeInfo.pathname;
2258
- // Check if we have relative routes (routes that don't start with '/')
2259
- const hasRelativeRoutes = sortedRoutes.some((r) => r.props.path && !r.props.path.startsWith('/'));
2260
- const hasIndexRoute = sortedRoutes.some((r) => r.props.index);
2261
- // SIMPLIFIED: Trust React Router 6's matching more, compute relative path when parent is known
2262
- if ((hasRelativeRoutes || hasIndexRoute) && parentPath) {
2263
- const parentPrefix = parentPath.replace('/*', '');
2264
- // Normalize both paths to start with '/' for consistent comparison
2265
- const normalizedParent = stripTrailingSlash(parentPrefix.startsWith('/') ? parentPrefix : `/${parentPrefix}`);
2266
- const normalizedPathname = stripTrailingSlash(routeInfo.pathname);
2267
- // Only compute relative path if pathname is within parent scope
2268
- if (normalizedPathname.startsWith(normalizedParent + '/') || normalizedPathname === normalizedParent) {
2269
- const pathSegments = routeInfo.pathname.split('/').filter(Boolean);
2270
- const parentSegments = normalizedParent.split('/').filter(Boolean);
2271
- const relativeSegments = pathSegments.slice(parentSegments.length);
2272
- relativePathnameToMatch = relativeSegments.join('/'); // Empty string is valid for index routes
2273
- }
2274
- }
2275
- // Find the first matching route
2276
- for (const child of sortedRoutes) {
2277
- const childPath = child.props.path;
2278
- const isAbsoluteRoute = childPath && childPath.startsWith('/');
2279
- // Determine which pathname to match against:
2280
- // - For absolute routes: use the original full pathname
2281
- // - For relative routes with a parent: use the computed relative pathname
2282
- // - For relative routes at root level (no parent): use the original pathname
2283
- // (matchPath will handle the relative-to-absolute normalization)
2284
- const pathnameToMatch = isAbsoluteRoute ? originalPathname : relativePathnameToMatch;
2285
- // Determine the path portion to match:
2286
- // - For absolute routes: use derivePathnameToMatch
2287
- // - For relative routes at root level (no parent): use original pathname
2288
- // directly since matchPath normalizes both path and pathname
2289
- // - For relative routes with parent: use derivePathnameToMatch for wildcards,
2290
- // or the computed relative pathname for non-wildcards
2291
- let pathForMatch;
2292
- if (isAbsoluteRoute) {
2293
- pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
2294
- }
2295
- else if (!parentPath && childPath) {
2296
- // Root-level relative route: use the full pathname and let matchPath
2297
- // handle the normalization (it adds '/' to both path and pathname)
2298
- pathForMatch = originalPathname;
2299
- }
2300
- else if (childPath && childPath.includes('*')) {
2301
- // Relative wildcard route with parent path: use derivePathnameToMatch
2302
- pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
2303
- }
2304
- else {
2305
- pathForMatch = pathnameToMatch;
2306
- }
564
+ React.Children.forEach(node, (child) => {
2307
565
  const match = matchPath({
2308
- pathname: pathForMatch,
566
+ pathname: routeInfo.pathname,
2309
567
  componentProps: child.props,
2310
568
  });
2311
569
  if (match) {
2312
570
  matchedNode = child;
2313
- break;
2314
571
  }
2315
- }
572
+ });
2316
573
  if (matchedNode) {
2317
574
  return matchedNode;
2318
575
  }
2319
- // If we haven't found a node, try to find one that doesn't have a path prop (fallback route)
2320
- // BUT only return the fallback if the current pathname is within the outlet's scope.
2321
- // For outlets with absolute paths, compute the common prefix to determine scope.
2322
- const absolutePathRoutes = routeChildren.filter((r) => r.props.path && r.props.path.startsWith('/'));
2323
- // Determine if pathname is within scope before returning fallback
2324
- let isPathnameInScope = true;
2325
- if (absolutePathRoutes.length > 0) {
2326
- // Find common prefix of all absolute paths to determine outlet scope
2327
- const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
2328
- const commonPrefix = computeCommonPrefix(absolutePaths);
2329
- // If we have a common prefix, check if the current pathname is within that scope
2330
- if (commonPrefix && commonPrefix !== '/') {
2331
- isPathnameInScope = routeInfo.pathname.startsWith(commonPrefix);
2332
- }
2333
- }
2334
- // Only look for fallback route if pathname is within scope
2335
- if (isPathnameInScope) {
2336
- for (const child of routeChildren) {
2337
- if (!child.props.path) {
2338
- fallbackNode = child;
2339
- break;
2340
- }
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;
2341
581
  }
2342
- }
2343
- return matchedNode !== null && matchedNode !== void 0 ? matchedNode : fallbackNode;
582
+ });
583
+ return matchedNode;
2344
584
  }
2345
585
  function matchComponent(node, pathname, forceExact) {
2346
- var _a;
2347
- const routePath = (_a = node === null || node === void 0 ? void 0 : node.props) === null || _a === void 0 ? void 0 : _a.path;
2348
- const pathnameToMatch = derivePathnameToMatch(pathname, routePath);
2349
586
  return matchPath({
2350
- pathname: pathnameToMatch,
2351
- componentProps: Object.assign(Object.assign({}, node.props), { end: forceExact }),
587
+ pathname,
588
+ componentProps: Object.assign(Object.assign({}, node.props), { exact: forceExact }),
2352
589
  });
2353
590
  }
2354
591
 
2355
- /**
2356
- * `IonRouter` is responsible for managing the application's navigation
2357
- * state, tracking the history of visited routes, and coordinating
2358
- * transitions between different views. It intercepts route changes from
2359
- * React Router and translates them into actions that Ionic can understand
2360
- * and animate.
2361
- */
2362
- const filterUndefinedParams = (params) => {
2363
- const result = {};
2364
- for (const key of Object.keys(params)) {
2365
- const value = params[key];
2366
- if (value !== undefined) {
2367
- result[key] = value;
2368
- }
2369
- }
2370
- return result;
2371
- };
2372
- const areParamsEqual = (a, b) => {
2373
- const paramsA = a || {};
2374
- const paramsB = b || {};
2375
- const keysA = Object.keys(paramsA);
2376
- const keysB = Object.keys(paramsB);
2377
- if (keysA.length !== keysB.length) {
2378
- 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
+ };
2379
626
  }
2380
- return keysA.every((key) => {
2381
- const valueA = paramsA[key];
2382
- const valueB = paramsB[key];
2383
- if (Array.isArray(valueA) && Array.isArray(valueB)) {
2384
- if (valueA.length !== valueB.length) {
2385
- return false;
2386
- }
2387
- return valueA.every((entry, idx) => entry === valueB[idx]);
2388
- }
2389
- return valueA === valueB;
2390
- });
2391
- };
2392
- const IonRouter = ({ children, registerHistoryListener }) => {
2393
- const location = useLocation();
2394
- const navigate = useNavigate();
2395
- const didMountRef = useRef(false);
2396
- const locationHistory = useRef(new LocationHistory());
2397
- const currentTab = useRef(undefined);
2398
- const viewStack = useRef(new ReactRouterViewStack());
2399
- const incomingRouteParams = useRef(null);
2400
- /**
2401
- * Tracks location keys that the user navigated away from via browser back.
2402
- * When a POP event's destination key matches the top of this stack, it's a
2403
- * browser forward navigation. Uses React Router's unique location.key
2404
- * instead of URLs to correctly handle duplicate URLs in history (e.g.,
2405
- * navigating to /details, then /settings, then /details via routerLink,
2406
- * then pressing back).
2407
- * Cleared on PUSH (new navigation invalidates forward history).
2408
- */
2409
- const forwardStack = useRef([]);
2410
- /**
2411
- * Tracks the current location key so we can push it onto the forward stack
2412
- * when navigating back. Updated after each history change.
2413
- */
2414
- const currentLocationKeyRef = useRef(location.key);
2415
- const [routeInfo, setRouteInfo] = useState({
2416
- id: generateId('routeInfo'),
2417
- pathname: location.pathname,
2418
- search: location.search,
2419
- params: {},
2420
- });
2421
- useEffect(() => {
2422
- if (didMountRef.current) {
627
+ handleChangeTab(tab, path, routeOptions) {
628
+ if (!path) {
2423
629
  return;
2424
630
  }
2425
- // Seed the history stack with the initial location and begin listening
2426
- // for future navigations once React has committed the mount. This avoids
2427
- // duplicate entries when React StrictMode runs an extra render pre-commit.
2428
- locationHistory.current.add(routeInfo);
2429
- registerHistoryListener(handleHistoryChange);
2430
- didMountRef.current = true;
2431
- }, []);
2432
- // Sync route params extracted by React Router's path matching back into routeInfo.
2433
- // The view stack's match may contain params (e.g., :id) not present in the initial routeInfo.
2434
- useEffect(() => {
2435
- var _a;
2436
- const activeView = viewStack.current.findViewItemByRouteInfo(routeInfo, undefined, true);
2437
- const matchedParams = (_a = activeView === null || activeView === void 0 ? void 0 : activeView.routeData.match) === null || _a === void 0 ? void 0 : _a.params;
2438
- if (matchedParams) {
2439
- const paramsCopy = filterUndefinedParams(Object.assign({}, matchedParams));
2440
- if (areParamsEqual(routeInfo.params, paramsCopy)) {
2441
- 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 : ''));
2442
644
  }
2443
- const updatedRouteInfo = Object.assign(Object.assign({}, routeInfo), { params: paramsCopy });
2444
- locationHistory.current.update(updatedRouteInfo);
2445
- setRouteInfo(updatedRouteInfo);
2446
645
  }
2447
- }, [routeInfo]);
2448
- /**
2449
- * Triggered whenever the history changes, either through user navigation
2450
- * or programmatic changes. It transforms the raw browser history changes
2451
- * into `RouteInfo` objects, which are needed Ionic's animations and
2452
- * navigation patterns.
2453
- *
2454
- * @param location The current location object from the history.
2455
- * @param action The action that triggered the history change.
2456
- */
2457
- const handleHistoryChange = (location, action) => {
2458
- 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;
2459
652
  let leavingLocationInfo;
2460
- /**
2461
- * A programmatic navigation was triggered.
2462
- * e.g., `<Navigate />`, `navigate()`, or `handleNavigate()`
2463
- */
2464
- if (incomingRouteParams.current) {
2465
- /**
2466
- * The current history entry is overwritten, so the previous entry
2467
- * is the one we are leaving.
2468
- */
2469
- if (((_a = incomingRouteParams.current) === null || _a === void 0 ? void 0 : _a.routeAction) === 'replace') {
2470
- leavingLocationInfo = locationHistory.current.previous();
653
+ if (this.incomingRouteParams) {
654
+ if (this.incomingRouteParams.routeAction === 'replace') {
655
+ leavingLocationInfo = this.locationHistory.previous();
2471
656
  }
2472
657
  else {
2473
- // If the action is 'push' or 'pop', we want to use the current route.
2474
- leavingLocationInfo = locationHistory.current.current();
658
+ leavingLocationInfo = this.locationHistory.current();
2475
659
  }
2476
660
  }
2477
661
  else {
2478
- /**
2479
- * An external navigation was triggered
2480
- * e.g., browser back/forward button or direct link
2481
- *
2482
- * The leaving location is the current route.
2483
- */
2484
- leavingLocationInfo = locationHistory.current.current();
662
+ leavingLocationInfo = this.locationHistory.current();
2485
663
  }
2486
664
  const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
2487
665
  if (leavingUrl !== location.pathname) {
2488
- if (!incomingRouteParams.current) {
2489
- // Determine if the destination is a tab route by checking if it matches
2490
- // the pattern of tab routes (containing /tabs/ in the path)
2491
- const isTabRoute = /\/tabs(\/|$)/.test(location.pathname);
2492
- const tabToUse = isTabRoute ? currentTab.current : undefined;
2493
- // If we're leaving tabs entirely, clear the current tab
2494
- if (!isTabRoute && currentTab.current) {
2495
- currentTab.current = undefined;
2496
- }
2497
- /**
2498
- * A `REPLACE` action can be triggered by React Router's
2499
- * `<Navigate />` component.
2500
- */
666
+ if (!this.incomingRouteParams) {
2501
667
  if (action === 'REPLACE') {
2502
- incomingRouteParams.current = {
668
+ this.incomingRouteParams = {
2503
669
  routeAction: 'replace',
2504
670
  routeDirection: 'none',
2505
- tab: tabToUse,
671
+ tab: this.currentTab,
2506
672
  };
2507
673
  }
2508
- /**
2509
- * A `POP` action can be triggered by the browser's back/forward
2510
- * button. Both fire as POP events, so we use a forward stack to
2511
- * distinguish them: when going back, we push the leaving pathname
2512
- * onto the stack. When the next POP's destination matches the top
2513
- * of the stack, it's a forward navigation.
2514
- */
2515
674
  if (action === 'POP') {
2516
- const currentRoute = locationHistory.current.current();
2517
- const isForwardNavigation = forwardStack.current.length > 0 &&
2518
- forwardStack.current[forwardStack.current.length - 1] === location.key;
2519
- if (isForwardNavigation) {
2520
- forwardStack.current.pop();
2521
- incomingRouteParams.current = {
2522
- routeAction: 'push',
2523
- routeDirection: 'forward',
2524
- tab: tabToUse,
2525
- };
2526
- }
2527
- else if (currentRoute && currentRoute.pushedByRoute) {
2528
- // Back navigation. Record current location key for potential forward
2529
- forwardStack.current.push(currentLocationKeyRef.current);
2530
- const prevInfo = locationHistory.current.findLastLocation(currentRoute);
2531
- incomingRouteParams.current = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
675
+ const currentRoute = this.locationHistory.current();
676
+ if (currentRoute && currentRoute.pushedByRoute) {
677
+ const prevInfo = this.locationHistory.findLastLocation(currentRoute);
678
+ this.incomingRouteParams = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
2532
679
  }
2533
680
  else {
2534
- // It's a non-linear history path like a direct link.
2535
- incomingRouteParams.current = {
681
+ this.incomingRouteParams = {
2536
682
  routeAction: 'pop',
2537
683
  routeDirection: 'none',
2538
- tab: tabToUse,
684
+ tab: this.currentTab,
2539
685
  };
2540
686
  }
2541
687
  }
2542
- if (!incomingRouteParams.current) {
2543
- const state = location.state;
2544
- incomingRouteParams.current = {
688
+ if (!this.incomingRouteParams) {
689
+ this.incomingRouteParams = {
2545
690
  routeAction: 'push',
2546
- routeDirection: (state === null || state === void 0 ? void 0 : state.direction) || 'forward',
2547
- routeOptions: state === null || state === void 0 ? void 0 : state.routerOptions,
2548
- 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,
2549
694
  };
2550
695
  }
2551
696
  }
2552
- // New navigation (PUSH) invalidates browser forward history,
2553
- // so clear our forward stack to stay in sync.
2554
- if (action === 'PUSH') {
2555
- forwardStack.current = [];
2556
- }
2557
697
  let routeInfo;
2558
- // If we're navigating away from tabs to a non-tab route, clear the current tab
2559
- if (!/\/tabs(\/|$)/.test(location.pathname) && currentTab.current) {
2560
- currentTab.current = undefined;
2561
- }
2562
- /**
2563
- * An existing id indicates that it's re-activating an existing route.
2564
- * e.g., tab switching or navigating back to a previous route
2565
- */
2566
- if ((_b = incomingRouteParams.current) === null || _b === void 0 ? void 0 : _b.id) {
2567
- routeInfo = Object.assign(Object.assign({}, incomingRouteParams.current), { lastPathname: leavingLocationInfo.pathname });
2568
- locationHistory.current.add(routeInfo);
2569
- /**
2570
- * A new route is being created since it's not re-activating
2571
- * an existing route.
2572
- */
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);
2573
701
  }
2574
702
  else {
2575
- const isPushed = ((_c = incomingRouteParams.current) === null || _c === void 0 ? void 0 : _c.routeAction) === 'push' &&
2576
- incomingRouteParams.current.routeDirection === 'forward';
2577
- 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)
2578
- ? filterUndefinedParams(incomingRouteParams.current.params)
2579
- : {}, 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 });
2580
705
  if (isPushed) {
2581
- // Only inherit tab from leaving route if we don't already have one.
2582
- // This preserves tab context for same-tab navigation while allowing cross-tab navigation.
2583
- routeInfo.tab = routeInfo.tab || leavingLocationInfo.tab;
706
+ routeInfo.tab = leavingLocationInfo.tab;
2584
707
  routeInfo.pushedByRoute = leavingLocationInfo.pathname;
2585
- // Triggered by a browser back button or handleNavigateBack.
2586
708
  }
2587
709
  else if (routeInfo.routeAction === 'pop') {
2588
- // Find the route that pushed this one.
2589
- const r = locationHistory.current.findLastLocation(routeInfo);
710
+ const r = this.locationHistory.findLastLocation(routeInfo);
2590
711
  routeInfo.pushedByRoute = r === null || r === void 0 ? void 0 : r.pushedByRoute;
2591
- // Navigating to a new tab.
2592
712
  }
2593
713
  else if (routeInfo.routeAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
2594
- /**
2595
- * If we are switching tabs grab the last route info for the
2596
- * tab and use its `pushedByRoute`.
2597
- */
2598
- const lastRoute = locationHistory.current.getCurrentRouteInfoForTab(routeInfo.tab);
2599
- /**
2600
- * Tab bar switches (direction 'none') should not create cross-tab back
2601
- * navigation. Only inherit pushedByRoute from the tab's own history.
2602
- */
2603
- if (routeInfo.routeDirection === 'none') {
2604
- routeInfo.pushedByRoute = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute;
2605
- }
2606
- else {
2607
- routeInfo.pushedByRoute = (_e = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute) !== null && _e !== void 0 ? _e : leavingLocationInfo.pathname;
2608
- }
2609
- // Triggered by `navigate()` with replace or a `<Navigate />` 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;
2610
717
  }
2611
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();
2612
721
  /**
2613
- * Make sure to set the `lastPathname`, etc.. to the current route
2614
- * so the page transitions out.
2615
- */
2616
- const currentRouteInfo = locationHistory.current.current();
2617
- /**
2618
- * Special handling for `replace` to ensure correct `pushedByRoute`
2619
- * and `lastPathname`.
2620
- *
2621
- * If going from `/home` to `/child`, then replacing from
2622
- * `/child` to `/home`, we don't want the route info to
2623
- * 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.
2624
725
  */
2625
726
  const currentPushedBy = currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.pushedByRoute;
2626
727
  const pushedByRoute = currentPushedBy !== undefined && currentPushedBy !== routeInfo.pathname
@@ -2638,110 +739,46 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2638
739
  routeInfo.routeDirection = routeInfo.routeDirection || (currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.routeDirection);
2639
740
  routeInfo.routeAnimation = routeInfo.routeAnimation || (currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.routeAnimation);
2640
741
  }
2641
- locationHistory.current.add(routeInfo);
742
+ this.locationHistory.add(routeInfo);
2642
743
  }
2643
- setRouteInfo(routeInfo);
2644
- }
2645
- // Update the current location key after processing the history change.
2646
- // This ensures the forward stack records the correct key when navigating back.
2647
- currentLocationKeyRef.current = location.key;
2648
- incomingRouteParams.current = null;
2649
- };
2650
- /**
2651
- * Resets the specified tab to its initial, root route.
2652
- *
2653
- * @param tab The tab to reset.
2654
- * @param originalHref The original href for the tab.
2655
- * @param originalRouteOptions The original route options for the tab.
2656
- */
2657
- const handleResetTab = (tab, originalHref, originalRouteOptions) => {
2658
- const routeInfo = locationHistory.current.getFirstRouteInfoForTab(tab);
2659
- if (routeInfo) {
2660
- const newRouteInfo = Object.assign({}, routeInfo);
2661
- newRouteInfo.pathname = originalHref;
2662
- newRouteInfo.routeOptions = originalRouteOptions;
2663
- incomingRouteParams.current = Object.assign(Object.assign({}, newRouteInfo), { routeAction: 'pop', routeDirection: 'back' });
2664
- navigate(newRouteInfo.pathname + (newRouteInfo.search || ''));
744
+ this.setState({
745
+ routeInfo,
746
+ });
2665
747
  }
2666
- };
748
+ this.incomingRouteParams = undefined;
749
+ }
2667
750
  /**
2668
- * Handles tab changes.
2669
- *
2670
- * @param tab The tab to switch to.
2671
- * @param path The new path for the tab.
2672
- * @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.
2673
755
  */
2674
- const handleChangeTab = (tab, path, routeOptions) => {
2675
- if (!path) {
2676
- return;
2677
- }
2678
- const routeInfo = locationHistory.current.getCurrentRouteInfoForTab(tab);
2679
- const [pathname, search] = path.split('?');
2680
- // User has navigated to the current tab before.
2681
- if (routeInfo) {
2682
- const routeParams = Object.assign(Object.assign({}, routeInfo), { routeAction: 'push', routeDirection: 'none' });
2683
- /**
2684
- * User is navigating to the same tab.
2685
- * e.g., `/tabs/home` → `/tabs/home`
2686
- */
2687
- if (routeInfo.pathname === pathname) {
2688
- incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { routeOptions });
2689
- navigate(routeInfo.pathname + (routeInfo.search || ''));
2690
- /**
2691
- * User is navigating to a different tab.
2692
- * e.g., `/tabs/home` → `/tabs/settings`
2693
- */
2694
- }
2695
- else {
2696
- incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { pathname, search: search ? '?' + search : undefined, routeOptions });
2697
- navigate(pathname + (search ? '?' + search : ''));
2698
- }
2699
- // 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);
2700
771
  }
2701
772
  else {
2702
- handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
2703
- }
2704
- };
2705
- /**
2706
- * Set the current active tab in `locationHistory`.
2707
- * This is crucial for maintaining tab history since each tab has
2708
- * its own navigation stack.
2709
- *
2710
- * @param tab The tab to set as active.
2711
- */
2712
- const handleSetCurrentTab = (tab) => {
2713
- currentTab.current = tab;
2714
- const ri = Object.assign({}, locationHistory.current.current());
2715
- if (ri.tab !== tab) {
2716
- ri.tab = tab;
2717
- locationHistory.current.update(ri);
773
+ this.props.history.replace(path);
2718
774
  }
2719
- };
2720
- /**
2721
- * Handles the native back button press.
2722
- * It's usually called when a user presses the platform-native back action.
2723
- */
2724
- const handleNativeBack = () => {
2725
- navigate(-1);
2726
- };
2727
- /**
2728
- * Used to manage the back navigation within the Ionic React's routing
2729
- * system. It's deeply integrated with Ionic's view lifecycle, animations,
2730
- * and its custom history tracking (`locationHistory`) to provide a
2731
- * native-like transition and maintain correct application state.
2732
- *
2733
- * @param defaultHref The fallback URL to navigate to if there's no
2734
- * previous entry in the `locationHistory` stack.
2735
- * @param routeAnimation A custom animation builder to override the
2736
- * default "back" animation.
2737
- */
2738
- const handleNavigateBack = (defaultHref = '/', routeAnimation) => {
775
+ }
776
+ handleNavigateBack(defaultHref = '/', routeAnimation) {
2739
777
  const config = getConfig();
2740
778
  defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref');
2741
- const routeInfo = locationHistory.current.current();
2742
- // It's a linear navigation.
779
+ const routeInfo = this.locationHistory.current();
2743
780
  if (routeInfo && routeInfo.pushedByRoute) {
2744
- const prevInfo = locationHistory.current.findLastLocation(routeInfo);
781
+ const prevInfo = this.locationHistory.findLastLocation(routeInfo);
2745
782
  if (prevInfo) {
2746
783
  /**
2747
784
  * This needs to be passed to handleNavigate
@@ -2749,240 +786,160 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2749
786
  * will be overridden.
2750
787
  */
2751
788
  const incomingAnimation = routeAnimation || routeInfo.routeAnimation;
2752
- incomingRouteParams.current = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back', routeAnimation: incomingAnimation });
2753
- /**
2754
- * Check if it's a simple linear back navigation (not tabbed).
2755
- * e.g., `/home` → `/settings` → back to `/home`
2756
- */
2757
- const condition1 = routeInfo.lastPathname === routeInfo.pushedByRoute;
2758
- const condition2 = prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '';
2759
- if (condition1 || condition2) {
2760
- // Record the current location key so browser forward is detectable
2761
- forwardStack.current.push(currentLocationKeyRef.current);
2762
- navigate(-1);
2763
- }
2764
- else {
789
+ this.incomingRouteParams = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back', routeAnimation: incomingAnimation });
790
+ if (routeInfo.lastPathname === routeInfo.pushedByRoute ||
2765
791
  /**
2766
- * It's a non-linear back navigation.
2767
- * e.g., direct link or tab switch or nested navigation with redirects
2768
- * Clear forward stack since the REPLACE-based navigate resets history
2769
- * position, making any prior forward entries unreachable.
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.
2770
796
  */
2771
- forwardStack.current = [];
2772
- handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
797
+ (prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '')) {
798
+ /**
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.
803
+ */
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);
2773
810
  }
2774
- /**
2775
- * `pushedByRoute` exists, but no corresponding previous entry in
2776
- * the history stack.
2777
- */
2778
811
  }
2779
812
  else {
2780
- handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
813
+ this.handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
2781
814
  }
2782
- /**
2783
- * No `pushedByRoute` (e.g., initial page load or tab root).
2784
- * Tabs with no back history should not navigate.
2785
- */
2786
815
  }
2787
816
  else {
2788
- if (routeInfo && routeInfo.tab) {
2789
- return;
2790
- }
2791
- handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
817
+ this.handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
2792
818
  }
2793
- };
2794
- /**
2795
- * Used to programmatically navigate through the app.
2796
- *
2797
- * @param path The path to navigate to.
2798
- * @param routeAction The action to take (push, replace, etc.).
2799
- * @param routeDirection The direction of the navigation (forward,
2800
- * back, etc.).
2801
- * @param routeAnimation The animation to use for the transition.
2802
- * @param routeOptions Additional options for the route.
2803
- * @param tab The tab to navigate to, if applicable.
2804
- */
2805
- const handleNavigate = (path, routeAction, routeDirection, routeAnimation, routeOptions, tab) => {
2806
- var _a;
2807
- const normalizedRouteDirection = routeAction === 'push' && routeDirection === undefined ? 'forward' : routeDirection;
2808
- // When navigating from tabs context, we need to determine if the destination
2809
- // is also within tabs. If not, we should clear the tab context.
2810
- let navigationTab = tab;
2811
- // If no explicit tab is provided and we're in a tab context,
2812
- // check if the destination path is outside of the current tab context
2813
- if (!tab && currentTab.current && path) {
2814
- // Get the current route info to understand where we are
2815
- const currentRoute = locationHistory.current.current();
2816
- // If we're navigating from a tab route to a completely different path structure,
2817
- // we should clear the tab context. This is a simplified check that assumes
2818
- // tab routes share a common parent path.
2819
- if (currentRoute && currentRoute.pathname) {
2820
- // Extract the base tab path (e.g., /routing/tabs from /routing/tabs/home)
2821
- const tabBaseMatch = currentRoute.pathname.match(/^(.*\/tabs)/);
2822
- if (tabBaseMatch) {
2823
- const tabBasePath = tabBaseMatch[1];
2824
- // If the new path doesn't start with the tab base path, we're leaving tabs
2825
- if (!path.startsWith(tabBasePath)) {
2826
- currentTab.current = undefined;
2827
- navigationTab = undefined;
2828
- }
2829
- else {
2830
- // Still within tabs, preserve the tab context
2831
- navigationTab = currentTab.current;
2832
- }
2833
- }
2834
- }
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 || ''));
2835
828
  }
2836
- const baseParams = (_a = incomingRouteParams.current) !== null && _a !== void 0 ? _a : {};
2837
- incomingRouteParams.current = Object.assign(Object.assign({}, baseParams), { routeAction, routeDirection: normalizedRouteDirection, routeOptions,
2838
- routeAnimation, tab: navigationTab });
2839
- navigate(path, { replace: routeAction !== 'push' });
2840
- };
2841
- const routeMangerContextValue = {
2842
- canGoBack: () => locationHistory.current.canGoBack(),
2843
- clearOutlet: viewStack.current.clear,
2844
- findViewItemByPathname: viewStack.current.findViewItemByPathname,
2845
- getChildrenToRender: viewStack.current.getChildrenToRender,
2846
- getViewItemsForOutlet: viewStack.current.getViewItemsForOutlet.bind(viewStack.current),
2847
- goBack: () => handleNavigateBack(),
2848
- createViewItem: viewStack.current.createViewItem,
2849
- findViewItemByRouteInfo: viewStack.current.findViewItemByRouteInfo,
2850
- findLeavingViewItemByRouteInfo: viewStack.current.findLeavingViewItemByRouteInfo,
2851
- addViewItem: viewStack.current.add,
2852
- unMountViewItem: viewStack.current.remove,
2853
- };
2854
- return (React.createElement(RouteManagerContext.Provider, { value: routeMangerContextValue },
2855
- React.createElement(NavManager, { ionRoute: IonRouteInner, stackManager: StackManager, routeInfo: routeInfo, onNativeBack: handleNativeBack, onNavigateBack: handleNavigateBack, onNavigate: handleNavigate, onSetCurrentTab: handleSetCurrentTab, onChangeTab: handleChangeTab, onResetTab: handleResetTab, locationHistory: locationHistory.current }, children)));
2856
- };
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);
2857
844
  IonRouter.displayName = 'IonRouter';
2858
845
 
2859
- /**
2860
- * `IonReactRouter` facilitates the integration of Ionic's specific
2861
- * navigation and UI management with the standard React Router mechanisms,
2862
- * allowing an inner Ionic-specific router (`IonRouter`) to react to
2863
- * navigation events.
2864
- */
2865
- /**
2866
- * This component acts as a bridge to ensure React Router hooks like
2867
- * `useLocation` and `useNavigationType` are called within the valid
2868
- * context of a `<BrowserRouter>`.
2869
- *
2870
- * It was split from `IonReactRouter` because these hooks must be
2871
- * descendants of a `<Router>` component, which `BrowserRouter` provides.
2872
- */
2873
- const RouterContent$2 = ({ children }) => {
2874
- const location = useLocation();
2875
- const navigationType = useNavigationType();
2876
- const historyListenHandler = useRef();
2877
- const registerHistoryListener = useCallback((cb) => {
2878
- historyListenHandler.current = cb;
2879
- }, []);
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
+ }
2880
854
  /**
2881
- * Processes navigation changes within the application.
2882
- *
2883
- * Its purpose is to relay the current `location` and the associated
2884
- * `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
2885
- * primarily for `IonRouter` to manage Ionic-specific UI updates and
2886
- * navigation stack behavior.
2887
- *
2888
- * @param loc The current browser history location object.
2889
- * @param act The type of navigation action ('PUSH', 'POP', or
2890
- * '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.
2891
861
  */
2892
- const handleHistoryChange = useCallback((loc, act) => {
2893
- if (historyListenHandler.current) {
2894
- 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);
2895
867
  }
2896
- }, []);
2897
- useEffect(() => {
2898
- handleHistoryChange(location, navigationType);
2899
- }, [location, navigationType, handleHistoryChange]);
2900
- return React.createElement(IonRouter, { registerHistoryListener: registerHistoryListener }, children);
2901
- };
2902
- const IonReactRouter = (_a) => {
2903
- var { children } = _a, browserRouterProps = __rest(_a, ["children"]);
2904
- return (React.createElement(BrowserRouter, Object.assign({}, browserRouterProps),
2905
- React.createElement(RouterContent$2, null, children)));
2906
- };
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
+ }
2907
878
 
2908
- /**
2909
- * `IonReactMemoryRouter` provides a way to use `react-router` in
2910
- * environments where a traditional browser history (like `BrowserRouter`)
2911
- * isn't available or desirable.
2912
- */
2913
- const RouterContent$1 = ({ children }) => {
2914
- const location = useLocation$1();
2915
- const navigationType = useNavigationType$1();
2916
- const historyListenHandler = useRef();
2917
- const registerHistoryListener = (cb) => {
2918
- historyListenHandler.current = cb;
2919
- };
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
+ }
2920
886
  /**
2921
- * Processes navigation changes within the application.
2922
- *
2923
- * Its purpose is to relay the current `location` and the associated
2924
- * `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
2925
- * primarily for `IonRouter` to manage Ionic-specific UI updates and
2926
- * navigation stack behavior.
2927
- *
2928
- * @param location The current browser history location object.
2929
- * @param action The type of navigation action ('PUSH', 'POP', or
2930
- * '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.
2931
893
  */
2932
- const handleHistoryChange = (location, action) => {
2933
- if (historyListenHandler.current) {
2934
- 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);
2935
899
  }
2936
- };
2937
- useEffect(() => {
2938
- handleHistoryChange(location, navigationType);
2939
- }, [location, navigationType]);
2940
- return React.createElement(IonRouter, { registerHistoryListener: registerHistoryListener }, children);
2941
- };
2942
- const IonReactMemoryRouter = (_a) => {
2943
- var { children } = _a, routerProps = __rest(_a, ["children"]);
2944
- return (React.createElement(MemoryRouter, Object.assign({}, routerProps),
2945
- React.createElement(RouterContent$1, null, children)));
2946
- };
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
+ }
2947
910
 
2948
- /**
2949
- * `IonReactHashRouter` provides a way to use hash-based routing in Ionic
2950
- * React applications.
2951
- */
2952
- const RouterContent = ({ children }) => {
2953
- const location = useLocation();
2954
- const navigationType = useNavigationType();
2955
- const historyListenHandler = useRef();
2956
- const registerHistoryListener = (cb) => {
2957
- historyListenHandler.current = cb;
2958
- };
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
+ }
2959
919
  /**
2960
- * Processes navigation changes within the application.
2961
- *
2962
- * Its purpose is to relay the current `location` and the associated
2963
- * `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
2964
- * primarily for `IonRouter` to manage Ionic-specific UI updates and
2965
- * navigation stack behavior.
2966
- *
2967
- * @param location The current browser history location object.
2968
- * @param action The type of navigation action ('PUSH', 'POP', or
2969
- * '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.
2970
926
  */
2971
- const handleHistoryChange = (location, action) => {
2972
- if (historyListenHandler.current) {
2973
- 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);
2974
932
  }
2975
- };
2976
- useEffect(() => {
2977
- handleHistoryChange(location, navigationType);
2978
- }, [location, navigationType]);
2979
- return React.createElement(IonRouter, { registerHistoryListener: registerHistoryListener }, children);
2980
- };
2981
- const IonReactHashRouter = (_a) => {
2982
- var { children } = _a, routerProps = __rest(_a, ["children"]);
2983
- return (React.createElement(HashRouter, Object.assign({}, routerProps),
2984
- React.createElement(RouterContent, null, children)));
2985
- };
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
+ }
2986
943
 
2987
944
  export { IonReactHashRouter, IonReactMemoryRouter, IonReactRouter };
2988
945
  //# sourceMappingURL=index.js.map