Skip to content

Commit

Permalink
fix(react): adding swipe back functionality and routerOutlet ready im…
Browse files Browse the repository at this point in the history
…provements, fixes #19818 (#19849)
  • Loading branch information
elylucas committed Nov 6, 2019
1 parent 9fad416 commit bcc40c8
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 86 deletions.
39 changes: 2 additions & 37 deletions packages/react-router/src/ReactRouter/NavManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<NavManagerProps, NavContextState> {

listenUnregisterCallback: UnregisterCallback | undefined;
locationHistory: LocationHistory = new LocationHistory();

constructor(props: NavManagerProps) {
super(props);
Expand All @@ -40,16 +32,8 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
this.setState({
currentPath: location.pathname
});
this.locationHistory.add(location);
});

this.locationHistory.add({
hash: window.location.hash,
key: generateId(),
pathname: window.location.pathname,
search: window.location.search,
state: {}
});
}

componentWillUnmount() {
Expand All @@ -59,26 +43,7 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
}

goBack(defaultHref?: string) {
const { view: activeIonPage } = this.props.getActiveIonPage();
if (activeIonPage) {
const { view: enteringView } = this.props.findViewInfoById(activeIonPage.prevId!);
if (enteringView) {
const lastLocation = this.locationHistory.findLastLocation(enteringView.routeData.match.url);
if (lastLocation) {
this.props.onNavigate('replace', lastLocation.pathname + lastLocation.search, 'back');
} else {
this.props.onNavigate('replace', enteringView.routeData.match.url, 'back');
}
} else {
if (defaultHref) {
this.props.onNavigate('replace', defaultHref, 'back');
}
}
} else {
if (defaultHref) {
this.props.onNavigate('replace', defaultHref, 'back');
}
}
this.props.onNavigateBack(defaultHref);
}

navigate(path: string, direction?: RouterDirection | 'none') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface RouteManagerContextState {
syncView: (page: HTMLElement, viewId: string) => void;
hideView: (viewId: string) => void;
viewStacks: ViewStacks;
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => Promise<void>;
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => void;
removeViewStack: (stack: string) => void;
}

Expand Down
134 changes: 92 additions & 42 deletions packages/react-router/src/ReactRouter/Router.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,18 +22,28 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
listenUnregisterCallback: UnregisterCallback | undefined;
activeIonPageId?: string;
currentDirection?: RouterDirection;
locationHistory: LocationHistory = new LocationHistory();

constructor(props: RouteComponentProps) {
super(props);
this.listenUnregisterCallback = this.props.history.listen(this.historyChange.bind(this));
this.handleNavigate = this.handleNavigate.bind(this);
this.navigateBack = this.navigateBack.bind(this);
this.state = {
viewStacks: new ViewStacks(),
hideView: this.hideView.bind(this),
setupIonRouter: this.setupIonRouter.bind(this),
removeViewStack: this.removeViewStack.bind(this),
syncView: this.syncView.bind(this)
};

this.locationHistory.add({
hash: window.location.hash,
key: generateId(),
pathname: window.location.pathname,
search: window.location.search,
state: {}
});
}

componentDidUpdate(_prevProps: RouteComponentProps, prevState: RouteManagerState) {
Expand Down Expand Up @@ -66,6 +77,7 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
historyChange(location: HistoryLocation, action: HistoryAction) {
location.state = location.state || { direction: this.currentDirection };
this.currentDirection = undefined;
this.locationHistory.add(location);
this.setState({
location,
action
Expand Down Expand Up @@ -139,7 +151,7 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
if (enteringEl) {
// Don't animate from an empty view
const navDirection = leavingEl && leavingEl.innerHTML === '' ? undefined : direction === 'none' ? undefined : direction;
this.transitionView(
this.commitView(
enteringEl!,
leavingEl!,
viewStack.routerOutlet,
Expand Down Expand Up @@ -173,15 +185,15 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
});
}

async setupIonRouter(id: string, children: any, routerOutlet: HTMLIonRouterOutletElement) {
setupIonRouter(id: string, children: any, routerOutlet: HTMLIonRouterOutletElement) {
const views: ViewItem[] = [];
let activeId: string | undefined;
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
views.push(createViewItem(child, this.props.history.location));
});

await this.registerViewStack(id, activeId, views, routerOutlet, this.props.location);
this.registerViewStack(id, activeId, views, routerOutlet, this.props.location);

function createViewItem(child: React.ReactElement<any>, location: HistoryLocation) {
const viewId = generateId();
Expand Down Expand Up @@ -212,29 +224,61 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
}

async registerViewStack(stack: string, activeId: string | undefined, stackItems: ViewItem[], routerOutlet: HTMLIonRouterOutletElement, _location: HistoryLocation) {

return new Promise(resolve => {
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);
Expand All @@ -245,7 +289,6 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat

syncView(page: HTMLElement, viewId: string) {
this.setState(state => {

const viewStacks = Object.assign(new ViewStacks(), state.viewStacks);
const { view } = viewStacks.findViewInfoById(viewId);

Expand All @@ -261,20 +304,6 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
});
}

transitionView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOutlet: HTMLIonRouterOutletElement | undefined, direction: NavDirection | undefined, showGoBack: boolean) {
/**
* Super hacky workaround to make sure ionRouterOutlet is available
* since transitionView might be called before IonRouterOutlet is fully mounted
*/
if (ionRouterOutlet && ionRouterOutlet.componentOnReady) {
this.commitView(enteringEl, leavingEl, ionRouterOutlet, direction, showGoBack);
} else {
setTimeout(() => {
this.transitionView(enteringEl, leavingEl, ionRouterOutlet, direction, showGoBack);
}, 10);
}
}

private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean) {

if (enteringEl === leavingEl) {
Expand Down Expand Up @@ -305,15 +334,36 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
}

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.findLastLocation(enteringView.routeData.match!.url);
if (lastLocation) {
this.handleNavigate('replace', lastLocation.pathname + lastLocation.search, 'back');
} else {
this.handleNavigate('replace', enteringView.routeData.match!.url, 'back');
}
} else {
if (defaultHref) {
this.handleNavigate('replace', defaultHref, 'back');
}
}
} else {
if (defaultHref) {
this.handleNavigate('replace', defaultHref, 'back');
}
}
}

render() {
return (
<RouteManagerContext.Provider value={this.state}>
<NavManager
{...this.props}
onNavigateBack={this.navigateBack}
onNavigate={this.handleNavigate}
findViewInfoById={(id: string) => this.state.viewStacks.findViewInfoById(id)}
findViewInfoByLocation={(location: HistoryLocation) => this.state.viewStacks.findViewInfoByLocation(location)}
getActiveIonPage={() => this.state.viewStacks.findViewInfoById(this.activeIonPageId)}
>
{this.props.children}
</NavManager>
Expand Down
19 changes: 16 additions & 3 deletions packages/react-router/src/ReactRouter/StackManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ interface StackManagerProps {
id?: string;
}

export class StackManager extends React.Component<StackManagerProps, {}> {
interface StackManagerState {
routerOutletReady: boolean;
}

export class StackManager extends React.Component<StackManagerProps, StackManagerState> {
routerOutletEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
context!: React.ContextType<typeof RouteManagerContext>;
id: string;
Expand All @@ -21,10 +25,18 @@ export class StackManager extends React.Component<StackManagerProps, {}> {
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() {
Expand All @@ -51,8 +63,9 @@ export class StackManager extends React.Component<StackManagerProps, {}> {
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 (
<ViewTransitionManager
id={view.id}
Expand All @@ -68,7 +81,7 @@ export class StackManager extends React.Component<StackManagerProps, {}> {
</View>
</ViewTransitionManager>
);
});
}) : <div></div>;

const elementProps: any = {
ref: this.routerOutletEl
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion packages/react/src/components/utils/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
};

0 comments on commit bcc40c8

Please sign in to comment.