mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 01:52:19 +08:00
feat(IonRouter): migrate to functional component with react router 6
Co-authored-by: Sean Perkins <13732623+sean-perkins@users.noreply.github.com>
This commit is contained in:
@ -1,16 +1,9 @@
|
|||||||
import type {
|
import type { AnimationBuilder, RouteAction, RouteInfo, RouteManagerContextState, RouterDirection } from '@ionic/react';
|
||||||
AnimationBuilder,
|
|
||||||
RouteAction,
|
|
||||||
RouteInfo,
|
|
||||||
RouteManagerContextState,
|
|
||||||
RouterDirection,
|
|
||||||
ViewItem,
|
|
||||||
} from '@ionic/react';
|
|
||||||
import { LocationHistory, NavManager, RouteManagerContext, generateId, getConfig } from '@ionic/react';
|
import { LocationHistory, NavManager, RouteManagerContext, generateId, getConfig } from '@ionic/react';
|
||||||
import type { Action as HistoryAction, Location as HistoryLocation } from 'history';
|
import type { Action as HistoryAction, Location as HistoryLocation } from 'history';
|
||||||
import React from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import type { RouteComponentProps } from 'react-router-dom';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { IonRouteInner } from './IonRouteInner';
|
import { IonRouteInner } from './IonRouteInner';
|
||||||
import { ReactRouterViewStack } from './ReactRouterViewStack';
|
import { ReactRouterViewStack } from './ReactRouterViewStack';
|
||||||
@ -21,162 +14,178 @@ export interface LocationState {
|
|||||||
routerOptions?: { as?: string; unmount?: boolean };
|
routerOptions?: { as?: string; unmount?: boolean };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IonRouteProps extends RouteComponentProps<{}, {}, LocationState> {
|
interface IonRouterProps {
|
||||||
registerHistoryListener: (cb: (location: HistoryLocation<any>, action: HistoryAction) => void) => void;
|
registerHistoryListener: (cb: (location: HistoryLocation<any>, action: HistoryAction) => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IonRouteState {
|
export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildren<IonRouterProps>) => {
|
||||||
routeInfo: RouteInfo;
|
const location = useLocation();
|
||||||
}
|
const params = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
class IonRouterInner extends React.PureComponent<IonRouteProps, IonRouteState> {
|
const didMountRef = useRef(false);
|
||||||
currentTab?: string;
|
const locationHistory = useRef(new LocationHistory());
|
||||||
exitViewFromOtherOutletHandlers: ((pathname: string) => ViewItem | undefined)[] = [];
|
const currentTab = useRef<string | undefined>(undefined);
|
||||||
incomingRouteParams?: Partial<RouteInfo>;
|
const viewStack = useRef(new ReactRouterViewStack());
|
||||||
locationHistory = new LocationHistory();
|
const incomingRouteParams = useRef<Partial<RouteInfo> | null>(null);
|
||||||
viewStack = new ReactRouterViewStack();
|
|
||||||
routeMangerContextState: RouteManagerContextState = {
|
|
||||||
canGoBack: () => this.locationHistory.canGoBack(),
|
|
||||||
clearOutlet: this.viewStack.clear,
|
|
||||||
findViewItemByPathname: this.viewStack.findViewItemByPathname,
|
|
||||||
getChildrenToRender: this.viewStack.getChildrenToRender,
|
|
||||||
goBack: () => this.handleNavigateBack(),
|
|
||||||
createViewItem: this.viewStack.createViewItem,
|
|
||||||
findViewItemByRouteInfo: this.viewStack.findViewItemByRouteInfo,
|
|
||||||
findLeavingViewItemByRouteInfo: this.viewStack.findLeavingViewItemByRouteInfo,
|
|
||||||
addViewItem: this.viewStack.add,
|
|
||||||
unMountViewItem: this.viewStack.remove,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: IonRouteProps) {
|
const [routeInfo, setRouteInfo] = useState({
|
||||||
super(props);
|
id: generateId('routeInfo'),
|
||||||
|
pathname: location.pathname,
|
||||||
|
search: location.search,
|
||||||
|
});
|
||||||
|
|
||||||
const routeInfo = {
|
useEffect(() => {
|
||||||
id: generateId('routeInfo'),
|
didMountRef.current = true;
|
||||||
pathname: this.props.location.pathname,
|
}, []);
|
||||||
search: this.props.location.search,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.locationHistory.add(routeInfo);
|
/**
|
||||||
this.handleChangeTab = this.handleChangeTab.bind(this);
|
* Triggered whenever the history changes, either through user navigation
|
||||||
this.handleResetTab = this.handleResetTab.bind(this);
|
* or programmatic changes. It transforms the raw browser history changes
|
||||||
this.handleNativeBack = this.handleNativeBack.bind(this);
|
* into `RouteInfo` objects, which are needed Ionic's animations and
|
||||||
this.handleNavigate = this.handleNavigate.bind(this);
|
* navigation patterns.
|
||||||
this.handleNavigateBack = this.handleNavigateBack.bind(this);
|
*
|
||||||
this.props.registerHistoryListener(this.handleHistoryChange.bind(this));
|
* @param location The current location object from the history.
|
||||||
this.handleSetCurrentTab = this.handleSetCurrentTab.bind(this);
|
* @param action The action that triggered the history change.
|
||||||
|
*/
|
||||||
this.state = {
|
const handleHistoryChange = (location: HistoryLocation<LocationState>, action: HistoryAction) => {
|
||||||
routeInfo,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChangeTab(tab: string, path?: string, routeOptions?: any) {
|
|
||||||
if (!path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const routeInfo = this.locationHistory.getCurrentRouteInfoForTab(tab);
|
|
||||||
const [pathname, search] = path.split('?');
|
|
||||||
if (routeInfo) {
|
|
||||||
this.incomingRouteParams = { ...routeInfo, routeAction: 'push', routeDirection: 'none' };
|
|
||||||
if (routeInfo.pathname === pathname) {
|
|
||||||
this.incomingRouteParams.routeOptions = routeOptions;
|
|
||||||
this.props.history.push(routeInfo.pathname + (routeInfo.search || ''));
|
|
||||||
} else {
|
|
||||||
this.incomingRouteParams.pathname = pathname;
|
|
||||||
this.incomingRouteParams.search = search ? '?' + search : undefined;
|
|
||||||
this.incomingRouteParams.routeOptions = routeOptions;
|
|
||||||
this.props.history.push(pathname + (search ? '?' + search : ''));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleHistoryChange(location: HistoryLocation<LocationState>, action: HistoryAction) {
|
|
||||||
let leavingLocationInfo: RouteInfo;
|
let leavingLocationInfo: RouteInfo;
|
||||||
if (this.incomingRouteParams) {
|
/**
|
||||||
if (this.incomingRouteParams.routeAction === 'replace') {
|
* A programmatic navigation was triggered.
|
||||||
leavingLocationInfo = this.locationHistory.previous();
|
* e.g., `<Redirect />`, `history.push()`, or `handleNavigate()`
|
||||||
|
*/
|
||||||
|
if (incomingRouteParams) {
|
||||||
|
/**
|
||||||
|
* The current history entry is overwritten, so the previous entry
|
||||||
|
* is the one we are leaving.
|
||||||
|
*/
|
||||||
|
if (incomingRouteParams.current?.routeAction === 'replace') {
|
||||||
|
leavingLocationInfo = locationHistory.current.previous();
|
||||||
} else {
|
} else {
|
||||||
leavingLocationInfo = this.locationHistory.current();
|
// If the action is 'push' or 'pop', we want to use the current route.
|
||||||
|
leavingLocationInfo = locationHistory.current.current();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
leavingLocationInfo = this.locationHistory.current();
|
/**
|
||||||
|
* An external navigation was triggered
|
||||||
|
* e.g., browser back/forward button or direct link
|
||||||
|
*
|
||||||
|
* The leaving location is the current route.
|
||||||
|
*/
|
||||||
|
leavingLocationInfo = locationHistory.current.current();
|
||||||
}
|
}
|
||||||
|
|
||||||
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
|
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
|
||||||
|
// Check if the URL has changed.
|
||||||
if (leavingUrl !== location.pathname) {
|
if (leavingUrl !== location.pathname) {
|
||||||
if (!this.incomingRouteParams) {
|
// An external navigation was triggered.
|
||||||
|
if (!incomingRouteParams.current) {
|
||||||
|
/**
|
||||||
|
* A `REPLACE` action can be triggered by React Router's
|
||||||
|
* `<Redirect />` component.
|
||||||
|
*/
|
||||||
if (action === 'REPLACE') {
|
if (action === 'REPLACE') {
|
||||||
this.incomingRouteParams = {
|
incomingRouteParams.current = {
|
||||||
routeAction: 'replace',
|
routeAction: 'replace',
|
||||||
routeDirection: 'none',
|
routeDirection: 'none',
|
||||||
tab: this.currentTab,
|
tab: currentTab.current,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* A `POP` action can be triggered by the browser's back/forward
|
||||||
|
* button.
|
||||||
|
*/
|
||||||
if (action === 'POP') {
|
if (action === 'POP') {
|
||||||
const currentRoute = this.locationHistory.current();
|
const currentRoute = locationHistory.current.current();
|
||||||
|
/**
|
||||||
|
* Check if the current route was "pushed" by a previous route
|
||||||
|
* (indicates a linear history path).
|
||||||
|
*/
|
||||||
if (currentRoute && currentRoute.pushedByRoute) {
|
if (currentRoute && currentRoute.pushedByRoute) {
|
||||||
const prevInfo = this.locationHistory.findLastLocation(currentRoute);
|
const prevInfo = locationHistory.current.findLastLocation(currentRoute);
|
||||||
this.incomingRouteParams = { ...prevInfo, routeAction: 'pop', routeDirection: 'back' };
|
incomingRouteParams.current = { ...prevInfo, routeAction: 'pop', routeDirection: 'back' };
|
||||||
|
// It's a non-linear history path like a direct link.
|
||||||
} else {
|
} else {
|
||||||
this.incomingRouteParams = {
|
incomingRouteParams.current = {
|
||||||
routeAction: 'pop',
|
routeAction: 'pop',
|
||||||
routeDirection: 'none',
|
routeDirection: 'none',
|
||||||
tab: this.currentTab,
|
tab: currentTab.current,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.incomingRouteParams) {
|
// Still found no params, set it to a default state of forward.
|
||||||
this.incomingRouteParams = {
|
if (!incomingRouteParams.current) {
|
||||||
|
incomingRouteParams.current = {
|
||||||
routeAction: 'push',
|
routeAction: 'push',
|
||||||
routeDirection: location.state?.direction || 'forward',
|
routeDirection: location.state?.direction || 'forward',
|
||||||
routeOptions: location.state?.routerOptions,
|
routeOptions: location.state?.routerOptions,
|
||||||
tab: this.currentTab,
|
tab: currentTab.current,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let routeInfo: RouteInfo;
|
let routeInfo: RouteInfo;
|
||||||
|
|
||||||
if (this.incomingRouteParams?.id) {
|
/**
|
||||||
|
* An existing id indicates that it's re-activating an existing route.
|
||||||
|
* e.g., tab switching or navigating back to a previous route
|
||||||
|
*/
|
||||||
|
if (incomingRouteParams.current?.id) {
|
||||||
routeInfo = {
|
routeInfo = {
|
||||||
...(this.incomingRouteParams as RouteInfo),
|
...(incomingRouteParams.current as RouteInfo),
|
||||||
lastPathname: leavingLocationInfo.pathname,
|
lastPathname: leavingLocationInfo.pathname,
|
||||||
};
|
};
|
||||||
this.locationHistory.add(routeInfo);
|
locationHistory.current.add(routeInfo);
|
||||||
|
/**
|
||||||
|
* A new route is being created since it's not re-activating
|
||||||
|
* an existing route.
|
||||||
|
*/
|
||||||
} else {
|
} else {
|
||||||
const isPushed =
|
const isPushed =
|
||||||
this.incomingRouteParams.routeAction === 'push' && this.incomingRouteParams.routeDirection === 'forward';
|
incomingRouteParams.current?.routeAction === 'push' &&
|
||||||
|
incomingRouteParams.current.routeDirection === 'forward';
|
||||||
routeInfo = {
|
routeInfo = {
|
||||||
id: generateId('routeInfo'),
|
id: generateId('routeInfo'),
|
||||||
...this.incomingRouteParams,
|
...incomingRouteParams,
|
||||||
lastPathname: leavingLocationInfo.pathname,
|
lastPathname: leavingLocationInfo.pathname, // The URL we just came from
|
||||||
pathname: location.pathname,
|
pathname: location.pathname, // The current (destination) URL
|
||||||
search: location.search,
|
search: location.search,
|
||||||
params: this.props.match.params,
|
params: params as { [key: string]: string | string[] },
|
||||||
prevRouteLastPathname: leavingLocationInfo.lastPathname,
|
prevRouteLastPathname: leavingLocationInfo.lastPathname, // The lastPathname of the route we are leaving
|
||||||
};
|
};
|
||||||
|
// It's a linear navigation.
|
||||||
if (isPushed) {
|
if (isPushed) {
|
||||||
routeInfo.tab = leavingLocationInfo.tab;
|
routeInfo.tab = leavingLocationInfo.tab;
|
||||||
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
|
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
|
||||||
|
// Triggered by a browser back button or handleNavigateBack.
|
||||||
} else if (routeInfo.routeAction === 'pop') {
|
} else if (routeInfo.routeAction === 'pop') {
|
||||||
const r = this.locationHistory.findLastLocation(routeInfo);
|
// Find the route that pushed this one.
|
||||||
|
const r = locationHistory.current.findLastLocation(routeInfo);
|
||||||
routeInfo.pushedByRoute = r?.pushedByRoute;
|
routeInfo.pushedByRoute = r?.pushedByRoute;
|
||||||
|
// Navigating to a new tab.
|
||||||
} else if (routeInfo.routeAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
|
} else if (routeInfo.routeAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
|
||||||
// If we are switching tabs grab the last route info for the tab and use its pushedByRoute
|
/**
|
||||||
const lastRoute = this.locationHistory.getCurrentRouteInfoForTab(routeInfo.tab);
|
* If we are switching tabs grab the last route info for the
|
||||||
|
* tab and use its `pushedByRoute`.
|
||||||
|
*/
|
||||||
|
const lastRoute = locationHistory.current.getCurrentRouteInfoForTab(routeInfo.tab);
|
||||||
|
// This helps maintain correct back stack behavior within tabs.
|
||||||
routeInfo.pushedByRoute = lastRoute?.pushedByRoute;
|
routeInfo.pushedByRoute = lastRoute?.pushedByRoute;
|
||||||
|
// Triggered by `history.replace()` or a `<Redirect />` component, etc.
|
||||||
} else if (routeInfo.routeAction === 'replace') {
|
} else if (routeInfo.routeAction === 'replace') {
|
||||||
// Make sure to set the lastPathname, etc.. to the current route so the page transitions out
|
/**
|
||||||
const currentRouteInfo = this.locationHistory.current();
|
* Make sure to set the `lastPathname`, etc.. to the current route
|
||||||
|
* so the page transitions out.
|
||||||
|
*/
|
||||||
|
const currentRouteInfo = locationHistory.current.current();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If going from /home to /child, then replacing from
|
* Special handling for `replace` to ensure correct `pushedByRoute`
|
||||||
* /child to /home, we don't want the route info to
|
* and `lastPathname`.
|
||||||
* say that /home was pushed by /home which is not correct.
|
*
|
||||||
|
* If going from `/home` to `/child`, then replacing from
|
||||||
|
* `/child` to `/home`, we don't want the route info to
|
||||||
|
* say that `/home` was pushed by `/home` which is not correct.
|
||||||
*/
|
*/
|
||||||
const currentPushedBy = currentRouteInfo?.pushedByRoute;
|
const currentPushedBy = currentRouteInfo?.pushedByRoute;
|
||||||
const pushedByRoute =
|
const pushedByRoute =
|
||||||
@ -198,58 +207,127 @@ class IonRouterInner extends React.PureComponent<IonRouteProps, IonRouteState> {
|
|||||||
routeInfo.routeAnimation = routeInfo.routeAnimation || currentRouteInfo?.routeAnimation;
|
routeInfo.routeAnimation = routeInfo.routeAnimation || currentRouteInfo?.routeAnimation;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.locationHistory.add(routeInfo);
|
locationHistory.current.add(routeInfo);
|
||||||
}
|
}
|
||||||
|
setRouteInfo(routeInfo);
|
||||||
this.setState({
|
|
||||||
routeInfo,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.incomingRouteParams = undefined;
|
// Reset for the next navigation.
|
||||||
}
|
incomingRouteParams.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* history@4.x uses goBack(), history@5.x uses back()
|
* Resets the specified tab to its initial, root route.
|
||||||
* TODO: If support for React Router <=5 is dropped
|
*
|
||||||
* this logic is no longer needed. We can just
|
* @param tab The tab to reset.
|
||||||
* assume back() is available.
|
* @param originalHref The original href for the tab.
|
||||||
|
* @param originalRouteOptions The original route options for the tab.
|
||||||
*/
|
*/
|
||||||
handleNativeBack() {
|
const handleResetTab = (tab: string, originalHref: string, originalRouteOptions: any) => {
|
||||||
const history = this.props.history as any;
|
const routeInfo = locationHistory.current.getFirstRouteInfoForTab(tab);
|
||||||
const goBack = history.goBack || history.back;
|
if (routeInfo) {
|
||||||
goBack();
|
const newRouteInfo = { ...routeInfo };
|
||||||
}
|
newRouteInfo.pathname = originalHref;
|
||||||
|
newRouteInfo.routeOptions = originalRouteOptions;
|
||||||
handleNavigate(
|
incomingRouteParams.current = { ...newRouteInfo, routeAction: 'pop', routeDirection: 'back' };
|
||||||
path: string,
|
navigate(newRouteInfo.pathname + (newRouteInfo.search || ''));
|
||||||
routeAction: RouteAction,
|
|
||||||
routeDirection?: RouterDirection,
|
|
||||||
routeAnimation?: AnimationBuilder,
|
|
||||||
routeOptions?: any,
|
|
||||||
tab?: string
|
|
||||||
) {
|
|
||||||
this.incomingRouteParams = Object.assign(this.incomingRouteParams || {}, {
|
|
||||||
routeAction,
|
|
||||||
routeDirection,
|
|
||||||
routeOptions,
|
|
||||||
routeAnimation,
|
|
||||||
tab,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (routeAction === 'push') {
|
|
||||||
this.props.history.push(path);
|
|
||||||
} else {
|
|
||||||
this.props.history.replace(path);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
handleNavigateBack(defaultHref: string | RouteInfo = '/', routeAnimation?: AnimationBuilder) {
|
/**
|
||||||
|
* Handles tab changes.
|
||||||
|
*
|
||||||
|
* @param tab The tab to switch to.
|
||||||
|
* @param path The new path for the tab.
|
||||||
|
* @param routeOptions Additional route options.
|
||||||
|
*/
|
||||||
|
const handleChangeTab = (tab: string, path?: string, routeOptions?: any) => {
|
||||||
|
if (!path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeInfo = locationHistory.current.getCurrentRouteInfoForTab(tab);
|
||||||
|
const [pathname, search] = path.split('?');
|
||||||
|
// User has navigated to the current tab before.
|
||||||
|
if (routeInfo) {
|
||||||
|
const routeParams = {
|
||||||
|
...routeInfo,
|
||||||
|
routeAction: 'push' as RouteAction,
|
||||||
|
routeDirection: 'none' as RouterDirection,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* User is navigating to the same tab.
|
||||||
|
* e.g., `/tabs/home` → `/tabs/home`
|
||||||
|
*/
|
||||||
|
if (routeInfo.pathname === pathname) {
|
||||||
|
incomingRouteParams.current = {
|
||||||
|
...routeParams,
|
||||||
|
routeOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
navigate(routeInfo.pathname + (routeInfo.search || ''));
|
||||||
|
/**
|
||||||
|
* User is navigating to a different tab.
|
||||||
|
* e.g., `/tabs/home` → `/tabs/settings`
|
||||||
|
*/
|
||||||
|
} else {
|
||||||
|
incomingRouteParams.current = {
|
||||||
|
...routeParams,
|
||||||
|
pathname,
|
||||||
|
search: search ? '?' + search : undefined,
|
||||||
|
routeOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
navigate(pathname + (search ? '?' + search : ''));
|
||||||
|
}
|
||||||
|
// User has not navigated to this tab before.
|
||||||
|
} else {
|
||||||
|
handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current active tab in `locationHistory`.
|
||||||
|
* This is crucial for maintaining tab history since each tab has
|
||||||
|
* its own navigation stack.
|
||||||
|
*
|
||||||
|
* @param tab The tab to set as active.
|
||||||
|
*/
|
||||||
|
const handleSetCurrentTab = (tab: string) => {
|
||||||
|
currentTab.current = tab;
|
||||||
|
const ri = { ...locationHistory.current.current() };
|
||||||
|
if (ri.tab !== tab) {
|
||||||
|
ri.tab = tab;
|
||||||
|
locationHistory.current.update(ri);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the native back button press.
|
||||||
|
* It's usually called when a user presses the platform-native back action.
|
||||||
|
*/
|
||||||
|
const handleNativeBack = () => {
|
||||||
|
navigate(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to manage the back navigation within the Ionic React's routing
|
||||||
|
* system. It's deeply integrated with Ionic's view lifecycle, animations,
|
||||||
|
* and its custom history tracking (`locationHistory`) to provide a
|
||||||
|
* native-like transition and maintain correct application state.
|
||||||
|
*
|
||||||
|
* @param defaultHref The fallback URL to navigate to if there's no
|
||||||
|
* previous entry in the `locationHistory` stack.
|
||||||
|
* @param routeAnimation A custom animation builder to override the
|
||||||
|
* default "back" animation.
|
||||||
|
*/
|
||||||
|
const handleNavigateBack = (defaultHref: string | RouteInfo = '/', routeAnimation?: AnimationBuilder) => {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref' as any);
|
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref' as any);
|
||||||
const routeInfo = this.locationHistory.current();
|
const routeInfo = locationHistory.current.current();
|
||||||
|
// It's a linear navigation.
|
||||||
if (routeInfo && routeInfo.pushedByRoute) {
|
if (routeInfo && routeInfo.pushedByRoute) {
|
||||||
const prevInfo = this.locationHistory.findLastLocation(routeInfo);
|
const prevInfo = locationHistory.current.findLastLocation(routeInfo);
|
||||||
if (prevInfo) {
|
if (prevInfo) {
|
||||||
/**
|
/**
|
||||||
* This needs to be passed to handleNavigate
|
* This needs to be passed to handleNavigate
|
||||||
@ -257,12 +335,16 @@ class IonRouterInner extends React.PureComponent<IonRouteProps, IonRouteState> {
|
|||||||
* will be overridden.
|
* will be overridden.
|
||||||
*/
|
*/
|
||||||
const incomingAnimation = routeAnimation || routeInfo.routeAnimation;
|
const incomingAnimation = routeAnimation || routeInfo.routeAnimation;
|
||||||
this.incomingRouteParams = {
|
incomingRouteParams.current = {
|
||||||
...prevInfo,
|
...prevInfo,
|
||||||
routeAction: 'pop',
|
routeAction: 'pop',
|
||||||
routeDirection: 'back',
|
routeDirection: 'back',
|
||||||
routeAnimation: incomingAnimation,
|
routeAnimation: incomingAnimation,
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Check if it's a simple linear back navigation (not tabbed).
|
||||||
|
* e.g., `/home` → `/settings` → back to `/home`
|
||||||
|
*/
|
||||||
if (
|
if (
|
||||||
routeInfo.lastPathname === routeInfo.pushedByRoute ||
|
routeInfo.lastPathname === routeInfo.pushedByRoute ||
|
||||||
/**
|
/**
|
||||||
@ -273,68 +355,98 @@ class IonRouterInner extends React.PureComponent<IonRouteProps, IonRouteState> {
|
|||||||
*/
|
*/
|
||||||
(prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '')
|
(prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '')
|
||||||
) {
|
) {
|
||||||
/**
|
navigate(-1);
|
||||||
* history@4.x uses goBack(), history@5.x uses back()
|
|
||||||
* TODO: If support for React Router <=5 is dropped
|
|
||||||
* this logic is no longer needed. We can just
|
|
||||||
* assume back() is available.
|
|
||||||
*/
|
|
||||||
const history = this.props.history as any;
|
|
||||||
const goBack = history.goBack || history.back;
|
|
||||||
goBack();
|
|
||||||
} else {
|
} else {
|
||||||
this.handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
|
/**
|
||||||
|
* It's a non-linear back navigation.
|
||||||
|
* e.g., direct link or tab switch or nested navigation with redirects
|
||||||
|
*/
|
||||||
|
handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* `pushedByRoute` exists, but no corresponding previous entry in
|
||||||
|
* the history stack.
|
||||||
|
*/
|
||||||
} else {
|
} else {
|
||||||
this.handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
|
handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* No `pushedByRoute`
|
||||||
|
* e.g., initial page load
|
||||||
|
*/
|
||||||
} else {
|
} else {
|
||||||
this.handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
|
handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to programmatically navigate through the app.
|
||||||
|
*
|
||||||
|
* @param path The path to navigate to.
|
||||||
|
* @param routeAction The action to take (push, replace, etc.).
|
||||||
|
* @param routeDirection The direction of the navigation (forward,
|
||||||
|
* back, etc.).
|
||||||
|
* @param routeAnimation The animation to use for the transition.
|
||||||
|
* @param routeOptions Additional options for the route.
|
||||||
|
* @param tab The tab to navigate to, if applicable.
|
||||||
|
*/
|
||||||
|
const handleNavigate = (
|
||||||
|
path: string,
|
||||||
|
routeAction: RouteAction,
|
||||||
|
routeDirection?: RouterDirection,
|
||||||
|
routeAnimation?: AnimationBuilder,
|
||||||
|
routeOptions?: any,
|
||||||
|
tab?: string
|
||||||
|
) => {
|
||||||
|
incomingRouteParams.current = Object.assign(incomingRouteParams.current || {}, {
|
||||||
|
routeAction,
|
||||||
|
routeDirection,
|
||||||
|
routeOptions,
|
||||||
|
routeAnimation,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(path, { replace: routeAction !== 'push' });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!didMountRef.current) {
|
||||||
|
locationHistory.current.add(routeInfo);
|
||||||
|
|
||||||
|
registerHistoryListener(handleHistoryChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResetTab(tab: string, originalHref: string, originalRouteOptions: any) {
|
const routeMangerContextValue: RouteManagerContextState = {
|
||||||
const routeInfo = this.locationHistory.getFirstRouteInfoForTab(tab);
|
canGoBack: () => locationHistory.current.canGoBack(),
|
||||||
if (routeInfo) {
|
clearOutlet: viewStack.current.clear,
|
||||||
const newRouteInfo = { ...routeInfo };
|
findViewItemByPathname: viewStack.current.findViewItemByPathname,
|
||||||
newRouteInfo.pathname = originalHref;
|
getChildrenToRender: viewStack.current.getChildrenToRender,
|
||||||
newRouteInfo.routeOptions = originalRouteOptions;
|
goBack: () => handleNavigateBack(),
|
||||||
this.incomingRouteParams = { ...newRouteInfo, routeAction: 'pop', routeDirection: 'back' };
|
createViewItem: viewStack.current.createViewItem,
|
||||||
this.props.history.push(newRouteInfo.pathname + (newRouteInfo.search || ''));
|
findViewItemByRouteInfo: viewStack.current.findViewItemByRouteInfo,
|
||||||
}
|
findLeavingViewItemByRouteInfo: viewStack.current.findLeavingViewItemByRouteInfo,
|
||||||
}
|
addViewItem: viewStack.current.add,
|
||||||
|
unMountViewItem: viewStack.current.remove,
|
||||||
|
};
|
||||||
|
|
||||||
handleSetCurrentTab(tab: string) {
|
return (
|
||||||
this.currentTab = tab;
|
<RouteManagerContext.Provider value={routeMangerContextValue}>
|
||||||
const ri = { ...this.locationHistory.current() };
|
<NavManager
|
||||||
if (ri.tab !== tab) {
|
ionRoute={IonRouteInner}
|
||||||
ri.tab = tab;
|
ionRedirect={{}}
|
||||||
this.locationHistory.update(ri);
|
stackManager={StackManager}
|
||||||
}
|
routeInfo={routeInfo}
|
||||||
}
|
onNativeBack={handleNativeBack}
|
||||||
|
onNavigateBack={handleNavigateBack}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
|
onSetCurrentTab={handleSetCurrentTab}
|
||||||
|
onChangeTab={handleChangeTab}
|
||||||
|
onResetTab={handleResetTab}
|
||||||
|
locationHistory={locationHistory.current}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NavManager>
|
||||||
|
</RouteManagerContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<RouteManagerContext.Provider value={this.routeMangerContextState}>
|
|
||||||
<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}
|
|
||||||
</NavManager>
|
|
||||||
</RouteManagerContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const IonRouter = withRouter(IonRouterInner);
|
|
||||||
IonRouter.displayName = 'IonRouter';
|
IonRouter.displayName = 'IonRouter';
|
||||||
|
Reference in New Issue
Block a user