diff --git a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx index 96f902a1e1..07f18ee0f4 100644 --- a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx +++ b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx @@ -15,43 +15,30 @@ export class ReactRouterViewStack extends ViewStacks { this.findViewItemByPathname = this.findViewItemByPathname.bind(this); } - createViewItem(outletId: string, reactElement: React.ReactElement, routeInfo: RouteInfo, page?: HTMLElement) { - const viewItem: ViewItem = { + createViewItem( + outletId: string, + reactElement: React.ReactElement, + routeInfo: RouteInfo, + ionPage?: HTMLElement + ): ViewItem { + const ionRoute = reactElement.type === IonRoute; + + return { id: generateId('viewItem'), outletId, - ionPageElement: page, + ionPageElement: ionPage, reactElement, - mount: true, - ionRoute: false, + ionRoute, + disableIonPageManagement: ionRoute && reactElement.props.disableIonPageManagement, + mount: false, + routeData: { + match: matchPath({ + pathname: routeInfo.pathname, + componentProps: reactElement.props, + }), + childProps: reactElement.props, + }, }; - - // const matchProps = { - // exact: reactElement.props.exact, - // path: reactElement.props.path || reactElement.props.from, - // component: reactElement.props.component, - // }; - - // const match = matchPath({ - // pathname: routeInfo.pathname, - // componentProps: reactElement.props, - // }); - - // const match = matchPath(routeInfo.pathname, matchProps); - - if (reactElement.type === IonRoute) { - viewItem.ionRoute = true; - viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement; - } - - viewItem.routeData = { - match: matchPath({ - pathname: routeInfo.pathname, - componentProps: reactElement.props, - }), - childProps: reactElement.props, - }; - - return viewItem; } getChildrenToRender(outletId: string, ionRouterOutlet: React.ReactElement, routeInfo: RouteInfo) { @@ -68,7 +55,7 @@ export class ReactRouterViewStack extends ViewStacks { }); const children = viewItems.map((viewItem) => { - let clonedChild; + let clonedChild: React.ReactNode; if (viewItem.ionRoute && !viewItem.disableIonPageManagement) { clonedChild = ( ` element reference to + * the view item with the matching route info. + */ + registerIonPage(viewItem: ViewItem, ionPage: HTMLElement) { if (viewItem) { + // TODO view doesn't check if it exists viewItem.ionPageElement = ionPage; viewItem.ionRoute = true; } - - // TODO Vue has additional vue specific logic for viewItem.matchedRoute } findViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string, updateMatch?: boolean) { @@ -139,19 +127,18 @@ export class ReactRouterViewStack extends ViewStacks { private findViewItemByPath(pathname: string, outletId?: string, mustBeIonRoute = false) { let viewItem: ViewItem | undefined; let match: ReturnType | undefined; - let viewStack: ViewItem[]; if (outletId) { - viewStack = this.getViewItemsForOutlet(outletId); + const viewStack = this.getViewItemsForOutlet(outletId); viewStack.some(matchView); if (!viewItem) { viewStack.some(matchDefaultRoute); } } else { - const viewItems = this.getAllViewItems(); - viewItems.some(matchView); + const viewStack = this.getAllViewItems(); + viewStack.some(matchView); if (!viewItem) { - viewItems.some(matchDefaultRoute); + viewStack.some(matchDefaultRoute); } } diff --git a/packages/react-router/src/ReactRouter/StackManager.tsx b/packages/react-router/src/ReactRouter/StackManager.tsx index 89e9caff4c..939ff15385 100644 --- a/packages/react-router/src/ReactRouter/StackManager.tsx +++ b/packages/react-router/src/ReactRouter/StackManager.tsx @@ -1,24 +1,23 @@ -import type { RouteInfo, StackContextState, ViewItem } from '@ionic/react'; +import type { AnimationBuilder, RouteInfo, RouterDirection, StackContextState } from '@ionic/react'; import { RouteManagerContext, StackContext, generateId, getConfig } from '@ionic/react'; +import type { ReactNode } from 'react'; import React from 'react'; import { matchPath } from 'react-router-dom'; -import { clonePageElement } from './clonePageElement'; - -// TODO(FW-2959): types - interface StackManagerProps { routeInfo: RouteInfo; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface StackManagerState {} +interface StackManagerState { + components: ReactNode[]; +} -const isViewVisible = (el: HTMLElement) => - !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden'); +const isViewVisible = (enteringEl: HTMLElement) => { + return !enteringEl.classList.contains('ion-page-invisible') && !enteringEl.classList.contains('ion-page-hidden'); +}; export class StackManager extends React.PureComponent { - id: string; // TODO pretty sure this should be a number + id: string; context!: React.ContextType; ionRouterOutlet?: React.ReactElement; routerOutletElement: HTMLIonRouterOutletElement | undefined; @@ -31,16 +30,18 @@ export class StackManager extends React.PureComponent { this.handlePageTransition(); @@ -118,17 +138,25 @@ export class StackManager extends React.PureComponent component. Transitions and lifecycle methods may not work as expected. See https://ionicframework.com/docs/react/navigation#ionpage for more information.`); @@ -194,18 +221,25 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio * route or on an initial page load (i.e. refreshing). In cases when loading * /tabs/tab-1, we need to transition the /tabs page element into the view. */ - this.transitionPage(routeInfo, enteringViewItem, leavingViewItem); + this.transition( + enteringViewItem.ionPageElement!, + leavingViewItem.ionPageElement!, + routeDirection!, + !!pushedByRoute, + false, + routeAnimation + ); } else { /** * If there is no leaving element, just show * the entering element. Wrap it in an raf - * in case ion-content's fullscreen callback + * in case IonContent's fullscreen callback * is running. Otherwise we'd have a flicker. */ requestAnimationFrame(() => enteringEl?.classList.remove('ion-page-invisible')); } - this.forceUpdate(); + // this.forceUpdate(); } // async handlePageTransition(routeInfo: RouteInfo) { @@ -324,17 +358,9 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio registerIonPage(routeInfo: RouteInfo, ionPageEl: HTMLElement) { const { id } = this; const viewItem = this.context.findViewItemByRouteInfo(routeInfo, id)!; - - if (!viewItem) { - console.log('could not find view item for route info', routeInfo, id); - } - const oldIonPageEl = viewItem.ionPageElement; - // this.context.registerIonPage(viewItem, ionPageEl); - - // this.context.registerIonPage(viewItem, ionPageEl); - // viewStacks.registerIonPage(viewItem, ionpageEl); + this.context.registerIonPage(viewItem, ionPageEl); /** * If there is a registerCallback, @@ -362,33 +388,6 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio } } - // registerIonPage(page: HTMLElement, routeInfo: RouteInfo) { - // // was findViewItemByRouteInfo - // const foundView = this.context.findViewItemByPathname(routeInfo.pathname, this.id, false); - // // const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id, true); - // console.log('calling page transition from registering ion page...', { - // routeInfo, - // page, - // foundView, - // }); - // // const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id); - // if (foundView) { - // const oldPageElement = foundView.ionPageElement; - // foundView.ionPageElement = page; - // foundView.ionRoute = true; - - // /** - // * React 18 will unmount and remount IonPage - // * elements in development mode when using createRoot. - // * This can cause duplicate page transitions to occur. - // */ - // if (oldPageElement === page) { - // return; - // } - // } - // this.handlePageTransition(routeInfo); - // } - async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) { const canStart = () => { const config = getConfig(); @@ -427,24 +426,52 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio }; const onStart = async () => { + const { id } = this; const { routeInfo } = this.props; + let { routeAnimation: animationBuilder } = routeInfo; - const propsToUse = - this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute - ? this.prevProps.routeInfo - : ({ pathname: routeInfo.pushedByRoute || '' } as any); - const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false); - const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false); + // const propsToUse = + // this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute + // ? this.prevProps.routeInfo + // : ({ pathname: routeInfo.pushedByRoute || '' } as any); - /** - * When the gesture starts, kick off - * a transition that is controlled - * via a swipe gesture. - */ - if (enteringViewItem && leavingViewItem) { - await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true); + const enteringViewItem = this.context.findViewItemByRouteInfo( + { pathname: routeInfo.pushedByRoute || '' } as RouteInfo, + id, + false + ); + const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, id, false); + + if (leavingViewItem) { + const enteringEl = enteringViewItem?.ionPageElement; + const leavingEl = leavingViewItem?.ionPageElement; + + /** + * If we are going back from a page that + * was presented using a custom animation + * we should default to using that + * unless the developer explicitly + * provided another animation. + */ + const customAnimation = enteringViewItem!.routerAnimation; + if (animationBuilder === undefined && customAnimation !== undefined) { + animationBuilder = customAnimation; + } + + leavingViewItem.routerAnimation = animationBuilder; + + await this.transition(enteringEl!, leavingEl!, 'back', this.context.canGoBack(), true, animationBuilder); } + // /** + // * When the gesture starts, kick off + // * a transition that is controlled + // * via a swipe gesture. + // */ + // if (enteringViewItem && leavingViewItem) { + // await this.transition(enteringViewItem.ionPageElement!, leavingViewItem.ionPageElement!, 'back', true, true); + // } + return Promise.resolve(); }; const onEnd = (shouldContinue: boolean) => { @@ -491,88 +518,103 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio }; } - async transitionPage( - routeInfo: RouteInfo, - enteringViewItem: ViewItem, - leavingViewItem?: ViewItem, - direction?: 'forward' | 'back', - progressAnimation = false + async transition( + enteringEl: HTMLElement, + leavingEl: HTMLElement, + direction: RouterDirection, + showGoBack: boolean, + progressAnimation: boolean, + animationBuilder?: AnimationBuilder ) { - console.log('transitionPage', { - enteringViewItem, - leavingViewItem, + console.log('transition', { + enteringEl, + leavingEl, direction, - routeInfo, }); - const runCommit = async (enteringEl: HTMLElement, leavingEl?: HTMLElement) => { - const skipTransition = this.skipTransition; + const { skipTransition, routerOutletElement } = this; + /** + * If the transition was handled + * via the swipe to go back gesture, + * then we do not want to perform + * another transition. + * + * We skip adding ion-page or ion-page-invisible + * because the entering view already exists in the DOM. + * If we added the classes, there would be a flicker where + * the view would be briefly hidden. + */ + if (skipTransition) { /** - * If the transition was handled - * via the swipe to go back gesture, - * then we do not want to perform - * another transition. - * - * We skip adding ion-page or ion-page-invisible - * because the entering view already exists in the DOM. - * If we added the classes, there would be a flicker where - * the view would be briefly hidden. + * We need to reset skipTransition before + * we call routerOutlet.commit otherwise + * the transition triggered by the swipe + * to go back gesture would reset it. In + * that case you would see a duplicate + * transition triggered by handlePageTransition + * in componentDidUpdate. */ - if (skipTransition) { - /** - * We need to reset skipTransition before - * we call routerOutlet.commit otherwise - * the transition triggered by the swipe - * to go back gesture would reset it. In - * that case you would see a duplicate - * transition triggered by handlePageTransition - * in componentDidUpdate. - */ - this.skipTransition = false; - } else { - enteringEl.classList.add('ion-page'); - enteringEl.classList.add('ion-page-invisible'); - } + this.skipTransition = false; - await routerOutlet.commit(enteringEl, leavingEl, { - duration: skipTransition || directionToUse === undefined ? 0 : undefined, - direction: directionToUse, - showGoBack: !!routeInfo.pushedByRoute, - progressAnimation, - animationBuilder: routeInfo.routeAnimation, - }); - }; - - const routerOutlet = this.routerOutletElement!; - - const routeInfoFallbackDirection = - routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root' ? undefined : routeInfo.routeDirection; - const directionToUse = direction ?? routeInfoFallbackDirection; - - if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) { - if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) { - // If a page is transitioning to another version of itself - // we clone it so we can have an animation to show - - const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, true); - if (match) { - const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML); - if (newLeavingElement) { - this.routerOutletElement.appendChild(newLeavingElement); - await runCommit(enteringViewItem.ionPageElement, newLeavingElement); - this.routerOutletElement.removeChild(newLeavingElement); - } - } else { - await runCommit(enteringViewItem.ionPageElement, undefined); - } - } else { - await runCommit(enteringViewItem.ionPageElement, leavingViewItem?.ionPageElement); - if (leavingViewItem && leavingViewItem.ionPageElement && !progressAnimation) { - leavingViewItem.ionPageElement.classList.add('ion-page-hidden'); - leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true'); - } - } + return Promise.resolve(false); } + + if (enteringEl === leavingEl) { + return Promise.resolve(false); + } + + enteringEl.classList.add('ion-page-invisible'); + + const hasRootDirection = direction === undefined || direction === 'root' || direction === 'none'; + const result = await routerOutletElement!.commit(enteringEl, leavingEl, { + /** + * replace operations result in a direction of none. + * These typically do not have need animations, so we set + * the duration to 0. However, if a developer explicitly + * passes an animationBuilder, we should assume that + * they want an animation to be played even + * though it is a replace operation. + */ + duration: hasRootDirection && animationBuilder === undefined ? 0 : undefined, + direction: direction as any, // TODO none isn't a valid direction, investigate + showGoBack, + progressAnimation, + animationBuilder, + }); + + return result; + // }; + + // const routerOutlet = this.routerOutletElement!; + + // const routeInfoFallbackDirection = + // routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root' ? undefined : routeInfo.routeDirection; + // const directionToUse = direction ?? routeInfoFallbackDirection; + + // if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) { + // if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) { + // // If a page is transitioning to another version of itself + // // we clone it so we can have an animation to show + + // const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, true); + // if (match) { + // const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML); + // if (newLeavingElement) { + // this.routerOutletElement.appendChild(newLeavingElement); + // await runCommit(enteringViewItem.ionPageElement, newLeavingElement); + // this.routerOutletElement.removeChild(newLeavingElement); + // } + // } else { + // await runCommit(enteringViewItem.ionPageElement, undefined); + // } + // } else { + // await runCommit(enteringViewItem.ionPageElement, leavingViewItem?.ionPageElement); + // if (leavingViewItem && leavingViewItem.ionPageElement && !progressAnimation) { + // leavingViewItem.ionPageElement.classList.add('ion-page-hidden'); + // leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true'); + // } + // } + // } } render() { @@ -580,10 +622,6 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio const ionRouterOutlet = React.Children.only(children) as React.ReactElement; this.ionRouterOutlet = ionRouterOutlet; - const components = this.context.getChildrenToRender(this.id, this.ionRouterOutlet, this.props.routeInfo, () => { - this.forceUpdate(); - }); - return ( {React.cloneElement( @@ -596,14 +634,17 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio if (ionRouterOutlet.props.forwardedRef) { ionRouterOutlet.props.forwardedRef.current = node; } - this.routerOutletElement = node; + if (node) { + this.routerOutletElement = node; + console.log('assigned router outlet element node...', node); + } const { ref } = ionRouterOutlet as any; if (typeof ref === 'function') { ref(node); } }, }, - components + this.state.components )} ); @@ -644,13 +685,13 @@ function matchRoute(node: React.ReactNode, routeInfo: RouteInfo) { return matchedNode; } -function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) { - const matchProps = { - exact: forceExact ? true : node.props.exact, - path: node.props.path || node.props.from, - component: node.props.component, - }; - const match = matchPath(pathname, matchProps); +// function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) { +// const matchProps = { +// exact: forceExact ? true : node.props.exact, +// path: node.props.path || node.props.from, +// component: node.props.component, +// }; +// const match = matchPath(pathname, matchProps); - return match; -} +// return match; +// } diff --git a/packages/react-router/src/ReactRouter/utils/matchPath.ts b/packages/react-router/src/ReactRouter/utils/matchPath.ts index 8d7889f43a..0d999d32fe 100644 --- a/packages/react-router/src/ReactRouter/utils/matchPath.ts +++ b/packages/react-router/src/ReactRouter/utils/matchPath.ts @@ -41,6 +41,17 @@ export const matchPath = ({ pathname, componentProps }: MatchPathOptions): false } const hasParameter = match.path.includes(':'); + if (hasParameter) { + console.log('the match path has a parameter!!', { + pathname, + url: match.url, + match + }) + } + if (hasParameter && match.url.includes(':')) { + return false; + } + if (hasParameter && pathname !== match.url) { console.log('discarding the match because it has a path parameter', { pathname, diff --git a/packages/react/src/routing/RouteManagerContext.ts b/packages/react/src/routing/RouteManagerContext.ts index 2e6a996de3..15d98d8fa4 100644 --- a/packages/react/src/routing/RouteManagerContext.ts +++ b/packages/react/src/routing/RouteManagerContext.ts @@ -20,12 +20,11 @@ export interface RouteManagerContextState { getChildrenToRender: ( outletId: string, ionRouterOutlet: React.ReactElement, - routeInfo: RouteInfo, - reRender: () => void + routeInfo: RouteInfo ) => React.ReactNode[]; goBack: () => void; unMountViewItem: (viewItem: ViewItem) => void; - registerIonPage: (routeInfo: RouteInfo, ionPage: HTMLElement) => void; + registerIonPage: (viewItem: ViewItem, ionPage: HTMLElement) => void; } // TODO(FW-2959): types @@ -37,7 +36,7 @@ export const RouteManagerContext = /*@__PURE__*/ React.createContext undefined, findLeavingViewItemByRouteInfo: () => undefined, findViewItemByRouteInfo: () => undefined, - getChildrenToRender: () => undefined as any, + getChildrenToRender: () => [], goBack: () => undefined, unMountViewItem: () => undefined, registerIonPage: () => undefined diff --git a/packages/react/src/routing/ViewItem.ts b/packages/react/src/routing/ViewItem.ts index a5736f7a3d..06c8680e14 100644 --- a/packages/react/src/routing/ViewItem.ts +++ b/packages/react/src/routing/ViewItem.ts @@ -1,3 +1,4 @@ +import type { AnimationBuilder } from '@ionic/core'; import type { ReactElement } from 'react'; export interface ViewItem { @@ -10,6 +11,7 @@ export interface ViewItem { transitionHtml?: string; outletId: string; disableIonPageManagement?: boolean; + routerAnimation?: AnimationBuilder; /** * Callback function when the view item is registered. */ diff --git a/packages/react/src/routing/ViewStacks.ts b/packages/react/src/routing/ViewStacks.ts index ae7db8bf63..690b02e1a6 100644 --- a/packages/react/src/routing/ViewStacks.ts +++ b/packages/react/src/routing/ViewStacks.ts @@ -74,7 +74,5 @@ export abstract class ViewStacks { outletId: string, ionRouterOutlet: React.ReactElement, routeInfo: RouteInfo, - reRender: () => void, - setInTransition: () => void ): React.ReactNode[]; }