diff --git a/src/components/AppComponent.tsx b/src/components/AppComponent.tsx index 2306275bc4..be2b3976ba 100644 --- a/src/components/AppComponent.tsx +++ b/src/components/AppComponent.tsx @@ -76,8 +76,8 @@ const useDisableLongPressToSelect = () => { } /** Cancel gesture if there is an active text selection, drag, modal, or sidebar. */ -const shouldCancelGesture = (): boolean => - (selection.isActive() && !selection.isCollapsed()) || +const shouldCancelGesture = (x?: number, y?: number): boolean => + (x && y && selection.isNear(x, y)) || store.getState().dragInProgress || !!store.getState().showModal || store.getState().showSidebar diff --git a/src/components/MultiGesture.tsx b/src/components/MultiGesture.tsx index b8c0b52df3..ceabd6a738 100644 --- a/src/components/MultiGesture.tsx +++ b/src/components/MultiGesture.tsx @@ -48,7 +48,7 @@ type MultiGestureProps = PropsWithChildren<{ // related: https://github.com/cybersemics/em/issues/1268 minDistance?: number /** A hook that is called on touchstart if the user is in the gesture zone. If it returns true, the gesture is abandoned. Otherwise scrolling is disabled and a gesture may be entered. */ - shouldCancelGesture?: () => boolean + shouldCancelGesture?: (x?: number, y?: number) => boolean }> const TOOLBAR_HEIGHT = 50 @@ -133,7 +133,7 @@ class MultiGesture extends React.Component { const scrollZoneWidth = viewport.scrollZoneWidth const isInGestureZone = (this.leftHanded ? x > scrollZoneWidth : x < viewport.innerWidth - scrollZoneWidth) && y > TOOLBAR_HEIGHT - if (isInGestureZone && !props.shouldCancelGesture?.()) { + if (isInGestureZone && !props.shouldCancelGesture?.(x, y)) { this.disableScroll = true } else { this.abandon = true diff --git a/src/device/selection.ts b/src/device/selection.ts index 659fdbbb6e..4e4c672f6b 100644 --- a/src/device/selection.ts +++ b/src/device/selection.ts @@ -440,3 +440,29 @@ export const html = () => { const currentHtml = div.innerHTML return currentHtml } + +/** Returns the bounding rectangle for the current browser selection. */ +export const getBoundingClientRect = () => { + const selection = window.getSelection() + + if (selection && selection.rangeCount) { + return selection.getRangeAt(0).getBoundingClientRect() + } + + return null +} + +/** Returns true if the point is within 100 pixels of the browser selection. */ +export const isNear = (x: number, y: number): boolean => { + if (!isActive() || isCollapsed()) return false + + const rect = getBoundingClientRect() + if (!rect) return false + + const left = rect.left - 100 + const right = rect.right + 100 + const top = rect.top - 100 + const bottom = rect.bottom + 100 + + return x >= left && y >= top && x <= right && y <= bottom +} diff --git a/src/e2e/puppeteer/__tests__/gestures.ts b/src/e2e/puppeteer/__tests__/gestures.ts new file mode 100644 index 0000000000..d39936f1e1 --- /dev/null +++ b/src/e2e/puppeteer/__tests__/gestures.ts @@ -0,0 +1,24 @@ +import { KnownDevices } from 'puppeteer' +import { drawHorizontalLineGesture } from '../helpers/gesture' +import paste from '../helpers/paste' +import { page } from '../setup' + +vi.setConfig({ testTimeout: 20000, hookTimeout: 20000 }) +const iPhone = KnownDevices['iPhone 15 Pro'] + +describe('gestures', () => { + it.skip('when starting a gesture, the command palette will open', async () => { + const cursor = 'Hello' + const importText = ` + - ${cursor}` + + await page.emulate(iPhone) + await paste(importText) + + await drawHorizontalLineGesture() + + // the command palette should open + const popupValue = await page.$('[data-testid=popup-value]') + expect(popupValue).toBeTruthy() + }) +}) diff --git a/src/e2e/puppeteer/helpers/gesture.ts b/src/e2e/puppeteer/helpers/gesture.ts new file mode 100644 index 0000000000..c9c1d362c6 --- /dev/null +++ b/src/e2e/puppeteer/helpers/gesture.ts @@ -0,0 +1,43 @@ +import { page } from '../setup' + +/** Draw a gesture on the screen. */ +const gesture = async (points: { x: number; y: number }[], complete: boolean = true) => { + const start = points[0] + + await page.touchscreen.touchStart(start.x, start.y) + + for (let i = 1; i < points.length; i++) await page.touchscreen.touchMove(points[i].x, points[i].y) + + if (complete) await page.touchscreen.touchEnd() +} + +export default gesture + +/** Draw a horizontal line gesture at a certain y coordinate on the touch screen. */ +export const drawHorizontalLineGesture = (y: number = 100) => + gesture( + [ + { x: 100, y }, + { x: 110, y }, + { x: 120, y }, + { x: 130, y }, + { x: 140, y }, + { x: 150, y }, + { x: 160, y }, + { x: 170, y }, + { x: 180, y }, + { x: 190, y }, + { x: 200, y }, + { x: 210, y }, + { x: 220, y }, + { x: 230, y }, + { x: 240, y }, + { x: 250, y }, + { x: 260, y }, + { x: 270, y }, + { x: 280, y }, + { x: 290, y }, + { x: 300, y }, + ], + false, + )