UI: interactive back-swipe (left-band, snapshot backdrop) #40

Closed
developer wants to merge 2 commits from feature/ui-interactive-back-swipe into development
6 changed files with 41 additions and 13 deletions
Showing only changes of commit de11ae46a2 - Show all commits
+3 -2
View File
@@ -31,8 +31,9 @@ Login uses `Screen`.
back). The live parent mounts only on commit, which suppresses that one route slide so the back). The live parent mounts only on commit, which suppresses that one route slide so the
hand-off is seamless. The gesture yields to native **vertical scroll** on a vertical move hand-off is seamless. The gesture yields to native **vertical scroll** on a vertical move
and skips the rack, a draggable pending tile, text inputs and the board **while zoomed-in** and skips the rack, a draggable pending tile, text inputs and the board **while zoomed-in**
(so it never fights the board pan); it stays disabled inside Telegram, whose native (so it never fights the board pan). It works **inside Telegram** too — Telegram's only
BackButton owns this. close/minimise gesture is *vertical* (disabled separately via `disableVerticalSwipes`), so
a horizontal back-swipe has nothing to fight; the native header BackButton complements it.
- **No hamburger**: navigation is entirely bottom tab bars (the former `Menu.svelte` - **No hamburger**: navigation is entirely bottom tab bars (the former `Menu.svelte`
dropdown is gone — it fought the Telegram-fullscreen layout, where it had to be re-centred). dropdown is gone — it fought the Telegram-fullscreen layout, where it had to be re-centred).
Destinations beyond the lobby live behind **hub screens** reached from a tab: a ⚙️ Destinations beyond the lobby live behind **hub screens** reached from a tab: a ⚙️
+33
View File
@@ -22,6 +22,22 @@ async function openAnnGame(page: Page): Promise<void> {
await page.waitForTimeout(400); // let the open slide settle before measuring/aiming await page.waitForTimeout(400); // let the open slide settle before measuring/aiming
} }
// A minimal Telegram WebApp stub: non-empty initData makes insideTelegram() true and drives
// the Mini App auto-auth into the lobby (the mock gateway accepts any initData).
function telegramStub() {
return {
Telegram: {
WebApp: {
initData: 'query_id=test&user=%7B%22id%22%3A1%7D&auth_date=1&hash=deadbeef',
initDataUnsafe: {},
themeParams: {},
ready() {},
expand() {},
},
},
};
}
/** touchDrag dispatches a single-finger touch drag from (x0,y0) to (x1,y1) over the window. */ /** touchDrag dispatches a single-finger touch drag from (x0,y0) to (x1,y1) over the window. */
async function touchDrag(page: Page, x0: number, y0: number, x1: number, y1: number): Promise<void> { async function touchDrag(page: Page, x0: number, y0: number, x1: number, y1: number): Promise<void> {
await page.evaluate( await page.evaluate(
@@ -88,3 +104,20 @@ test('a vertical drag in the band scrolls instead of navigating', async ({ page
await page.waitForTimeout(300); await page.waitForTimeout(300);
expect(page.url()).toContain('/game/'); expect(page.url()).toContain('/game/');
}); });
test('the back-swipe also works inside Telegram (its only swipe gesture is vertical)', async ({ page }) => {
await page.addInitScript((stub) => {
Object.assign(window, stub);
}, telegramStub());
await page.goto('/');
// Telegram auto-authenticates into the lobby (no guest click), then open the seeded game.
await page.getByRole('button', { name: /Ann/ }).click();
await expect(page.locator('[data-cell]').first()).toBeVisible();
await page.waitForTimeout(400);
await touchDrag(page, START_X, MID_Y, 300, MID_Y);
await expect(page).toHaveURL(/#\/$/);
await expect(page.getByRole('button', { name: /Ann/ })).toBeVisible();
});
-2
View File
@@ -141,8 +141,6 @@
position: relative; position: relative;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
/* Keep a horizontal drag from also triggering the browser's history-swipe underneath. */
overscroll-behavior-x: contain;
} }
.pane { .pane {
position: absolute; position: absolute;
-2
View File
@@ -9,7 +9,6 @@
import { app } from './app.svelte'; import { app } from './app.svelte';
import { createBackSwipe, shouldArm } from './backswipe'; import { createBackSwipe, shouldArm } from './backswipe';
import { navigate } from './router.svelte'; import { navigate } from './router.svelte';
import { insideTelegram } from './telegram';
/** /**
* backSwipe is the reactive view of the in-flight gesture. App binds `progress` to the * backSwipe is the reactive view of the in-flight gesture. App binds `progress` to the
@@ -127,7 +126,6 @@ export function initBackSwipe(getParent: () => string | null): () => void {
width: window.innerWidth, width: window.innerWidth,
pointerType: e.pointerType, pointerType: e.pointerType,
hasParent: !!parent, hasParent: !!parent,
insideTelegram: insideTelegram(),
}) })
) )
return; return;
+1 -3
View File
@@ -7,7 +7,6 @@ describe('shouldArm', () => {
width: 400, width: 400,
pointerType: 'touch', pointerType: 'touch',
hasParent: true, hasParent: true,
insideTelegram: false,
}; };
it('arms a touch press inside the left band of a screen with a parent', () => { it('arms a touch press inside the left band of a screen with a parent', () => {
@@ -15,11 +14,10 @@ describe('shouldArm', () => {
expect(shouldArm({ ...base, clientX: 0.38 * 400 - 1 })).toBe(true); expect(shouldArm({ ...base, clientX: 0.38 * 400 - 1 })).toBe(true);
}); });
it('does not arm past the band, for a mouse, at a root, or inside Telegram', () => { it('does not arm past the band, for a mouse, or at a root', () => {
expect(shouldArm({ ...base, clientX: 0.38 * 400 + 1 })).toBe(false); expect(shouldArm({ ...base, clientX: 0.38 * 400 + 1 })).toBe(false);
expect(shouldArm({ ...base, pointerType: 'mouse' })).toBe(false); expect(shouldArm({ ...base, pointerType: 'mouse' })).toBe(false);
expect(shouldArm({ ...base, hasParent: false })).toBe(false); expect(shouldArm({ ...base, hasParent: false })).toBe(false);
expect(shouldArm({ ...base, insideTelegram: true })).toBe(false);
}); });
it('honours a custom band fraction', () => { it('honours a custom band fraction', () => {
+4 -4
View File
@@ -30,18 +30,18 @@ export interface ArmInput {
pointerType: string; pointerType: string;
/** Whether the current screen has a parent to return to (false on a navigation root). */ /** Whether the current screen has a parent to return to (false on a navigation root). */
hasParent: boolean; hasParent: boolean;
/** Whether the app is running inside Telegram, where its native Back button owns this. */
insideTelegram: boolean;
/** Fraction of the width forming the left activation band; defaults to 0.38. */ /** Fraction of the width forming the left activation band; defaults to 0.38. */
bandFrac?: number; bandFrac?: number;
} }
/** /**
* shouldArm decides whether a pointerdown may begin a back-swipe: a touch or pen press in * shouldArm decides whether a pointerdown may begin a back-swipe: a touch or pen press in
* the left band of a screen that has a parent, while not inside Telegram. * the left band of a screen that has a parent. It is intentionally Telegram-agnostic —
* Telegram's only close/minimise gesture is vertical (handled by disableVerticalSwipes), so
* a horizontal back-swipe has nothing to conflict with there.
*/ */
export function shouldArm(i: ArmInput): boolean { export function shouldArm(i: ArmInput): boolean {
if (!i.hasParent || i.insideTelegram) return false; if (!i.hasParent) return false;
if (i.pointerType === 'mouse') return false; if (i.pointerType === 'mouse') return false;
const band = (i.bandFrac ?? 0.38) * i.width; const band = (i.bandFrac ?? 0.38) * i.width;
return i.clientX <= band; return i.clientX <= band;