diff --git a/CHANGELOG.md b/CHANGELOG.md index d533e39c25..e5f96e4aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## [4.11.4](https://github.com/ionic-team/ionic/compare/v4.11.1...v4.11.4) (2019-11-07) + + +### Bug Fixes + +* **react:** check for component unmount, fixes [#19859](https://github.com/ionic-team/ionic/issues/19859) ([7356c40](https://github.com/ionic-team/ionic/commit/7356c401742ce2b3241d6ab05fce0fa65d2f1f8a)) +* **react:** adding multiple subscriptions to lifecycle events, fixes [#19792](https://github.com/ionic-team/ionic/issues/19792) ([#19858](https://github.com/ionic-team/ionic/issues/19858)) ([0a3014d](https://github.com/ionic-team/ionic/commit/0a3014d35e2102570fd3d8c5ada29eb01aab18e9)) +* **react:** add check to warn if no ionpage is found, fixes [#19832](https://github.com/ionic-team/ionic/issues/19832) ([#19857](https://github.com/ionic-team/ionic/issues/19857)) ([628e766](https://github.com/ionic-team/ionic/commit/628e76668ea72baebdb02b9dcfe24c0da837fb08)) +* **react:** expand the location stack to better support back button, fixes [#19748](https://github.com/ionic-team/ionic/issues/19748) ([#19856](https://github.com/ionic-team/ionic/issues/19856)) ([d89508b](https://github.com/ionic-team/ionic/commit/d89508b1b58481d518b89362a8792d05f3f451c9)) +* **react:** adding hardware back button support, fixes(19819) ([#19851](https://github.com/ionic-team/ionic/issues/19851)) ([fd9745d](https://github.com/ionic-team/ionic/commit/fd9745ddcddded76d64220838aef0f599bf4352f)) +* **react:** adding swipe back functionality and routerOutlet ready improvements, fixes [#19818](https://github.com/ionic-team/ionic/issues/19818) ([#19849](https://github.com/ionic-team/ionic/issues/19849)) ([bcc40c8](https://github.com/ionic-team/ionic/commit/bcc40c8d59b723bbdb1dfd318bfb2219eb8df3cf)) +* **react:** create a new overlay each time component is presented, fixes [#19841](https://github.com/ionic-team/ionic/issues/19841), [#19823](https://github.com/ionic-team/ionic/issues/19823) ([#19842](https://github.com/ionic-team/ionic/issues/19842)) ([9fad416](https://github.com/ionic-team/ionic/commit/9fad4161be4859969e14d4d33169ef022052d6bf)) + + ## [4.11.3](https://github.com/ionic-team/ionic/compare/v4.11.1...v4.11.3) (2019-10-30) diff --git a/core/src/utils/animation/test/animation.spec.ts b/core/src/utils/animation/test/animation.spec.ts index 6ecf4ed8ca..21c885af55 100644 --- a/core/src/utils/animation/test/animation.spec.ts +++ b/core/src/utils/animation/test/animation.spec.ts @@ -385,6 +385,18 @@ describe('cubic-bezier conversion', () => { shouldApproximatelyEqual(getTimeGivenProgression(...equation, 1.02), [0.35, 0.87]); }) + + it('cubic-bezier(0.32, 0.72, 0, 1) (with out of bounds progression)', () => { + const equation = [ + [0, 0], + [0.05, 0.2], + [.14, 1.72], + [1, 1] + ]; + + expect(getTimeGivenProgression(...equation, 1.32)).toEqual([]); + expect(getTimeGivenProgression(...equation, -0.32)).toEqual([]); + }) }) }); diff --git a/packages/react-router/src/ReactRouter/NavManager.tsx b/packages/react-router/src/ReactRouter/NavManager.tsx index 1423090fdf..a01bc7873b 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,15 @@ export class NavManager extends React.Component { + e.detail.register(0, () => { + this.props.history.goBack(); + }); + }); + } } componentWillUnmount() { @@ -59,26 +50,7 @@ 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..2da67dc729 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 { generateId, isDevMode } from '../utils'; +import { LocationHistory } from '../utils/LocationHistory'; import { IonRouteData } from './IonRouteData'; import { NavManager } from './NavManager'; @@ -21,11 +22,13 @@ class RouteManager extends React.Component { + const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId); + if (view!.routeData.match!.url !== location.pathname) { + console.warn('No IonPage was found to render. Make sure you wrap your page with an IonPage component.'); + } + }, 100); + } + } } }); } removeOrphanedViews(view: ViewItem, viewStack: ViewStack) { + // Note: This technique is a bit wonky for views that reference each other and get into a circular loop. + // It can still remove a view that probably shouldn't be. const viewsToRemove = viewStack.views.filter(v => v.prevId === view.id); viewsToRemove.forEach(v => { // Don't remove if view is currently active @@ -173,7 +207,7 @@ class RouteManager extends React.Component, location: HistoryLocation) { const viewId = generateId(); @@ -212,29 +246,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 +311,6 @@ class RouteManager extends React.Component { - const viewStacks = Object.assign(new ViewStacks(), state.viewStacks); const { view } = viewStacks.findViewInfoById(viewId); @@ -261,20 +326,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 +356,41 @@ 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/src/utils/LocationHistory.ts b/packages/react-router/src/utils/LocationHistory.ts index 1ede451692..7d2708f7bc 100644 --- a/packages/react-router/src/utils/LocationHistory.ts +++ b/packages/react-router/src/utils/LocationHistory.ts @@ -12,9 +12,30 @@ export class LocationHistory { } } - findLastLocation(url: string) { - const reversedLocations = [...this.locationHistory].reverse(); - const last = reversedLocations.find(x => x.pathname.toLowerCase() === url.toLowerCase()); - return last; + pop() { + this.locationHistory.pop(); + } + + replace(location: HistoryLocation) { + this.locationHistory.pop(); + this.locationHistory.push(location); + } + + findLastLocationByUrl(url: string) { + for (let i = this.locationHistory.length - 1; i >= 0; i--) { + const location = this.locationHistory[i]; + if (location.pathname.toLocaleLowerCase() === url.toLocaleLowerCase()) { + return location; + } + } + return undefined; + } + + previous() { + return this.locationHistory[this.locationHistory.length - 2]; + } + + current() { + return this.locationHistory[this.locationHistory.length - 1]; } } 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/createControllerComponent.tsx b/packages/react/src/components/createControllerComponent.tsx index 83c0d08537..62def561c7 100644 --- a/packages/react/src/components/createControllerComponent.tsx +++ b/packages/react/src/components/createControllerComponent.tsx @@ -23,6 +23,7 @@ export const createControllerComponent = { overlay?: OverlayType; + isUnmounted = false; constructor(props: Props) { super(props); @@ -40,6 +41,7 @@ export const createControllerComponent = { 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; +}; diff --git a/packages/react/src/contexts/IonLifeCycleContext.tsx b/packages/react/src/contexts/IonLifeCycleContext.tsx index b1cf020fbe..f5d1051183 100644 --- a/packages/react/src/contexts/IonLifeCycleContext.tsx +++ b/packages/react/src/contexts/IonLifeCycleContext.tsx @@ -22,52 +22,46 @@ export const IonLifeCycleContext = /*@__PURE__*/React.createContext { return; }, }); +type LifeCycleCallback = () => void; + export const DefaultIonLifeCycleContext = class implements IonLifeCycleContextInterface { - ionViewWillEnterCallback?: () => void; - ionViewDidEnterCallback?: () => void; - ionViewWillLeaveCallback?: () => void; - ionViewDidLeaveCallback?: () => void; + ionViewWillEnterCallbacks: LifeCycleCallback[] = []; + ionViewDidEnterCallbacks: LifeCycleCallback[] = []; + ionViewWillLeaveCallbacks: LifeCycleCallback[] = []; + ionViewDidLeaveCallbacks: LifeCycleCallback[] = []; componentCanBeDestroyedCallback?: () => void; - onIonViewWillEnter(callback: () => void) { - this.ionViewWillEnterCallback = callback; + onIonViewWillEnter(callback: LifeCycleCallback) { + this.ionViewWillEnterCallbacks.push(callback); } ionViewWillEnter() { - if (this.ionViewWillEnterCallback) { - this.ionViewWillEnterCallback(); - } + this.ionViewWillEnterCallbacks.forEach(cb => cb()); } - onIonViewDidEnter(callback: () => void) { - this.ionViewDidEnterCallback = callback; + onIonViewDidEnter(callback: LifeCycleCallback) { + this.ionViewDidEnterCallbacks.push(callback); } ionViewDidEnter() { - if (this.ionViewDidEnterCallback) { - this.ionViewDidEnterCallback(); - } + this.ionViewDidEnterCallbacks.forEach(cb => cb()); } - onIonViewWillLeave(callback: () => void) { - this.ionViewWillLeaveCallback = callback; + onIonViewWillLeave(callback: LifeCycleCallback) { + this.ionViewWillLeaveCallbacks.push(callback); } ionViewWillLeave() { - if (this.ionViewWillLeaveCallback) { - this.ionViewWillLeaveCallback(); - } + this.ionViewWillLeaveCallbacks.forEach(cb => cb()); } - onIonViewDidLeave(callback: () => void) { - this.ionViewDidLeaveCallback = callback; + onIonViewDidLeave(callback: LifeCycleCallback) { + this.ionViewDidLeaveCallbacks.push(callback); } ionViewDidLeave() { - if (this.ionViewDidLeaveCallback) { - this.ionViewDidLeaveCallback(); - } + this.ionViewDidLeaveCallbacks.forEach(cb => cb()); this.componentCanBeDestroyed(); } diff --git a/packages/react/src/lifecycle/hooks.ts b/packages/react/src/lifecycle/hooks.ts index e93b2dd170..181f2dbe56 100644 --- a/packages/react/src/lifecycle/hooks.ts +++ b/packages/react/src/lifecycle/hooks.ts @@ -1,23 +1,31 @@ -import { useContext } from 'react'; +import { useContext, useEffect } from 'react'; import { IonLifeCycleContext } from '../contexts/IonLifeCycleContext'; export const useIonViewWillEnter = (callback: () => void) => { - const value = useContext(IonLifeCycleContext); - value.onIonViewWillEnter(callback); + const context = useContext(IonLifeCycleContext); + useEffect(() => { + context.onIonViewWillEnter(callback); + }, []); }; export const useIonViewDidEnter = (callback: () => void) => { - const value = useContext(IonLifeCycleContext); - value.onIonViewDidEnter(callback); + const context = useContext(IonLifeCycleContext); + useEffect(() => { + context.onIonViewDidEnter(callback); + }, []); }; export const useIonViewWillLeave = (callback: () => void) => { - const value = useContext(IonLifeCycleContext); - value.onIonViewWillLeave(callback); + const context = useContext(IonLifeCycleContext); + useEffect(() => { + context.onIonViewWillLeave(callback); + }, []); }; export const useIonViewDidLeave = (callback: () => void) => { - const value = useContext(IonLifeCycleContext); - value.onIonViewDidLeave(callback); + const context = useContext(IonLifeCycleContext); + useEffect(() => { + context.onIonViewDidLeave(callback); + }, []); };