Skip to content

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

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

Merged
merged 32 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,10 @@ export class ReactRouterViewStack extends ViewStacks {
return children;
}

findViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string) {
findViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string, updateMatch?: boolean) {
const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
if (viewItem && match) {
const shouldUpdateMatch = updateMatch === undefined || updateMatch === true;
if (shouldUpdateMatch && viewItem && match) {
viewItem.routeData.match = match;
}
return viewItem;
Expand Down
165 changes: 139 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,151 @@ 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, false);

return (
!!enteringViewItem &&
/**
* The root url '/' is treated as
* the first view item (but is never mounted),
* so we do not want to swipe back to the
* root url.
*/
enteringViewItem.mount &&

/**
* When on the first page (whatever view
* you land on after the root url) it
* is possible for findViewItemByRouteInfo to
* return the exact same view you are currently on.
* Make sure that we are not swiping back to the same
* instances of a view.
*/
enteringViewItem.routeData.match.path !== routeInfo.pathname
);
};

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, false);
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);

/**
* 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, false);
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);

/**
* Ionic React has a design defect where it
* a) Unmounts the leaving view item when using parameterized routes
* b) Considers the current view to be the entering view when using
* parameterized routes
*
* As a result, we should not hide the view item here
* as it will cause the current view to be hidden.
*/
if (
enteringViewItem !== leavingViewItem &&
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 +365,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,139 @@ 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(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);

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');
})

it('should not swipe to go back to the same view you are on', () => {
cy.visit(`http://localhost:${port}`);
cy.ionPageVisible('home');

cy.ionSwipeToGoBack(false);
cy.ionPageVisible('home');
})

it('should not hide a parameterized page when swiping and aborting', () => {
cy.visit(`http://localhost:${port}/params/0`);
cy.ionPageVisible('params-0');

cy.get('#next-page').click();
cy.ionPageVisible('params-1');

cy.ionSwipeToGoBack(false);

cy.ionPageVisible('params-1');
})
});
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);
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');
});
});
Loading