fix(app): restore coordinated bottom sheet gestures (#4431)

Supplying a list controls scroll-container ownership, not whether content can move the sheet. Preserve the shared expand-scroll-dismiss interaction for model selectors and plugin lists, with native gesture regressions and updated plugin guidance.
This commit is contained in:
Mohamed Boudra
2026-09-07 17:39:17 +02:00
committed by GitHub
parent a5d2b7d863
commit 8c89e72a06
9 changed files with 134 additions and 7 deletions
@@ -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.
@@ -0,0 +1,52 @@
# Install plugin-examples/modal-ui on the connected host and set EXAMPLES_URL to
# paseo://h/<serverId>/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
@@ -0,0 +1,22 @@
# FORM_URL is paseo://new?serverId=<connected-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
@@ -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"
+6 -2
View File
@@ -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.
+6 -2
View File
@@ -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";