@ionic/react 8.8.3-nightly.20260401 → 8.8.4-dev.11775078622.1402ffa2

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
@@ -380,9 +380,7 @@ const useIonViewDidLeave = (callback, deps = []) => {
380
380
  };
381
381
 
382
382
  const NavContext = /*@__PURE__*/ React.createContext({
383
- getIonRedirect: () => undefined,
384
383
  getIonRoute: () => undefined,
385
- getPageManager: () => undefined,
386
384
  getStackManager: () => undefined,
387
385
  goBack: (route) => {
388
386
  if (typeof window !== 'undefined') {
@@ -731,6 +729,11 @@ const createRoutingComponent = (tagName, customElement) => {
731
729
  this.handleClick = (e) => {
732
730
  const { routerLink, routerDirection, routerOptions, routerAnimation } = this.props;
733
731
  if (routerLink !== undefined) {
732
+ // Allow modifier key clicks (ctrl/cmd/shift) to open the link in a new tab/window
733
+ // without triggering SPA navigation on the current page.
734
+ if (e.metaKey || e.ctrlKey || e.shiftKey) {
735
+ return;
736
+ }
734
737
  e.preventDefault();
735
738
  this.context.navigate(routerLink, routerDirection, undefined, routerAnimation, routerOptions);
736
739
  }
@@ -1137,7 +1140,18 @@ class PageManager extends React.PureComponent {
1137
1140
  super(props);
1138
1141
  this.ionPageElementRef = React.createRef();
1139
1142
  // React refs must be stable (not created inline).
1140
- this.stableMergedRefs = mergeRefs(this.ionPageElementRef, this.props.forwardedRef);
1143
+ // Wrap merged refs to add ion-page-invisible synchronously when element is created
1144
+ const baseMergedRefs = mergeRefs(this.ionPageElementRef, this.props.forwardedRef);
1145
+ this.stableMergedRefs = (node) => {
1146
+ if (node && !node.classList.contains('ion-page-invisible') && !node.classList.contains('ion-page-hidden')) {
1147
+ // Add ion-page-invisible synchronously before first paint (if in an outlet)
1148
+ // This prevents the flash that occurs when componentDidMount runs after paint
1149
+ if (this.context?.isInOutlet?.()) {
1150
+ node.classList.add('ion-page-invisible');
1151
+ }
1152
+ }
1153
+ baseMergedRefs(node);
1154
+ };
1141
1155
  /**
1142
1156
  * This binds the scope of the following methods to the class scope.
1143
1157
  * The `.bind` method returns a new function, so we need to assign it
@@ -1149,11 +1163,38 @@ class PageManager extends React.PureComponent {
1149
1163
  this.ionViewWillLeaveHandler = this.ionViewWillLeaveHandler.bind(this);
1150
1164
  this.ionViewDidLeaveHandler = this.ionViewDidLeaveHandler.bind(this);
1151
1165
  }
1166
+ parseClasses(className) {
1167
+ if (!className)
1168
+ return new Set();
1169
+ return new Set(className.split(/\s+/).filter(Boolean));
1170
+ }
1171
+ /**
1172
+ * Updates classList by diffing old/new className props.
1173
+ * Preserves framework-added classes (can-go-back, ion-page-invisible, etc.).
1174
+ */
1175
+ updateUserClasses(oldClassName, newClassName) {
1176
+ if (!this.ionPageElementRef.current)
1177
+ return;
1178
+ const oldClasses = this.parseClasses(oldClassName);
1179
+ const newClasses = this.parseClasses(newClassName);
1180
+ oldClasses.forEach((cls) => {
1181
+ if (!newClasses.has(cls)) {
1182
+ this.ionPageElementRef.current.classList.remove(cls);
1183
+ }
1184
+ });
1185
+ newClasses.forEach((cls) => {
1186
+ if (!oldClasses.has(cls)) {
1187
+ this.ionPageElementRef.current.classList.add(cls);
1188
+ }
1189
+ });
1190
+ }
1152
1191
  componentDidMount() {
1153
1192
  if (this.ionPageElementRef.current) {
1154
- if (this.context.isInOutlet()) {
1155
- this.ionPageElementRef.current.classList.add('ion-page-invisible');
1156
- }
1193
+ // Add user classes via DOM manipulation to preserve framework-added classes.
1194
+ // We only set "ion-page" in JSX; user classes are added here.
1195
+ // Note: ion-page-invisible is added in the ref callback (stableMergedRefs) to prevent flash.
1196
+ // The ref callback runs synchronously when the element is created, before the browser paints.
1197
+ this.updateUserClasses(undefined, this.props.className);
1157
1198
  this.context.registerIonPage(this.ionPageElementRef.current, this.props.routeInfo);
1158
1199
  this.ionPageElementRef.current.addEventListener('ionViewWillEnter', this.ionViewWillEnterHandler);
1159
1200
  this.ionPageElementRef.current.addEventListener('ionViewDidEnter', this.ionViewDidEnterHandler);
@@ -1161,6 +1202,11 @@ class PageManager extends React.PureComponent {
1161
1202
  this.ionPageElementRef.current.addEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler);
1162
1203
  }
1163
1204
  }
1205
+ componentDidUpdate(prevProps) {
1206
+ if (prevProps.className !== this.props.className) {
1207
+ this.updateUserClasses(prevProps.className, this.props.className);
1208
+ }
1209
+ }
1164
1210
  componentWillUnmount() {
1165
1211
  if (this.ionPageElementRef.current) {
1166
1212
  this.ionPageElementRef.current.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler);
@@ -1190,9 +1236,11 @@ class PageManager extends React.PureComponent {
1190
1236
  render() {
1191
1237
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1192
1238
  const { className, children, routeInfo, forwardedRef, ...props } = this.props;
1239
+ // Only set "ion-page" in JSX. User classes are managed via DOM in componentDidMount/componentDidUpdate
1240
+ // to preserve framework-added classes (can-go-back, ion-page-invisible, etc.) when className prop changes.
1193
1241
  return (jsx(IonLifeCycleContext.Consumer, { children: (context) => {
1194
1242
  this.ionLifeCycleContext = context;
1195
- return (jsx("div", { className: className ? `${className} ion-page` : `ion-page`, ref: this.stableMergedRefs, ...props, children: children }));
1243
+ return (jsx("div", { className: "ion-page", ref: this.stableMergedRefs, ...props, children: children }));
1196
1244
  } }));
1197
1245
  }
1198
1246
  static get contextType() {
@@ -1331,10 +1379,10 @@ class OutletPageManager extends React.Component {
1331
1379
  this.ionLifeCycleContext.ionViewDidLeave();
1332
1380
  }
1333
1381
  render() {
1334
- const { StackManager, children, routeInfo, ...props } = this.props;
1382
+ const { StackManager, children, routeInfo, id, ...props } = this.props;
1335
1383
  return (jsx(IonLifeCycleContext.Consumer, { children: (context) => {
1336
1384
  this.ionLifeCycleContext = context;
1337
- return (jsx(StackManager, { routeInfo: routeInfo, children: jsx(IonRouterOutletInner, { setRef: (val) => (this.ionRouterOutlet = val), ...props, children: children }) }));
1385
+ return (jsx(StackManager, { id: id, routeInfo: routeInfo, children: jsx(IonRouterOutletInner, { id: id, setRef: (val) => (this.ionRouterOutlet = val), ...props, children: children }) }));
1338
1386
  } }));
1339
1387
  }
1340
1388
  static get contextType() {
@@ -1345,11 +1393,13 @@ class OutletPageManager extends React.Component {
1345
1393
  class IonRouterOutletContainer extends React.Component {
1346
1394
  constructor(props) {
1347
1395
  super(props);
1396
+ this.outletId = props.id ?? `routerOutlet-${generateId('routerOutlet')}`;
1348
1397
  }
1349
1398
  render() {
1350
1399
  const StackManager = this.context.getStackManager();
1351
1400
  const { children, forwardedRef, ...props } = this.props;
1352
- return this.context.hasIonicRouter() ? (props.ionPage ? (jsx(OutletPageManager, { StackManager: StackManager, routeInfo: this.context.routeInfo, ...props, children: children })) : (jsx(StackManager, { routeInfo: this.context.routeInfo, children: jsx(IonRouterOutletInner, { ...props, forwardedRef: forwardedRef, children: children }) }))) : (jsx(IonRouterOutletInner, { ref: forwardedRef, ...this.props, children: this.props.children }));
1401
+ const outletId = props.id ?? this.outletId;
1402
+ return this.context.hasIonicRouter() ? (props.ionPage ? (jsx(OutletPageManager, { StackManager: StackManager, routeInfo: this.context.routeInfo, ...props, children: children })) : (jsx(StackManager, { routeInfo: this.context.routeInfo, id: outletId, children: jsx(IonRouterOutletInner, { ...props, id: outletId, forwardedRef: forwardedRef, children: children }) }))) : (jsx(IonRouterOutletInner, { ref: forwardedRef, ...this.props, children: this.props.children }));
1353
1403
  }
1354
1404
  static get contextType() {
1355
1405
  return NavContext;
@@ -1546,7 +1596,9 @@ const matchesTab = (pathname, href) => {
1546
1596
  if (href === undefined) {
1547
1597
  return false;
1548
1598
  }
1549
- const normalizedHref = href.endsWith('/') && href !== '/' ? href.slice(0, -1) : href;
1599
+ // Strip query string before comparing — href may contain search params (e.g., "/tabs/home?foo=bar")
1600
+ const hrefPathname = href.split('?')[0];
1601
+ const normalizedHref = hrefPathname.endsWith('/') && hrefPathname !== '/' ? hrefPathname.slice(0, -1) : hrefPathname;
1550
1602
  return pathname === normalizedHref || pathname.startsWith(normalizedHref + '/');
1551
1603
  };
1552
1604
  class IonTabBarUnwrapped extends React.PureComponent {
@@ -1638,7 +1690,7 @@ class IonTabBarUnwrapped extends React.PureComponent {
1638
1690
  const prevHref = state.tabs[prevActiveTab].currentHref;
1639
1691
  const prevRouteOptions = state.tabs[prevActiveTab].currentRouteOptions;
1640
1692
  if (activeTab !== prevActiveTab ||
1641
- prevHref !== props.routeInfo?.pathname ||
1693
+ prevHref !== (props.routeInfo?.pathname || '') + (props.routeInfo?.search || '') ||
1642
1694
  prevRouteOptions !== props.routeInfo?.routeOptions) {
1643
1695
  tabs[activeTab] = {
1644
1696
  originalHref: tabs[activeTab].originalHref,
@@ -1705,7 +1757,7 @@ class IonTabBarUnwrapped extends React.PureComponent {
1705
1757
  return (child) => {
1706
1758
  if (child != null && child.props && (child.type === IonTabButton || child.type.isTabButton)) {
1707
1759
  const href = child.props.tab === activeTab
1708
- ? this.props.routeInfo?.pathname
1760
+ ? (this.props.routeInfo?.pathname || '') + (this.props.routeInfo?.search || '')
1709
1761
  : this.state.tabs[child.props.tab].currentHref;
1710
1762
  const routeOptions = child.props.tab === activeTab
1711
1763
  ? this.props.routeInfo?.routeOptions
@@ -1830,20 +1882,6 @@ class IonRoute extends React.PureComponent {
1830
1882
  }
1831
1883
  }
1832
1884
 
1833
- class IonRedirect extends React.PureComponent {
1834
- render() {
1835
- const IonRedirectInner = this.context.getIonRedirect();
1836
- if (!this.context.hasIonicRouter() || !IonRedirect) {
1837
- console.error('You either do not have an Ionic Router package, or your router does not support using <IonRedirect>');
1838
- return null;
1839
- }
1840
- return jsx(IonRedirectInner, { ...this.props });
1841
- }
1842
- static get contextType() {
1843
- return NavContext;
1844
- }
1845
- }
1846
-
1847
1885
  const IonRouterContext = React.createContext({
1848
1886
  routeInfo: undefined, // TODO(FW-2959): type
1849
1887
  push: () => {
@@ -1852,6 +1890,9 @@ const IonRouterContext = React.createContext({
1852
1890
  back: () => {
1853
1891
  throw new Error('An Ionic Router is required for IonRouterContext');
1854
1892
  },
1893
+ navigateRoot: () => {
1894
+ throw new Error('An Ionic Router is required for IonRouterContext');
1895
+ },
1855
1896
  canGoBack: () => {
1856
1897
  throw new Error('An Ionic Router is required for IonRouterContext');
1857
1898
  },
@@ -1868,9 +1909,10 @@ function useIonRouter() {
1868
1909
  back: context.back,
1869
1910
  push: context.push,
1870
1911
  goBack: context.back,
1912
+ navigateRoot: context.navigateRoot,
1871
1913
  canGoBack: context.canGoBack,
1872
1914
  routeInfo: context.routeInfo,
1873
- }), [context.back, context.push, context.canGoBack, context.routeInfo]);
1915
+ }), [context.back, context.push, context.navigateRoot, context.canGoBack, context.routeInfo]);
1874
1916
  }
1875
1917
 
1876
1918
  class CreateAnimation extends React.PureComponent {
@@ -2240,6 +2282,7 @@ const RouteManagerContext = /*@__PURE__*/ React.createContext({
2240
2282
  findLeavingViewItemByRouteInfo: () => undefined,
2241
2283
  findViewItemByRouteInfo: () => undefined,
2242
2284
  getChildrenToRender: () => undefined,
2285
+ getViewItemsForOutlet: () => [],
2243
2286
  goBack: () => undefined,
2244
2287
  unMountViewItem: () => undefined,
2245
2288
  });
@@ -2358,7 +2401,14 @@ class LocationHistory {
2358
2401
  _replace(routeInfo) {
2359
2402
  const routeInfos = this._getRouteInfosByKey(routeInfo.tab);
2360
2403
  routeInfos && routeInfos.pop();
2361
- this.locationHistory.pop();
2404
+ // Get the current route that's being replaced
2405
+ const currentRoute = this.locationHistory[this.locationHistory.length - 1];
2406
+ // Only pop from global history if we're replacing in the same outlet context.
2407
+ // Don't pop if we're entering a nested outlet (current route has no tab, new route has a tab)
2408
+ const isEnteringNestedOutlet = currentRoute && !currentRoute.tab && !!routeInfo.tab;
2409
+ if (!isEnteringNestedOutlet) {
2410
+ this.locationHistory.pop();
2411
+ }
2362
2412
  this._add(routeInfo);
2363
2413
  }
2364
2414
  _clear() {
@@ -2390,6 +2440,20 @@ class LocationHistory {
2390
2440
  }
2391
2441
  return undefined;
2392
2442
  }
2443
+ /**
2444
+ * Returns the most recent RouteInfo in global history (excluding the current
2445
+ * entry) whose pathname matches the given value. Unlike findLastLocation,
2446
+ * this search is tab-agnostic. Used by the multi-step back detection.
2447
+ */
2448
+ findLastLocationByPathname(pathname) {
2449
+ for (let i = this.locationHistory.length - 2; i >= 0; i--) {
2450
+ const ri = this.locationHistory[i];
2451
+ if (ri && ri.pathname === pathname) {
2452
+ return ri;
2453
+ }
2454
+ }
2455
+ return undefined;
2456
+ }
2393
2457
  findLastLocation(routeInfo) {
2394
2458
  const routeInfos = this._getRouteInfosByKey(routeInfo.tab);
2395
2459
  if (routeInfos) {
@@ -2421,6 +2485,17 @@ class LocationHistory {
2421
2485
  canGoBack() {
2422
2486
  return this.locationHistory.length > 1;
2423
2487
  }
2488
+ findTabForPathname(pathname) {
2489
+ for (const tab of Object.keys(this.tabHistory)) {
2490
+ const routeInfos = this.tabHistory[tab];
2491
+ for (let i = routeInfos.length - 1; i >= 0; i--) {
2492
+ if (routeInfos[i].pathname === pathname) {
2493
+ return tab;
2494
+ }
2495
+ }
2496
+ }
2497
+ return undefined;
2498
+ }
2424
2499
  }
2425
2500
 
2426
2501
  class NavManager extends React.PureComponent {
@@ -2433,6 +2508,9 @@ class NavManager extends React.PureComponent {
2433
2508
  back: (animationBuilder) => {
2434
2509
  this.goBack(undefined, animationBuilder);
2435
2510
  },
2511
+ navigateRoot: (pathname, animationBuilder) => {
2512
+ this.props.onNavigateRoot(pathname, animationBuilder);
2513
+ },
2436
2514
  canGoBack: () => this.props.locationHistory.canGoBack(),
2437
2515
  nativeBack: () => this.props.onNativeBack(),
2438
2516
  routeInfo: this.props.routeInfo,
@@ -2441,10 +2519,8 @@ class NavManager extends React.PureComponent {
2441
2519
  goBack: this.goBack.bind(this),
2442
2520
  hasIonicRouter: () => true,
2443
2521
  navigate: this.navigate.bind(this),
2444
- getIonRedirect: this.getIonRedirect.bind(this),
2445
2522
  getIonRoute: this.getIonRoute.bind(this),
2446
2523
  getStackManager: this.getStackManager.bind(this),
2447
- getPageManager: this.getPageManager.bind(this),
2448
2524
  routeInfo: this.props.routeInfo,
2449
2525
  setCurrentTab: this.props.onSetCurrentTab,
2450
2526
  changeTab: this.props.onChangeTab,
@@ -2477,12 +2553,6 @@ class NavManager extends React.PureComponent {
2477
2553
  navigate(path, direction = 'forward', action = 'push', animationBuilder, options, tab) {
2478
2554
  this.props.onNavigate(path, action, direction, animationBuilder, options, tab);
2479
2555
  }
2480
- getPageManager() {
2481
- return PageManager;
2482
- }
2483
- getIonRedirect() {
2484
- return this.props.ionRedirect;
2485
- }
2486
2556
  getIonRoute() {
2487
2557
  return this.props.ionRoute;
2488
2558
  }
@@ -2512,10 +2582,7 @@ class ViewStacks {
2512
2582
  }
2513
2583
  }
2514
2584
  clear(outletId) {
2515
- // Give some time for the leaving views to transition before removing
2516
- return setTimeout(() => {
2517
- delete this.viewStacks[outletId];
2518
- }, 500);
2585
+ delete this.viewStacks[outletId];
2519
2586
  }
2520
2587
  getViewItemsForOutlet(outletId) {
2521
2588
  return this.viewStacks[outletId] || [];
@@ -2544,5 +2611,5 @@ class ViewStacks {
2544
2611
  }
2545
2612
  }
2546
2613
 
2547
- export { CreateAnimation, DefaultIonLifeCycleContext, IonAccordion, IonAccordionGroup, IonActionSheet, IonAlert, IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonDatetimeButton, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonInputOtp, IonInputPasswordToggle, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonLifeCycleContext, IonList, IonListHeader, IonLoading, IonMenu, IonMenuButton, IonMenuToggle, IonModal, IonNav, IonNavLink, IonNote, IonPage, IonPicker, IonPickerColumn, IonPickerColumnOption, IonPickerLegacy, IonPopover, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRedirect, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRoute, IonRouterContext, IonRouterLink, IonRouterOutlet, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSegmentContent, IonSegmentView, IonSelect, IonSelectModal, IonSelectOption, IonSkeletonText, IonSpinner, IonSplitPane, IonTab, IonTabBar, IonTabButton, IonTabs, IonTabsContext, IonText, IonTextarea, IonThumbnail, IonTitle, IonToast, IonToggle, IonToolbar, LocationHistory, NavContext, NavManager, RouteManagerContext, StackContext, ViewLifeCycleManager, ViewStacks, generateId, getConfig, getPlatforms, isPlatform, setupIonicReact, useIonActionSheet, useIonAlert, useIonLoading, useIonModal, useIonPicker, useIonPopover, useIonRouter, useIonToast, useIonViewDidEnter, useIonViewDidLeave, useIonViewWillEnter, useIonViewWillLeave, withIonLifeCycle };
2614
+ export { CreateAnimation, DefaultIonLifeCycleContext, IonAccordion, IonAccordionGroup, IonActionSheet, IonAlert, IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonDatetimeButton, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonInputOtp, IonInputPasswordToggle, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonLifeCycleContext, IonList, IonListHeader, IonLoading, IonMenu, IonMenuButton, IonMenuToggle, IonModal, IonNav, IonNavLink, IonNote, IonPage, IonPicker, IonPickerColumn, IonPickerColumnOption, IonPickerLegacy, IonPopover, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRoute, IonRouterContext, IonRouterLink, IonRouterOutlet, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSegmentContent, IonSegmentView, IonSelect, IonSelectModal, IonSelectOption, IonSkeletonText, IonSpinner, IonSplitPane, IonTab, IonTabBar, IonTabButton, IonTabs, IonTabsContext, IonText, IonTextarea, IonThumbnail, IonTitle, IonToast, IonToggle, IonToolbar, LocationHistory, NavContext, NavManager, RouteManagerContext, StackContext, ViewLifeCycleManager, ViewStacks, generateId, getConfig, getPlatforms, isPlatform, setupIonicReact, useIonActionSheet, useIonAlert, useIonLoading, useIonModal, useIonPicker, useIonPopover, useIonRouter, useIonToast, useIonViewDidEnter, useIonViewDidLeave, useIonViewWillEnter, useIonViewWillLeave, withIonLifeCycle };
2548
2615
  //# sourceMappingURL=index.js.map