diff --git a/CHANGELOG.md b/CHANGELOG.md index bcedc5b7e8..08698b8343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## [4.11.6](https://github.com/ionic-team/ionic/compare/v4.11.5...v4.11.6) (2019-12-11) + +### Bug Fixes + +* **react:** don't show back button when not appropriate ([684293d](https://github.com/ionic-team/ionic/commit/684293ddbf1ad4edce590d56f7ff66fcd6c817a5)) +* **react:** first render performance improvements ([1c7d1e5](https://github.com/ionic-team/ionic/commit/1c7d1e5cf1ad7e53ebbee2566e8fa89f567f7fb5)) +* **react:** fix refs for controllers, overlays, ionpage, and ionrouteroutlet, fixes [#19924](https://github.com/ionic-team/ionic/issues/19924) ([#20012](https://github.com/ionic-team/ionic/issues/20012)) ([eef55bb](https://github.com/ionic-team/ionic/commit/eef55bb0072a9e54b1fd7d1c8c69e7fd43b2a5c5)) +* **react:** support for 'root' router direction, fixes [#19982](https://github.com/ionic-team/ionic/issues/19982) ([#20052](https://github.com/ionic-team/ionic/issues/20052)) ([e116712](https://github.com/ionic-team/ionic/commit/e1167122758b23221935e897bcd65839b75c59aa)) +* **react:** support navigating to same page and route updates in IonRouterOutlet, fixes [#19891](https://github.com/ionic-team/ionic/issues/19891), [#19892](https://github.com/ionic-team/ionic/issues/19892), [#19986](https://github.com/ionic-team/ionic/issues/19986) ([f9bf8db](https://github.com/ionic-team/ionic/commit/f9bf8dbe6f952ee53b6b213a4c0d043d25f49b93)) + +### Upgrade Note + +If you run into a "Property 'translate' is missing in type" error building after updating to 4.11.6, update your React Typings library to the latest: + +npm i @types/react@latest @types/react-dom@latest + # [5.0.0-beta.1](https://github.com/ionic-team/ionic/compare/v5.0.0-beta.0...v5.0.0-beta.1) (2019-11-20) diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 80894d8e61..ef7d269004 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -50,7 +50,7 @@ "@ionic/core": "5.0.0-beta.1", "@ionic/react": "5.0.0-beta.1", "@types/jest": "^23.3.9", - "@types/node": "12.6.9", + "@types/node": "^12.12.14", "@types/react": "^16.9.2", "@types/react-dom": "^16.9.0", "@types/react-router": "^5.0.3", @@ -70,7 +70,7 @@ "tslint": "^5.20.0", "tslint-ionic-rules": "0.0.21", "tslint-react": "^4.1.0", - "typescript": "3.5.3" + "typescript": "^3.7.2" }, "jest": { "preset": "ts-jest", diff --git a/packages/react-router/src/ReactRouter/IonRouteAction.ts b/packages/react-router/src/ReactRouter/IonRouteAction.ts new file mode 100644 index 0000000000..7e03063166 --- /dev/null +++ b/packages/react-router/src/ReactRouter/IonRouteAction.ts @@ -0,0 +1 @@ +export type IonRouteAction = 'push' | 'replace' | 'pop'; diff --git a/packages/react-router/src/ReactRouter/NavManager.tsx b/packages/react-router/src/ReactRouter/NavManager.tsx index a01bc7873b..98dcf602bc 100644 --- a/packages/react-router/src/ReactRouter/NavManager.tsx +++ b/packages/react-router/src/ReactRouter/NavManager.tsx @@ -4,11 +4,12 @@ import { Location as HistoryLocation, UnregisterCallback } from 'history'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { IonRouteAction } from './IonRouteAction'; import { StackManager } from './StackManager'; interface NavManagerProps extends RouteComponentProps { onNavigateBack: (defaultHref?: string) => void; - onNavigate: (type: 'push' | 'replace', path: string, state?: any) => void; + onNavigate: (ionRouteAction: IonRouteAction, path: string, state?: any) => void; } export class NavManager extends React.Component { @@ -24,8 +25,7 @@ export class NavManager extends React.Component { return; }, // overridden in View for each IonPage - tabNavigate: this.tabNavigate.bind(this) + registerIonPage: () => { return; } // overridden in View for each IonPage }; this.listenUnregisterCallback = this.props.history.listen((location: HistoryLocation) => { @@ -53,12 +53,8 @@ export class NavManager extends React.Component void; + syncRoute: (id: string, route: any) => void; hideView: (viewId: string) => void; viewStacks: ViewStacks; setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => void; removeViewStack: (stack: string) => void; + getRoute: (id: string) => any; } export const RouteManagerContext = /*@__PURE__*/React.createContext({ viewStacks: new ViewStacks(), syncView: () => { navContextNotFoundError(); }, + syncRoute: () => { navContextNotFoundError(); }, hideView: () => { navContextNotFoundError(); }, setupIonRouter: () => Promise.reject(navContextNotFoundError()), - removeViewStack: () => { navContextNotFoundError(); } + removeViewStack: () => { navContextNotFoundError(); }, + getRoute: () => { navContextNotFoundError(); } }); function navContextNotFoundError() { diff --git a/packages/react-router/src/ReactRouter/Router.tsx b/packages/react-router/src/ReactRouter/Router.tsx index 2da67dc729..78ce6ab44b 100644 --- a/packages/react-router/src/ReactRouter/Router.tsx +++ b/packages/react-router/src/ReactRouter/Router.tsx @@ -7,6 +7,7 @@ import { RouteComponentProps, matchPath, withRouter } from 'react-router-dom'; import { generateId, isDevMode } from '../utils'; import { LocationHistory } from '../utils/LocationHistory'; +import { IonRouteAction } from './IonRouteAction'; import { IonRouteData } from './IonRouteData'; import { NavManager } from './NavManager'; import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext'; @@ -15,14 +16,19 @@ import { ViewStack, ViewStacks } from './ViewStacks'; interface RouteManagerState extends RouteManagerContextState { location?: HistoryLocation; - action?: HistoryAction; + action?: IonRouteAction; } class RouteManager extends React.Component { listenUnregisterCallback: UnregisterCallback | undefined; activeIonPageId?: string; - currentDirection?: RouterDirection; + currentIonRouteAction?: IonRouteAction; + currentRouteDirection?: RouterDirection; locationHistory = new LocationHistory(); + routes: { [key: string]: React.ReactElement; } = {}; + ionPageElements: { [key: string]: HTMLElement; } = {}; + routerOutlets: { [key: string]: HTMLIonRouterOutletElement; } = {}; + firstRender = true; constructor(props: RouteComponentProps) { super(props); @@ -34,7 +40,9 @@ class RouteManager extends React.Component { const { view: enteringView, viewStack: enteringViewStack, match } = viewStacks.findViewInfoByLocation(location, key); if (!enteringView || !enteringViewStack) { return; } + leavingView = viewStacks.findViewInfoById(this.activeIonPageId).view; if (enteringView.isIonRoute) { enteringView.show = true; enteringView.mount = true; enteringView.routeData.match = match!; + shouldTransitionPage = true; this.activeIonPageId = enteringView.id; if (leavingView) { - if (direction === 'forward') { - if (action === 'PUSH') { - /** - * If the page is being pushed into the stack by another view, - * record the view that originally directed to the new view for back button purposes. - */ - enteringView.prevId = leavingView.id; - } else if (action === 'POP') { - direction = leavingView.prevId === enteringView.id ? 'back' : 'none'; - } else { - direction = direction || 'back'; - leavingView.mount = false; - } - } - if (direction === 'back' || action === 'REPLACE') { + if (action === 'push' && direction === 'forward') { + /** + * If the page is being pushed into the stack by another view, + * record the view that originally directed to the new view for back button purposes. + */ + enteringView.prevId = leavingView.id; + } else if (action === 'pop' || action === 'replace') { leavingView.mount = false; this.removeOrphanedViews(enteringView, enteringViewStack); } + + leavingViewHtml = enteringView.id === leavingView.id ? this.ionPageElements[leavingView.id].outerHTML : undefined; } else { // If there is not a leavingView, then we shouldn't provide a direction direction = undefined; } + } else { enteringView.show = true; enteringView.mount = true; @@ -151,28 +174,39 @@ class RouteManager extends React.Component { - const { view: enteringView, viewStack } = this.state.viewStacks.findViewInfoById(this.activeIonPageId); - if (enteringView && viewStack) { - const enteringEl = enteringView.ionPageElement ? enteringView.ionPageElement : undefined; - const leavingEl = leavingView && leavingView.ionPageElement ? leavingView.ionPageElement : undefined; - if (enteringEl) { - // Don't animate from an empty view - const navDirection = leavingEl && leavingEl.innerHTML === '' ? undefined : direction === 'none' ? undefined : direction; - const shouldGoBack = !!enteringView.prevId || !!this.locationHistory.previous(); - this.commitView( - enteringEl!, - leavingEl!, - viewStack.routerOutlet, - navDirection, - shouldGoBack); - } else if (leavingEl) { - leavingEl.classList.add('ion-page-hidden'); - leavingEl.setAttribute('aria-hidden', 'true'); + if (shouldTransitionPage) { + const { view: enteringView, viewStack } = this.state.viewStacks.findViewInfoById(this.activeIonPageId); + if (enteringView && viewStack) { + const enteringEl = this.ionPageElements[enteringView.id]; + const leavingEl = leavingView && this.ionPageElements[leavingView.id]; + if (enteringEl) { + let navDirection: NavDirection | undefined; + if (leavingEl && leavingEl.innerHTML === '') { + // Don't animate from an empty view + navDirection = undefined; + } else if (direction === 'none' || direction === 'root') { + navDirection = undefined; + } else { + navDirection = direction; + } + const shouldGoBack = !!enteringView.prevId; + const routerOutlet = this.routerOutlets[viewStack.id]; + this.commitView( + enteringEl!, + leavingEl!, + routerOutlet, + navDirection, + shouldGoBack, + leavingViewHtml); + } else if (leavingEl) { + leavingEl.classList.add('ion-page-hidden'); + leavingEl.setAttribute('aria-hidden', 'true'); + } } // Warn if an IonPage was not eventually rendered in Dev Mode if (isDevMode()) { - if (enteringView.routeData.match!.url !== location.pathname) { + if (enteringView && enteringView.routeData.match!.url !== location.pathname) { setTimeout(() => { const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId); if (view!.routeData.match!.url !== location.pathname) { @@ -195,12 +229,13 @@ class RouteManager extends React.Component { - views.push(createViewItem(child, this.props.history.location)); + const routeId = generateId(); + this.routes[routeId] = child; + views.push(createViewItem(child, routeId, this.props.history.location)); }); this.registerViewStack(id, activeId, views, routerOutlet, this.props.location); - function createViewItem(child: React.ReactElement, location: HistoryLocation) { + function createViewItem(child: React.ReactElement, routeId: string, location: HistoryLocation) { const viewId = generateId(); const key = generateId(); - const route = child; + + // const route = child; const matchProps = { exact: child.props.exact, path: child.props.path || child.props.from, @@ -234,7 +272,7 @@ class RouteManager extends React.Component { - if (routerOutlet.componentOnReady) { - routerOutlet.dispatchEvent(new Event('routerOutletReady')); - return; - } else { - setTimeout(() => { - waitUntilReady(); - }, 0); - } - }; - - await waitUntilReady(); const canStart = () => { const config = getConfig(); @@ -310,70 +336,115 @@ class RouteManager extends React.Component { - const viewStacks = Object.assign(new ViewStacks(), state.viewStacks); - const { view } = viewStacks.findViewInfoById(viewId); - - view!.ionPageElement = page; - view!.isIonRoute = true; - - return { - viewStacks - }; - - }, () => { - this.setActiveView(this.state.location || this.props.location, this.state.action!); - }); - } - - private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean) { - - if (enteringEl === leavingEl) { - return; - } - - await ionRouterOuter.commit(enteringEl, leavingEl, { - deepWait: true, - duration: direction === undefined ? 0 : undefined, - direction, - showGoBack, - progressAnimation: false - }); - - if (leavingEl && (enteringEl !== leavingEl)) { - /** add hidden attributes */ - leavingEl.classList.add('ion-page-hidden'); - leavingEl.setAttribute('aria-hidden', 'true'); + const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks); + const { view } = viewStacks.findViewInfoById(viewId); + if (view) { + view.isIonRoute = true; + this.ionPageElements[view.id] = page; + this.setActiveView(this.state.location || this.props.location, this.state.action!, viewStacks); } } - handleNavigate(type: 'push' | 'replace', path: string, direction?: RouterDirection) { - this.currentDirection = direction; - if (type === 'push') { - this.props.history.push(path); + syncRoute(_id: string, routerOutlet: any) { + const ionRouterOutlet = React.Children.only(routerOutlet) as React.ReactElement; + + React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => { + for (const routeKey in this.routes) { + const route = this.routes[routeKey]; + if (route.props.path === child.props.path) { + this.routes[routeKey] = child; + } + } + }); + } + + private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOutlet: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean, leavingViewHtml?: string) { + if (!this.firstRender) { + + if (!('componentOnReady' in ionRouterOutlet)) { + await waitUntilRouterOutletReady(ionRouterOutlet); + } + + if ((enteringEl === leavingEl) && direction && leavingViewHtml) { + // If a page is transitioning to another version of itself + // we clone it so we can have an animation to show + const newLeavingElement = clonePageElement(leavingViewHtml); + ionRouterOutlet.appendChild(newLeavingElement); + await ionRouterOutlet.commit(enteringEl, newLeavingElement, { + deepWait: true, + duration: direction === undefined ? 0 : undefined, + direction, + showGoBack, + progressAnimation: false + }); + ionRouterOutlet.removeChild(newLeavingElement); + } else { + await ionRouterOutlet.commit(enteringEl, leavingEl, { + deepWait: true, + duration: direction === undefined ? 0 : undefined, + direction, + showGoBack, + progressAnimation: false + }); + } + + if (leavingEl && (enteringEl !== leavingEl)) { + /** add hidden attributes */ + leavingEl.classList.add('ion-page-hidden'); + leavingEl.setAttribute('aria-hidden', 'true'); + } } else { - this.props.history.replace(path); + enteringEl.classList.remove('ion-page-invisible'); + enteringEl.style.zIndex = '101'; + this.firstRender = false; + } + } + + handleNavigate(ionRouteAction: IonRouteAction, path: string, direction?: RouterDirection) { + this.currentIonRouteAction = ionRouteAction; + switch (ionRouteAction) { + case 'push': + this.currentRouteDirection = direction; + this.props.history.push(path); + break; + case 'pop': + this.currentRouteDirection = direction || 'back'; + this.props.history.replace(path); + break; + case 'replace': + this.currentRouteDirection = 'none'; + this.props.history.replace(path); + break; } } navigateBack(defaultHref?: string) { - const { view: activeIonPage } = this.state.viewStacks.findViewInfoById(this.activeIonPageId); - if (activeIonPage) { - const { view: enteringView } = this.state.viewStacks.findViewInfoById(activeIonPage.prevId); - if (enteringView) { - const lastLocation = this.locationHistory.findLastLocationByUrl(enteringView.routeData.match!.url); - if (lastLocation) { - this.handleNavigate('replace', lastLocation.pathname + lastLocation.search, 'back'); + const { view: leavingView } = this.state.viewStacks.findViewInfoById(this.activeIonPageId); + if (leavingView) { + if (leavingView.id === leavingView.prevId) { + const previousLocation = this.locationHistory.previous(); + if (previousLocation) { + this.handleNavigate('pop', previousLocation.pathname + previousLocation.search); } else { - this.handleNavigate('replace', enteringView.routeData.match!.url, 'back'); + defaultHref && this.handleNavigate('pop', defaultHref); } } else { - const currentLocation = this.locationHistory.previous(); - if (currentLocation) { - this.handleNavigate('replace', currentLocation.pathname + currentLocation.search, 'back'); + const { view: enteringView } = this.state.viewStacks.findViewInfoById(leavingView.prevId); + if (enteringView) { + const lastLocation = this.locationHistory.findLastLocationByUrl(enteringView.routeData.match!.url); + if (lastLocation) { + this.handleNavigate('pop', lastLocation.pathname + lastLocation.search); + } else { + this.handleNavigate('pop', enteringView.routeData.match!.url); + } } else { - if (defaultHref) { - this.handleNavigate('replace', defaultHref, 'back'); + const currentLocation = this.locationHistory.previous(); + if (currentLocation) { + this.handleNavigate('pop', currentLocation.pathname + currentLocation.search); + } else { + if (defaultHref) { + this.handleNavigate('pop', defaultHref); + } } } } @@ -399,5 +470,28 @@ class RouteManager extends React.Component { + waitUntilRouterOutletReady(ionRouterOutlet); + }, 0); + } +} + export const RouteManagerWithRouter = withRouter(RouteManager); RouteManagerWithRouter.displayName = 'RouteManager'; diff --git a/packages/react-router/src/ReactRouter/StackManager.tsx b/packages/react-router/src/ReactRouter/StackManager.tsx index 6cccf00fbd..b16eab335b 100644 --- a/packages/react-router/src/ReactRouter/StackManager.tsx +++ b/packages/react-router/src/ReactRouter/StackManager.tsx @@ -2,22 +2,21 @@ import React from 'react'; import { generateId, isDevMode } from '../utils'; -import { RouteManagerContext } from './RouteManagerContext'; +import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext'; import { View } from './View'; import { ViewItem } from './ViewItem'; import { ViewTransitionManager } from './ViewTransitionManager'; interface StackManagerProps { id?: string; + routeManager: RouteManagerContextState; + children?: React.ReactNode; } -interface StackManagerState { - routerOutletReady: boolean; -} +interface StackManagerState { } -export class StackManager extends React.Component { +class StackManagerInner extends React.Component { routerOutletEl: React.RefObject = React.createRef(); - context!: React.ContextType; id: string; constructor(props: StackManagerProps) { @@ -25,47 +24,44 @@ export class StackManager extends React.Component { - this.setState({ - routerOutletReady: true - }); - }); + this.props.routeManager.setupIonRouter(this.id, this.props.children, this.routerOutletEl.current!); + } + + static getDerivedStateFromProps(props: StackManagerProps, state: StackManagerState) { + props.routeManager.syncRoute('', props.children); + return state; } componentWillUnmount() { - this.context.removeViewStack(this.id); + this.props.routeManager.removeViewStack(this.id); } handleViewSync(page: HTMLElement, viewId: string) { - this.context.syncView(page, viewId); + this.props.routeManager.syncView(page, viewId); } handleHideView(viewId: string) { - this.context.hideView(viewId); + this.props.routeManager.hideView(viewId); } - renderChild(item: ViewItem) { - const component = React.cloneElement(item.route, { + renderChild(item: ViewItem, route: any) { + const component = React.cloneElement(route, { computedMatch: item.routeData.match }); return component; } render() { - const context = this.context; - const viewStack = context.viewStacks.get(this.id); + const routeManager = this.props.routeManager; + const viewStack = routeManager.viewStacks.get(this.id); const views = (viewStack || { views: [] }).views.filter(x => x.show); const ionRouterOutlet = React.Children.only(this.props.children) as React.ReactElement; - const { routerOutletReady } = this.state; - - const childElements = routerOutletReady ? views.map(view => { + const childElements = views.map(view => { + const route = routeManager.getRoute(view.routeId); return ( - {this.renderChild(view)} + {this.renderChild(view, route)} ); - }) :
; + }); const elementProps: any = { ref: this.routerOutletEl }; + if (ionRouterOutlet.props.forwardedRef) { + ionRouterOutlet.props.forwardedRef.current = this.routerOutletEl; + } + if (isDevMode()) { elementProps['data-stack-id'] = this.id; } @@ -95,8 +96,14 @@ export class StackManager extends React.Component { + return (props: any) => ( + + {context => } + + ); +}; + +export const StackManager = withContext(StackManagerInner); diff --git a/packages/react-router/src/ReactRouter/View.tsx b/packages/react-router/src/ReactRouter/View.tsx index 95043e0858..d31a6f4265 100644 --- a/packages/react-router/src/ReactRouter/View.tsx +++ b/packages/react-router/src/ReactRouter/View.tsx @@ -1,6 +1,5 @@ import { IonLifeCycleContext, NavContext } from '@ionic/react'; import React from 'react'; -import { Redirect, Route } from 'react-router-dom'; import { isDevMode } from '../utils'; @@ -10,6 +9,7 @@ interface ViewProps extends React.HTMLAttributes { onViewSync: (page: HTMLElement, viewId: string) => void; onHideView: (viewId: string) => void; view: ViewItem; + route: any; } /** @@ -19,20 +19,6 @@ export class View extends React.Component { context!: React.ContextType; ionPage?: HTMLElement; - componentDidMount() { - /** - * If we can tell if view is a redirect, hide it so it will work again in future - */ - const { view } = this.props; - if (view.route.type === Redirect) { - this.props.onHideView(view.id); - } else if (view.route.type === Route && view.route.props.render) { - if (view.route.props.render().type === Redirect) { - this.props.onHideView(view.id); - } - } - } - componentWillUnmount() { if (this.ionPage) { this.ionPage.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this)); diff --git a/packages/react-router/src/ReactRouter/ViewItem.ts b/packages/react-router/src/ReactRouter/ViewItem.ts index 22b7e597e9..d5c47907d0 100644 --- a/packages/react-router/src/ReactRouter/ViewItem.ts +++ b/packages/react-router/src/ReactRouter/ViewItem.ts @@ -3,10 +3,9 @@ export interface ViewItem { id: string; /** The key used by React. A new key is generated each time the view comes into the DOM so React thinks its a completely new element. */ key: string; - /** The or component associated with the view */ - route: React.ReactElement; - /** The reference to the element. */ - ionPageElement?: HTMLElement; + + routeId: string; + /** The routeData for the view. */ routeData: RouteData; /** Used to track which page pushed the page into view. Used for back button purposes. */ @@ -23,4 +22,9 @@ export interface ViewItem { * An IonRoute is a Route that contains an IonPage. Only IonPages participate in transition and lifecycle events. */ isIonRoute: boolean; + + /** + * location of the view + */ + location?: string; } diff --git a/packages/react-router/src/ReactRouter/ViewStacks.ts b/packages/react-router/src/ReactRouter/ViewStacks.ts index 660baa87c5..1b98d0802f 100644 --- a/packages/react-router/src/ReactRouter/ViewStacks.ts +++ b/packages/react-router/src/ReactRouter/ViewStacks.ts @@ -6,7 +6,6 @@ import { ViewItem } from './ViewItem'; export interface ViewStack { id: string; - routerOutlet: HTMLIonRouterOutletElement; views: ViewItem[]; } @@ -58,10 +57,11 @@ export class ViewStacks { path: v.routeData.childProps.path || v.routeData.childProps.from, component: v.routeData.childProps.component }; - match = matchPath(location.pathname, matchProps); - if (match) { + const myMatch: IonRouteData['match'] | null | undefined = matchPath(location.pathname, matchProps); + if (myMatch) { view = v; - return true; + match = myMatch; + return view.location === location.pathname; } return false; } diff --git a/packages/react-router/src/utils/LocationHistory.ts b/packages/react-router/src/utils/LocationHistory.ts index 7d2708f7bc..ec5eee44ef 100644 --- a/packages/react-router/src/utils/LocationHistory.ts +++ b/packages/react-router/src/utils/LocationHistory.ts @@ -3,7 +3,7 @@ import { Location as HistoryLocation } from 'history'; const RESTRICT_SIZE = 25; export class LocationHistory { - locationHistory: HistoryLocation[] = []; + private locationHistory: HistoryLocation[] = []; add(location: HistoryLocation) { this.locationHistory.push(location); @@ -21,6 +21,10 @@ export class LocationHistory { this.locationHistory.push(location); } + clear() { + this.locationHistory = []; + } + findLastLocationByUrl(url: string) { for (let i = this.locationHistory.length - 1; i >= 0; i--) { const location = this.locationHistory[i]; diff --git a/packages/react-router/tslint.json b/packages/react-router/tslint.json index 2f90574c64..ae565fc39b 100644 --- a/packages/react-router/tslint.json +++ b/packages/react-router/tslint.json @@ -27,6 +27,11 @@ "jsx-no-bind": false, "jsx-no-lambda": false, "jsx-no-multiline-js": false, - "jsx-wrap-multiline": false + "jsx-wrap-multiline": false, + "forin": false, + "strict-type-predicates": false, + "no-unused-expression": false, + "no-constant-condition": false, + "no-empty-interface": false } } diff --git a/packages/react/package.json b/packages/react/package.json index 3ab1f0e3db..091b933ff3 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -49,7 +49,7 @@ }, "devDependencies": { "@types/jest": "^23.3.9", - "@types/node": "10.12.9", + "@types/node": "^12.12.14", "@types/react": "^16.9.2", "@types/react-dom": "^16.9.0", "fs-extra": "^8.1.0", @@ -67,7 +67,7 @@ "tslint": "^5.18.0", "tslint-ionic-rules": "0.0.21", "tslint-react": "^4.0.0", - "typescript": "3.5.3" + "typescript": "^3.7.2" }, "jest": { "preset": "ts-jest", diff --git a/packages/react/src/components/IonPage.tsx b/packages/react/src/components/IonPage.tsx index f602368126..17ff6efe37 100644 --- a/packages/react/src/components/IonPage.tsx +++ b/packages/react/src/components/IonPage.tsx @@ -3,13 +3,26 @@ import React from 'react'; import { NavContext } from '../contexts/NavContext'; import { IonicReactProps } from './IonicReactProps'; +import { createForwardRef } from './utils'; -export const IonPage = /*@__PURE__*/(() => class IonPageInternal extends React.Component & IonicReactProps> { +interface IonPageProps extends IonicReactProps { +} + +interface IonPageInternalProps extends IonPageProps { + forwardedRef?: React.RefObject; +} + +class IonPageInternal extends React.Component { context!: React.ContextType; - ref = React.createRef(); + ref: React.RefObject; + + constructor(props: IonPageInternalProps) { + super(props); + this.ref = this.props.forwardedRef || React.createRef(); + } componentDidMount() { - if (this.context && this.ref.current) { + if (this.context && this.ref && this.ref.current) { if (this.context.hasIonicRouter()) { this.context.registerIonPage(this.ref.current); } @@ -17,7 +30,7 @@ export const IonPage = /*@__PURE__*/(() => class IonPageInternal extends React.C } render() { - const { className, children, ...props } = this.props; + const { className, children, forwardedRef, ...props } = this.props; return (
@@ -33,4 +46,6 @@ export const IonPage = /*@__PURE__*/(() => class IonPageInternal extends React.C static get contextType() { return NavContext; } -})(); +} + +export const IonPage = createForwardRef(IonPageInternal, 'IonPage'); diff --git a/packages/react/src/components/IonRouterOutlet.tsx b/packages/react/src/components/IonRouterOutlet.tsx index 0c31bf3cc0..63d4a2e44d 100644 --- a/packages/react/src/components/IonRouterOutlet.tsx +++ b/packages/react/src/components/IonRouterOutlet.tsx @@ -13,7 +13,7 @@ type Props = LocalJSX.IonRouterOutlet & { }; type InternalProps = Props & { - forwardedRef: any; + forwardedRef?: React.RefObject; }; const IonRouterOutletContainer = /*@__PURE__*/(() => class extends React.Component { diff --git a/packages/react/src/components/IonicReactProps.ts b/packages/react/src/components/IonicReactProps.ts index a18e0b7f16..2ea45b08b3 100644 --- a/packages/react/src/components/IonicReactProps.ts +++ b/packages/react/src/components/IonicReactProps.ts @@ -1,5 +1,6 @@ export interface IonicReactProps { class?: string; + className?: string; style?: {[key: string]: any }; } diff --git a/packages/react/src/components/createControllerComponent.tsx b/packages/react/src/components/createControllerComponent.tsx index 62def561c7..b0bb79ee4e 100644 --- a/packages/react/src/components/createControllerComponent.tsx +++ b/packages/react/src/components/createControllerComponent.tsx @@ -15,18 +15,21 @@ export interface ReactControllerProps { export const createControllerComponent = ( displayName: string, - controller: { create: (options: OptionsType) => Promise } + controller: { create: (options: OptionsType) => Promise; } ) => { const dismissEventName = `on${displayName}DidDismiss`; - type Props = OptionsType & ReactControllerProps; + type Props = OptionsType & ReactControllerProps & { + forwardedRef?: React.RefObject; + }; - return class extends React.Component { + class Overlay extends React.Component { overlay?: OverlayType; isUnmounted = false; constructor(props: Props) { super(props); + this.handleDismiss = this.handleDismiss.bind(this); } static get displayName() { @@ -54,17 +57,29 @@ export const createControllerComponent = >) { + if (this.props.onDidDismiss) { + this.props.onDidDismiss(event); + } + if (this.props.forwardedRef) { + (this.props.forwardedRef as any).current = undefined; + } + } + async present(prevProps?: Props) { const { isOpen, onDidDismiss, ...cProps } = this.props; this.overlay = await controller.create({ ...cProps as any }); attachProps(this.overlay, { - [dismissEventName]: onDidDismiss + [dismissEventName]: this.handleDismiss }, prevProps); // Check isOpen again since the value could have changed during the async call to controller.create // It's also possible for the component to have become unmounted. if (this.props.isOpen === true && this.isUnmounted === false) { + if (this.props.forwardedRef) { + (this.props.forwardedRef as any).current = this.overlay; + } await this.overlay.present(); } } @@ -72,5 +87,9 @@ export const createControllerComponent = ((props, ref) => { + return ; + }); }; diff --git a/packages/react/src/components/createOverlayComponent.tsx b/packages/react/src/components/createOverlayComponent.tsx index 113736e2cf..63bd121dc1 100644 --- a/packages/react/src/components/createOverlayComponent.tsx +++ b/packages/react/src/components/createOverlayComponent.tsx @@ -15,21 +15,24 @@ export interface ReactOverlayProps { onDidDismiss?: (event: CustomEvent) => void; } -export const createOverlayComponent = ( +export const createOverlayComponent = ( displayName: string, - controller: { create: (options: any) => Promise } + controller: { create: (options: any) => Promise; } ) => { const dismissEventName = `on${displayName}DidDismiss`; - type Props = T & ReactOverlayProps; + type Props = OverlayComponent & ReactOverlayProps & { + forwardedRef?: React.RefObject; + }; - return class extends React.Component { + class Overlay extends React.Component { overlay?: OverlayType; el: HTMLDivElement; constructor(props: Props) { super(props); this.el = document.createElement('div'); + this.handleDismiss = this.handleDismiss.bind(this); } static get displayName() { @@ -46,6 +49,15 @@ export const createOverlayComponent = >) { + if (this.props.onDidDismiss) { + this.props.onDidDismiss(event); + } + if (this.props.forwardedRef) { + (this.props.forwardedRef as any).current = undefined; + } + } + async componentDidUpdate(prevProps: Props) { if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) { this.present(prevProps); @@ -56,10 +68,11 @@ export const createOverlayComponent = { return; }, ...cProps } = this.props; + const { children, isOpen, onDidDismiss, ...cProps } = this.props; const elementProps = { ...cProps, - [dismissEventName]: onDidDismiss + ref: this.props.forwardedRef, + [dismissEventName]: this.handleDismiss }; const overlay = this.overlay = await controller.create({ @@ -68,6 +81,10 @@ export const createOverlayComponent = ((props, ref) => { + return ; + }); }; diff --git a/packages/react/src/components/hrefprops.ts b/packages/react/src/components/hrefprops.ts index 760907d517..61a3257008 100644 --- a/packages/react/src/components/hrefprops.ts +++ b/packages/react/src/components/hrefprops.ts @@ -1,4 +1,4 @@ -export declare type RouterDirection = 'forward' | 'back' | 'none'; +export declare type RouterDirection = 'forward' | 'back' | 'root' | 'none'; export type HrefProps = Omit & { routerLink?: string; diff --git a/packages/react/src/components/navigation/IonTabBar.tsx b/packages/react/src/components/navigation/IonTabBar.tsx index fb656acfd0..067661f023 100644 --- a/packages/react/src/components/navigation/IonTabBar.tsx +++ b/packages/react/src/components/navigation/IonTabBar.tsx @@ -69,12 +69,13 @@ const IonTabBarUnwrapped = /*@__PURE__*/(() => class extends React.Component) => { + const originalHref = this.state.tabs[e.detail.tab].originalHref; + const currentHref = this.state.tabs[e.detail.tab].currentHref; if (this.state.activeTab === e.detail.tab) { - const originalHref = this.state.tabs[e.detail.tab].originalHref; - if (this.context.hasIonicRouter()) { - this.context.tabNavigate(originalHref); + if (originalHref === currentHref) { + this.context.navigate(originalHref, 'none'); } else { - this.context.navigate(originalHref, 'back'); + this.context.navigate(originalHref, 'back', 'pop'); } } else { if (this.props.onIonTabsWillChange) { @@ -83,7 +84,7 @@ const IonTabBarUnwrapped = /*@__PURE__*/(() => class extends React.Component any; getStackManager: () => any; goBack: (defaultHref?: string) => void; - navigate: (path: string, direction?: RouterDirection | 'none') => void; + navigate: (path: string, direction?: RouterDirection | 'none', ionRouteAction?: 'push' | 'replace' | 'pop') => void; hasIonicRouter: () => boolean; registerIonPage: (page: HTMLElement) => void; - tabNavigate: (url: string) => void; currentPath: string | undefined; } @@ -23,7 +22,6 @@ export const NavContext = /*@__PURE__*/React.createContext({ } }, navigate: (path: string) => { window.location.pathname = path; }, - tabNavigate: () => undefined, hasIonicRouter: () => false, registerIonPage: () => undefined, currentPath: undefined diff --git a/packages/react/tslint.json b/packages/react/tslint.json index 3ba6e0b026..fb8fdaa478 100644 --- a/packages/react/tslint.json +++ b/packages/react/tslint.json @@ -14,7 +14,6 @@ "trailing-comma": false, "no-null-keyword": false, "no-console": false, - "no-unbound-method": true, "no-floating-promises": false, "no-invalid-template-strings": true, "ban-export-const-enum": true, @@ -27,6 +26,8 @@ "jsx-no-bind": false, "jsx-no-lambda": false, "jsx-no-multiline-js": false, - "jsx-wrap-multiline": false + "jsx-wrap-multiline": false, + "no-empty-interface": false, + "no-unbound-method": false } }