mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-22 21:48:42 +08:00
fix(ios): swipe to go back now works in rtl mode (#24866)
resolves #19488
This commit is contained in:
@ -173,10 +173,6 @@
|
|||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
/* stylelint-disable property-disallowed-list */
|
|
||||||
left: -100%;
|
|
||||||
/* stylelint-enable property-disallowed-list */
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
@ -185,6 +181,18 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host(.content-ltr) .transition-effect {
|
||||||
|
/* stylelint-disable property-disallowed-list */
|
||||||
|
left: -100%;
|
||||||
|
/* stylelint-enable property-disallowed-list */
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.content-rtl) .transition-effect {
|
||||||
|
/* stylelint-disable property-disallowed-list */
|
||||||
|
right: -100%;
|
||||||
|
/* stylelint-enable property-disallowed-list */
|
||||||
|
}
|
||||||
|
|
||||||
.transition-cover {
|
.transition-cover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
@ -204,10 +212,6 @@
|
|||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
/* stylelint-disable property-disallowed-list */
|
|
||||||
right: 0;
|
|
||||||
/* stylelint-enable property-disallowed-list */
|
|
||||||
|
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@ -216,6 +220,20 @@
|
|||||||
background-size: 10px 16px;
|
background-size: 10px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host(.content-ltr) .transition-shadow {
|
||||||
|
/* stylelint-disable property-disallowed-list */
|
||||||
|
right: 0;
|
||||||
|
/* stylelint-enable property-disallowed-list */
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.content-rtl) .transition-shadow {
|
||||||
|
/* stylelint-disable property-disallowed-list */
|
||||||
|
left: 0;
|
||||||
|
/* stylelint-enable property-disallowed-list */
|
||||||
|
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Content: Fixed
|
// Content: Fixed
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
@ -4,6 +4,7 @@ import { getIonMode } from '../../global/ionic-global';
|
|||||||
import { Color, ScrollBaseDetail, ScrollDetail } from '../../interface';
|
import { Color, ScrollBaseDetail, ScrollDetail } from '../../interface';
|
||||||
import { componentOnReady } from '../../utils/helpers';
|
import { componentOnReady } from '../../utils/helpers';
|
||||||
import { isPlatform } from '../../utils/platform';
|
import { isPlatform } from '../../utils/platform';
|
||||||
|
import { isRTL } from '../../utils/rtl';
|
||||||
import { createColorClasses, hostContext } from '../../utils/theme';
|
import { createColorClasses, hostContext } from '../../utils/theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -311,7 +312,8 @@ export class Content implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isMainContent, scrollX, scrollY } = this;
|
const { isMainContent, scrollX, scrollY, el } = this;
|
||||||
|
const rtl = isRTL(el) ? 'rtl' : 'ltr';
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const forceOverscroll = this.shouldForceOverscroll();
|
const forceOverscroll = this.shouldForceOverscroll();
|
||||||
const transitionShadow = mode === 'ios';
|
const transitionShadow = mode === 'ios';
|
||||||
@ -325,6 +327,7 @@ export class Content implements ComponentInterface {
|
|||||||
[mode]: true,
|
[mode]: true,
|
||||||
'content-sizing': hostContext('ion-popover', this.el),
|
'content-sizing': hostContext('ion-popover', this.el),
|
||||||
'overscroll': forceOverscroll,
|
'overscroll': forceOverscroll,
|
||||||
|
[`content-${rtl}`]: true
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
'--offset-top': `${this.cTop}px`,
|
'--offset-top': `${this.cTop}px`,
|
||||||
@ -339,7 +342,7 @@ export class Content implements ComponentInterface {
|
|||||||
'scroll-y': scrollY,
|
'scroll-y': scrollY,
|
||||||
'overscroll': (scrollX || scrollY) && forceOverscroll
|
'overscroll': (scrollX || scrollY) && forceOverscroll
|
||||||
}}
|
}}
|
||||||
ref={(el: HTMLElement) => this.scrollEl = el!}
|
ref={(scrollEl: HTMLElement) => this.scrollEl = scrollEl!}
|
||||||
onScroll={(this.scrollEvents) ? (ev: UIEvent) => this.onScroll(ev) : undefined}
|
onScroll={(this.scrollEvents) ? (ev: UIEvent) => this.onScroll(ev) : undefined}
|
||||||
part="scroll"
|
part="scroll"
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { clamp } from '../helpers';
|
import { clamp } from '../helpers';
|
||||||
|
import { isRTL } from '../rtl';
|
||||||
|
|
||||||
import { Gesture, GestureDetail, createGesture } from './index';
|
import { Gesture, GestureDetail, createGesture } from './index';
|
||||||
|
|
||||||
@ -10,26 +11,52 @@ export const createSwipeBackGesture = (
|
|||||||
onEndHandler: (shouldComplete: boolean, step: number, dur: number) => void,
|
onEndHandler: (shouldComplete: boolean, step: number, dur: number) => void,
|
||||||
): Gesture => {
|
): Gesture => {
|
||||||
const win = el.ownerDocument!.defaultView!;
|
const win = el.ownerDocument!.defaultView!;
|
||||||
|
const rtl = isRTL(el);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a gesture is near the edge
|
||||||
|
* of the screen. If true, then the swipe
|
||||||
|
* to go back gesture should proceed.
|
||||||
|
*/
|
||||||
|
const isAtEdge = (detail: GestureDetail) => {
|
||||||
|
const threshold = 50;
|
||||||
|
const { startX } = detail;
|
||||||
|
|
||||||
|
if (rtl) {
|
||||||
|
return startX >= win.innerWidth - threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
return startX <= threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDeltaX = (detail: GestureDetail) => {
|
||||||
|
return rtl ? -detail.deltaX : detail.deltaX;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVelocityX = (detail: GestureDetail) => {
|
||||||
|
return rtl ? -detail.velocityX : detail.velocityX;
|
||||||
|
}
|
||||||
|
|
||||||
const canStart = (detail: GestureDetail) => {
|
const canStart = (detail: GestureDetail) => {
|
||||||
return detail.startX <= 50 && canStartHandler();
|
return isAtEdge(detail) && canStartHandler();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMove = (detail: GestureDetail) => {
|
const onMove = (detail: GestureDetail) => {
|
||||||
// set the transition animation's progress
|
// set the transition animation's progress
|
||||||
const delta = detail.deltaX;
|
const delta = getDeltaX(detail);
|
||||||
const stepValue = delta / win.innerWidth;
|
const stepValue = delta / win.innerWidth;
|
||||||
onMoveHandler(stepValue);
|
onMoveHandler(stepValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEnd = (detail: GestureDetail) => {
|
const onEnd = (detail: GestureDetail) => {
|
||||||
// the swipe back gesture has ended
|
// the swipe back gesture has ended
|
||||||
const delta = detail.deltaX;
|
const delta = getDeltaX(detail);
|
||||||
const width = win.innerWidth;
|
const width = win.innerWidth;
|
||||||
const stepValue = delta / width;
|
const stepValue = delta / width;
|
||||||
const velocity = detail.velocityX;
|
const velocity = getVelocityX(detail);
|
||||||
const z = width / 2.0;
|
const z = width / 2.0;
|
||||||
const shouldComplete =
|
const shouldComplete =
|
||||||
velocity >= 0 && (velocity > 0.2 || detail.deltaX > z);
|
velocity >= 0 && (velocity > 0.2 || delta > z);
|
||||||
|
|
||||||
const missing = shouldComplete ? 1 - stepValue : stepValue;
|
const missing = shouldComplete ? 1 - stepValue : stepValue;
|
||||||
const missingDistance = missing * width;
|
const missingDistance = missing * width;
|
||||||
|
36
core/src/utils/gesture/test/e2e.ts
Normal file
36
core/src/utils/gesture/test/e2e.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { newE2EPage } from '@stencil/core/testing';
|
||||||
|
import { dragElementBy } from '@utils/test';
|
||||||
|
|
||||||
|
test('swipe to go back should complete', async () => {
|
||||||
|
const page = await newE2EPage({ url: '/src/utils/gesture/test?ionic:mode=ios' });
|
||||||
|
|
||||||
|
const nav = await page.find('ion-nav');
|
||||||
|
const ionNavDidChange = await nav.spyOnEvent('ionNavDidChange');
|
||||||
|
|
||||||
|
await page.click('.next');
|
||||||
|
await ionNavDidChange.next();
|
||||||
|
|
||||||
|
const content = await page.$('.page-two-content');
|
||||||
|
|
||||||
|
const width = await page.evaluate(() => window.innerWidth);
|
||||||
|
await dragElementBy(content, page, width, 0, { x: 25, y: 100 });
|
||||||
|
|
||||||
|
await ionNavDidChange.next();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('swipe to go back should complete in rtl', async () => {
|
||||||
|
const page = await newE2EPage({ url: '/src/utils/gesture/test?rtl=true&ionic:mode=ios' });
|
||||||
|
|
||||||
|
const nav = await page.find('ion-nav');
|
||||||
|
const ionNavDidChange = await nav.spyOnEvent('ionNavDidChange');
|
||||||
|
|
||||||
|
await page.click('.next');
|
||||||
|
await ionNavDidChange.next();
|
||||||
|
|
||||||
|
const width = await page.evaluate(() => window.innerWidth);
|
||||||
|
|
||||||
|
const content = await page.$('.page-two-content');
|
||||||
|
await dragElementBy(content, page, -width, 0, { x: width - 25, y: 100 });
|
||||||
|
|
||||||
|
await ionNavDidChange.next();
|
||||||
|
});
|
64
core/src/utils/gesture/test/index.html
Normal file
64
core/src/utils/gesture/test/index.html
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Swipe to go back</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
|
||||||
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||||
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||||
|
<script>
|
||||||
|
class PageOne extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
this.innerHTML = `
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Page One</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<h1>Page One</h1>
|
||||||
|
<ion-nav-link router-direction="forward" component="page-two">
|
||||||
|
<ion-button class="next">Go to Page Two</ion-button>
|
||||||
|
</ion-nav-link>
|
||||||
|
</ion-content>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class PageTwo extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
this.innerHTML = `
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>Page Two</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="page-two-content ion-padding">
|
||||||
|
<h1>Page Two</h1>
|
||||||
|
</ion-content>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('page-one', PageOne);
|
||||||
|
customElements.define('page-two', PageTwo);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ion-app>
|
||||||
|
<ion-nav root="page-one"></ion-nav>
|
||||||
|
</ion-app>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.Ionic = {
|
||||||
|
config: {
|
||||||
|
mode: 'ios'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -77,26 +77,34 @@ export const listenForEvent = async (page: any, eventType: string, element: any,
|
|||||||
* @param page - The Puppeteer 'page' object
|
* @param page - The Puppeteer 'page' object
|
||||||
* @param x: number - Amount to drag `element` by on the x-axis
|
* @param x: number - Amount to drag `element` by on the x-axis
|
||||||
* @param y: number - Amount to drag `element` by on the y-axis
|
* @param y: number - Amount to drag `element` by on the y-axis
|
||||||
|
* @param startCoordinates (optional) - Coordinates of where to start the drag
|
||||||
|
* gesture. If not provided, the drag gesture will start in the middle of the
|
||||||
|
* element.
|
||||||
*/
|
*/
|
||||||
export const dragElementBy = async (element: any, page: any, x = 0, y = 0): Promise<void> => {
|
export const dragElementBy = async (
|
||||||
|
element: any,
|
||||||
|
page: any,
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
startCoordinates?: { x: number, y: number }
|
||||||
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const boundingBox = await element.boundingBox();
|
const boundingBox = await element.boundingBox();
|
||||||
|
|
||||||
const startX = boundingBox.x + boundingBox.width / 2;
|
const startX = (startCoordinates?.x === undefined) ? boundingBox.x + boundingBox.width / 2 : startCoordinates.x;
|
||||||
const startY = boundingBox.y + boundingBox.height / 2;
|
const startY = (startCoordinates?.y === undefined) ? boundingBox.y + boundingBox.height / 2 : startCoordinates.y;
|
||||||
|
|
||||||
|
const midX = startX + (x / 2);
|
||||||
|
const midY = startY + (y / 2);
|
||||||
|
|
||||||
const endX = startX + x;
|
const endX = startX + x;
|
||||||
const endY = startY + y;
|
const endY = startY + y;
|
||||||
|
|
||||||
const midX = endX / 2;
|
|
||||||
const midY = endY / 2;
|
|
||||||
|
|
||||||
await page.mouse.move(startX, startY);
|
await page.mouse.move(startX, startY);
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.mouse.move(midX, midY);
|
await page.mouse.move(midX, midY);
|
||||||
await page.mouse.move(endX, endY);
|
await page.mouse.move(endX, endY);
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user