fix(ios): swipe to go back now works in rtl mode (#24866)

resolves #19488
This commit is contained in:
Liam DeBeasi
2022-03-07 08:59:21 -05:00
committed by GitHub
parent 331ce6d676
commit 2ac9105796
6 changed files with 179 additions and 23 deletions

View File

@ -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
// -------------------------------------------------- // --------------------------------------------------

View File

@ -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"
> >

View File

@ -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;

View 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();
});

View 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>

View File

@ -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;
} }