diff --git a/packages/app/e2e/mobile/modal-sheet/README.md b/packages/app/e2e/mobile/modal-sheet/README.md new file mode 100644 index 0000000000..f72dc883eb --- /dev/null +++ b/packages/app/e2e/mobile/modal-sheet/README.md @@ -0,0 +1,47 @@ +# Native sheet gestures + +Use an Android emulator at 1080×1920, density 420, with the checkout's native app bundle loaded. +Connect the app to an isolated daemon, enable plugins, and install `plugin-examples/modal-ui` there. +Set `SHEET_QA_SERVER_ID` to that host's server ID, then run from the repository root: + +```sh +agent-device test packages/app/e2e/mobile/modal-sheet/gestures.android.ad \ + --env EXAMPLES_URL="paseo://h/${SHEET_QA_SERVER_ID}/plugin/modal-ui-example/surface/main" \ + --record-video --artifacts-dir .dev/sheet-qa/plugin + +agent-device test packages/app/e2e/mobile/modal-sheet/model.android.ad \ + --env FORM_URL="paseo://new?serverId=${SHEET_QA_SERVER_ID}" \ + --record-video --artifacts-dir .dev/sheet-qa/model +``` + +The plugin journey checks body dismissal, reopening, expansion and last-row reachability with SDK +ScrollView and FlatList, programmatic scrolling after expansion, downward list scrolling, and +horizontal tab selection. The model journey checks body dismissal and the nested sheet's return to +its parent without selecting a model or submitting a prompt. It does not assert catalog contents. + +These scripts live outside the default mobile suite because they require the installed example and +an explicit connected host. See [mobile testing](../../../../../docs/mobile-testing.md) for device setup. + +## Recorded verification + +Android API 35, 1080×1920 at density 420, development binary 0.7.2 with this checkout's JavaScript. +Plugin tests used an isolated daemon. Recordings show the same body drag +[before](evidence/android-before.mp4) and [after](evidence/android-after.mp4) the fix. +The [browser recording](evidence/browser.webm) covers the existing plugin modal journey at wide and +compact sizes. + +```text +Baseline dismissal regression: +failed at step 6: wait timed out for selector: label="Open ScrollView" +Current surface: Bottom sheet handle, Bottom Sheet, Close, Row 1. + +Fixed dismissal regression: 1 passed (6.47s) +Full plugin gesture journey: 1 passed (44.3s) +Model sheet dismissal/reopen journey: 1 passed (13.7s) +Browser plugin-modal-body.spec.ts: 1 passed (33.4s) +Root typecheck, lint and format: passed +``` + +The model form's catalog stayed on “Loading…” in the debug app even though the isolated daemon's +provider API returned models. Its sheet dismissal/reopen was exercised; populated model selection +was not. iOS and Electron were not exercised. diff --git a/packages/app/e2e/mobile/modal-sheet/evidence/android-after.mp4 b/packages/app/e2e/mobile/modal-sheet/evidence/android-after.mp4 new file mode 100644 index 0000000000..d258f9da7f Binary files /dev/null and b/packages/app/e2e/mobile/modal-sheet/evidence/android-after.mp4 differ diff --git a/packages/app/e2e/mobile/modal-sheet/evidence/android-before.mp4 b/packages/app/e2e/mobile/modal-sheet/evidence/android-before.mp4 new file mode 100644 index 0000000000..3a878c9378 Binary files /dev/null and b/packages/app/e2e/mobile/modal-sheet/evidence/android-before.mp4 differ diff --git a/packages/app/e2e/mobile/modal-sheet/evidence/browser.webm b/packages/app/e2e/mobile/modal-sheet/evidence/browser.webm new file mode 100644 index 0000000000..14b6d05fc8 Binary files /dev/null and b/packages/app/e2e/mobile/modal-sheet/evidence/browser.webm differ diff --git a/packages/app/e2e/mobile/modal-sheet/gestures.android.ad b/packages/app/e2e/mobile/modal-sheet/gestures.android.ad new file mode 100644 index 0000000000..083e766bcf --- /dev/null +++ b/packages/app/e2e/mobile/modal-sheet/gestures.android.ad @@ -0,0 +1,52 @@ +# Install plugin-examples/modal-ui on the connected host and set EXAMPLES_URL to +# paseo://h//plugin/modal-ui-example/surface/main. +# Gesture coordinates target a 1080x1920 Android emulator at density 420. +context platform=android timeout=120000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" "${EXAMPLES_URL}" +wait "label=\"Open ScrollView\"" 20000 +press "label=\"Open ScrollView\"" +wait "label=\"Row 1\"" 10000 +gesture pan 540 980 0 820 500 +wait "label=\"Open ScrollView\"" 3000 + +# The caller-owned ScrollView expands with a body drag and reaches its final row. +press "label=\"Open ScrollView\"" +wait "label=\"Row 1\"" 10000 +gesture pan 540 980 0 -600 500 +wait "label=\"Row 10\"" 3000 +scroll bottom +wait "label=\"Row 100\"" 3000 +press "role=\"button\" label=\"Row 100\"" +press "label=\"Close\"" +wait "text=\"Selected: Row 100\"" 3000 + +# FlatList has the same body dismissal, then supports refs after expansion. +press "label=\"Open FlatList\"" +wait "label=\"Row 1\"" 10000 +gesture pan 540 1100 0 700 500 +wait "label=\"Open FlatList\"" 3000 +press "label=\"Open FlatList\"" +wait "label=\"Row 1\"" 10000 +gesture pan 540 1140 0 -750 500 +wait "label=\"Row 9\"" 3000 +press "label=\"Jump to last row\"" +wait "label=\"Row 100\"" 3000 +# Dragging down while scrolled moves the list, leaving the sheet open. +gesture pan 540 650 0 900 500 +wait "label=\"Row 88\"" 3000 +is visible "label=\"Close\"" +press "label=\"Close\"" +wait "label=\"Open Horizontal\"" 3000 + +# Horizontal tabs still receive gestures inside the host-owned vertical body. +press "label=\"Open Horizontal\"" +wait "label=\"Overview\"" 10000 +gesture pan 960 980 -880 0 500 +wait stable 300 5000 +press "role=\"button\" label=\"System logs\"" +wait stable 300 5000 +press "label=\"Close\"" +wait "text=\"Selected: System logs\"" 3000 +close diff --git a/packages/app/e2e/mobile/modal-sheet/model.android.ad b/packages/app/e2e/mobile/modal-sheet/model.android.ad new file mode 100644 index 0000000000..8a2d534b7b --- /dev/null +++ b/packages/app/e2e/mobile/modal-sheet/model.android.ad @@ -0,0 +1,22 @@ +# FORM_URL is paseo://new?serverId=. +# Gesture coordinates target a 1080x1920 Android emulator at density 420. +context platform=android timeout=60000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" "${FORM_URL}" +wait "id=\"combined-model-selector\"" 20000 +press "id=\"combined-model-selector\"" +wait "id=\"agent-controls-model\"" 10000 +gesture pan 540 790 0 960 500 +wait "id=\"combined-model-selector\"" 3000 + +# Reopen, dismiss the nested model browser, then dismiss its restored parent. +press "id=\"combined-model-selector\"" +wait "id=\"agent-controls-model\"" 10000 +press "id=\"agent-controls-model\"" +wait stable 300 5000 +gesture pan 540 790 0 960 500 +wait "id=\"agent-controls-model\"" 3000 +gesture pan 540 790 0 960 500 +wait "id=\"combined-model-selector\"" 3000 +close diff --git a/packages/app/src/components/adaptive-modal-sheet.tsx b/packages/app/src/components/adaptive-modal-sheet.tsx index 99c3b5117f..31f3f2f9c8 100644 --- a/packages/app/src/components/adaptive-modal-sheet.tsx +++ b/packages/app/src/components/adaptive-modal-sheet.tsx @@ -449,6 +449,7 @@ export interface AdaptiveModalSheetProps { desktopMaxWidth?: number; /** Bound an author-owned list without changing content-sized first-party dialogs. */ desktopHeight?: DimensionValue; + /** Whether the host supplies the scroll container. Caller-owned lists still share sheet gestures. */ scrollable?: boolean; presentation?: "push" | "replace"; /** Full body viewport below the header, including space beyond the content. */ @@ -646,9 +647,6 @@ export function AdaptiveModalSheet({ onDismiss={handleDismiss} backdropComponent={renderBackdrop} enablePanDownToClose - // A custom scroll owner must also own body gestures. Gorhom otherwise - // locks even imperative list offsets until the sheet reaches its top snap. - enableContentPanningGesture={scrollable} backgroundComponent={SheetBackground} handleIndicatorStyle={handleIndicatorStyle} keyboardBehavior="extend" diff --git a/plugin-examples/modal-ui/README.md b/plugin-examples/modal-ui/README.md index 96cbbd8b78..0f1e429d18 100644 --- a/plugin-examples/modal-ui/README.md +++ b/plugin-examples/modal-ui/README.md @@ -13,11 +13,15 @@ cd packages/app npx playwright test e2e/browser/plugin-modal-body.spec.ts --project=browser --workers=1 ``` -On Android, open each example and swipe through its content. In FlatList, use **Jump to last row** -before expanding the sheet; row 100 should be visible. Drag the handle to expand the sheet. In Form, +On Android, open each example and drag up on its content to expand the sheet, then scroll the list. +In FlatList, expand before using **Jump to last row**; row 100 should be visible. At the top of either +list, drag down on a row to dismiss the sheet. In Form, press **Copy text**, long-press the input, and choose **Paste**. The input should contain “Copied from Paseo”. With the system keyboard enabled, focusing the input should keep it visible. +Run the [native sheet regression](../../packages/app/e2e/mobile/modal-sheet/README.md) to check body +dismissal, list scrolling and horizontal tabs together. + These captures show Android copy/paste and custom padding on browser and wide native layouts. Android API 35 and Chromium were exercised; iOS and Electron were not tested. diff --git a/public-docs/plugins/v0.8/reference.md b/public-docs/plugins/v0.8/reference.md index d934679768..a4529d013a 100644 --- a/public-docs/plugins/v0.8/reference.md +++ b/public-docs/plugins/v0.8/reference.md @@ -366,8 +366,7 @@ intact. The host reserves bottom safe-area space on compact native layouts; sett to zero removes the decorative inset, not that space. Keyboard clearance is handled separately. With `scrollable={false}`, the body fills the available sheet height, and the centered dialog uses -85% of the available height. Use `flex: 1, minHeight: 0` on your list. In this mode the body scrolls -at every sheet height; drag the handle to resize or dismiss the sheet. Default scrolling dialogs stay +85% of the available height. Use `flex: 1, minHeight: 0` on your list. Default scrolling dialogs stay content-sized on wide layouts. Presentation follows window size, including narrow desktop windows and wide tablets. @@ -387,6 +386,11 @@ Use one vertical scroll owner: either the default modal body, or your own list w `scrollable={false}`. A fixed-height vertical list nested inside the default scrolling body can compete with the sheet for gestures on Android. Horizontal scrolling can coexist with the host's vertical body. +Both the default body and SDK lists share native sheet gestures: drag up to expand before scrolling; +drag down at the top of the list to collapse or dismiss. `scrollable={false}` removes the host's scroll +container without changing these gestures. Expand the sheet before using list methods such as +`scrollToEnd`; the sheet locks list offsets below its largest height. + ```tsx import { FlatList, Modal } from "@getpaseo/plugin/react-native"; import { Text } from "react-native";