Skip to content

Commit 66a3e8a

Browse files
authored
feat(router-outlet): add swipeGesture prop to control swipe-to-go-back per outlet (#31055)
Issue number: resolves internal --------- ## What is the current behavior? The swipe-to-go-back gesture on ion-router-outlet is controlled differently across frameworks: - React and Vue check the `swipeBackEnabled` config option on every swipe attempt in their framework wrappers (`StackManager.canStart()` and `IonRouterOutlet.canStart()`) - Angular reads the config once at mount and controls the gesture through its directive's `swipeGesture` setter - There is no public, per-outlet property to enable or disable the gesture. The only mechanism is the global `swipeBackEnabled` config option ## What is the new behavior? `ion-router-outlet` now exposes a public `swipeGesture` property in core that controls the swipe-to-go-back gesture per outlet instance. It defaults to true in "ios" mode and false in "md" mode, matching existing behavior. - The `swipeBackEnabled` config is read once when the outlet mounts (as the default for `swipeGesture`) rather than checked on every swipe attempt - The config check has been removed from React's StackManager and Vue's IonRouterOutlet -- core now owns this state - Angular's directive forwards the value to the core component's `swipeGesture` property for consistency - Apps can disable the gesture on a specific outlet: `<IonRouterOutlet swipeGesture={false} />` - Apps that set `swipeBackEnabled` once at startup require no changes ## Does this introduce a breaking change? - [X] Yes - [ ] No ## Other information I tried to align the updates to the BREAKING.md documents with the RR6 PR's changes to it to prevent issues from merging as much as possible
1 parent 9c0bcb3 commit 66a3e8a

24 files changed

Lines changed: 647 additions & 283 deletions

File tree

BREAKING.md

Lines changed: 22 additions & 263 deletions
Large diffs are not rendered by default.

BREAKING_ARCHIVE/v8.md

Lines changed: 282 additions & 0 deletions
Large diffs are not rendered by default.

core/api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,6 +1589,7 @@ ion-router-outlet,shadow
15891589
ion-router-outlet,prop,animated,boolean,true,false,false
15901590
ion-router-outlet,prop,animation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
15911591
ion-router-outlet,prop,mode,"ios" | "md",getIonMode(this),false,false
1592+
ion-router-outlet,prop,swipeGesture,boolean | undefined,undefined,false,false
15921593

15931594
ion-row,shadow
15941595

core/src/components.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2915,6 +2915,10 @@ export namespace Components {
29152915
*/
29162916
"mode": "ios" | "md";
29172917
"setRouteId": (id: string, params: ComponentProps | undefined, direction: RouterDirection, animation?: AnimationBuilder) => Promise<RouteWrite>;
2918+
/**
2919+
* If `true`, the router-outlet should allow navigation via swipe-to-go-back gesture. Defaults to `true` for `"ios"` mode and `false` for `"md"` mode.
2920+
*/
2921+
"swipeGesture"?: boolean;
29182922
"swipeHandler"?: SwipeGestureHandler;
29192923
}
29202924
interface IonRow {
@@ -8229,6 +8233,10 @@ declare namespace LocalJSX {
82298233
"onIonNavDidChange"?: (event: IonRouterOutletCustomEvent<void>) => void;
82308234
"onIonNavWillChange"?: (event: IonRouterOutletCustomEvent<void>) => void;
82318235
"onIonNavWillLoad"?: (event: IonRouterOutletCustomEvent<void>) => void;
8236+
/**
8237+
* If `true`, the router-outlet should allow navigation via swipe-to-go-back gesture. Defaults to `true` for `"ios"` mode and `false` for `"md"` mode.
8238+
*/
8239+
"swipeGesture"?: boolean;
82328240
"swipeHandler"?: SwipeGestureHandler;
82338241
}
82348242
interface IonRow {
@@ -9723,6 +9731,7 @@ declare namespace LocalJSX {
97239731
interface IonRouterOutletAttributes {
97249732
"mode": "ios" | "md";
97259733
"animated": boolean;
9734+
"swipeGesture": boolean;
97269735
}
97279736
interface IonSearchbarAttributes {
97289737
"color": Color;

core/src/components/router-outlet/router-outlet.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,21 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
5353
/** This property allows to create custom transition using AnimationBuilder functions. */
5454
@Prop() animation?: AnimationBuilder;
5555

56+
/**
57+
* If `true`, the router-outlet should allow navigation via swipe-to-go-back gesture.
58+
* Defaults to `true` for `"ios"` mode and `false` for `"md"` mode.
59+
*/
60+
@Prop({ mutable: true }) swipeGesture?: boolean;
61+
@Watch('swipeGesture')
62+
swipeGestureChanged() {
63+
this.updateGestureEnabled();
64+
}
65+
5666
/** @internal */
5767
@Prop() swipeHandler?: SwipeGestureHandler;
5868
@Watch('swipeHandler')
5969
swipeHandlerChanged() {
60-
if (this.gesture) {
61-
this.gesture.enable(this.swipeHandler !== undefined);
62-
}
70+
this.updateGestureEnabled();
6371
}
6472

6573
/** @internal */
@@ -121,13 +129,24 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
121129
}
122130
}
123131
);
124-
this.swipeHandlerChanged();
132+
133+
if (this.swipeGesture === undefined) {
134+
this.swipeGesture = config.getBoolean('swipeBackEnabled', this.mode === 'ios');
135+
} else {
136+
this.updateGestureEnabled();
137+
}
125138
}
126139

127140
componentWillLoad() {
128141
this.ionNavWillLoad.emit();
129142
}
130143

144+
private updateGestureEnabled() {
145+
if (this.gesture) {
146+
this.gesture.enable(this.swipeHandler !== undefined && this.swipeGesture === true);
147+
}
148+
}
149+
131150
disconnectedCallback() {
132151
if (this.gesture) {
133152
this.gesture.destroy();

packages/angular/common/src/directives/navigation/router-outlet.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ export abstract class IonRouterOutlet implements OnDestroy, OnInit {
108108
onEnd: (shouldContinue) => this.stackCtrl.endBackTransition(shouldContinue),
109109
}
110110
: undefined;
111+
112+
this.nativeEl.swipeGesture = swipe;
111113
}
112114

113115
constructor(
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { test } from '@playwright/test';
2+
import { ionSwipeToGoBack } from '../../utils/drag-utils';
3+
import { ionPageVisible, ionPageHidden } from '../../utils/test-utils';
4+
5+
test.describe('Swipe Gesture Disabled', () => {
6+
test.beforeEach(async ({ page }) => {
7+
await page.goto('/standalone/swipe-gesture-disabled?ionic:mode=ios');
8+
});
9+
10+
test('should not swipe back when swipeGesture is false', async ({ page }) => {
11+
await ionPageVisible(page, 'app-swipe-gesture-disabled-main');
12+
13+
await page.locator('#swipe-disabled-details').click();
14+
await ionPageVisible(page, 'app-swipe-gesture-disabled-details');
15+
await ionPageHidden(page, 'app-swipe-gesture-disabled-main');
16+
17+
await ionSwipeToGoBack(page, true);
18+
19+
await ionPageVisible(page, 'app-swipe-gesture-disabled-details');
20+
await ionPageHidden(page, 'app-swipe-gesture-disabled-main');
21+
});
22+
});

packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ export const routes: Routes = [
5555
]
5656
},
5757
{ path: 'tabs-basic', loadComponent: () => import('../tabs-basic/tabs-basic.component').then(c => c.TabsBasicComponent) },
58+
{
59+
path: 'swipe-gesture-disabled',
60+
loadComponent: () => import('../swipe-gesture-disabled/swipe-gesture-disabled.component').then(c => c.SwipeGestureDisabledComponent),
61+
children: [
62+
{ path: '', loadComponent: () => import('../swipe-gesture-disabled/swipe-gesture-disabled-main.component').then(c => c.SwipeGestureDisabledMainComponent) },
63+
{ path: 'details', loadComponent: () => import('../swipe-gesture-disabled/swipe-gesture-disabled-details.component').then(c => c.SwipeGestureDisabledDetailsComponent) }
64+
]
65+
},
5866
{
5967
path: 'validation',
6068
children: [

packages/angular/test/base/src/app/standalone/home-page/home-page.component.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@
7979
Tabs Basic Test
8080
</ion-label>
8181
</ion-item>
82+
<ion-item routerLink="/standalone/swipe-gesture-disabled">
83+
<ion-label>
84+
Swipe Gesture Disabled Test
85+
</ion-label>
86+
</ion-item>
8287
</ion-list>
8388

8489
<ion-list>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Component } from '@angular/core';
2+
import { IonBackButton, IonButtons, IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular/standalone';
3+
4+
@Component({
5+
selector: 'app-swipe-gesture-disabled-details',
6+
template: `
7+
<ion-header>
8+
<ion-toolbar>
9+
<ion-buttons slot="start">
10+
<ion-back-button defaultHref="/standalone/swipe-gesture-disabled"></ion-back-button>
11+
</ion-buttons>
12+
<ion-title>Details</ion-title>
13+
</ion-toolbar>
14+
</ion-header>
15+
<ion-content>
16+
<div>Details (swipe disabled)</div>
17+
</ion-content>
18+
`,
19+
standalone: true,
20+
imports: [IonBackButton, IonButtons, IonContent, IonHeader, IonTitle, IonToolbar]
21+
})
22+
export class SwipeGestureDisabledDetailsComponent {}

0 commit comments

Comments
 (0)