mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-23 14:01:20 +08:00
merge release-4.11.6
This commit is contained in:
16
CHANGELOG.md
16
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)
|
# [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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
"@ionic/core": "5.0.0-beta.1",
|
"@ionic/core": "5.0.0-beta.1",
|
||||||
"@ionic/react": "5.0.0-beta.1",
|
"@ionic/react": "5.0.0-beta.1",
|
||||||
"@types/jest": "^23.3.9",
|
"@types/jest": "^23.3.9",
|
||||||
"@types/node": "12.6.9",
|
"@types/node": "^12.12.14",
|
||||||
"@types/react": "^16.9.2",
|
"@types/react": "^16.9.2",
|
||||||
"@types/react-dom": "^16.9.0",
|
"@types/react-dom": "^16.9.0",
|
||||||
"@types/react-router": "^5.0.3",
|
"@types/react-router": "^5.0.3",
|
||||||
@ -70,7 +70,7 @@
|
|||||||
"tslint": "^5.20.0",
|
"tslint": "^5.20.0",
|
||||||
"tslint-ionic-rules": "0.0.21",
|
"tslint-ionic-rules": "0.0.21",
|
||||||
"tslint-react": "^4.1.0",
|
"tslint-react": "^4.1.0",
|
||||||
"typescript": "3.5.3"
|
"typescript": "^3.7.2"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
1
packages/react-router/src/ReactRouter/IonRouteAction.ts
Normal file
1
packages/react-router/src/ReactRouter/IonRouteAction.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type IonRouteAction = 'push' | 'replace' | 'pop';
|
@ -4,11 +4,12 @@ import { Location as HistoryLocation, UnregisterCallback } from 'history';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { IonRouteAction } from './IonRouteAction';
|
||||||
import { StackManager } from './StackManager';
|
import { StackManager } from './StackManager';
|
||||||
|
|
||||||
interface NavManagerProps extends RouteComponentProps {
|
interface NavManagerProps extends RouteComponentProps {
|
||||||
onNavigateBack: (defaultHref?: string) => void;
|
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<NavManagerProps, NavContextState> {
|
export class NavManager extends React.Component<NavManagerProps, NavContextState> {
|
||||||
@ -24,8 +25,7 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
|
|||||||
getStackManager: this.getStackManager.bind(this),
|
getStackManager: this.getStackManager.bind(this),
|
||||||
getPageManager: this.getPageManager.bind(this),
|
getPageManager: this.getPageManager.bind(this),
|
||||||
currentPath: this.props.location.pathname,
|
currentPath: this.props.location.pathname,
|
||||||
registerIonPage: () => { return; }, // overridden in View for each IonPage
|
registerIonPage: () => { return; } // overridden in View for each IonPage
|
||||||
tabNavigate: this.tabNavigate.bind(this)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.listenUnregisterCallback = this.props.history.listen((location: HistoryLocation) => {
|
this.listenUnregisterCallback = this.props.history.listen((location: HistoryLocation) => {
|
||||||
@ -53,12 +53,8 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
|
|||||||
this.props.onNavigateBack(defaultHref);
|
this.props.onNavigateBack(defaultHref);
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(path: string, direction?: RouterDirection | 'none') {
|
navigate(path: string, direction?: RouterDirection | 'none', ionRouteAction: IonRouteAction = 'push') {
|
||||||
this.props.onNavigate('push', path, direction);
|
this.props.onNavigate(ionRouteAction, path, direction);
|
||||||
}
|
|
||||||
|
|
||||||
tabNavigate(path: string) {
|
|
||||||
this.props.onNavigate('replace', path, 'back');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageManager() {
|
getPageManager() {
|
||||||
|
@ -4,18 +4,22 @@ import { ViewStacks } from './ViewStacks';
|
|||||||
|
|
||||||
export interface RouteManagerContextState {
|
export interface RouteManagerContextState {
|
||||||
syncView: (page: HTMLElement, viewId: string) => void;
|
syncView: (page: HTMLElement, viewId: string) => void;
|
||||||
|
syncRoute: (id: string, route: any) => void;
|
||||||
hideView: (viewId: string) => void;
|
hideView: (viewId: string) => void;
|
||||||
viewStacks: ViewStacks;
|
viewStacks: ViewStacks;
|
||||||
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => void;
|
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => void;
|
||||||
removeViewStack: (stack: string) => void;
|
removeViewStack: (stack: string) => void;
|
||||||
|
getRoute: (id: string) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RouteManagerContext = /*@__PURE__*/React.createContext<RouteManagerContextState>({
|
export const RouteManagerContext = /*@__PURE__*/React.createContext<RouteManagerContextState>({
|
||||||
viewStacks: new ViewStacks(),
|
viewStacks: new ViewStacks(),
|
||||||
syncView: () => { navContextNotFoundError(); },
|
syncView: () => { navContextNotFoundError(); },
|
||||||
|
syncRoute: () => { navContextNotFoundError(); },
|
||||||
hideView: () => { navContextNotFoundError(); },
|
hideView: () => { navContextNotFoundError(); },
|
||||||
setupIonRouter: () => Promise.reject(navContextNotFoundError()),
|
setupIonRouter: () => Promise.reject(navContextNotFoundError()),
|
||||||
removeViewStack: () => { navContextNotFoundError(); }
|
removeViewStack: () => { navContextNotFoundError(); },
|
||||||
|
getRoute: () => { navContextNotFoundError(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
function navContextNotFoundError() {
|
function navContextNotFoundError() {
|
||||||
|
@ -7,6 +7,7 @@ import { RouteComponentProps, matchPath, withRouter } from 'react-router-dom';
|
|||||||
import { generateId, isDevMode } from '../utils';
|
import { generateId, isDevMode } from '../utils';
|
||||||
import { LocationHistory } from '../utils/LocationHistory';
|
import { LocationHistory } from '../utils/LocationHistory';
|
||||||
|
|
||||||
|
import { IonRouteAction } from './IonRouteAction';
|
||||||
import { IonRouteData } from './IonRouteData';
|
import { IonRouteData } from './IonRouteData';
|
||||||
import { NavManager } from './NavManager';
|
import { NavManager } from './NavManager';
|
||||||
import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext';
|
import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext';
|
||||||
@ -15,14 +16,19 @@ import { ViewStack, ViewStacks } from './ViewStacks';
|
|||||||
|
|
||||||
interface RouteManagerState extends RouteManagerContextState {
|
interface RouteManagerState extends RouteManagerContextState {
|
||||||
location?: HistoryLocation;
|
location?: HistoryLocation;
|
||||||
action?: HistoryAction;
|
action?: IonRouteAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RouteManager extends React.Component<RouteComponentProps, RouteManagerState> {
|
class RouteManager extends React.Component<RouteComponentProps, RouteManagerState> {
|
||||||
listenUnregisterCallback: UnregisterCallback | undefined;
|
listenUnregisterCallback: UnregisterCallback | undefined;
|
||||||
activeIonPageId?: string;
|
activeIonPageId?: string;
|
||||||
currentDirection?: RouterDirection;
|
currentIonRouteAction?: IonRouteAction;
|
||||||
|
currentRouteDirection?: RouterDirection;
|
||||||
locationHistory = new LocationHistory();
|
locationHistory = new LocationHistory();
|
||||||
|
routes: { [key: string]: React.ReactElement<any>; } = {};
|
||||||
|
ionPageElements: { [key: string]: HTMLElement; } = {};
|
||||||
|
routerOutlets: { [key: string]: HTMLIonRouterOutletElement; } = {};
|
||||||
|
firstRender = true;
|
||||||
|
|
||||||
constructor(props: RouteComponentProps) {
|
constructor(props: RouteComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -34,7 +40,9 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
hideView: this.hideView.bind(this),
|
hideView: this.hideView.bind(this),
|
||||||
setupIonRouter: this.setupIonRouter.bind(this),
|
setupIonRouter: this.setupIonRouter.bind(this),
|
||||||
removeViewStack: this.removeViewStack.bind(this),
|
removeViewStack: this.removeViewStack.bind(this),
|
||||||
syncView: this.syncView.bind(this)
|
syncView: this.syncView.bind(this),
|
||||||
|
syncRoute: this.syncRoute.bind(this),
|
||||||
|
getRoute: this.getRoute.bind(this)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.locationHistory.add({
|
this.locationHistory.add({
|
||||||
@ -49,7 +57,8 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
componentDidUpdate(_prevProps: RouteComponentProps, prevState: RouteManagerState) {
|
componentDidUpdate(_prevProps: RouteComponentProps, prevState: RouteManagerState) {
|
||||||
// Trigger a page change if the location or action is different
|
// Trigger a page change if the location or action is different
|
||||||
if (this.state.location && prevState.location !== this.state.location || prevState.action !== this.state.action) {
|
if (this.state.location && prevState.location !== this.state.location || prevState.action !== this.state.action) {
|
||||||
this.setActiveView(this.state.location!, this.state.action!);
|
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||||
|
this.setActiveView(this.state.location!, this.state.action!, viewStacks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,15 +68,19 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRoute(id: string) {
|
||||||
|
return this.routes[id];
|
||||||
|
}
|
||||||
|
|
||||||
hideView(viewId: string) {
|
hideView(viewId: string) {
|
||||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||||
const { view } = viewStacks.findViewInfoById(viewId);
|
const { view } = viewStacks.findViewInfoById(viewId);
|
||||||
if (view) {
|
if (view) {
|
||||||
view.show = false;
|
view.show = false;
|
||||||
view.ionPageElement = undefined;
|
|
||||||
view.isIonRoute = false;
|
view.isIonRoute = false;
|
||||||
view.prevId = undefined;
|
view.prevId = undefined;
|
||||||
view.key = generateId();
|
view.key = generateId();
|
||||||
|
delete this.ionPageElements[view.id];
|
||||||
this.setState({
|
this.setState({
|
||||||
viewStacks
|
viewStacks
|
||||||
});
|
});
|
||||||
@ -75,64 +88,74 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
historyChange(location: HistoryLocation, action: HistoryAction) {
|
historyChange(location: HistoryLocation, action: HistoryAction) {
|
||||||
location.state = location.state || { direction: this.currentDirection };
|
const ionRouteAction = this.currentIonRouteAction === 'pop' ? 'pop' : action.toLowerCase() as IonRouteAction;
|
||||||
this.currentDirection = undefined;
|
let direction = this.currentRouteDirection;
|
||||||
if (action === 'PUSH') {
|
|
||||||
|
if (ionRouteAction === 'push') {
|
||||||
this.locationHistory.add(location);
|
this.locationHistory.add(location);
|
||||||
} else if ((action === 'REPLACE' && location.state.direction === 'back') || action === 'POP') {
|
} else if (ionRouteAction === 'pop') {
|
||||||
this.locationHistory.pop();
|
this.locationHistory.pop();
|
||||||
} else {
|
direction = direction || 'back';
|
||||||
|
} else if (ionRouteAction === 'replace') {
|
||||||
this.locationHistory.replace(location);
|
this.locationHistory.replace(location);
|
||||||
}
|
direction = 'none';
|
||||||
this.setState({
|
|
||||||
location,
|
|
||||||
action
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveView(location: HistoryLocation, action: HistoryAction) {
|
if (direction === 'root') {
|
||||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
this.locationHistory.clear();
|
||||||
|
this.locationHistory.add(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
location.state = location.state || { direction };
|
||||||
|
this.setState({
|
||||||
|
location,
|
||||||
|
action: ionRouteAction as IonRouteAction
|
||||||
|
});
|
||||||
|
this.currentRouteDirection = undefined;
|
||||||
|
this.currentIonRouteAction = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveView(location: HistoryLocation, action: IonRouteAction, viewStacks: ViewStacks) {
|
||||||
let direction: RouterDirection | undefined = (location.state && location.state.direction) || 'forward';
|
let direction: RouterDirection | undefined = (location.state && location.state.direction) || 'forward';
|
||||||
let leavingView: ViewItem | undefined;
|
let leavingView: ViewItem | undefined;
|
||||||
const viewStackKeys = viewStacks.getKeys();
|
const viewStackKeys = viewStacks.getKeys();
|
||||||
|
let shouldTransitionPage = false;
|
||||||
|
let leavingViewHtml: string | undefined;
|
||||||
|
|
||||||
viewStackKeys.forEach(key => {
|
viewStackKeys.forEach(key => {
|
||||||
const { view: enteringView, viewStack: enteringViewStack, match } = viewStacks.findViewInfoByLocation(location, key);
|
const { view: enteringView, viewStack: enteringViewStack, match } = viewStacks.findViewInfoByLocation(location, key);
|
||||||
if (!enteringView || !enteringViewStack) {
|
if (!enteringView || !enteringViewStack) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
leavingView = viewStacks.findViewInfoById(this.activeIonPageId).view;
|
leavingView = viewStacks.findViewInfoById(this.activeIonPageId).view;
|
||||||
|
|
||||||
if (enteringView.isIonRoute) {
|
if (enteringView.isIonRoute) {
|
||||||
enteringView.show = true;
|
enteringView.show = true;
|
||||||
enteringView.mount = true;
|
enteringView.mount = true;
|
||||||
enteringView.routeData.match = match!;
|
enteringView.routeData.match = match!;
|
||||||
|
shouldTransitionPage = true;
|
||||||
|
|
||||||
this.activeIonPageId = enteringView.id;
|
this.activeIonPageId = enteringView.id;
|
||||||
|
|
||||||
if (leavingView) {
|
if (leavingView) {
|
||||||
if (direction === 'forward') {
|
if (action === 'push' && direction === 'forward') {
|
||||||
if (action === 'PUSH') {
|
|
||||||
/**
|
/**
|
||||||
* If the page is being pushed into the stack by another view,
|
* 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.
|
* record the view that originally directed to the new view for back button purposes.
|
||||||
*/
|
*/
|
||||||
enteringView.prevId = leavingView.id;
|
enteringView.prevId = leavingView.id;
|
||||||
} else if (action === 'POP') {
|
} else if (action === 'pop' || action === 'replace') {
|
||||||
direction = leavingView.prevId === enteringView.id ? 'back' : 'none';
|
|
||||||
} else {
|
|
||||||
direction = direction || 'back';
|
|
||||||
leavingView.mount = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (direction === 'back' || action === 'REPLACE') {
|
|
||||||
leavingView.mount = false;
|
leavingView.mount = false;
|
||||||
this.removeOrphanedViews(enteringView, enteringViewStack);
|
this.removeOrphanedViews(enteringView, enteringViewStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leavingViewHtml = enteringView.id === leavingView.id ? this.ionPageElements[leavingView.id].outerHTML : undefined;
|
||||||
} else {
|
} else {
|
||||||
// If there is not a leavingView, then we shouldn't provide a direction
|
// If there is not a leavingView, then we shouldn't provide a direction
|
||||||
direction = undefined;
|
direction = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
enteringView.show = true;
|
enteringView.show = true;
|
||||||
enteringView.mount = true;
|
enteringView.mount = true;
|
||||||
@ -151,28 +174,39 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
this.setState({
|
this.setState({
|
||||||
viewStacks
|
viewStacks
|
||||||
}, () => {
|
}, () => {
|
||||||
|
if (shouldTransitionPage) {
|
||||||
const { view: enteringView, viewStack } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
|
const { view: enteringView, viewStack } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
|
||||||
if (enteringView && viewStack) {
|
if (enteringView && viewStack) {
|
||||||
const enteringEl = enteringView.ionPageElement ? enteringView.ionPageElement : undefined;
|
const enteringEl = this.ionPageElements[enteringView.id];
|
||||||
const leavingEl = leavingView && leavingView.ionPageElement ? leavingView.ionPageElement : undefined;
|
const leavingEl = leavingView && this.ionPageElements[leavingView.id];
|
||||||
if (enteringEl) {
|
if (enteringEl) {
|
||||||
|
let navDirection: NavDirection | undefined;
|
||||||
|
if (leavingEl && leavingEl.innerHTML === '') {
|
||||||
// Don't animate from an empty view
|
// Don't animate from an empty view
|
||||||
const navDirection = leavingEl && leavingEl.innerHTML === '' ? undefined : direction === 'none' ? undefined : direction;
|
navDirection = undefined;
|
||||||
const shouldGoBack = !!enteringView.prevId || !!this.locationHistory.previous();
|
} else if (direction === 'none' || direction === 'root') {
|
||||||
|
navDirection = undefined;
|
||||||
|
} else {
|
||||||
|
navDirection = direction;
|
||||||
|
}
|
||||||
|
const shouldGoBack = !!enteringView.prevId;
|
||||||
|
const routerOutlet = this.routerOutlets[viewStack.id];
|
||||||
this.commitView(
|
this.commitView(
|
||||||
enteringEl!,
|
enteringEl!,
|
||||||
leavingEl!,
|
leavingEl!,
|
||||||
viewStack.routerOutlet,
|
routerOutlet,
|
||||||
navDirection,
|
navDirection,
|
||||||
shouldGoBack);
|
shouldGoBack,
|
||||||
|
leavingViewHtml);
|
||||||
} else if (leavingEl) {
|
} else if (leavingEl) {
|
||||||
leavingEl.classList.add('ion-page-hidden');
|
leavingEl.classList.add('ion-page-hidden');
|
||||||
leavingEl.setAttribute('aria-hidden', 'true');
|
leavingEl.setAttribute('aria-hidden', 'true');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Warn if an IonPage was not eventually rendered in Dev Mode
|
// Warn if an IonPage was not eventually rendered in Dev Mode
|
||||||
if (isDevMode()) {
|
if (isDevMode()) {
|
||||||
if (enteringView.routeData.match!.url !== location.pathname) {
|
if (enteringView && enteringView.routeData.match!.url !== location.pathname) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
|
const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
|
||||||
if (view!.routeData.match!.url !== location.pathname) {
|
if (view!.routeData.match!.url !== location.pathname) {
|
||||||
@ -195,12 +229,13 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
this.removeOrphanedViews(v, viewStack);
|
this.removeOrphanedViews(v, viewStack);
|
||||||
|
|
||||||
// If view is not currently visible, go ahead and remove it from DOM
|
// If view is not currently visible, go ahead and remove it from DOM
|
||||||
if (v.ionPageElement!.classList.contains('ion-page-hidden')) {
|
const page = this.ionPageElements[v.id];
|
||||||
|
if (page.classList.contains('ion-page-hidden')) {
|
||||||
v.show = false;
|
v.show = false;
|
||||||
v.ionPageElement = undefined;
|
|
||||||
v.isIonRoute = false;
|
v.isIonRoute = false;
|
||||||
v.prevId = undefined;
|
v.prevId = undefined;
|
||||||
v.key = generateId();
|
v.key = generateId();
|
||||||
|
delete this.ionPageElements[v.id];
|
||||||
}
|
}
|
||||||
v.mount = false;
|
v.mount = false;
|
||||||
}
|
}
|
||||||
@ -212,15 +247,18 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
let activeId: string | undefined;
|
let activeId: string | undefined;
|
||||||
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
|
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
|
||||||
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
|
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
|
||||||
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);
|
this.registerViewStack(id, activeId, views, routerOutlet, this.props.location);
|
||||||
|
|
||||||
function createViewItem(child: React.ReactElement<any>, location: HistoryLocation) {
|
function createViewItem(child: React.ReactElement<any>, routeId: string, location: HistoryLocation) {
|
||||||
const viewId = generateId();
|
const viewId = generateId();
|
||||||
const key = generateId();
|
const key = generateId();
|
||||||
const route = child;
|
|
||||||
|
// const route = child;
|
||||||
const matchProps = {
|
const matchProps = {
|
||||||
exact: child.props.exact,
|
exact: child.props.exact,
|
||||||
path: child.props.path || child.props.from,
|
path: child.props.path || child.props.from,
|
||||||
@ -234,7 +272,7 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
match,
|
match,
|
||||||
childProps: child.props
|
childProps: child.props
|
||||||
},
|
},
|
||||||
route,
|
routeId,
|
||||||
mount: true,
|
mount: true,
|
||||||
show: !!match,
|
show: !!match,
|
||||||
isIonRoute: false
|
isIonRoute: false
|
||||||
@ -251,9 +289,9 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
const prevViewStacks = Object.assign(new ViewStacks(), prevState.viewStacks);
|
const prevViewStacks = Object.assign(new ViewStacks(), prevState.viewStacks);
|
||||||
const newStack: ViewStack = {
|
const newStack: ViewStack = {
|
||||||
id: stack,
|
id: stack,
|
||||||
views: stackItems,
|
views: stackItems
|
||||||
routerOutlet
|
|
||||||
};
|
};
|
||||||
|
this.routerOutlets[stack] = routerOutlet;
|
||||||
if (activeId) {
|
if (activeId) {
|
||||||
this.activeIonPageId = activeId;
|
this.activeIonPageId = activeId;
|
||||||
}
|
}
|
||||||
@ -267,18 +305,6 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) {
|
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 canStart = () => {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
@ -310,70 +336,115 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
syncView(page: HTMLElement, viewId: string) {
|
syncView(page: HTMLElement, viewId: string) {
|
||||||
this.setState(state => {
|
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||||
const viewStacks = Object.assign(new ViewStacks(), state.viewStacks);
|
|
||||||
const { view } = viewStacks.findViewInfoById(viewId);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view!.ionPageElement = page;
|
syncRoute(_id: string, routerOutlet: any) {
|
||||||
view!.isIonRoute = true;
|
const ionRouterOutlet = React.Children.only(routerOutlet) as React.ReactElement;
|
||||||
|
|
||||||
return {
|
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
|
||||||
viewStacks
|
for (const routeKey in this.routes) {
|
||||||
};
|
const route = this.routes[routeKey];
|
||||||
|
if (route.props.path === child.props.path) {
|
||||||
}, () => {
|
this.routes[routeKey] = child;
|
||||||
this.setActiveView(this.state.location || this.props.location, this.state.action!);
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean) {
|
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOutlet: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean, leavingViewHtml?: string) {
|
||||||
|
if (!this.firstRender) {
|
||||||
|
|
||||||
if (enteringEl === leavingEl) {
|
if (!('componentOnReady' in ionRouterOutlet)) {
|
||||||
return;
|
await waitUntilRouterOutletReady(ionRouterOutlet);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ionRouterOuter.commit(enteringEl, leavingEl, {
|
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,
|
deepWait: true,
|
||||||
duration: direction === undefined ? 0 : undefined,
|
duration: direction === undefined ? 0 : undefined,
|
||||||
direction,
|
direction,
|
||||||
showGoBack,
|
showGoBack,
|
||||||
progressAnimation: false
|
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)) {
|
if (leavingEl && (enteringEl !== leavingEl)) {
|
||||||
/** add hidden attributes */
|
/** add hidden attributes */
|
||||||
leavingEl.classList.add('ion-page-hidden');
|
leavingEl.classList.add('ion-page-hidden');
|
||||||
leavingEl.setAttribute('aria-hidden', 'true');
|
leavingEl.setAttribute('aria-hidden', 'true');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
enteringEl.classList.remove('ion-page-invisible');
|
||||||
|
enteringEl.style.zIndex = '101';
|
||||||
|
this.firstRender = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNavigate(type: 'push' | 'replace', path: string, direction?: RouterDirection) {
|
handleNavigate(ionRouteAction: IonRouteAction, path: string, direction?: RouterDirection) {
|
||||||
this.currentDirection = direction;
|
this.currentIonRouteAction = ionRouteAction;
|
||||||
if (type === 'push') {
|
switch (ionRouteAction) {
|
||||||
|
case 'push':
|
||||||
|
this.currentRouteDirection = direction;
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
} else {
|
break;
|
||||||
|
case 'pop':
|
||||||
|
this.currentRouteDirection = direction || 'back';
|
||||||
this.props.history.replace(path);
|
this.props.history.replace(path);
|
||||||
|
break;
|
||||||
|
case 'replace':
|
||||||
|
this.currentRouteDirection = 'none';
|
||||||
|
this.props.history.replace(path);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateBack(defaultHref?: string) {
|
navigateBack(defaultHref?: string) {
|
||||||
const { view: activeIonPage } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
|
const { view: leavingView } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
|
||||||
if (activeIonPage) {
|
if (leavingView) {
|
||||||
const { view: enteringView } = this.state.viewStacks.findViewInfoById(activeIonPage.prevId);
|
if (leavingView.id === leavingView.prevId) {
|
||||||
|
const previousLocation = this.locationHistory.previous();
|
||||||
|
if (previousLocation) {
|
||||||
|
this.handleNavigate('pop', previousLocation.pathname + previousLocation.search);
|
||||||
|
} else {
|
||||||
|
defaultHref && this.handleNavigate('pop', defaultHref);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { view: enteringView } = this.state.viewStacks.findViewInfoById(leavingView.prevId);
|
||||||
if (enteringView) {
|
if (enteringView) {
|
||||||
const lastLocation = this.locationHistory.findLastLocationByUrl(enteringView.routeData.match!.url);
|
const lastLocation = this.locationHistory.findLastLocationByUrl(enteringView.routeData.match!.url);
|
||||||
if (lastLocation) {
|
if (lastLocation) {
|
||||||
this.handleNavigate('replace', lastLocation.pathname + lastLocation.search, 'back');
|
this.handleNavigate('pop', lastLocation.pathname + lastLocation.search);
|
||||||
} else {
|
} else {
|
||||||
this.handleNavigate('replace', enteringView.routeData.match!.url, 'back');
|
this.handleNavigate('pop', enteringView.routeData.match!.url);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const currentLocation = this.locationHistory.previous();
|
const currentLocation = this.locationHistory.previous();
|
||||||
if (currentLocation) {
|
if (currentLocation) {
|
||||||
this.handleNavigate('replace', currentLocation.pathname + currentLocation.search, 'back');
|
this.handleNavigate('pop', currentLocation.pathname + currentLocation.search);
|
||||||
} else {
|
} else {
|
||||||
if (defaultHref) {
|
if (defaultHref) {
|
||||||
this.handleNavigate('replace', defaultHref, 'back');
|
this.handleNavigate('pop', defaultHref);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -399,5 +470,28 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clonePageElement(leavingViewHtml: string) {
|
||||||
|
const newEl = document.createElement('div');
|
||||||
|
newEl.innerHTML = leavingViewHtml;
|
||||||
|
newEl.classList.add('ion-page-hidden');
|
||||||
|
newEl.style.zIndex = '';
|
||||||
|
// Remove an existing back button so the new element doesn't get two of them
|
||||||
|
const ionBackButton = newEl.getElementsByTagName('ion-back-button');
|
||||||
|
if (ionBackButton[0]) {
|
||||||
|
ionBackButton[0].innerHTML = '';
|
||||||
|
}
|
||||||
|
return newEl.firstChild as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitUntilRouterOutletReady(ionRouterOutlet: HTMLIonRouterElement) {
|
||||||
|
if ('componentOnReady' in ionRouterOutlet) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
waitUntilRouterOutletReady(ionRouterOutlet);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const RouteManagerWithRouter = withRouter(RouteManager);
|
export const RouteManagerWithRouter = withRouter(RouteManager);
|
||||||
RouteManagerWithRouter.displayName = 'RouteManager';
|
RouteManagerWithRouter.displayName = 'RouteManager';
|
||||||
|
@ -2,22 +2,21 @@ import React from 'react';
|
|||||||
|
|
||||||
import { generateId, isDevMode } from '../utils';
|
import { generateId, isDevMode } from '../utils';
|
||||||
|
|
||||||
import { RouteManagerContext } from './RouteManagerContext';
|
import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext';
|
||||||
import { View } from './View';
|
import { View } from './View';
|
||||||
import { ViewItem } from './ViewItem';
|
import { ViewItem } from './ViewItem';
|
||||||
import { ViewTransitionManager } from './ViewTransitionManager';
|
import { ViewTransitionManager } from './ViewTransitionManager';
|
||||||
|
|
||||||
interface StackManagerProps {
|
interface StackManagerProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
routeManager: RouteManagerContextState;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StackManagerState {
|
interface StackManagerState { }
|
||||||
routerOutletReady: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StackManager extends React.Component<StackManagerProps, StackManagerState> {
|
class StackManagerInner extends React.Component<StackManagerProps, StackManagerState> {
|
||||||
routerOutletEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
|
routerOutletEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
|
||||||
context!: React.ContextType<typeof RouteManagerContext>;
|
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
constructor(props: StackManagerProps) {
|
constructor(props: StackManagerProps) {
|
||||||
@ -25,47 +24,44 @@ export class StackManager extends React.Component<StackManagerProps, StackManage
|
|||||||
this.id = this.props.id || generateId();
|
this.id = this.props.id || generateId();
|
||||||
this.handleViewSync = this.handleViewSync.bind(this);
|
this.handleViewSync = this.handleViewSync.bind(this);
|
||||||
this.handleHideView = this.handleHideView.bind(this);
|
this.handleHideView = this.handleHideView.bind(this);
|
||||||
this.state = {
|
this.state = {};
|
||||||
routerOutletReady: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.context.setupIonRouter(this.id, this.props.children, this.routerOutletEl.current!);
|
this.props.routeManager.setupIonRouter(this.id, this.props.children, this.routerOutletEl.current!);
|
||||||
this.routerOutletEl.current!.addEventListener('routerOutletReady', () => {
|
}
|
||||||
this.setState({
|
|
||||||
routerOutletReady: true
|
static getDerivedStateFromProps(props: StackManagerProps, state: StackManagerState) {
|
||||||
});
|
props.routeManager.syncRoute('', props.children);
|
||||||
});
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.context.removeViewStack(this.id);
|
this.props.routeManager.removeViewStack(this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleViewSync(page: HTMLElement, viewId: string) {
|
handleViewSync(page: HTMLElement, viewId: string) {
|
||||||
this.context.syncView(page, viewId);
|
this.props.routeManager.syncView(page, viewId);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHideView(viewId: string) {
|
handleHideView(viewId: string) {
|
||||||
this.context.hideView(viewId);
|
this.props.routeManager.hideView(viewId);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderChild(item: ViewItem) {
|
renderChild(item: ViewItem, route: any) {
|
||||||
const component = React.cloneElement(item.route, {
|
const component = React.cloneElement(route, {
|
||||||
computedMatch: item.routeData.match
|
computedMatch: item.routeData.match
|
||||||
});
|
});
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const context = this.context;
|
const routeManager = this.props.routeManager;
|
||||||
const viewStack = context.viewStacks.get(this.id);
|
const viewStack = routeManager.viewStacks.get(this.id);
|
||||||
const views = (viewStack || { views: [] }).views.filter(x => x.show);
|
const views = (viewStack || { views: [] }).views.filter(x => x.show);
|
||||||
const ionRouterOutlet = React.Children.only(this.props.children) as React.ReactElement;
|
const ionRouterOutlet = React.Children.only(this.props.children) as React.ReactElement;
|
||||||
const { routerOutletReady } = this.state;
|
const childElements = views.map(view => {
|
||||||
|
const route = routeManager.getRoute(view.routeId);
|
||||||
const childElements = routerOutletReady ? views.map(view => {
|
|
||||||
return (
|
return (
|
||||||
<ViewTransitionManager
|
<ViewTransitionManager
|
||||||
id={view.id}
|
id={view.id}
|
||||||
@ -76,17 +72,22 @@ export class StackManager extends React.Component<StackManagerProps, StackManage
|
|||||||
onViewSync={this.handleViewSync}
|
onViewSync={this.handleViewSync}
|
||||||
onHideView={this.handleHideView}
|
onHideView={this.handleHideView}
|
||||||
view={view}
|
view={view}
|
||||||
|
route={route}
|
||||||
>
|
>
|
||||||
{this.renderChild(view)}
|
{this.renderChild(view, route)}
|
||||||
</View>
|
</View>
|
||||||
</ViewTransitionManager>
|
</ViewTransitionManager>
|
||||||
);
|
);
|
||||||
}) : <div></div>;
|
});
|
||||||
|
|
||||||
const elementProps: any = {
|
const elementProps: any = {
|
||||||
ref: this.routerOutletEl
|
ref: this.routerOutletEl
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ionRouterOutlet.props.forwardedRef) {
|
||||||
|
ionRouterOutlet.props.forwardedRef.current = this.routerOutletEl;
|
||||||
|
}
|
||||||
|
|
||||||
if (isDevMode()) {
|
if (isDevMode()) {
|
||||||
elementProps['data-stack-id'] = this.id;
|
elementProps['data-stack-id'] = this.id;
|
||||||
}
|
}
|
||||||
@ -95,8 +96,14 @@ export class StackManager extends React.Component<StackManagerProps, StackManage
|
|||||||
|
|
||||||
return routerOutletChild;
|
return routerOutletChild;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static get contextType() {
|
const withContext = (Component: any) => {
|
||||||
return RouteManagerContext;
|
return (props: any) => (
|
||||||
}
|
<RouteManagerContext.Consumer>
|
||||||
}
|
{context => <Component {...props} routeManager={context} />}
|
||||||
|
</RouteManagerContext.Consumer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StackManager = withContext(StackManagerInner);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { IonLifeCycleContext, NavContext } from '@ionic/react';
|
import { IonLifeCycleContext, NavContext } from '@ionic/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { isDevMode } from '../utils';
|
import { isDevMode } from '../utils';
|
||||||
|
|
||||||
@ -10,6 +9,7 @@ interface ViewProps extends React.HTMLAttributes<HTMLElement> {
|
|||||||
onViewSync: (page: HTMLElement, viewId: string) => void;
|
onViewSync: (page: HTMLElement, viewId: string) => void;
|
||||||
onHideView: (viewId: string) => void;
|
onHideView: (viewId: string) => void;
|
||||||
view: ViewItem;
|
view: ViewItem;
|
||||||
|
route: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,20 +19,6 @@ export class View extends React.Component<ViewProps, {}> {
|
|||||||
context!: React.ContextType<typeof IonLifeCycleContext>;
|
context!: React.ContextType<typeof IonLifeCycleContext>;
|
||||||
ionPage?: HTMLElement;
|
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() {
|
componentWillUnmount() {
|
||||||
if (this.ionPage) {
|
if (this.ionPage) {
|
||||||
this.ionPage.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
|
this.ionPage.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
|
||||||
|
@ -3,10 +3,9 @@ export interface ViewItem<RouteData = any> {
|
|||||||
id: string;
|
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. */
|
/** 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;
|
key: string;
|
||||||
/** The <Route /> or <Redirect /> component associated with the view */
|
|
||||||
route: React.ReactElement<any>;
|
routeId: string;
|
||||||
/** The reference to the <IonPage /> element. */
|
|
||||||
ionPageElement?: HTMLElement;
|
|
||||||
/** The routeData for the view. */
|
/** The routeData for the view. */
|
||||||
routeData: RouteData;
|
routeData: RouteData;
|
||||||
/** Used to track which page pushed the page into view. Used for back button purposes. */
|
/** Used to track which page pushed the page into view. Used for back button purposes. */
|
||||||
@ -23,4 +22,9 @@ export interface ViewItem<RouteData = any> {
|
|||||||
* An IonRoute is a Route that contains an IonPage. Only IonPages participate in transition and lifecycle events.
|
* An IonRoute is a Route that contains an IonPage. Only IonPages participate in transition and lifecycle events.
|
||||||
*/
|
*/
|
||||||
isIonRoute: boolean;
|
isIonRoute: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* location of the view
|
||||||
|
*/
|
||||||
|
location?: string;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import { ViewItem } from './ViewItem';
|
|||||||
|
|
||||||
export interface ViewStack {
|
export interface ViewStack {
|
||||||
id: string;
|
id: string;
|
||||||
routerOutlet: HTMLIonRouterOutletElement;
|
|
||||||
views: ViewItem[];
|
views: ViewItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,10 +57,11 @@ export class ViewStacks {
|
|||||||
path: v.routeData.childProps.path || v.routeData.childProps.from,
|
path: v.routeData.childProps.path || v.routeData.childProps.from,
|
||||||
component: v.routeData.childProps.component
|
component: v.routeData.childProps.component
|
||||||
};
|
};
|
||||||
match = matchPath(location.pathname, matchProps);
|
const myMatch: IonRouteData['match'] | null | undefined = matchPath(location.pathname, matchProps);
|
||||||
if (match) {
|
if (myMatch) {
|
||||||
view = v;
|
view = v;
|
||||||
return true;
|
match = myMatch;
|
||||||
|
return view.location === location.pathname;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { Location as HistoryLocation } from 'history';
|
|||||||
const RESTRICT_SIZE = 25;
|
const RESTRICT_SIZE = 25;
|
||||||
|
|
||||||
export class LocationHistory {
|
export class LocationHistory {
|
||||||
locationHistory: HistoryLocation[] = [];
|
private locationHistory: HistoryLocation[] = [];
|
||||||
|
|
||||||
add(location: HistoryLocation) {
|
add(location: HistoryLocation) {
|
||||||
this.locationHistory.push(location);
|
this.locationHistory.push(location);
|
||||||
@ -21,6 +21,10 @@ export class LocationHistory {
|
|||||||
this.locationHistory.push(location);
|
this.locationHistory.push(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.locationHistory = [];
|
||||||
|
}
|
||||||
|
|
||||||
findLastLocationByUrl(url: string) {
|
findLastLocationByUrl(url: string) {
|
||||||
for (let i = this.locationHistory.length - 1; i >= 0; i--) {
|
for (let i = this.locationHistory.length - 1; i >= 0; i--) {
|
||||||
const location = this.locationHistory[i];
|
const location = this.locationHistory[i];
|
||||||
|
@ -27,6 +27,11 @@
|
|||||||
"jsx-no-bind": false,
|
"jsx-no-bind": false,
|
||||||
"jsx-no-lambda": false,
|
"jsx-no-lambda": false,
|
||||||
"jsx-no-multiline-js": 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^23.3.9",
|
"@types/jest": "^23.3.9",
|
||||||
"@types/node": "10.12.9",
|
"@types/node": "^12.12.14",
|
||||||
"@types/react": "^16.9.2",
|
"@types/react": "^16.9.2",
|
||||||
"@types/react-dom": "^16.9.0",
|
"@types/react-dom": "^16.9.0",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
@ -67,7 +67,7 @@
|
|||||||
"tslint": "^5.18.0",
|
"tslint": "^5.18.0",
|
||||||
"tslint-ionic-rules": "0.0.21",
|
"tslint-ionic-rules": "0.0.21",
|
||||||
"tslint-react": "^4.0.0",
|
"tslint-react": "^4.0.0",
|
||||||
"typescript": "3.5.3"
|
"typescript": "^3.7.2"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
@ -3,13 +3,26 @@ import React from 'react';
|
|||||||
import { NavContext } from '../contexts/NavContext';
|
import { NavContext } from '../contexts/NavContext';
|
||||||
|
|
||||||
import { IonicReactProps } from './IonicReactProps';
|
import { IonicReactProps } from './IonicReactProps';
|
||||||
|
import { createForwardRef } from './utils';
|
||||||
|
|
||||||
export const IonPage = /*@__PURE__*/(() => class IonPageInternal extends React.Component<React.HTMLAttributes<HTMLElement> & IonicReactProps> {
|
interface IonPageProps extends IonicReactProps {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IonPageInternalProps extends IonPageProps {
|
||||||
|
forwardedRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class IonPageInternal extends React.Component<IonPageInternalProps> {
|
||||||
context!: React.ContextType<typeof NavContext>;
|
context!: React.ContextType<typeof NavContext>;
|
||||||
ref = React.createRef<HTMLDivElement>();
|
ref: React.RefObject<HTMLDivElement>;
|
||||||
|
|
||||||
|
constructor(props: IonPageInternalProps) {
|
||||||
|
super(props);
|
||||||
|
this.ref = this.props.forwardedRef || React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.context && this.ref.current) {
|
if (this.context && this.ref && this.ref.current) {
|
||||||
if (this.context.hasIonicRouter()) {
|
if (this.context.hasIonicRouter()) {
|
||||||
this.context.registerIonPage(this.ref.current);
|
this.context.registerIonPage(this.ref.current);
|
||||||
}
|
}
|
||||||
@ -17,7 +30,7 @@ export const IonPage = /*@__PURE__*/(() => class IonPageInternal extends React.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, children, ...props } = this.props;
|
const { className, children, forwardedRef, ...props } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className ? `ion-page ${className}` : 'ion-page'} ref={this.ref} {...props}>
|
<div className={className ? `ion-page ${className}` : 'ion-page'} ref={this.ref} {...props}>
|
||||||
@ -33,4 +46,6 @@ export const IonPage = /*@__PURE__*/(() => class IonPageInternal extends React.C
|
|||||||
static get contextType() {
|
static get contextType() {
|
||||||
return NavContext;
|
return NavContext;
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
export const IonPage = createForwardRef(IonPageInternal, 'IonPage');
|
||||||
|
@ -13,7 +13,7 @@ type Props = LocalJSX.IonRouterOutlet & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type InternalProps = Props & {
|
type InternalProps = Props & {
|
||||||
forwardedRef: any;
|
forwardedRef?: React.RefObject<HTMLIonRouterOutletElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IonRouterOutletContainer = /*@__PURE__*/(() => class extends React.Component<InternalProps> {
|
const IonRouterOutletContainer = /*@__PURE__*/(() => class extends React.Component<InternalProps> {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
export interface IonicReactProps {
|
export interface IonicReactProps {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
className?: string;
|
||||||
style?: {[key: string]: any };
|
style?: {[key: string]: any };
|
||||||
}
|
}
|
||||||
|
@ -15,18 +15,21 @@ export interface ReactControllerProps {
|
|||||||
|
|
||||||
export const createControllerComponent = <OptionsType extends object, OverlayType extends OverlayBase>(
|
export const createControllerComponent = <OptionsType extends object, OverlayType extends OverlayBase>(
|
||||||
displayName: string,
|
displayName: string,
|
||||||
controller: { create: (options: OptionsType) => Promise<OverlayType> }
|
controller: { create: (options: OptionsType) => Promise<OverlayType>; }
|
||||||
) => {
|
) => {
|
||||||
const dismissEventName = `on${displayName}DidDismiss`;
|
const dismissEventName = `on${displayName}DidDismiss`;
|
||||||
|
|
||||||
type Props = OptionsType & ReactControllerProps;
|
type Props = OptionsType & ReactControllerProps & {
|
||||||
|
forwardedRef?: React.RefObject<OverlayType>;
|
||||||
|
};
|
||||||
|
|
||||||
return class extends React.Component<Props> {
|
class Overlay extends React.Component<Props> {
|
||||||
overlay?: OverlayType;
|
overlay?: OverlayType;
|
||||||
isUnmounted = false;
|
isUnmounted = false;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.handleDismiss = this.handleDismiss.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get displayName() {
|
static get displayName() {
|
||||||
@ -54,17 +57,29 @@ export const createControllerComponent = <OptionsType extends object, OverlayTyp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDismiss(event: CustomEvent<OverlayEventDetail<any>>) {
|
||||||
|
if (this.props.onDidDismiss) {
|
||||||
|
this.props.onDidDismiss(event);
|
||||||
|
}
|
||||||
|
if (this.props.forwardedRef) {
|
||||||
|
(this.props.forwardedRef as any).current = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async present(prevProps?: Props) {
|
async present(prevProps?: Props) {
|
||||||
const { isOpen, onDidDismiss, ...cProps } = this.props;
|
const { isOpen, onDidDismiss, ...cProps } = this.props;
|
||||||
this.overlay = await controller.create({
|
this.overlay = await controller.create({
|
||||||
...cProps as any
|
...cProps as any
|
||||||
});
|
});
|
||||||
attachProps(this.overlay, {
|
attachProps(this.overlay, {
|
||||||
[dismissEventName]: onDidDismiss
|
[dismissEventName]: this.handleDismiss
|
||||||
}, prevProps);
|
}, prevProps);
|
||||||
// Check isOpen again since the value could have changed during the async call to controller.create
|
// 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.
|
// It's also possible for the component to have become unmounted.
|
||||||
if (this.props.isOpen === true && this.isUnmounted === false) {
|
if (this.props.isOpen === true && this.isUnmounted === false) {
|
||||||
|
if (this.props.forwardedRef) {
|
||||||
|
(this.props.forwardedRef as any).current = this.overlay;
|
||||||
|
}
|
||||||
await this.overlay.present();
|
await this.overlay.present();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,5 +87,9 @@ export const createControllerComponent = <OptionsType extends object, OverlayTyp
|
|||||||
render(): null {
|
render(): null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return React.forwardRef<OverlayType, Props>((props, ref) => {
|
||||||
|
return <Overlay {...props} forwardedRef={ref} />;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -15,21 +15,24 @@ export interface ReactOverlayProps {
|
|||||||
onDidDismiss?: (event: CustomEvent<OverlayEventDetail>) => void;
|
onDidDismiss?: (event: CustomEvent<OverlayEventDetail>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createOverlayComponent = <T extends object, OverlayType extends OverlayElement>(
|
export const createOverlayComponent = <OverlayComponent extends object, OverlayType extends OverlayElement>(
|
||||||
displayName: string,
|
displayName: string,
|
||||||
controller: { create: (options: any) => Promise<OverlayType> }
|
controller: { create: (options: any) => Promise<OverlayType>; }
|
||||||
) => {
|
) => {
|
||||||
const dismissEventName = `on${displayName}DidDismiss`;
|
const dismissEventName = `on${displayName}DidDismiss`;
|
||||||
|
|
||||||
type Props = T & ReactOverlayProps;
|
type Props = OverlayComponent & ReactOverlayProps & {
|
||||||
|
forwardedRef?: React.RefObject<OverlayType>;
|
||||||
|
};
|
||||||
|
|
||||||
return class extends React.Component<Props> {
|
class Overlay extends React.Component<Props> {
|
||||||
overlay?: OverlayType;
|
overlay?: OverlayType;
|
||||||
el: HTMLDivElement;
|
el: HTMLDivElement;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.el = document.createElement('div');
|
this.el = document.createElement('div');
|
||||||
|
this.handleDismiss = this.handleDismiss.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get displayName() {
|
static get displayName() {
|
||||||
@ -46,6 +49,15 @@ export const createOverlayComponent = <T extends object, OverlayType extends Ove
|
|||||||
if (this.overlay) { this.overlay.dismiss(); }
|
if (this.overlay) { this.overlay.dismiss(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDismiss(event: CustomEvent<OverlayEventDetail<any>>) {
|
||||||
|
if (this.props.onDidDismiss) {
|
||||||
|
this.props.onDidDismiss(event);
|
||||||
|
}
|
||||||
|
if (this.props.forwardedRef) {
|
||||||
|
(this.props.forwardedRef as any).current = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async componentDidUpdate(prevProps: Props) {
|
async componentDidUpdate(prevProps: Props) {
|
||||||
if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) {
|
if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) {
|
||||||
this.present(prevProps);
|
this.present(prevProps);
|
||||||
@ -56,10 +68,11 @@ export const createOverlayComponent = <T extends object, OverlayType extends Ove
|
|||||||
}
|
}
|
||||||
|
|
||||||
async present(prevProps?: Props) {
|
async present(prevProps?: Props) {
|
||||||
const { children, isOpen, onDidDismiss = () => { return; }, ...cProps } = this.props;
|
const { children, isOpen, onDidDismiss, ...cProps } = this.props;
|
||||||
const elementProps = {
|
const elementProps = {
|
||||||
...cProps,
|
...cProps,
|
||||||
[dismissEventName]: onDidDismiss
|
ref: this.props.forwardedRef,
|
||||||
|
[dismissEventName]: this.handleDismiss
|
||||||
};
|
};
|
||||||
|
|
||||||
const overlay = this.overlay = await controller.create({
|
const overlay = this.overlay = await controller.create({
|
||||||
@ -68,6 +81,10 @@ export const createOverlayComponent = <T extends object, OverlayType extends Ove
|
|||||||
componentProps: {}
|
componentProps: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.props.forwardedRef) {
|
||||||
|
(this.props.forwardedRef as any).current = overlay;
|
||||||
|
}
|
||||||
|
|
||||||
attachProps(overlay, elementProps, prevProps);
|
attachProps(overlay, elementProps, prevProps);
|
||||||
|
|
||||||
await overlay.present();
|
await overlay.present();
|
||||||
@ -76,8 +93,12 @@ export const createOverlayComponent = <T extends object, OverlayType extends Ove
|
|||||||
render() {
|
render() {
|
||||||
return ReactDOM.createPortal(
|
return ReactDOM.createPortal(
|
||||||
this.props.children,
|
this.props.children,
|
||||||
this.el,
|
this.el
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return React.forwardRef<OverlayType, Props>((props, ref) => {
|
||||||
|
return <Overlay {...props} forwardedRef={ref} />;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export declare type RouterDirection = 'forward' | 'back' | 'none';
|
export declare type RouterDirection = 'forward' | 'back' | 'root' | 'none';
|
||||||
|
|
||||||
export type HrefProps<T> = Omit<T, 'routerDirection'> & {
|
export type HrefProps<T> = Omit<T, 'routerDirection'> & {
|
||||||
routerLink?: string;
|
routerLink?: string;
|
||||||
|
@ -69,12 +69,13 @@ const IonTabBarUnwrapped = /*@__PURE__*/(() => class extends React.Component<Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onTabButtonClick = (e: CustomEvent<{ href: string, selected: boolean, tab: string }>) => {
|
private onTabButtonClick = (e: CustomEvent<{ href: string, selected: boolean, tab: string }>) => {
|
||||||
if (this.state.activeTab === e.detail.tab) {
|
|
||||||
const originalHref = this.state.tabs[e.detail.tab].originalHref;
|
const originalHref = this.state.tabs[e.detail.tab].originalHref;
|
||||||
if (this.context.hasIonicRouter()) {
|
const currentHref = this.state.tabs[e.detail.tab].currentHref;
|
||||||
this.context.tabNavigate(originalHref);
|
if (this.state.activeTab === e.detail.tab) {
|
||||||
|
if (originalHref === currentHref) {
|
||||||
|
this.context.navigate(originalHref, 'none');
|
||||||
} else {
|
} else {
|
||||||
this.context.navigate(originalHref, 'back');
|
this.context.navigate(originalHref, 'back', 'pop');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.props.onIonTabsWillChange) {
|
if (this.props.onIonTabsWillChange) {
|
||||||
@ -83,7 +84,7 @@ const IonTabBarUnwrapped = /*@__PURE__*/(() => class extends React.Component<Pro
|
|||||||
if (this.props.onIonTabsDidChange) {
|
if (this.props.onIonTabsDidChange) {
|
||||||
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
|
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
|
||||||
}
|
}
|
||||||
this.context.navigate(this.state.tabs[e.detail.tab].currentHref, 'none');
|
this.context.navigate(currentHref, 'none');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,10 +5,9 @@ export interface NavContextState {
|
|||||||
getPageManager: () => any;
|
getPageManager: () => any;
|
||||||
getStackManager: () => any;
|
getStackManager: () => any;
|
||||||
goBack: (defaultHref?: string) => void;
|
goBack: (defaultHref?: string) => void;
|
||||||
navigate: (path: string, direction?: RouterDirection | 'none') => void;
|
navigate: (path: string, direction?: RouterDirection | 'none', ionRouteAction?: 'push' | 'replace' | 'pop') => void;
|
||||||
hasIonicRouter: () => boolean;
|
hasIonicRouter: () => boolean;
|
||||||
registerIonPage: (page: HTMLElement) => void;
|
registerIonPage: (page: HTMLElement) => void;
|
||||||
tabNavigate: (url: string) => void;
|
|
||||||
currentPath: string | undefined;
|
currentPath: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +22,6 @@ export const NavContext = /*@__PURE__*/React.createContext<NavContextState>({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigate: (path: string) => { window.location.pathname = path; },
|
navigate: (path: string) => { window.location.pathname = path; },
|
||||||
tabNavigate: () => undefined,
|
|
||||||
hasIonicRouter: () => false,
|
hasIonicRouter: () => false,
|
||||||
registerIonPage: () => undefined,
|
registerIonPage: () => undefined,
|
||||||
currentPath: undefined
|
currentPath: undefined
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
"trailing-comma": false,
|
"trailing-comma": false,
|
||||||
"no-null-keyword": false,
|
"no-null-keyword": false,
|
||||||
"no-console": false,
|
"no-console": false,
|
||||||
"no-unbound-method": true,
|
|
||||||
"no-floating-promises": false,
|
"no-floating-promises": false,
|
||||||
"no-invalid-template-strings": true,
|
"no-invalid-template-strings": true,
|
||||||
"ban-export-const-enum": true,
|
"ban-export-const-enum": true,
|
||||||
@ -27,6 +26,8 @@
|
|||||||
"jsx-no-bind": false,
|
"jsx-no-bind": false,
|
||||||
"jsx-no-lambda": false,
|
"jsx-no-lambda": false,
|
||||||
"jsx-no-multiline-js": false,
|
"jsx-no-multiline-js": false,
|
||||||
"jsx-wrap-multiline": false
|
"jsx-wrap-multiline": false,
|
||||||
|
"no-empty-interface": false,
|
||||||
|
"no-unbound-method": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user