Compare commits

...
Author SHA1 Message Date
Mohamed Boudra a45dfaf0e8 fix(app): keep dropped rows in place until their new order arrives
Sheet portals forward new children after the local drop commit. Renew transform hosts with the incoming row order so clearing drag offsets cannot expose the old slots. Reset same-slot drops explicitly because their data order does not change.
2026-11-15 12:35:05 +01:00
Mohamed Boudra f69dc31086 fix(app): type composed sheet gesture relations 2026-11-15 12:35:05 +01:00
Mohamed Boudra 6adf92f089 fix(app): keep card drags inside bottom sheets
Coordinate registered content drags as simultaneous while the sheet waits for them. Spread Gorhom gesture relations so RNGH resolves the handler refs instead of dropping nested arrays.
2026-11-15 12:35:05 +01:00
7 changed files with 473 additions and 34 deletions
@@ -0,0 +1,27 @@
# Android Usage drop regressions
Use a device you own and fixed reports from an isolated daemon. Keep recordings in `.artifacts/`. Do not upload recordings containing account details.
## Same-slot drop
Open Usage with two cards that fit without scrolling. Run:
```sh
ANDROID_SERIAL=emulator-5580 node packages/app/e2e/mobile/usage-drag/android.mjs
```
Set `PASEO_DEVICE_SESSION` to reuse your Agent Device session. The test holds the first handle, moves within its slot, drops it, then swipes the sheet body. It asserts sheet movement while the finger is still down: after release, the sheet can snap back to its starting position.
## Old-order frame
Record a drop with Android `screenrecord`. Include the original order, the held arrangement, the release, and at least five seconds after release. Take a reference screenshot before recording. Use fixed labels that stay visible throughout the drop.
Create a JSON map of label names to screenshot rectangles `[x, y, width, height]`, in physical pixels. Crop each label tightly, without changing text or the pointer. Install `opencv-python-headless` in a local virtual environment under `.artifacts/`, then run:
```sh
.artifacts/frames-venv/bin/python packages/app/e2e/mobile/usage-drag/frames.py \
--reference .artifacts/before.png --labels .artifacts/labels.json \
.artifacts/drop-up.mp4 .artifacts/drop-down.mp4
```
The check requires recognized labels in the first and last frames and a changed final order. After every label first reaches its final slot, it fails on any frame showing all original slots again. It prints the offending frame numbers for each recording. This exercises the Android compositor; a JavaScript render test cannot establish which intermediate commit reached the screen.
@@ -0,0 +1,71 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { setTimeout } from "node:timers/promises";
// Open Usage with a fixture whose cards fit without scrolling before running.
// A same-slot drop must release the list's recognizer so a body swipe moves the sheet.
const serial = process.env.ANDROID_SERIAL;
assert.ok(serial, "Set ANDROID_SERIAL to the device owned by this test");
const session = process.env.PASEO_DEVICE_SESSION ?? "usage-drag-regression";
const artifacts = resolve(".artifacts/usage-drag-regression");
mkdirSync(artifacts, { recursive: true });
function ad(...args) {
return execFileSync(
"agent-device",
[...args, "--platform", "android", "--serial", serial, "--session", session],
{ encoding: "utf8", timeout: 30_000 },
);
}
function adb(...args) {
return execFileSync("adb", ["-s", serial, ...args], { timeout: 30_000 });
}
function snapshot() {
return JSON.parse(ad("snapshot", "--json")).data.nodes;
}
function touch(kind, x, y) {
adb("shell", "input", "motionevent", kind, String(Math.round(x)), String(Math.round(y)));
}
try {
ad("open", process.env.PASEO_MOBILE_APP_ID ?? "sh.paseo.debug");
const nodes = snapshot();
writeFileSync(resolve(artifacts, "before.json"), JSON.stringify(nodes));
const handle = nodes.find((node) => node.identifier === "usage-card-drag-handle");
const list = nodes.find((node) => node.identifier === "usage-list");
assert.ok(handle?.rect && list?.rect, "Open Usage with a visible card handle");
const x = handle.rect.x + handle.rect.width / 2;
const y = handle.rect.y + handle.rect.height / 2;
touch("DOWN", x, y);
await setTimeout(600);
touch("MOVE", x, y + 60);
await setTimeout(200);
touch("MOVE", x, y + 90);
touch("MOVE", x, y + 60);
ad("screenshot", resolve(artifacts, "dragging.png"));
touch("UP", x, y + 60);
await setTimeout(2000);
ad("screenshot", resolve(artifacts, "dropped.png"));
const swipeX = list.rect.x + list.rect.width / 2;
touch("DOWN", swipeX, y + 80);
for (let offset = 90; offset <= 500; offset += 10) {
touch("MOVE", swipeX, y + offset);
}
const during = snapshot();
writeFileSync(resolve(artifacts, "during.json"), JSON.stringify(during));
ad("screenshot", resolve(artifacts, "during.png"));
touch("UP", swipeX, y + 500);
await setTimeout(1500);
const after = snapshot();
writeFileSync(resolve(artifacts, "after.json"), JSON.stringify(after));
ad("screenshot", resolve(artifacts, "after.png"));
const beforeSheet = nodes.find((node) => node.label === "Bottom sheet handle");
const afterSheet = during.find((node) => node.label === "Bottom sheet handle");
assert.ok(beforeSheet?.rect, "Usage must start as a sheet");
assert.ok(
!afterSheet || afterSheet.rect.y > beforeSheet.rect.y + 50,
"A body swipe after a same-slot drop must move the sheet",
);
} finally {
ad("close");
}
@@ -0,0 +1,59 @@
"""Check native drop recordings against visible row labels (requires opencv-python-headless).
Supply a before screenshot and a JSON map of label names to [x, y, width, height].
A recording must start in the original order and end in a different order. Once
all labels reach their final slots, any frame returning to the original slots fails.
Keep the pointer clear of the labels and use fixed fixture data.
"""
import argparse
import json
from pathlib import Path
import cv2
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--reference", required=True)
parser.add_argument("--labels", required=True)
parser.add_argument("--top", type=int, default=0)
parser.add_argument("--bottom", type=int)
parser.add_argument("videos", nargs="+")
args = parser.parse_args()
reference = cv2.imread(args.reference, cv2.IMREAD_GRAYSCALE)
assert reference is not None, "Cannot read reference image"
labels = json.loads(Path(args.labels).read_text())
results = []
for video in args.videos:
capture = cv2.VideoCapture(video)
frames = []
while True:
ok, image = capture.read()
if not ok:
break
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
positions = {}
for label, (x, y, width, height) in labels.items():
template = reference[y:y + height, x:x + width]
matches = cv2.matchTemplate(gray[args.top:args.bottom, x:x + width], template, cv2.TM_CCOEFF_NORMED)
_, confidence, _, point = cv2.minMaxLoc(matches)
positions[label] = (point[1] + args.top, confidence)
frames.append(positions)
capture.release()
assert frames, f"No frames in {video}"
initial, final = frames[0], frames[-1]
assert all(confidence >= 0.95 for _, confidence in initial.values()), f"Unrecognized initial labels: {video}"
assert all(confidence >= 0.95 for _, confidence in final.values()), f"Unrecognized final labels: {video}"
assert sorted(initial, key=lambda key: initial[key][0]) != sorted(final, key=lambda key: final[key][0]), f"Drop did not reorder: {video}"
def matches_slots(frame, target):
return all(confidence >= 0.95 and abs(y - target[label][0]) <= 2
for label, (y, confidence) in frame.items())
reached_final = False
wrong = []
for number, frame in enumerate(frames, start=1):
reached_final = reached_final or matches_slots(frame, final)
if reached_final and matches_slots(frame, initial):
wrong.append(number)
results.append({"recording": video, "oldOrderFrames": wrong, "count": len(wrong)})
print(json.dumps(results, indent=2))
raise SystemExit(1 if any(result["count"] for result in results) else 0)
@@ -43,6 +43,7 @@ vi.mock("@gorhom/portal", () => ({
/** Every `waitFor` Gorhom received, in render order. Gorhom rebuilds its content pan per value. */
const sheetWaitFor = vi.hoisted(() => [] as unknown[]);
const sheetSimultaneousHandlers = vi.hoisted(() => [] as unknown[]);
vi.mock("@gorhom/bottom-sheet", async () => {
const { Portal } = await import("@gorhom/portal");
@@ -55,10 +56,17 @@ vi.mock("@gorhom/bottom-sheet", async () => {
children,
stackBehavior,
waitFor,
}: { children?: React.ReactNode; stackBehavior?: string; waitFor?: unknown },
simultaneousHandlers,
}: {
children?: React.ReactNode;
stackBehavior?: string;
waitFor?: unknown;
simultaneousHandlers?: unknown;
},
_ref,
) => {
sheetWaitFor.push(waitFor);
sheetSimultaneousHandlers.push(simultaneousHandlers);
return React.createElement(
Portal,
null,
@@ -259,6 +267,7 @@ describe("IsolatedBottomSheetModal content drag", () => {
document.body.appendChild(container);
root = createRoot(container);
sheetWaitFor.length = 0;
sheetSimultaneousHandlers.length = 0;
});
afterEach(() => {
@@ -275,11 +284,19 @@ describe("IsolatedBottomSheetModal content drag", () => {
return null;
}
function renderSheet(content: React.ReactNode) {
function renderSheet(
content: React.ReactNode,
handlers: Pick<
React.ComponentProps<typeof IsolatedBottomSheetModal>,
"waitFor" | "simultaneousHandlers"
> = {},
) {
act(() => {
root.render(
<>
<IsolatedBottomSheetModal contextBridge={null}>{content}</IsolatedBottomSheetModal>
<IsolatedBottomSheetModal contextBridge={null} {...handlers}>
{content}
</IsolatedBottomSheetModal>
<PortalHostProbe />
</>,
);
@@ -291,9 +308,106 @@ describe("IsolatedBottomSheetModal content drag", () => {
expect(sheetWaitFor.at(-1)).toBeUndefined();
renderSheet(<ContentDrag />);
expect(sheetWaitFor.at(-1)).toBe(dragGesture);
expect(sheetSimultaneousHandlers.at(-1)).toEqual([dragGesture]);
expect(sheetWaitFor.at(-1)).toEqual([dragGesture]);
renderSheet(null);
expect(sheetWaitFor.at(-1)).toBeUndefined();
expect(sheetSimultaneousHandlers.at(-1)).toBeUndefined();
});
it.each([false, true])(
"preserves supplied relations when a content drag mounts and unmounts (array=%s)",
(array) => {
const waiting = React.createRef<React.ComponentType>();
const simultaneous = React.createRef<React.ComponentType>();
const handlers = {
waitFor: array ? [waiting] : waiting,
simultaneousHandlers: array ? [simultaneous] : simultaneous,
};
renderSheet(<ContentDrag />, handlers);
expect(sheetWaitFor.at(-1)).toEqual([waiting, dragGesture]);
expect(sheetSimultaneousHandlers.at(-1)).toEqual([simultaneous, dragGesture]);
renderSheet(null, handlers);
expect(sheetWaitFor.at(-1)).toBe(handlers.waitFor);
expect(sheetSimultaneousHandlers.at(-1)).toBe(handlers.simultaneousHandlers);
},
);
});
// Load the real vendored implementations at runtime without typechecking Gorhom's entire source
// tree (its hooks barrel reaches upstream files that don't compile under the app's TS settings).
const gorhomSource = "@gorhom/bottom-sheet/src";
const { default: BottomSheetDraggableView } = await import(
`${gorhomSource}/components/bottomSheetDraggableView/BottomSheetDraggableView`
);
const { BottomSheetInternalContext } = await import(`${gorhomSource}/contexts/internal`);
const { BottomSheetDraggableContext, BottomSheetGestureHandlersContext } = await import(
`${gorhomSource}/contexts/gesture`
);
const rnghSource = "react-native-gesture-handler/src";
const { extractGestureRelations } = await import(
`${rnghSource}/handlers/gestures/GestureDetector/utils`
);
import { Gesture, type GestureType } from "react-native-gesture-handler";
const noGestureWork = () => {};
const gestureHandlerContext = {
contentPanGestureHandler: {
handleOnStart: noGestureWork,
handleOnChange: noGestureWork,
handleOnEnd: noGestureWork,
handleOnFinalize: noGestureWork,
},
handlePanGestureHandler: {
handleOnStart: noGestureWork,
handleOnChange: noGestureWork,
handleOnEnd: noGestureWork,
handleOnFinalize: noGestureWork,
},
};
function dragInternalContext(relations: SheetContentDragGesture | SheetContentDragGesture[]) {
return {
enableContentPanningGesture: true,
waitFor: relations,
simultaneousHandlers: relations,
};
}
describe("bottom sheet native drag relations", () => {
it.each([false, true])(
"resolves both relations through the real content pan (array=%s)",
(array) => {
const container = document.createElement("div");
const root = createRoot(container);
const child = Gesture.Pan();
child.handlerTag = 42;
const ref = { current: child };
const relations = array ? [ref] : ref;
let resolved: ReturnType<typeof extractGestureRelations> | undefined;
const internal = dragInternalContext(relations);
function Probe() {
const pan = useContext(BottomSheetDraggableContext) as GestureType | null;
if (pan) resolved = extractGestureRelations(pan);
return null;
}
try {
act(() =>
root.render(
<BottomSheetInternalContext.Provider value={internal}>
<BottomSheetGestureHandlersContext.Provider value={gestureHandlerContext}>
<BottomSheetDraggableView>
<Probe />
</BottomSheetDraggableView>
</BottomSheetGestureHandlersContext.Provider>
</BottomSheetInternalContext.Provider>,
),
);
expect(resolved?.waitFor).toEqual([42]);
expect(resolved?.simultaneousHandlers).toEqual([42]);
} finally {
act(() => root.unmount());
}
},
);
});
@@ -105,6 +105,8 @@ export const IsolatedBottomSheetModal = forwardRef<
contextBridge,
backdropOpacity,
enableDismissOnClose = true,
waitFor,
simultaneousHandlers,
...bottomSheetProps
} = props;
// State, not a ref the child fills in: Gorhom builds its pan once per `waitFor`, and RNGH reads
@@ -115,6 +117,19 @@ export const IsolatedBottomSheetModal = forwardRef<
setContentDragGesture(gesture);
return () => setContentDragGesture((current) => (current === gesture ? undefined : current));
}, []);
// RNGH Android's shouldBeCancelledByActiveHandler treats a waiting ancestor's raw ACTIVE
// state as ownership and cancels its child at activation. Simultaneity lets the child activate;
// waitFor still keeps the sheet stationary until the child fails.
const contentDragHandlers = useMemo(() => {
if (!contentDragGesture) return { waitFor, simultaneousHandlers };
return {
waitFor: [...(waitFor ? [waitFor].flat() : []), contentDragGesture],
simultaneousHandlers: [
...(simultaneousHandlers ? [simultaneousHandlers].flat() : []),
contentDragGesture,
],
};
}, [contentDragGesture, waitFor, simultaneousHandlers]);
const renderBackdrop = useCallback(
(backdropProps: BottomSheetBackdropProps) => (
<SheetBackdrop
@@ -132,7 +147,8 @@ export const IsolatedBottomSheetModal = forwardRef<
enableDismissOnClose={enableDismissOnClose}
stackBehavior={presentation}
backdropComponent={backdropOpacity === undefined ? undefined : renderBackdrop}
waitFor={contentDragGesture}
waitFor={contentDragHandlers.waitFor}
simultaneousHandlers={contentDragHandlers.simultaneousHandlers}
>
<BottomSheetScope attachContentDrag={attachContentDrag}>
{contextBridge ? contextBridge(children) : children}
+132
View File
@@ -1,3 +1,37 @@
diff --git a/node_modules/@gorhom/bottom-sheet/lib/commonjs/components/bottomSheetDraggableView/BottomSheetDraggableView.js b/node_modules/@gorhom/bottom-sheet/lib/commonjs/components/bottomSheetDraggableView/BottomSheetDraggableView.js
index f3d8f55..72d4322 100644
--- a/node_modules/@gorhom/bottom-sheet/lib/commonjs/components/bottomSheetDraggableView/BottomSheetDraggableView.js
+++ b/node_modules/@gorhom/bottom-sheet/lib/commonjs/components/bottomSheetDraggableView/BottomSheetDraggableView.js
@@ -55,10 +55,10 @@ const BottomSheetDraggableViewComponent = ({
const draggableGesture = (0, _react.useMemo)(() => {
let gesture = _reactNativeGestureHandler.Gesture.Pan().enabled(enableContentPanningGesture).shouldCancelWhenOutside(false).runOnJS(false).onStart(contentPanGestureHandler.handleOnStart).onChange(contentPanGestureHandler.handleOnChange).onEnd(contentPanGestureHandler.handleOnEnd).onFinalize(contentPanGestureHandler.handleOnFinalize);
if (waitFor) {
- gesture = gesture.requireExternalGestureToFail(waitFor);
+ gesture = gesture.requireExternalGestureToFail(...(Array.isArray(waitFor) ? waitFor : [waitFor]));
}
if (simultaneousHandlers) {
- gesture = gesture.simultaneousWithExternalGesture(simultaneousHandlers);
+ gesture = gesture.simultaneousWithExternalGesture(...simultaneousHandlers);
}
if (activeOffsetX) {
gesture = gesture.activeOffsetX(activeOffsetX);
diff --git a/node_modules/@gorhom/bottom-sheet/lib/commonjs/components/bottomSheetHandle/BottomSheetHandleContainer.js b/node_modules/@gorhom/bottom-sheet/lib/commonjs/components/bottomSheetHandle/BottomSheetHandleContainer.js
index ee9e898..1500886 100644
--- a/node_modules/@gorhom/bottom-sheet/lib/commonjs/components/bottomSheetHandle/BottomSheetHandleContainer.js
+++ b/node_modules/@gorhom/bottom-sheet/lib/commonjs/components/bottomSheetHandle/BottomSheetHandleContainer.js
@@ -59,10 +59,10 @@ function BottomSheetHandleContainerComponent({
const panGesture = (0, _react.useMemo)(() => {
let gesture = _reactNativeGestureHandler.Gesture.Pan().enabled(enableHandlePanningGesture).shouldCancelWhenOutside(false).runOnJS(false).onStart(handlePanGestureHandler.handleOnStart).onChange(handlePanGestureHandler.handleOnChange).onEnd(handlePanGestureHandler.handleOnEnd).onFinalize(handlePanGestureHandler.handleOnFinalize);
if (waitFor) {
- gesture = gesture.requireExternalGestureToFail(waitFor);
+ gesture = gesture.requireExternalGestureToFail(...(Array.isArray(waitFor) ? waitFor : [waitFor]));
}
if (simultaneousHandlers) {
- gesture = gesture.simultaneousWithExternalGesture(simultaneousHandlers);
+ gesture = gesture.simultaneousWithExternalGesture(...simultaneousHandlers);
}
if (activeOffsetX) {
gesture = gesture.activeOffsetX(activeOffsetX);
diff --git a/node_modules/@gorhom/bottom-sheet/lib/commonjs/components/bottomSheetModal/BottomSheetModal.js b/node_modules/@gorhom/bottom-sheet/lib/commonjs/components/bottomSheetModal/BottomSheetModal.js
index ee51e76..5fe8f75 100644
--- a/node_modules/@gorhom/bottom-sheet/lib/commonjs/components/bottomSheetModal/BottomSheetModal.js
@@ -70,6 +104,40 @@ index e44ad86..96555ef 100644
var _useScrollEventsHandlersDefault = require("./hooks/useScrollEventsHandlersDefault");
var _useGestureEventsHandlersDefault = require("./hooks/useGestureEventsHandlersDefault");
var _useBottomSheetGestureHandlers = require("./hooks/useBottomSheetGestureHandlers");
diff --git a/node_modules/@gorhom/bottom-sheet/lib/module/components/bottomSheetDraggableView/BottomSheetDraggableView.js b/node_modules/@gorhom/bottom-sheet/lib/module/components/bottomSheetDraggableView/BottomSheetDraggableView.js
index 9efadd2..942b78a 100644
--- a/node_modules/@gorhom/bottom-sheet/lib/module/components/bottomSheetDraggableView/BottomSheetDraggableView.js
+++ b/node_modules/@gorhom/bottom-sheet/lib/module/components/bottomSheetDraggableView/BottomSheetDraggableView.js
@@ -49,10 +49,10 @@ const BottomSheetDraggableViewComponent = ({
const draggableGesture = useMemo(() => {
let gesture = Gesture.Pan().enabled(enableContentPanningGesture).shouldCancelWhenOutside(false).runOnJS(false).onStart(contentPanGestureHandler.handleOnStart).onChange(contentPanGestureHandler.handleOnChange).onEnd(contentPanGestureHandler.handleOnEnd).onFinalize(contentPanGestureHandler.handleOnFinalize);
if (waitFor) {
- gesture = gesture.requireExternalGestureToFail(waitFor);
+ gesture = gesture.requireExternalGestureToFail(...(Array.isArray(waitFor) ? waitFor : [waitFor]));
}
if (simultaneousHandlers) {
- gesture = gesture.simultaneousWithExternalGesture(simultaneousHandlers);
+ gesture = gesture.simultaneousWithExternalGesture(...simultaneousHandlers);
}
if (activeOffsetX) {
gesture = gesture.activeOffsetX(activeOffsetX);
diff --git a/node_modules/@gorhom/bottom-sheet/lib/module/components/bottomSheetHandle/BottomSheetHandleContainer.js b/node_modules/@gorhom/bottom-sheet/lib/module/components/bottomSheetHandle/BottomSheetHandleContainer.js
index 3d1eafe..bf8e366 100644
--- a/node_modules/@gorhom/bottom-sheet/lib/module/components/bottomSheetHandle/BottomSheetHandleContainer.js
+++ b/node_modules/@gorhom/bottom-sheet/lib/module/components/bottomSheetHandle/BottomSheetHandleContainer.js
@@ -53,10 +53,10 @@ function BottomSheetHandleContainerComponent({
const panGesture = useMemo(() => {
let gesture = Gesture.Pan().enabled(enableHandlePanningGesture).shouldCancelWhenOutside(false).runOnJS(false).onStart(handlePanGestureHandler.handleOnStart).onChange(handlePanGestureHandler.handleOnChange).onEnd(handlePanGestureHandler.handleOnEnd).onFinalize(handlePanGestureHandler.handleOnFinalize);
if (waitFor) {
- gesture = gesture.requireExternalGestureToFail(waitFor);
+ gesture = gesture.requireExternalGestureToFail(...(Array.isArray(waitFor) ? waitFor : [waitFor]));
}
if (simultaneousHandlers) {
- gesture = gesture.simultaneousWithExternalGesture(simultaneousHandlers);
+ gesture = gesture.simultaneousWithExternalGesture(...simultaneousHandlers);
}
if (activeOffsetX) {
gesture = gesture.activeOffsetX(activeOffsetX);
diff --git a/node_modules/@gorhom/bottom-sheet/lib/module/components/bottomSheetModal/BottomSheetModal.js b/node_modules/@gorhom/bottom-sheet/lib/module/components/bottomSheetModal/BottomSheetModal.js
index 94fabdb..d870aa7 100644
--- a/node_modules/@gorhom/bottom-sheet/lib/module/components/bottomSheetModal/BottomSheetModal.js
@@ -128,6 +196,19 @@ index 8259637..eb057fe 100644
export { useScrollEventsHandlersDefault } from './hooks/useScrollEventsHandlersDefault';
export { useGestureEventsHandlersDefault } from './hooks/useGestureEventsHandlersDefault';
export { useBottomSheetGestureHandlers } from './hooks/useBottomSheetGestureHandlers';
diff --git a/node_modules/@gorhom/bottom-sheet/lib/typescript/components/bottomSheet/types.d.ts b/node_modules/@gorhom/bottom-sheet/lib/typescript/components/bottomSheet/types.d.ts
index 6b23056..8d92aae 100644
--- a/node_modules/@gorhom/bottom-sheet/lib/typescript/components/bottomSheet/types.d.ts
+++ b/node_modules/@gorhom/bottom-sheet/lib/typescript/components/bottomSheet/types.d.ts
@@ -353,6 +353,6 @@ export type BottomSheetGestureProps = {
simultaneousHandlers: Parameters<
PanGesture['simultaneousWithExternalGesture']
- >[0];
- waitFor: Parameters<PanGesture['requireExternalGestureToFail']>[0];
+ >[0] | Parameters<PanGesture['simultaneousWithExternalGesture']>;
+ waitFor: Parameters<PanGesture['requireExternalGestureToFail']>[0] | Parameters<PanGesture['requireExternalGestureToFail']>;
};
diff --git a/node_modules/@gorhom/bottom-sheet/lib/typescript/index.d.ts b/node_modules/@gorhom/bottom-sheet/lib/typescript/index.d.ts
index ffc6caa..550f1b3 100644
--- a/node_modules/@gorhom/bottom-sheet/lib/typescript/index.d.ts
@@ -141,6 +222,57 @@ index ffc6caa..550f1b3 100644
export { useScrollEventsHandlersDefault } from './hooks/useScrollEventsHandlersDefault';
export { useGestureEventsHandlersDefault } from './hooks/useGestureEventsHandlersDefault';
export { useBottomSheetGestureHandlers } from './hooks/useBottomSheetGestureHandlers';
diff --git a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheet/types.d.ts b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheet/types.d.ts
index 6b23056..8d92aae 100644
--- a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheet/types.d.ts
+++ b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheet/types.d.ts
@@ -353,6 +353,6 @@ export type BottomSheetGestureProps = {
simultaneousHandlers: Parameters<
PanGesture['simultaneousWithExternalGesture']
- >[0];
- waitFor: Parameters<PanGesture['requireExternalGestureToFail']>[0];
+ >[0] | Parameters<PanGesture['simultaneousWithExternalGesture']>;
+ waitFor: Parameters<PanGesture['requireExternalGestureToFail']>[0] | Parameters<PanGesture['requireExternalGestureToFail']>;
};
diff --git a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetDraggableView/BottomSheetDraggableView.tsx b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetDraggableView/BottomSheetDraggableView.tsx
index 923e430..eeef8cf 100644
--- a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetDraggableView/BottomSheetDraggableView.tsx
+++ b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetDraggableView/BottomSheetDraggableView.tsx
@@ -65,12 +65,12 @@ const BottomSheetDraggableViewComponent = ({
.onFinalize(contentPanGestureHandler.handleOnFinalize);
if (waitFor) {
- gesture = gesture.requireExternalGestureToFail(waitFor);
+ gesture = gesture.requireExternalGestureToFail(...(Array.isArray(waitFor) ? waitFor : [waitFor]));
}
if (simultaneousHandlers) {
gesture = gesture.simultaneousWithExternalGesture(
- simultaneousHandlers as never
+ ...(simultaneousHandlers as Parameters<typeof gesture.simultaneousWithExternalGesture>)
);
}
diff --git a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetHandle/BottomSheetHandleContainer.tsx b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetHandle/BottomSheetHandleContainer.tsx
index 0223843..4ca56e5 100644
--- a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetHandle/BottomSheetHandleContainer.tsx
+++ b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetHandle/BottomSheetHandleContainer.tsx
@@ -67,12 +67,12 @@ function BottomSheetHandleContainerComponent({
.onFinalize(handlePanGestureHandler.handleOnFinalize);
if (waitFor) {
- gesture = gesture.requireExternalGestureToFail(waitFor);
+ gesture = gesture.requireExternalGestureToFail(...(Array.isArray(waitFor) ? waitFor : [waitFor]));
}
if (simultaneousHandlers) {
gesture = gesture.simultaneousWithExternalGesture(
- simultaneousHandlers as never
+ ...(simultaneousHandlers as Parameters<typeof gesture.simultaneousWithExternalGesture>)
);
}
diff --git a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetModal/BottomSheetModal.tsx b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetModal/BottomSheetModal.tsx
index 2954b3f..936300b 100644
--- a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetModal/BottomSheetModal.tsx
@@ -181,7 +181,7 @@ index e4e861a..f485139 100644
declare global {
diff --git a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
index 7c88afc..a97cb80 100644
index 7c88afc..05618ec 100644
--- a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
+++ b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
@@ -16,9 +16,11 @@ import {
@@ -227,7 +227,37 @@ index 7c88afc..a97cb80 100644
const [layoutAnimationDisabled, setLayoutAnimationDisabled] = useState(
!propsRef.current.enableLayoutAnimationExperimental
);
@@ -144,19 +163,88 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
@@ -113,10 +132,26 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
const dataRef = useRef(props.data);
const dataHasChanged =
- dataRef.current.map(keyExtractor).join("") !==
- props.data.map(keyExtractor).join("");
- dataRef.current = props.data;
+ dataRef.current.map(keyExtractor).join("") !== props.data.map(keyExtractor).join("");
+ // Sheet portals can deliver reordered data after the drop callback commits.
+ // Keep the held transforms until that data arrives, then renew moved hosts
+ // in the same render as their new slots.
if (dataHasChanged) {
+ const wasDragging = activeKey !== null;
+ const previousIndices = new Map(
+ dataRef.current.map((item, index) => [keyExtractor(item, index), index]),
+ );
+ setTransformHostKeys((previous) => {
+ const next = new Map<string, number>();
+ props.data.forEach((item, index) => {
+ const key = keyExtractor(item, index);
+ const generation = previous.get(key) ?? 0;
+ next.set(key, generation + (wasDragging && previousIndices.get(key) !== index ? 1 : 0));
+ });
+ return next;
+ });
+ dataRef.current = props.data;
+ setActiveKey(null);
// When data changes make sure `activeKey` is nulled out in the same render pass
activeKey = null;
InteractionManager.runAfterInteractions(() => {
@@ -144,19 +179,88 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
});
}, [props.data, keyExtractor, keyToIndexRef]);
@@ -323,7 +353,7 @@ index 7c88afc..a97cb80 100644
setActiveKey(activeKey);
onDragBegin?.(index);
}
@@ -218,14 +306,46 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
@@ -218,8 +322,25 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
props.onRelease?.(index);
});
@@ -350,28 +380,18 @@ index 7c88afc..a97cb80 100644
const { onDragEnd, data } = props;
const newData = [...data];
if (from !== to) {
newData.splice(from, 1);
newData.splice(to, 0, data[from]);
+ const movedKeys = data
+ .slice(Math.min(from, to), Math.max(from, to) + 1)
+ .map((item, offset) => keyExtractor(item, Math.min(from, to) + offset));
+ setTransformHostKeys((previous) => {
+ const next = new Map<string, number>();
+ data.forEach((item, index) => {
+ const itemKey = keyExtractor(item, index);
+ const hostKey = previous.get(itemKey);
+ if (hostKey !== undefined) next.set(itemKey, hostKey);
+ });
+ for (const movedKey of movedKeys) {
+ next.set(movedKey, (previous.get(movedKey) ?? 0) + 1);
+ }
+ return next;
+ });
}
@@ -230,34 +351,15 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
onDragEnd?.({ from, to, data: newData });
@@ -238,26 +358,6 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
- setActiveKey(null);
- }
+ // A drop in place doesn't change data, so no data-change reset will clear the armed index.
+ if (from === to) reset();
+ },
);
const onPlaceholderIndexChange = useStableCallback((index: number) => {
props.onPlaceholderIndexChange?.(index);
});
@@ -398,7 +418,7 @@ index 7c88afc..a97cb80 100644
useAnimatedReaction(
() => {
return spacerIndexAnim.value;
@@ -271,11 +371,26 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
@@ -271,11 +373,26 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
);
const gestureDisabled = useSharedValue(false);
@@ -426,7 +446,7 @@ index 7c88afc..a97cb80 100644
panGestureState.value = evt.state;
})
.onUpdate((evt) => {
@@ -286,8 +401,15 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
@@ -286,8 +403,15 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
: evt.translationY;
touchTranslate.value = translation;
})
@@ -443,7 +463,7 @@ index 7c88afc..a97cb80 100644
// Set touch val to current translate val
isTouchActiveNative.value = false;
const translation = horizontalAnim.value
@@ -299,6 +421,7 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
@@ -299,6 +423,7 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
// Only call onDragEnd if actually dragging a cell
if (activeIndexAnim.value === -1 || disabled.value) return;
@@ -451,7 +471,7 @@ index 7c88afc..a97cb80 100644
disabled.value = true;
runOnJS(onRelease)(activeIndexAnim.value);
const springTo = placeholderOffset.value - activeCellOffset.value;
@@ -309,22 +432,85 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
@@ -309,22 +434,85 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
runOnJS(onDragEnd)({
from: activeIndexAnim.value,
to: spacerIndexAnim.value,
@@ -540,7 +560,7 @@ index 7c88afc..a97cb80 100644
const activeOffset = [-activationDistanceProp, activationDistanceProp];
if (props.horizontal) {
panGesture.activeOffsetX(activeOffset);
@@ -332,6 +518,21 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
@@ -332,6 +520,21 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
panGesture.activeOffsetY(activeOffset);
}
}
@@ -562,7 +582,7 @@ index 7c88afc..a97cb80 100644
const onScroll = useStableCallback((scrollOffset: number) => {
props.onScrollOffsetChange?.(scrollOffset);
@@ -372,9 +573,11 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
@@ -372,9 +575,11 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
keyExtractor={keyExtractor}
horizontal={!!props.horizontal}
layoutAnimationDisabled={layoutAnimationDisabled}