From bcc40c8d59b723bbdb1dfd318bfb2219eb8df3cf Mon Sep 17 00:00:00 2001 From: Ely Lucas Date: Wed, 6 Nov 2019 10:08:57 -0700 Subject: [PATCH] fix(react): adding swipe back functionality and routerOutlet ready improvements, fixes #19818 (#19849) --- .../src/ReactRouter/NavManager.tsx | 39 +---- .../src/ReactRouter/RouteManagerContext.ts | 2 +- .../react-router/src/ReactRouter/Router.tsx | 134 ++++++++++++------ .../src/ReactRouter/StackManager.tsx | 19 ++- packages/react-router/tslint.json | 2 +- packages/react/src/components/index.ts | 2 +- packages/react/src/components/utils/index.tsx | 12 +- 7 files changed, 124 insertions(+), 86 deletions(-) diff --git a/packages/react-router/src/ReactRouter/NavManager.tsx b/packages/react-router/src/ReactRouter/NavManager.tsx index 1423090fdf..60b39bab9e 100644 --- a/packages/react-router/src/ReactRouter/NavManager.tsx +++ b/packages/react-router/src/ReactRouter/NavManager.tsx @@ -4,24 +4,16 @@ import { Location as HistoryLocation, UnregisterCallback } from 'history'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { generateId } from '../utils'; -import { LocationHistory } from '../utils/LocationHistory'; - import { StackManager } from './StackManager'; -import { ViewItem } from './ViewItem'; -import { ViewStack } from './ViewStacks'; interface NavManagerProps extends RouteComponentProps { - findViewInfoByLocation: (location: HistoryLocation) => { view?: ViewItem, viewStack?: ViewStack }; - findViewInfoById: (id: string) => { view?: ViewItem, viewStack?: ViewStack }; - getActiveIonPage: () => { view?: ViewItem, viewStack?: ViewStack }; + onNavigateBack: (defaultHref?: string) => void; onNavigate: (type: 'push' | 'replace', path: string, state?: any) => void; } export class NavManager extends React.Component { listenUnregisterCallback: UnregisterCallback | undefined; - locationHistory: LocationHistory = new LocationHistory(); constructor(props: NavManagerProps) { super(props); @@ -40,16 +32,8 @@ export class NavManager extends React.Component void; hideView: (viewId: string) => void; viewStacks: ViewStacks; - setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => Promise; + setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => void; removeViewStack: (stack: string) => void; } diff --git a/packages/react-router/src/ReactRouter/Router.tsx b/packages/react-router/src/ReactRouter/Router.tsx index 9dda553276..d6b735e435 100644 --- a/packages/react-router/src/ReactRouter/Router.tsx +++ b/packages/react-router/src/ReactRouter/Router.tsx @@ -1,10 +1,11 @@ import { NavDirection } from '@ionic/core'; -import { RouterDirection } from '@ionic/react'; +import { RouterDirection, getConfig } from '@ionic/react'; import { Action as HistoryAction, Location as HistoryLocation, UnregisterCallback } from 'history'; import React from 'react'; import { RouteComponentProps, matchPath, withRouter } from 'react-router-dom'; import { generateId } from '../utils'; +import { LocationHistory } from '../utils/LocationHistory'; import { IonRouteData } from './IonRouteData'; import { NavManager } from './NavManager'; @@ -21,11 +22,13 @@ class RouteManager extends React.Component, location: HistoryLocation) { const viewId = generateId(); @@ -212,29 +224,61 @@ class RouteManager extends React.Component { - this.setState(prevState => { - const prevViewStacks = Object.assign(new ViewStacks(), prevState.viewStacks); - const newStack: ViewStack = { - id: stack, - views: stackItems, - routerOutlet - }; - if (activeId) { - this.activeIonPageId = activeId; - } - prevViewStacks.set(stack, newStack); - return { - viewStacks: prevViewStacks - }; - }, () => { - resolve(); - }); + registerViewStack(stack: string, activeId: string | undefined, stackItems: ViewItem[], routerOutlet: HTMLIonRouterOutletElement, _location: HistoryLocation) { + this.setState(prevState => { + const prevViewStacks = Object.assign(new ViewStacks(), prevState.viewStacks); + const newStack: ViewStack = { + id: stack, + views: stackItems, + routerOutlet + }; + if (activeId) { + this.activeIonPageId = activeId; + } + prevViewStacks.set(stack, newStack); + return { + viewStacks: prevViewStacks + }; + }, () => { + this.setupRouterOutlet(routerOutlet); }); } + async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) { + const waitUntilReady = async () => { + if (routerOutlet.componentOnReady) { + routerOutlet.dispatchEvent(new Event('routerOutletReady')); + return; + } else { + setTimeout(() => { + waitUntilReady(); + }, 0); + } + }; + + await waitUntilReady(); + + const canStart = () => { + const config = getConfig(); + const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios'); + if (swipeEnabled) { + const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId); + return !!(view && view.prevId); + } else { + return false; + } + }; + + const onStart = () => { + this.navigateBack(); + }; + routerOutlet.swipeHandler = { + canStart, + onStart, + onEnd: _shouldContinue => true + }; + } + removeViewStack(stack: string) { const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks); viewStacks.delete(stack); @@ -245,7 +289,6 @@ class RouteManager extends React.Component { - const viewStacks = Object.assign(new ViewStacks(), state.viewStacks); const { view } = viewStacks.findViewInfoById(viewId); @@ -261,20 +304,6 @@ class RouteManager extends React.Component { - this.transitionView(enteringEl, leavingEl, ionRouterOutlet, direction, showGoBack); - }, 10); - } - } - private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean) { if (enteringEl === leavingEl) { @@ -305,15 +334,36 @@ class RouteManager extends React.Component this.state.viewStacks.findViewInfoById(id)} - findViewInfoByLocation={(location: HistoryLocation) => this.state.viewStacks.findViewInfoByLocation(location)} - getActiveIonPage={() => this.state.viewStacks.findViewInfoById(this.activeIonPageId)} > {this.props.children} diff --git a/packages/react-router/src/ReactRouter/StackManager.tsx b/packages/react-router/src/ReactRouter/StackManager.tsx index 8fa7a605b9..6cccf00fbd 100644 --- a/packages/react-router/src/ReactRouter/StackManager.tsx +++ b/packages/react-router/src/ReactRouter/StackManager.tsx @@ -11,7 +11,11 @@ interface StackManagerProps { id?: string; } -export class StackManager extends React.Component { +interface StackManagerState { + routerOutletReady: boolean; +} + +export class StackManager extends React.Component { routerOutletEl: React.RefObject = React.createRef(); context!: React.ContextType; id: string; @@ -21,10 +25,18 @@ export class StackManager extends React.Component { this.id = this.props.id || generateId(); this.handleViewSync = this.handleViewSync.bind(this); this.handleHideView = this.handleHideView.bind(this); + this.state = { + routerOutletReady: false + }; } componentDidMount() { this.context.setupIonRouter(this.id, this.props.children, this.routerOutletEl.current!); + this.routerOutletEl.current!.addEventListener('routerOutletReady', () => { + this.setState({ + routerOutletReady: true + }); + }); } componentWillUnmount() { @@ -51,8 +63,9 @@ export class StackManager extends React.Component { const viewStack = context.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 = views.map(view => { + const childElements = routerOutletReady ? views.map(view => { return ( { ); - }); + }) :
; const elementProps: any = { ref: this.routerOutletEl diff --git a/packages/react-router/tslint.json b/packages/react-router/tslint.json index b53ba7ef77..2f90574c64 100644 --- a/packages/react-router/tslint.json +++ b/packages/react-router/tslint.json @@ -19,7 +19,7 @@ "no-invalid-template-strings": true, "ban-export-const-enum": true, "only-arrow-functions": false, - "strict-boolean-conditions": [true, "allow-null-union", "allow-undefined-union", "allow-boolean-or-undefined", "allow-string"], + "strict-boolean-conditions": [false], "jsx-key": false, "jsx-self-close": false, "jsx-curly-spacing": [true, "never"], diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index f2e2952028..ad015b9fea 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -24,7 +24,7 @@ export { IonBackButton } from './navigation/IonBackButton'; export { IonRouterOutlet } from './IonRouterOutlet'; // Utils -export { isPlatform, getPlatforms } from './utils'; +export { isPlatform, getPlatforms, getConfig } from './utils'; export { RouterDirection } from './hrefprops'; // Icons that are used by internal components diff --git a/packages/react/src/components/utils/index.tsx b/packages/react/src/components/utils/index.tsx index ae87b14ae6..405d62ae70 100644 --- a/packages/react/src/components/utils/index.tsx +++ b/packages/react/src/components/utils/index.tsx @@ -1,4 +1,4 @@ -import { Platforms, getPlatforms as getPlatformsCore, isPlatform as isPlatformCore } from '@ionic/core'; +import { Platforms, getPlatforms as getPlatformsCore, isPlatform as isPlatformCore, Config as CoreConfig } from '@ionic/core'; import React from 'react'; import { IonicReactProps } from '../IonicReactProps'; @@ -24,3 +24,13 @@ export const isPlatform = (platform: Platforms) => { export const getPlatforms = () => { return getPlatformsCore(window); }; + +export const getConfig = (): CoreConfig | null => { + if (typeof (window as any) !== 'undefined') { + const Ionic = (window as any).Ionic; + if (Ionic && Ionic.config) { + return Ionic.config; + } + } + return null; +};