From 2ac9105796a0765fabc48592b5b44ac58c568579 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Mon, 7 Mar 2022 08:59:21 -0500 Subject: [PATCH] fix(ios): swipe to go back now works in rtl mode (#24866) resolves #19488 --- core/src/components/content/content.scss | 36 +++++++++---- core/src/components/content/content.tsx | 7 ++- core/src/utils/gesture/swipe-back.ts | 37 ++++++++++++-- core/src/utils/gesture/test/e2e.ts | 36 +++++++++++++ core/src/utils/gesture/test/index.html | 64 ++++++++++++++++++++++++ core/src/utils/test/utils.ts | 22 +++++--- 6 files changed, 179 insertions(+), 23 deletions(-) create mode 100644 core/src/utils/gesture/test/e2e.ts create mode 100644 core/src/utils/gesture/test/index.html diff --git a/core/src/components/content/content.scss b/core/src/components/content/content.scss index 7dc20b0b4b..7798b1c629 100644 --- a/core/src/components/content/content.scss +++ b/core/src/components/content/content.scss @@ -164,7 +164,7 @@ */ top: 0; bottom: 0; - + margin-top: calc(var(--offset-top) * -1); margin-bottom: calc(var(--offset-bottom) * -1); } @@ -173,10 +173,6 @@ display: none; position: absolute; - /* stylelint-disable property-disallowed-list */ - left: -100%; - /* stylelint-enable property-disallowed-list */ - width: 100%; height: 100vh; @@ -185,6 +181,18 @@ 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 { position: absolute; @@ -204,10 +212,6 @@ display: block; position: absolute; - /* stylelint-disable property-disallowed-list */ - right: 0; - /* stylelint-enable property-disallowed-list */ - width: 10px; height: 100%; @@ -216,6 +220,20 @@ 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 // -------------------------------------------------- diff --git a/core/src/components/content/content.tsx b/core/src/components/content/content.tsx index 4e17283076..9e09c8fd8f 100644 --- a/core/src/components/content/content.tsx +++ b/core/src/components/content/content.tsx @@ -4,6 +4,7 @@ import { getIonMode } from '../../global/ionic-global'; import { Color, ScrollBaseDetail, ScrollDetail } from '../../interface'; import { componentOnReady } from '../../utils/helpers'; import { isPlatform } from '../../utils/platform'; +import { isRTL } from '../../utils/rtl'; import { createColorClasses, hostContext } from '../../utils/theme'; /** @@ -311,7 +312,8 @@ export class Content implements ComponentInterface { } render() { - const { isMainContent, scrollX, scrollY } = this; + const { isMainContent, scrollX, scrollY, el } = this; + const rtl = isRTL(el) ? 'rtl' : 'ltr'; const mode = getIonMode(this); const forceOverscroll = this.shouldForceOverscroll(); const transitionShadow = mode === 'ios'; @@ -325,6 +327,7 @@ export class Content implements ComponentInterface { [mode]: true, 'content-sizing': hostContext('ion-popover', this.el), 'overscroll': forceOverscroll, + [`content-${rtl}`]: true })} style={{ '--offset-top': `${this.cTop}px`, @@ -339,7 +342,7 @@ export class Content implements ComponentInterface { 'scroll-y': scrollY, '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} part="scroll" > diff --git a/core/src/utils/gesture/swipe-back.ts b/core/src/utils/gesture/swipe-back.ts index 6345ff4002..6ebd013381 100644 --- a/core/src/utils/gesture/swipe-back.ts +++ b/core/src/utils/gesture/swipe-back.ts @@ -1,4 +1,5 @@ import { clamp } from '../helpers'; +import { isRTL } from '../rtl'; import { Gesture, GestureDetail, createGesture } from './index'; @@ -10,26 +11,52 @@ export const createSwipeBackGesture = ( onEndHandler: (shouldComplete: boolean, step: number, dur: number) => void, ): Gesture => { 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) => { - return detail.startX <= 50 && canStartHandler(); + return isAtEdge(detail) && canStartHandler(); }; const onMove = (detail: GestureDetail) => { // set the transition animation's progress - const delta = detail.deltaX; + const delta = getDeltaX(detail); const stepValue = delta / win.innerWidth; onMoveHandler(stepValue); }; const onEnd = (detail: GestureDetail) => { // the swipe back gesture has ended - const delta = detail.deltaX; + const delta = getDeltaX(detail); const width = win.innerWidth; const stepValue = delta / width; - const velocity = detail.velocityX; + const velocity = getVelocityX(detail); const z = width / 2.0; const shouldComplete = - velocity >= 0 && (velocity > 0.2 || detail.deltaX > z); + velocity >= 0 && (velocity > 0.2 || delta > z); const missing = shouldComplete ? 1 - stepValue : stepValue; const missingDistance = missing * width; diff --git a/core/src/utils/gesture/test/e2e.ts b/core/src/utils/gesture/test/e2e.ts new file mode 100644 index 0000000000..445c1ace37 --- /dev/null +++ b/core/src/utils/gesture/test/e2e.ts @@ -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(); +}); diff --git a/core/src/utils/gesture/test/index.html b/core/src/utils/gesture/test/index.html new file mode 100644 index 0000000000..a82ed31fbd --- /dev/null +++ b/core/src/utils/gesture/test/index.html @@ -0,0 +1,64 @@ + + + + + Swipe to go back + + + + + + + + + + + + + + + + diff --git a/core/src/utils/test/utils.ts b/core/src/utils/test/utils.ts index 99f2c7dfe4..196535a745 100644 --- a/core/src/utils/test/utils.ts +++ b/core/src/utils/test/utils.ts @@ -77,26 +77,34 @@ export const listenForEvent = async (page: any, eventType: string, element: any, * @param page - The Puppeteer 'page' object * @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 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 => { +export const dragElementBy = async ( + element: any, + page: any, + x = 0, + y = 0, + startCoordinates?: { x: number, y: number } +): Promise => { try { const boundingBox = await element.boundingBox(); - const startX = boundingBox.x + boundingBox.width / 2; - const startY = boundingBox.y + boundingBox.height / 2; + const startX = (startCoordinates?.x === undefined) ? boundingBox.x + boundingBox.width / 2 : startCoordinates.x; + 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 endY = startY + y; - const midX = endX / 2; - const midY = endY / 2; - await page.mouse.move(startX, startY); await page.mouse.down(); await page.mouse.move(midX, midY); await page.mouse.move(endX, endY); await page.mouse.up(); - } catch (err) { throw err; }