Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(react): swipe to go back gesture works on ios #25563

Merged
merged 32 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
49232f1
fix(react): add base swipe to go back
liamdebeasi Jun 13, 2022
e220387
chore(): base onEnd implementation
liamdebeasi Jun 13, 2022
6f9a5b6
fix(react-router): hide entering page when aborting a swipe
liamdebeasi Jun 30, 2022
bd20383
fix(react-router): do not duplicate transition when completing swipe …
liamdebeasi Jun 30, 2022
810d8c0
fix(react-router): do not hide leaving view if using a gesture
liamdebeasi Jun 30, 2022
0ef8f58
fix(react-router): correctly hide page when aborting swipe
liamdebeasi Jun 30, 2022
c1dd0ff
fix(react-router): leaving page is now unmounted after gesture
liamdebeasi Jun 30, 2022
25935d4
chore(): add comments
liamdebeasi Jun 30, 2022
3c91cd8
chore(): code clean up
liamdebeasi Jun 30, 2022
522a201
test(react): add tests
liamdebeasi Jun 30, 2022
efa1aeb
Merge branch 'main' into FW-276
liamdebeasi Jun 30, 2022
973f14d
fix(react): fix duration animation
liamdebeasi Jul 1, 2022
a2b5905
Merge branch 'main' into FW-276
liamdebeasi Jul 1, 2022
95ce7d9
fix(react-router): improve reliability of fix
liamdebeasi Jul 1, 2022
c7491a7
fix(react-router): improve unmounting logic
liamdebeasi Jul 2, 2022
38d7fa5
test(react-router): test swiping in tabs mulitple times
liamdebeasi Jul 2, 2022
2f349fc
fix(react-router): avoid flicker when unmounting
liamdebeasi Jul 2, 2022
c589215
chore(): bandaid for flaky test
liamdebeasi Jul 2, 2022
75a6f30
fix(react-router): avoid flickering with swipe to go back
liamdebeasi Jul 5, 2022
ed275da
fix(react-router): ensure leaving view is unmounted without a flicker
liamdebeasi Jul 5, 2022
a506cab
fix(react-router): do not update view item match in swipe gesture
liamdebeasi Jul 7, 2022
138e16e
chore(): fix types
liamdebeasi Jul 7, 2022
4fdde4d
fix(react-router): do not swipe back to instance of same page, do not…
liamdebeasi Jul 7, 2022
422955e
test(react-router): add another test
liamdebeasi Jul 7, 2022
9fa6e35
fix(react-router): do not hide parameterized views
liamdebeasi Jul 8, 2022
e4dd5c3
test(react-router): add another test
liamdebeasi Jul 8, 2022
fc0bfbd
Merge branch 'main' into FW-276
liamdebeasi Jul 8, 2022
175d5c0
Merge branch 'FW-276' of https://github.com/ionic-team/ionic-framewor…
liamdebeasi Jul 8, 2022
03d80f9
Merge branch 'main' into FW-276
liamdebeasi Jul 12, 2022
599522a
Merge branch 'main' into FW-276
sean-perkins Jul 15, 2022
3819643
Merge branch 'main' into FW-276
liamdebeasi Jul 15, 2022
1a04548
Merge branch 'main' into FW-276
liamdebeasi Jul 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 107 additions & 26 deletions packages/react-router/src/ReactRouter/StackManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
context!: React.ContextType<typeof RouteManagerContext>;
ionRouterOutlet?: React.ReactElement;
routerOutletElement: HTMLIonRouterOutletElement | undefined;
prevProps?: StackManagerProps;
skipTransition: boolean;

stackContextValue: StackContextState = {
registerIonPage: this.registerIonPage.bind(this),
Expand All @@ -39,6 +41,8 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
this.transitionPage = this.transitionPage.bind(this);
this.handlePageTransition = this.handlePageTransition.bind(this);
this.id = generateId('routerOutlet');
this.prevProps = undefined;
this.skipTransition = false;
}

componentDidMount() {
Expand All @@ -50,7 +54,13 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
}

componentDidUpdate(prevProps: StackManagerProps) {
if (this.props.routeInfo.pathname !== prevProps.routeInfo.pathname || this.pendingPageTransition) {
const { pathname } = this.props.routeInfo;
const { pathname: prevPathname } = prevProps.routeInfo;

if (pathname !== prevPathname) {
this.prevProps = prevProps;
this.handlePageTransition(this.props.routeInfo);
} else if (this.pendingPageTransition) {
this.handlePageTransition(this.props.routeInfo);
this.pendingPageTransition = false;
}
Expand Down Expand Up @@ -187,34 +197,119 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
const canStart = () => {
const config = getConfig();
const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios');
if (swipeEnabled) {
return this.context.canGoBack();
} else {
return false;
}
if (!swipeEnabled) { return false; }

const { routeInfo } = this.props;

const propsToUse = (this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute) ? this.prevProps.routeInfo : { pathname: routeInfo.pushedByRoute || '' } as any;
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id);

return !!enteringViewItem;
};

const onStart = () => {
this.context.goBack();
const onStart = async () => {
const { routeInfo } = this.props;

const propsToUse = (this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute) ? this.prevProps.routeInfo : { pathname: routeInfo.pushedByRoute || '' } as any;
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id);
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);

/**
* When the gesture starts, kick off
* a transition that is controlled
* via a swipe gesture.
*/
if (enteringViewItem && leavingViewItem) {
await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
}

return Promise.resolve();
};
const onEnd = (shouldContinue: boolean) => {
if (shouldContinue) {
this.skipTransition = true;

this.context.goBack();
} else {
/**
* In the event that the swipe
* gesture was aborted, we should
* re-hide the page that was going to enter.
*/
const { routeInfo } = this.props;

const propsToUse = (this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute) ? this.prevProps.routeInfo : { pathname: routeInfo.pushedByRoute || '' } as any;
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id);

if (enteringViewItem?.ionPageElement !== undefined) {
const { ionPageElement } = enteringViewItem;
ionPageElement.setAttribute('aria-hidden', 'true');
ionPageElement.classList.add('ion-page-hidden');
}
}
}

routerOutlet.swipeHandler = {
canStart,
onStart,
onEnd: (_shouldContinue) => true,
onEnd
};
}

async transitionPage(
routeInfo: RouteInfo,
enteringViewItem: ViewItem,
leavingViewItem?: ViewItem
leavingViewItem?: ViewItem,
direction?: 'forward' | 'back',
progressAnimation = false
) {
const runCommit = async (enteringEl: HTMLElement, leavingEl?: HTMLElement) => {
const skipTransition = this.skipTransition;

/**
* If the transition was handled
* via the swipe to go back gesture,
* then we do not want to perform
* another transition.
*
* We skip adding ion-page or ion-page-invisible
* because the entering view already exists in the DOM.
* If we added the classes, there would be a flicker where
* the view would be briefly hidden.
*/
if (skipTransition) {
/**
* We need to reset skipTransition before
* we call routerOutlet.commit otherwise
* the transition triggered by the swipe
* to go back gesture would reset it. In
* that case you would see a duplicate
* transition triggered by handlePageTransition
* in componentDidUpdate.
*/
this.skipTransition = false;
} else {
enteringEl.classList.add('ion-page');
enteringEl.classList.add('ion-page-invisible');
}

await routerOutlet.commit(enteringEl, leavingEl, {
deepWait: true,
duration: skipTransition || directionToUse === undefined ? 0 : undefined,
direction: directionToUse,
showGoBack: !!routeInfo.pushedByRoute,
progressAnimation,
animationBuilder: routeInfo.routeAnimation,
});
}

const routerOutlet = this.routerOutletElement!;

const direction =
const routeInfoFallbackDirection =
routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root'
? undefined
: routeInfo.routeDirection;
const directionToUse = direction ?? routeInfoFallbackDirection;

if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
if (
Expand All @@ -238,26 +333,12 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
}
} else {
await runCommit(enteringViewItem.ionPageElement, leavingViewItem?.ionPageElement);
if (leavingViewItem && leavingViewItem.ionPageElement) {
if (leavingViewItem && leavingViewItem.ionPageElement && !progressAnimation) {
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
}
}
}

async function runCommit(enteringEl: HTMLElement, leavingEl?: HTMLElement) {
enteringEl.classList.add('ion-page');
enteringEl.classList.add('ion-page-invisible');

await routerOutlet.commit(enteringEl, leavingEl, {
deepWait: true,
duration: direction === undefined ? 0 : undefined,
direction: direction as any,
showGoBack: !!routeInfo.pushedByRoute,
progressAnimation: false,
animationBuilder: routeInfo.routeAnimation,
});
}
}

render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,119 @@ describe('Swipe To Go Back', () => {
This spec tests that swipe to go back works
*/

it('/swipe-to-go-back, ', () => {
it('should swipe and abort', () => {
cy.visit(`http://localhost:${port}/swipe-to-go-back`);
cy.ionPageVisible('main');

cy.ionNav('ion-item', 'Details');
cy.ionPageVisible('details');
cy.ionPageHidden('main');
cy.ionSwipeToGoBack(true);

cy.ionSwipeToGoBack(false, 'ion-router-outlet#swipe-to-go-back');
cy.ionPageVisible('details');
cy.ionPageHidden('main');
});

it('should swipe and go back', () => {
cy.visit(`http://localhost:${port}/swipe-to-go-back`);
cy.ionPageVisible('main');

cy.ionNav('ion-item', 'Details');
cy.ionPageVisible('details');
cy.ionPageHidden('main');

cy.ionSwipeToGoBack(true, 'ion-router-outlet#swipe-to-go-back');
cy.ionPageVisible('main');
});

it('should swipe and abort within a tab', () => {
cy.visit(`http://localhost:${port}/tabs/tab1`);
cy.ionPageVisible('tab1');

cy.get('#child-one').click();
cy.ionPageHidden('tab1');
cy.ionPageVisible('tab1child1');

cy.ionSwipeToGoBack(false, 'ion-tabs ion-router-outlet');

cy.ionPageHidden('tab1');
cy.ionPageVisible('tab1child1')
});

it('should swipe and go back within a tab', () => {
cy.visit(`http://localhost:${port}/tabs/tab1`);
cy.ionPageVisible('tab1');

cy.get('#child-one').click();
cy.ionPageHidden('tab1');
cy.ionPageVisible('tab1child1');

cy.ionSwipeToGoBack(true, 'ion-tabs ion-router-outlet');

cy.ionPageVisible('tab1');
cy.ionPageDoesNotExist('tab1child1')
});

it('should swipe and go back to correct tab after switching tabs', () => {
cy.visit(`http://localhost:${port}`);
cy.ionPageVisible('home');

cy.get('#go-to-tabs').click();
cy.ionPageHidden('home');
cy.ionPageVisible('tab1');
cy.ionPageVisible('tabs');

cy.get('#child-one').click();
cy.ionPageHidden('tab1');
cy.ionPageVisible('tab1child1');

cy.get('ion-tab-button#tab-button-tab2').click();
cy.ionPageVisible('tab2');
cy.ionPageHidden('tab1child1');

cy.get('ion-tab-button#tab-button-tab1').click();
cy.ionPageVisible('tab1child1');
cy.ionPageHidden('tab2');

cy.ionSwipeToGoBack(true, 'ion-tabs ion-router-outlet');

cy.ionPageVisible('tab1');
cy.ionPageDoesNotExist('tab1child1');

cy.ionSwipeToGoBack(true, 'ion-tabs ion-router-outlet');
cy.ionPageVisible('home');
cy.ionPageDoesNotExist('tabs');
});

it('should be able to swipe back from child tab page after visiting', () => {
cy.visit(`http://localhost:${port}/tabs/tab1`);
cy.ionPageVisible('tab1');

cy.get('#child-one').click();
cy.ionPageHidden('tab1');
cy.ionPageVisible('tab1child1');

cy.get('#child-two').click();
cy.ionPageHidden('tab1child1');
cy.ionPageVisible('tab1child2');

cy.ionSwipeToGoBack(true, 'ion-tabs ion-router-outlet');

cy.ionPageDoesNotExist('tab1child2');
cy.ionPageVisible('tab1child1');

cy.ionSwipeToGoBack(true, 'ion-tabs ion-router-outlet');

cy.ionPageDoesNotExist('tab1child1');
cy.ionPageVisible('tab1');

cy.get('#child-one').click();
cy.ionPageHidden('tab1');
cy.ionPageVisible('tab1child1');

cy.ionSwipeToGoBack(true, 'ion-tabs ion-router-outlet');

cy.ionPageDoesNotExist('tab1child1');
cy.ionPageVisible('tab1');
})
});
4 changes: 2 additions & 2 deletions packages/react-router/test-app/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Cypress.Commands.add('ionMenuNav', (contains) => {

Cypress.Commands.add('ionTabClick', (tabText) => {
// TODO: figure out how to get rid of this wait. Switching tabs after a forward nav to a details page needs it
cy.wait(250);
cy.wait(500);
averyjohnston marked this conversation as resolved.
Show resolved Hide resolved
cy.contains('ion-tab-button', tabText).click({ force: true });
// cy.get('ion-tab-button.tab-selected').contains(tabText)
});
Expand All @@ -126,4 +126,4 @@ Cypress.Commands.add('ionMenuClick', () => {

Cypress.Commands.add('ionHardwareBackEvent', () => {
cy.document().trigger('backbutton');
});
});
34 changes: 18 additions & 16 deletions packages/react-router/test-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IonApp, setupIonicReact } from '@ionic/react';
import { IonApp, setupIonicReact, IonRouterOutlet } from '@ionic/react';
import React from 'react';
import { Route } from 'react-router-dom';

Expand Down Expand Up @@ -43,21 +43,23 @@ const App: React.FC = () => {
return (
<IonApp>
<IonReactRouter>
<Route path="/" component={Main} exact />
<Route path="/routing" component={Routing} />
<Route path="/dynamic-routes" component={DynamicRoutes} />
<Route path="/multiple-tabs" component={MultipleTabs} />
<Route path="/dynamic-tabs" component={DynamicTabs} />
<Route path="/nested-outlet" component={NestedOutlet} />
<Route path="/nested-outlet2" component={NestedOutlet2} />
<Route path="/replace-action" component={ReplaceAction} />
<Route path="/tab-context" component={TabsContext} />
<Route path="/outlet-ref" component={OutletRef} />
<Route path="/swipe-to-go-back" component={SwipeToGoBack} />
<Route path="/dynamic-ionpage-classnames" component={DynamicIonpageClassnames} />
<Route path="/tabs" component={Tabs} />
<Route path="/tabs-secondary" component={TabsSecondary} />
<Route path="/refs" component={Refs} />
<IonRouterOutlet>
<Route path="/" component={Main} exact />
<Route path="/routing" component={Routing} />
<Route path="/dynamic-routes" component={DynamicRoutes} />
<Route path="/multiple-tabs" component={MultipleTabs} />
<Route path="/dynamic-tabs" component={DynamicTabs} />
<Route path="/nested-outlet" component={NestedOutlet} />
<Route path="/nested-outlet2" component={NestedOutlet2} />
<Route path="/replace-action" component={ReplaceAction} />
<Route path="/tab-context" component={TabsContext} />
<Route path="/outlet-ref" component={OutletRef} />
<Route path="/swipe-to-go-back" component={SwipeToGoBack} />
<Route path="/dynamic-ionpage-classnames" component={DynamicIonpageClassnames} />
<Route path="/tabs" component={Tabs} />
<Route path="/tabs-secondary" component={TabsSecondary} />
<Route path="/refs" component={Refs} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);
Expand Down
5 changes: 4 additions & 1 deletion packages/react-router/test-app/src/pages/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface MainProps {}

const Main: React.FC<MainProps> = () => {
return (
<IonPage>
<IonPage data-pageid="home">
<IonHeader>
<IonToolbar>
<IonTitle>Main</IonTitle>
Expand Down Expand Up @@ -58,6 +58,9 @@ const Main: React.FC<MainProps> = () => {
<IonItem routerLink="/Refs">
<IonLabel>Refs</IonLabel>
</IonItem>
<IonItem routerLink="/tabs" id="go-to-tabs">
<IonLabel>Tabs</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
Expand Down
Loading