Fix mobile sidebar swipe and drag failures (#2709)
* fix(app): prevent text selection during Android swipes Gesture interception did not cancel the native view where the touch began, leaving Android's pending text-selection action alive. * fix(app): restore iOS sidebar close swipes Fabric could fail to attach close handlers when their target lived inside retained but display-hidden panel content. Keep a registered native gesture host mounted outside the presentation wrapper so close swipes work over nested lists without activating rows. * fix(app): recover sidebar drags after cancellation Cancelled native drags could leave a lifted row and the shared outer list locked. Scope terminal cleanup to the owning gesture session and release the lock on cancellation or unmount. * test(app): clean up partial drag fixtures
This commit is contained in:
@@ -3,6 +3,31 @@
|
|||||||
This directory contains local mobile UI flows. Keep flows small enough that a
|
This directory contains local mobile UI flows. Keep flows small enough that a
|
||||||
failure screenshot proves the intended behavior, not just that the app launched.
|
failure screenshot proves the intended behavior, not just that the app launched.
|
||||||
|
|
||||||
|
## iOS Sidebar Close Regression
|
||||||
|
|
||||||
|
`ios-sidebar-close-regression.yaml` exercises close swipes over a semantic header
|
||||||
|
and the nested workspace list without activating the content below the swipe.
|
||||||
|
Start the `sh.paseo.debug` dev client against the intended Metro server first;
|
||||||
|
the flow preserves that running connection.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
maestro test packages/app/maestro/ios-sidebar-close-regression.yaml --udid <simulator-udid>
|
||||||
|
```
|
||||||
|
|
||||||
|
## iOS Sidebar Drag Cancellation Regression
|
||||||
|
|
||||||
|
`test-sidebar-drag-cancellation-ios.sh` creates three temporary projects and
|
||||||
|
uses real native slow swipes to cover successful reorder, a second touch during
|
||||||
|
the drop spring, horizontal close cancellation, outer-list scroll recovery, and
|
||||||
|
a subsequent reorder. The harness removes its daemon projects and local fixture
|
||||||
|
directories on exit.
|
||||||
|
|
||||||
|
Start the debug dev client against the intended Metro server, then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash packages/app/maestro/test-sidebar-drag-cancellation-ios.sh
|
||||||
|
```
|
||||||
|
|
||||||
## New Workspace Android Flow
|
## New Workspace Android Flow
|
||||||
|
|
||||||
Use these files when debugging or extending workspace creation on Android:
|
Use these files when debugging or extending workspace creation on Android:
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
appId: sh.paseo.debug
|
||||||
|
---
|
||||||
|
# Establish History with the sidebar closed. The host filter then proves a close
|
||||||
|
# swipe did not activate the row below it.
|
||||||
|
- tapOn:
|
||||||
|
id: "menu-button"
|
||||||
|
- tapOn:
|
||||||
|
id: "sidebar-sessions"
|
||||||
|
- assertVisible:
|
||||||
|
id: "sessions-host-filter-trigger"
|
||||||
|
|
||||||
|
# A close swipe over a semantic header row must close without navigating.
|
||||||
|
- swipe:
|
||||||
|
direction: RIGHT
|
||||||
|
duration: 300
|
||||||
|
- assertVisible:
|
||||||
|
id: "sidebar-schedules"
|
||||||
|
- swipe:
|
||||||
|
start: "85%,19%"
|
||||||
|
end: "10%,19%"
|
||||||
|
duration: 300
|
||||||
|
- assertNotVisible:
|
||||||
|
id: "sidebar-schedules"
|
||||||
|
- assertVisible:
|
||||||
|
id: "sessions-host-filter-trigger"
|
||||||
|
|
||||||
|
# A close swipe over the nested workspace list must beat horizontal list intent.
|
||||||
|
- swipe:
|
||||||
|
direction: RIGHT
|
||||||
|
duration: 300
|
||||||
|
- assertVisible:
|
||||||
|
id: "sidebar-project-workspace-list-scroll"
|
||||||
|
- swipe:
|
||||||
|
start: "85%,46%"
|
||||||
|
end: "10%,46%"
|
||||||
|
duration: 300
|
||||||
|
- assertNotVisible:
|
||||||
|
id: "sidebar-project-workspace-list-scroll"
|
||||||
|
- assertVisible:
|
||||||
|
id: "sessions-host-filter-trigger"
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
appId: ${PASEO_MAESTRO_APP_ID}
|
||||||
|
---
|
||||||
|
- launchApp:
|
||||||
|
clearState: true
|
||||||
|
|
||||||
|
- runFlow: flows/dev-client.yaml
|
||||||
|
- runFlow: flows/connect-direct-if-welcome.yaml
|
||||||
|
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
id: "menu-button"
|
||||||
|
timeout: 60000
|
||||||
|
|
||||||
|
- tapOn:
|
||||||
|
id: "menu-button"
|
||||||
|
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
id: "sidebar-project-list"
|
||||||
|
timeout: 10000
|
||||||
|
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_A_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_A_NAME}, Project actions"
|
||||||
|
timeout: 30000
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_B_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_B_NAME}, Project actions"
|
||||||
|
timeout: 30000
|
||||||
|
|
||||||
|
# A slow swipe stays inside the stationary slop long enough to arm the native
|
||||||
|
# long-press drag before it moves the second project above the first.
|
||||||
|
- assertVisible:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_A_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_A_NAME}, Project actions"
|
||||||
|
above:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_B_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_B_NAME}, Project actions"
|
||||||
|
- swipe:
|
||||||
|
from:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_B_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_B_NAME}, Project actions"
|
||||||
|
direction: UP
|
||||||
|
duration: 20000
|
||||||
|
waitToSettleTimeoutMs: 0
|
||||||
|
|
||||||
|
# A second touch during the drop spring must not cancel the committed reorder.
|
||||||
|
- tapOn:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_B_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_B_NAME}, Project actions"
|
||||||
|
- assertVisible:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_B_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_B_NAME}, Project actions"
|
||||||
|
above:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_A_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_A_NAME}, Project actions"
|
||||||
|
|
||||||
|
# Horizontal intent after the lift belongs to panel close. It must cancel the
|
||||||
|
# drag without committing another reorder or leaving the outer list locked.
|
||||||
|
- swipe:
|
||||||
|
from:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_B_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_B_NAME}, Project actions"
|
||||||
|
direction: LEFT
|
||||||
|
duration: 20000
|
||||||
|
- assertNotVisible:
|
||||||
|
id: "sidebar-project-list"
|
||||||
|
|
||||||
|
- tapOn:
|
||||||
|
id: "menu-button"
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
id: "sidebar-project-list"
|
||||||
|
timeout: 10000
|
||||||
|
- assertVisible:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_B_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_B_NAME}, Project actions"
|
||||||
|
above:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_A_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_A_NAME}, Project actions"
|
||||||
|
|
||||||
|
# Reaching a fixture at the bottom proves the nestable outer scroll was
|
||||||
|
# unlocked by cancellation.
|
||||||
|
- scrollUntilVisible:
|
||||||
|
element:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_Z_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_Z_NAME}, Project actions"
|
||||||
|
direction: DOWN
|
||||||
|
timeout: 30000
|
||||||
|
speed: 40
|
||||||
|
visibilityPercentage: 50
|
||||||
|
centerElement: true
|
||||||
|
- assertVisible:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_Z_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_Z_NAME}, Project actions"
|
||||||
|
|
||||||
|
- scrollUntilVisible:
|
||||||
|
element:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_A_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_A_NAME}, Project actions"
|
||||||
|
direction: UP
|
||||||
|
timeout: 30000
|
||||||
|
speed: 40
|
||||||
|
visibilityPercentage: 50
|
||||||
|
centerElement: true
|
||||||
|
|
||||||
|
# A real reorder after cancellation proves the list is not merely scrollable;
|
||||||
|
# its drag recognizer and semantic order update both recovered.
|
||||||
|
- swipe:
|
||||||
|
from:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_A_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_A_NAME}, Project actions"
|
||||||
|
direction: UP
|
||||||
|
duration: 20000
|
||||||
|
- assertVisible:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_A_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_A_NAME}, Project actions"
|
||||||
|
above:
|
||||||
|
text: ".*${PASEO_MAESTRO_DRAG_B_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_B_NAME}, Project actions"
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# iOS native regression harness for sidebar drag cancellation and recovery.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||||
|
FLOW_TEMPLATE="$REPO_ROOT/packages/app/maestro/sidebar-drag-cancellation-regression.yaml"
|
||||||
|
FLOW_TEMPLATE_DIR="$REPO_ROOT/packages/app/maestro"
|
||||||
|
OUT_DIR="/tmp/paseo-sidebar-drag-cancellation-$(date +%s)"
|
||||||
|
CLIENT_EXPORTS="$REPO_ROOT/packages/client/dist/daemon-client.js"
|
||||||
|
RELAY_EXPORTS="$REPO_ROOT/node_modules/@getpaseo/relay/dist/e2ee.js"
|
||||||
|
FIXTURE_ROOT=""
|
||||||
|
PROJECT_IDS_FILE="$OUT_DIR/project-ids.json"
|
||||||
|
|
||||||
|
export PASEO_MAESTRO_APP_ID="${PASEO_MAESTRO_APP_ID:-sh.paseo.debug}"
|
||||||
|
export PASEO_MAESTRO_DIRECT_ENDPOINT="${PASEO_MAESTRO_DIRECT_ENDPOINT:-127.0.0.1:6767}"
|
||||||
|
export PASEO_MAESTRO_DAEMON_WS_URL="${PASEO_MAESTRO_DAEMON_WS_URL:-ws://127.0.0.1:6767/ws}"
|
||||||
|
export PASEO_MAESTRO_DAEMON_HEALTH_URL="${PASEO_MAESTRO_DAEMON_HEALTH_URL:-http://127.0.0.1:6767/api/health}"
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
echo "Missing required command: $1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_command git
|
||||||
|
require_command curl
|
||||||
|
require_command maestro
|
||||||
|
require_command node
|
||||||
|
require_command perl
|
||||||
|
require_command xcrun
|
||||||
|
|
||||||
|
if [ ! -f "$CLIENT_EXPORTS" ] || [ ! -f "$RELAY_EXPORTS" ]; then
|
||||||
|
echo "Missing client or relay build artifacts." >&2
|
||||||
|
echo "Run: npm run build:server" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! curl --fail --silent --show-error --max-time 3 "$PASEO_MAESTRO_DAEMON_HEALTH_URL" >/dev/null; then
|
||||||
|
echo "Paseo daemon is unavailable at $PASEO_MAESTRO_DAEMON_HEALTH_URL" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FIXTURE_ROOT="$(mktemp -d /tmp/paseo-sidebar-drag-fixture-XXXXXX)"
|
||||||
|
export PASEO_MAESTRO_DRAG_A_NAME="000-paseo-drag-a-$(basename "$FIXTURE_ROOT")"
|
||||||
|
export PASEO_MAESTRO_DRAG_B_NAME="001-paseo-drag-b-$(basename "$FIXTURE_ROOT")"
|
||||||
|
export PASEO_MAESTRO_DRAG_Z_NAME="zzz-paseo-drag-z-$(basename "$FIXTURE_ROOT")"
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR/flows"
|
||||||
|
|
||||||
|
if [ -z "${PASEO_MAESTRO_IOS_UDID:-}" ]; then
|
||||||
|
export PASEO_MAESTRO_IOS_UDID
|
||||||
|
PASEO_MAESTRO_IOS_UDID="$({ xcrun simctl list devices booted -j || true; } | node -e '
|
||||||
|
let input = "";
|
||||||
|
process.stdin.on("data", (chunk) => (input += chunk));
|
||||||
|
process.stdin.on("end", () => {
|
||||||
|
const devices = Object.values(JSON.parse(input).devices ?? {}).flat();
|
||||||
|
const booted = devices.find((device) => device.state === "Booted");
|
||||||
|
if (booted?.udid) process.stdout.write(booted.udid);
|
||||||
|
});
|
||||||
|
')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$PASEO_MAESTRO_IOS_UDID" ]; then
|
||||||
|
echo "No booted iOS simulator found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
render_flow() {
|
||||||
|
local source="$1"
|
||||||
|
local target="$2"
|
||||||
|
perl -0pe '
|
||||||
|
s/^appId: sh\.paseo$/appId: $ENV{PASEO_MAESTRO_APP_ID}/m;
|
||||||
|
s/\$\{PASEO_MAESTRO_APP_ID\}/$ENV{PASEO_MAESTRO_APP_ID}/g;
|
||||||
|
s/\$\{PASEO_MAESTRO_DIRECT_ENDPOINT\}/$ENV{PASEO_MAESTRO_DIRECT_ENDPOINT}/g;
|
||||||
|
s/\$\{PASEO_MAESTRO_DRAG_A_NAME\}/$ENV{PASEO_MAESTRO_DRAG_A_NAME}/g;
|
||||||
|
s/\$\{PASEO_MAESTRO_DRAG_B_NAME\}/$ENV{PASEO_MAESTRO_DRAG_B_NAME}/g;
|
||||||
|
s/\$\{PASEO_MAESTRO_DRAG_Z_NAME\}/$ENV{PASEO_MAESTRO_DRAG_Z_NAME}/g;
|
||||||
|
' "$source" > "$target"
|
||||||
|
}
|
||||||
|
|
||||||
|
for project_name in \
|
||||||
|
"$PASEO_MAESTRO_DRAG_A_NAME" \
|
||||||
|
"$PASEO_MAESTRO_DRAG_B_NAME" \
|
||||||
|
"$PASEO_MAESTRO_DRAG_Z_NAME"; do
|
||||||
|
project_path="$FIXTURE_ROOT/$project_name"
|
||||||
|
mkdir -p "$project_path"
|
||||||
|
git -C "$project_path" init >/dev/null
|
||||||
|
git -C "$project_path" checkout -b main >/dev/null 2>&1 || true
|
||||||
|
git -C "$project_path" config user.name "Paseo Maestro"
|
||||||
|
git -C "$project_path" config user.email "maestro@getpaseo.local"
|
||||||
|
printf '# Sidebar drag cancellation fixture\n' > "$project_path/README.md"
|
||||||
|
git -C "$project_path" add README.md
|
||||||
|
git -C "$project_path" commit -m "Initial commit" >/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ -s "$PROJECT_IDS_FILE" ]; then
|
||||||
|
REPO_ROOT="$REPO_ROOT" PROJECT_IDS_FILE="$PROJECT_IDS_FILE" node --input-type=module <<'NODE' || true
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
|
const moduleUrl = pathToFileURL(`${process.env.REPO_ROOT}/packages/client/dist/daemon-client.js`).href;
|
||||||
|
const { DaemonClient } = await import(moduleUrl);
|
||||||
|
const projectIds = JSON.parse(await readFile(process.env.PROJECT_IDS_FILE, "utf8"));
|
||||||
|
const client = new DaemonClient({
|
||||||
|
url: process.env.PASEO_MAESTRO_DAEMON_WS_URL,
|
||||||
|
clientId: `maestro-sidebar-drag-cleanup-${Date.now()}`,
|
||||||
|
clientType: "cli",
|
||||||
|
webSocketFactory: (url, options) => new WebSocket(url, { headers: options?.headers }),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
for (const projectId of projectIds) {
|
||||||
|
await client.removeProject(projectId).catch(() => undefined);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await client.close().catch(() => undefined);
|
||||||
|
}
|
||||||
|
NODE
|
||||||
|
fi
|
||||||
|
rm -rf "$FIXTURE_ROOT"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
REPO_ROOT="$REPO_ROOT" FIXTURE_ROOT="$FIXTURE_ROOT" PROJECT_IDS_FILE="$PROJECT_IDS_FILE" node --input-type=module <<'NODE'
|
||||||
|
import { writeFile } from "node:fs/promises";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
|
const moduleUrl = pathToFileURL(`${process.env.REPO_ROOT}/packages/client/dist/daemon-client.js`).href;
|
||||||
|
const { DaemonClient } = await import(moduleUrl);
|
||||||
|
const projectNames = [
|
||||||
|
process.env.PASEO_MAESTRO_DRAG_A_NAME,
|
||||||
|
process.env.PASEO_MAESTRO_DRAG_B_NAME,
|
||||||
|
process.env.PASEO_MAESTRO_DRAG_Z_NAME,
|
||||||
|
];
|
||||||
|
const client = new DaemonClient({
|
||||||
|
url: process.env.PASEO_MAESTRO_DAEMON_WS_URL,
|
||||||
|
clientId: `maestro-sidebar-drag-setup-${Date.now()}`,
|
||||||
|
clientType: "cli",
|
||||||
|
webSocketFactory: (url, options) => new WebSocket(url, { headers: options?.headers }),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
const projectIds = [];
|
||||||
|
for (const projectName of projectNames) {
|
||||||
|
const payload = await client.addProject(`${process.env.FIXTURE_ROOT}/${projectName}`);
|
||||||
|
if (payload.error || !payload.project) {
|
||||||
|
throw new Error(payload.error ?? `addProject returned no project for ${projectName}`);
|
||||||
|
}
|
||||||
|
projectIds.push(payload.project.id);
|
||||||
|
await writeFile(process.env.PROJECT_IDS_FILE, JSON.stringify(projectIds));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await client.close().catch(() => undefined);
|
||||||
|
}
|
||||||
|
NODE
|
||||||
|
|
||||||
|
FLOW="$OUT_DIR/sidebar-drag-cancellation-regression.rendered.yaml"
|
||||||
|
render_flow "$FLOW_TEMPLATE" "$FLOW"
|
||||||
|
render_flow "$FLOW_TEMPLATE_DIR/flows/dev-client.yaml" "$OUT_DIR/flows/dev-client.yaml"
|
||||||
|
render_flow "$FLOW_TEMPLATE_DIR/flows/connect-direct-if-welcome.yaml" "$OUT_DIR/flows/connect-direct-if-welcome.yaml"
|
||||||
|
|
||||||
|
echo "Running sidebar drag cancellation regression on $PASEO_MAESTRO_IOS_UDID"
|
||||||
|
echo "Artifacts: $OUT_DIR"
|
||||||
|
(cd "$OUT_DIR" && maestro test "$FLOW" --udid "$PASEO_MAESTRO_IOS_UDID")
|
||||||
@@ -30,6 +30,7 @@ export function DraggableList<T>({
|
|||||||
onRefresh,
|
onRefresh,
|
||||||
extraData,
|
extraData,
|
||||||
simultaneousGestureRef,
|
simultaneousGestureRef,
|
||||||
|
gestureHostPresented,
|
||||||
waitFor,
|
waitFor,
|
||||||
onDragBegin: onDragBeginProp,
|
onDragBegin: onDragBeginProp,
|
||||||
nestable = false,
|
nestable = false,
|
||||||
@@ -118,6 +119,7 @@ export function DraggableList<T>({
|
|||||||
scrollEnabled={scrollEnabled}
|
scrollEnabled={scrollEnabled}
|
||||||
extraData={extraData}
|
extraData={extraData}
|
||||||
simultaneousHandlers={simultaneousHandlers}
|
simultaneousHandlers={simultaneousHandlers}
|
||||||
|
dragGestureHostPresented={gestureHostPresented}
|
||||||
// Higher activation distance reduces accidental drag capture while nested
|
// Higher activation distance reduces accidental drag capture while nested
|
||||||
// lists are inside a scroll container.
|
// lists are inside a scroll container.
|
||||||
activationDistance={20}
|
activationDistance={20}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export interface DraggableListProps<T> {
|
|||||||
extraData?: unknown;
|
extraData?: unknown;
|
||||||
/** Gesture ref for simultaneous handling with parent gestures (e.g., sidebar close) */
|
/** Gesture ref for simultaneous handling with parent gestures (e.g., sidebar close) */
|
||||||
simultaneousGestureRef?: MutableRefObject<GestureType | undefined>;
|
simultaneousGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||||
|
/** Whether the retained native gesture host is currently presented. */
|
||||||
|
gestureHostPresented?: boolean;
|
||||||
/** Gesture ref(s) that the list should wait for before handling scroll */
|
/** Gesture ref(s) that the list should wait for before handling scroll */
|
||||||
waitFor?: MutableRefObject<GestureType | undefined> | MutableRefObject<GestureType | undefined>[];
|
waitFor?: MutableRefObject<GestureType | undefined> | MutableRefObject<GestureType | undefined>[];
|
||||||
/** Called when a drag gesture begins (before items are reordered) */
|
/** Called when a drag gesture begins (before items are reordered) */
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import { usePanelStore } from "@/stores/panel-store";
|
|||||||
import { useOwnsWindowChromeCorner, WindowChromeSafeArea } from "@/utils/desktop-window";
|
import { useOwnsWindowChromeCorner, WindowChromeSafeArea } from "@/utils/desktop-window";
|
||||||
import { useCloseAgentListGesture } from "@/mobile-panels/gestures";
|
import { useCloseAgentListGesture } from "@/mobile-panels/gestures";
|
||||||
import { MobilePanelOverlay } from "@/mobile-panels/presentation";
|
import { MobilePanelOverlay } from "@/mobile-panels/presentation";
|
||||||
|
import { useIsMobilePanelPresented } from "@/mobile-panels/provider";
|
||||||
import {
|
import {
|
||||||
buildOpenProjectRoute,
|
buildOpenProjectRoute,
|
||||||
buildNewWorkspaceRoute,
|
buildNewWorkspaceRoute,
|
||||||
@@ -619,6 +620,7 @@ function MobileSidebar({
|
|||||||
const isSessionsActive = pathname.includes("/sessions");
|
const isSessionsActive = pathname.includes("/sessions");
|
||||||
const isSchedulesActive = pathname.includes("/schedules");
|
const isSchedulesActive = pathname.includes("/schedules");
|
||||||
const { gesture: closeGesture, gestureRef: closeGestureRef } = useCloseAgentListGesture();
|
const { gesture: closeGesture, gestureRef: closeGestureRef } = useCloseAgentListGesture();
|
||||||
|
const dragGestureHostPresented = useIsMobilePanelPresented("agent-list");
|
||||||
|
|
||||||
const handleViewMore = useCallback(() => {
|
const handleViewMore = useCallback(() => {
|
||||||
closeSidebar();
|
closeSidebar();
|
||||||
@@ -714,6 +716,7 @@ function MobileSidebar({
|
|||||||
onWorkspacePress={handleWorkspacePress}
|
onWorkspacePress={handleWorkspacePress}
|
||||||
onAddProject={handleOpenProject}
|
onAddProject={handleOpenProject}
|
||||||
parentGestureRef={closeGestureRef}
|
parentGestureRef={closeGestureRef}
|
||||||
|
dragGestureHostPresented={dragGestureHostPresented}
|
||||||
listHeaderComponent={workspacesSectionHeaderElement}
|
listHeaderComponent={workspacesSectionHeaderElement}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ interface SidebarWorkspaceListProps {
|
|||||||
listHeaderComponent?: ReactElement | null;
|
listHeaderComponent?: ReactElement | null;
|
||||||
/** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */
|
/** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */
|
||||||
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||||
|
dragGestureHostPresented?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectHeaderRowProps {
|
interface ProjectHeaderRowProps {
|
||||||
@@ -1605,6 +1606,7 @@ function ProjectBlock({
|
|||||||
isDragging,
|
isDragging,
|
||||||
dragHandleProps,
|
dragHandleProps,
|
||||||
useNestable,
|
useNestable,
|
||||||
|
dragGestureHostPresented,
|
||||||
creatingWorkspaceIds,
|
creatingWorkspaceIds,
|
||||||
activeWorkspaceSelection,
|
activeWorkspaceSelection,
|
||||||
hostLabelByServerId,
|
hostLabelByServerId,
|
||||||
@@ -1630,6 +1632,7 @@ function ProjectBlock({
|
|||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
dragHandleProps?: DraggableListDragHandleProps;
|
dragHandleProps?: DraggableListDragHandleProps;
|
||||||
useNestable: boolean;
|
useNestable: boolean;
|
||||||
|
dragGestureHostPresented?: boolean;
|
||||||
creatingWorkspaceIds: ReadonlySet<string>;
|
creatingWorkspaceIds: ReadonlySet<string>;
|
||||||
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
||||||
hostLabelByServerId: ReadonlyMap<string, string>;
|
hostLabelByServerId: ReadonlyMap<string, string>;
|
||||||
@@ -1806,6 +1809,7 @@ function ProjectBlock({
|
|||||||
useDragHandle
|
useDragHandle
|
||||||
nestable={useNestable}
|
nestable={useNestable}
|
||||||
simultaneousGestureRef={parentGestureRef}
|
simultaneousGestureRef={parentGestureRef}
|
||||||
|
gestureHostPresented={dragGestureHostPresented}
|
||||||
containerStyle={styles.workspaceListContainer}
|
containerStyle={styles.workspaceListContainer}
|
||||||
/>
|
/>
|
||||||
{canToggleWorkspaces ? (
|
{canToggleWorkspaces ? (
|
||||||
@@ -1886,6 +1890,7 @@ function areProjectBlockPropsEqual(previous: ProjectBlockProps, next: ProjectBlo
|
|||||||
previous.isDragging === next.isDragging &&
|
previous.isDragging === next.isDragging &&
|
||||||
previous.dragHandleProps === next.dragHandleProps &&
|
previous.dragHandleProps === next.dragHandleProps &&
|
||||||
previous.useNestable === next.useNestable &&
|
previous.useNestable === next.useNestable &&
|
||||||
|
previous.dragGestureHostPresented === next.dragGestureHostPresented &&
|
||||||
previous.creatingWorkspaceIds === next.creatingWorkspaceIds &&
|
previous.creatingWorkspaceIds === next.creatingWorkspaceIds &&
|
||||||
areProjectBlockSelectionsEqual(previous, next)
|
areProjectBlockSelectionsEqual(previous, next)
|
||||||
);
|
);
|
||||||
@@ -1936,6 +1941,7 @@ export function SidebarWorkspaceList({
|
|||||||
listFooterComponent,
|
listFooterComponent,
|
||||||
listHeaderComponent,
|
listHeaderComponent,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
|
dragGestureHostPresented,
|
||||||
}: SidebarWorkspaceListProps) {
|
}: SidebarWorkspaceListProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const hosts = useHosts();
|
const hosts = useHosts();
|
||||||
@@ -1991,6 +1997,7 @@ export function SidebarWorkspaceList({
|
|||||||
listFooterComponent={listFooterComponent}
|
listFooterComponent={listFooterComponent}
|
||||||
listHeaderComponent={listHeaderComponent}
|
listHeaderComponent={listHeaderComponent}
|
||||||
parentGestureRef={parentGestureRef}
|
parentGestureRef={parentGestureRef}
|
||||||
|
dragGestureHostPresented={dragGestureHostPresented}
|
||||||
pathname={pathname}
|
pathname={pathname}
|
||||||
hostLabelByServerId={hostLabelByServerId}
|
hostLabelByServerId={hostLabelByServerId}
|
||||||
showHostLabels={showHostLabels}
|
showHostLabels={showHostLabels}
|
||||||
@@ -2065,6 +2072,7 @@ function ProjectModeList({
|
|||||||
listFooterComponent,
|
listFooterComponent,
|
||||||
listHeaderComponent,
|
listHeaderComponent,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
|
dragGestureHostPresented,
|
||||||
pathname,
|
pathname,
|
||||||
hostLabelByServerId,
|
hostLabelByServerId,
|
||||||
showHostLabels,
|
showHostLabels,
|
||||||
@@ -2279,6 +2287,7 @@ function ProjectModeList({
|
|||||||
isDragging={dragState.isDragging}
|
isDragging={dragState.isDragging}
|
||||||
dragHandleProps={dragState.dragHandleProps}
|
dragHandleProps={dragState.dragHandleProps}
|
||||||
useNestable={platformIsNative}
|
useNestable={platformIsNative}
|
||||||
|
dragGestureHostPresented={dragGestureHostPresented}
|
||||||
creatingWorkspaceIds={creatingWorkspaceIds}
|
creatingWorkspaceIds={creatingWorkspaceIds}
|
||||||
activeWorkspaceSelection={activeWorkspaceSelection}
|
activeWorkspaceSelection={activeWorkspaceSelection}
|
||||||
hostLabelByServerId={hostLabelByServerId}
|
hostLabelByServerId={hostLabelByServerId}
|
||||||
@@ -2302,6 +2311,7 @@ function ProjectModeList({
|
|||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
onToggleProjectCollapsed,
|
onToggleProjectCollapsed,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
|
dragGestureHostPresented,
|
||||||
projectIconByProjectViewKey,
|
projectIconByProjectViewKey,
|
||||||
selectionEnabled,
|
selectionEnabled,
|
||||||
shortcutIndexByWorkspaceKey,
|
shortcutIndexByWorkspaceKey,
|
||||||
@@ -2399,6 +2409,7 @@ function ProjectModeList({
|
|||||||
useDragHandle
|
useDragHandle
|
||||||
nestable={platformIsNative}
|
nestable={platformIsNative}
|
||||||
simultaneousGestureRef={parentGestureRef}
|
simultaneousGestureRef={parentGestureRef}
|
||||||
|
gestureHostPresented={dragGestureHostPresented}
|
||||||
containerStyle={styles.projectListContainer}
|
containerStyle={styles.projectListContainer}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -69,24 +69,33 @@ export function MobilePanelOverlay({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={overlayStyle} pointerEvents={overlayPointerEvents}>
|
<GestureDetector gesture={closeGesture} touchAction="pan-y">
|
||||||
<Pressable
|
{/* Fabric needs an always-mounted native host to attach the close handler while the
|
||||||
accessibilityElementsHidden
|
retained panel content is hidden. nativeID keeps that host registered. */}
|
||||||
importantForAccessibility="no-hide-descendants"
|
<View
|
||||||
onPress={showMobileAgent}
|
collapsable={false}
|
||||||
pointerEvents={isOpen ? "auto" : "none"}
|
nativeID={`${panel}-gesture-host`}
|
||||||
style={StyleSheet.absoluteFillObject}
|
pointerEvents={overlayPointerEvents}
|
||||||
testID={`${panel}-backdrop`}
|
style={styles.overlay}
|
||||||
>
|
>
|
||||||
<Animated.View pointerEvents="none" style={backdropStyle} />
|
<View style={overlayStyle} pointerEvents={overlayPointerEvents}>
|
||||||
</Pressable>
|
<Pressable
|
||||||
|
accessibilityElementsHidden
|
||||||
|
importantForAccessibility="no-hide-descendants"
|
||||||
|
onPress={showMobileAgent}
|
||||||
|
pointerEvents={isOpen ? "auto" : "none"}
|
||||||
|
style={StyleSheet.absoluteFillObject}
|
||||||
|
testID={`${panel}-backdrop`}
|
||||||
|
>
|
||||||
|
<Animated.View pointerEvents="none" style={backdropStyle} />
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
<GestureDetector gesture={closeGesture} touchAction="pan-y">
|
<Animated.View pointerEvents={isOpen ? "auto" : "none"} style={combinedPanelStyle}>
|
||||||
<Animated.View pointerEvents={isOpen ? "auto" : "none"} style={combinedPanelStyle}>
|
<WindowChromeRootRegion corners="both">{children}</WindowChromeRootRegion>
|
||||||
<WindowChromeRootRegion corners="both">{children}</WindowChromeRootRegion>
|
</Animated.View>
|
||||||
</Animated.View>
|
</View>
|
||||||
</GestureDetector>
|
</View>
|
||||||
</View>
|
</GestureDetector>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,360 @@
|
|||||||
|
diff --git a/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts b/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts
|
||||||
|
index d3a4f1f..083bf19 100644
|
||||||
|
--- a/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts
|
||||||
|
+++ b/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts
|
||||||
|
@@ -16,12 +16,14 @@ declare const _default: <T>(props: Omit<FlatListProps<T>, "keyExtractor" | "data
|
||||||
|
keyExtractor: (item: T, index: number) => string;
|
||||||
|
onDragBegin?: ((index: number) => void) | undefined;
|
||||||
|
onDragEnd?: ((params: import("../types").DragEndParams<T>) => void) | undefined;
|
||||||
|
+ onDragTerminate?: (() => void) | undefined;
|
||||||
|
onPlaceholderIndexChange?: ((placeholderIndex: number) => void) | undefined;
|
||||||
|
onRelease?: ((index: number) => void) | undefined;
|
||||||
|
onScrollOffsetChange?: ((scrollOffset: number) => void) | undefined;
|
||||||
|
renderItem: import("../types").RenderItem<T>;
|
||||||
|
renderPlaceholder?: import("../types").RenderPlaceholder<T> | undefined;
|
||||||
|
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[] | undefined;
|
||||||
|
+ dragGestureHostPresented?: boolean | undefined;
|
||||||
|
outerScrollOffset?: Animated.SharedValue<number> | undefined;
|
||||||
|
onAnimValInit?: ((animVals: {
|
||||||
|
activeCellOffset: import("react-native-reanimated").SharedValue<number>;
|
||||||
|
diff --git a/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts b/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts
|
||||||
|
index d3c6cbb..36e9645 100644
|
||||||
|
--- a/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts
|
||||||
|
+++ b/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts
|
||||||
|
@@ -15,12 +15,14 @@ export declare const NestableDraggableFlatList: <T>(props: Omit<import("react-na
|
||||||
|
keyExtractor: (item: T, index: number) => string;
|
||||||
|
onDragBegin?: ((index: number) => void) | undefined;
|
||||||
|
onDragEnd?: ((params: import("../types").DragEndParams<T>) => void) | undefined;
|
||||||
|
+ onDragTerminate?: (() => void) | undefined;
|
||||||
|
onPlaceholderIndexChange?: ((placeholderIndex: number) => void) | undefined;
|
||||||
|
onRelease?: ((index: number) => void) | undefined;
|
||||||
|
onScrollOffsetChange?: ((scrollOffset: number) => void) | undefined;
|
||||||
|
renderItem: import("../types").RenderItem<T>;
|
||||||
|
renderPlaceholder?: import("../types").RenderPlaceholder<T> | undefined;
|
||||||
|
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[] | undefined;
|
||||||
|
+ dragGestureHostPresented?: boolean | undefined;
|
||||||
|
outerScrollOffset?: Animated.SharedValue<number> | undefined;
|
||||||
|
onAnimValInit?: ((animVals: {
|
||||||
|
activeCellOffset: import("react-native-reanimated").SharedValue<number>;
|
||||||
|
diff --git a/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts b/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts
|
||||||
|
index 2abb4a2..8ca967f 100644
|
||||||
|
--- a/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts
|
||||||
|
+++ b/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts
|
||||||
|
@@ -23,12 +23,16 @@ export declare type DraggableFlatListProps<T> = Modify<FlatListProps<T>, {
|
||||||
|
keyExtractor: (item: T, index: number) => string;
|
||||||
|
onDragBegin?: (index: number) => void;
|
||||||
|
onDragEnd?: (params: DragEndParams<T>) => void;
|
||||||
|
+ /** @internal Used by the nestable wrapper to release its outer-scroll lock on cancellation. */
|
||||||
|
+ onDragTerminate?: () => void;
|
||||||
|
onPlaceholderIndexChange?: (placeholderIndex: number) => void;
|
||||||
|
onRelease?: (index: number) => void;
|
||||||
|
onScrollOffsetChange?: (scrollOffset: number) => void;
|
||||||
|
renderItem: RenderItem<T>;
|
||||||
|
renderPlaceholder?: RenderPlaceholder<T>;
|
||||||
|
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[];
|
||||||
|
+ /** @internal Forces handler reattachment when a retained native host is presented. */
|
||||||
|
+ dragGestureHostPresented?: boolean;
|
||||||
|
outerScrollOffset?: Animated.SharedValue<number>;
|
||||||
|
onAnimValInit?: (animVals: ReturnType<typeof useAnimatedValues>) => void;
|
||||||
|
itemEnteringAnimation?: AnimateProps<Animated.View>["entering"];
|
||||||
diff --git a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
|
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..b630940 100644
|
index 7c88afc..b2f103e 100644
|
||||||
--- a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
|
--- a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
|
||||||
+++ b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
|
+++ b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
|
||||||
@@ -332,6 +332,13 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
@@ -19,6 +19,7 @@ import {
|
||||||
|
} from "react-native-gesture-handler";
|
||||||
|
import Animated, {
|
||||||
|
runOnJS,
|
||||||
|
+ runOnUI,
|
||||||
|
useAnimatedReaction,
|
||||||
|
useAnimatedScrollHandler,
|
||||||
|
useSharedValue,
|
||||||
|
@@ -92,6 +93,17 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||||
|
activeCellOffset.value = -1;
|
||||||
|
setActiveKey(null);
|
||||||
|
});
|
||||||
|
+ const terminalHandled = useSharedValue(false);
|
||||||
|
+ const gestureSessionId = useSharedValue(0);
|
||||||
|
+ const gestureSessionActive = useSharedValue(false);
|
||||||
|
+ const currentGestureSessionRef = useRef<{
|
||||||
|
+ id: number;
|
||||||
|
+ active: boolean;
|
||||||
|
+ } | null>(null);
|
||||||
|
+ const activeDragRef = useRef<{
|
||||||
|
+ sessionId: number;
|
||||||
|
+ index: number;
|
||||||
|
+ } | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
dragHitSlop = DEFAULT_PROPS.dragHitSlop,
|
||||||
|
@@ -144,19 +156,88 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||||
|
});
|
||||||
|
}, [props.data, keyExtractor, keyToIndexRef]);
|
||||||
|
|
||||||
|
+ const onDragCancel = useStableCallback((sessionId: number, index: number) => {
|
||||||
|
+ if (activeDragRef.current?.sessionId !== sessionId) return;
|
||||||
|
+ activeDragRef.current = null;
|
||||||
|
+ reset();
|
||||||
|
+ props.onRelease?.(index);
|
||||||
|
+ props.onDragTerminate?.();
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ useEffect(() => {
|
||||||
|
+ return () => {
|
||||||
|
+ const activeDrag = activeDragRef.current;
|
||||||
|
+ if (!activeDrag) return;
|
||||||
|
+ activeDragRef.current = null;
|
||||||
|
+ propsRef.current.onRelease?.(activeDrag.index);
|
||||||
|
+ propsRef.current.onDragTerminate?.();
|
||||||
|
+ };
|
||||||
|
+ }, [propsRef]);
|
||||||
|
+
|
||||||
|
+ const onGestureBegin = useStableCallback((sessionId: number) => {
|
||||||
|
+ currentGestureSessionRef.current = { id: sessionId, active: true };
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ const onGestureFinalize = useStableCallback(
|
||||||
|
+ (sessionId: number, committed: boolean) => {
|
||||||
|
+ if (currentGestureSessionRef.current?.id === sessionId) {
|
||||||
|
+ currentGestureSessionRef.current.active = false;
|
||||||
|
+ }
|
||||||
|
+ if (!committed && activeDragRef.current?.sessionId === sessionId) {
|
||||||
|
+ onDragCancel(sessionId, activeDragRef.current.index);
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ );
|
||||||
|
+
|
||||||
|
+ const onDragActivationRejected = useStableCallback((sessionId: number) => {
|
||||||
|
+ if (activeDragRef.current?.sessionId === sessionId) {
|
||||||
|
+ onDragCancel(sessionId, activeDragRef.current.index);
|
||||||
|
+ }
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ const activateDragOnUI = (
|
||||||
|
+ sessionId: number,
|
||||||
|
+ index: number,
|
||||||
|
+ cellOffset: number,
|
||||||
|
+ cellSize: number,
|
||||||
|
+ hasCellMeasurements: boolean
|
||||||
|
+ ) => {
|
||||||
|
+ "worklet";
|
||||||
|
+ if (
|
||||||
|
+ gestureSessionId.value !== sessionId ||
|
||||||
|
+ !gestureSessionActive.value ||
|
||||||
|
+ terminalHandled.value
|
||||||
|
+ ) {
|
||||||
|
+ runOnJS(onDragActivationRejected)(sessionId);
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+ if (hasCellMeasurements) {
|
||||||
|
+ activeCellOffset.value = cellOffset;
|
||||||
|
+ activeCellSize.value = cellSize;
|
||||||
|
+ }
|
||||||
|
+ spacerIndexAnim.value = index;
|
||||||
|
+ activeIndexAnim.value = index;
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
const drag = useStableCallback((activeKey: string) => {
|
||||||
|
- if (disabled.value) return;
|
||||||
|
+ const gestureSession = currentGestureSessionRef.current;
|
||||||
|
+ if (disabled.value || !gestureSession?.active) return;
|
||||||
|
const index = keyToIndexRef.current.get(activeKey);
|
||||||
|
const cellData = cellDataRef.current.get(activeKey);
|
||||||
|
- if (cellData) {
|
||||||
|
- activeCellOffset.value = cellData.measurements.offset;
|
||||||
|
- activeCellSize.value = cellData.measurements.size;
|
||||||
|
- }
|
||||||
|
|
||||||
|
const { onDragBegin } = propsRef.current;
|
||||||
|
if (index !== undefined) {
|
||||||
|
- spacerIndexAnim.value = index;
|
||||||
|
- activeIndexAnim.value = index;
|
||||||
|
+ runOnUI(activateDragOnUI)(
|
||||||
|
+ gestureSession.id,
|
||||||
|
+ index,
|
||||||
|
+ cellData?.measurements.offset ?? -1,
|
||||||
|
+ cellData?.measurements.size ?? -1,
|
||||||
|
+ Boolean(cellData)
|
||||||
|
+ );
|
||||||
|
+ activeDragRef.current = {
|
||||||
|
+ sessionId: gestureSession.id,
|
||||||
|
+ index,
|
||||||
|
+ };
|
||||||
|
setActiveKey(activeKey);
|
||||||
|
onDragBegin?.(index);
|
||||||
|
}
|
||||||
|
@@ -218,8 +299,25 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||||
|
props.onRelease?.(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
+ const finalizeCancelledDrag = () => {
|
||||||
|
+ "worklet";
|
||||||
|
+ const activeIndex = activeIndexAnim.value;
|
||||||
|
+ if (terminalHandled.value || activeIndex < 0) return;
|
||||||
|
+
|
||||||
|
+ terminalHandled.value = true;
|
||||||
|
+ isTouchActiveNative.value = false;
|
||||||
|
+ touchTranslate.value = 0;
|
||||||
|
+ activeIndexAnim.value = -1;
|
||||||
|
+ spacerIndexAnim.value = -1;
|
||||||
|
+ activeCellSize.value = -1;
|
||||||
|
+ activeCellOffset.value = -1;
|
||||||
|
+ disabled.value = false;
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
const onDragEnd = useStableCallback(
|
||||||
|
- ({ from, to }: { from: number; to: number }) => {
|
||||||
|
+ ({ from, to, sessionId }: { from: number; to: number; sessionId: number }) => {
|
||||||
|
+ if (activeDragRef.current?.sessionId !== sessionId) return;
|
||||||
|
+ activeDragRef.current = null;
|
||||||
|
const { onDragEnd, data } = props;
|
||||||
|
|
||||||
|
const newData = [...data];
|
||||||
|
@@ -238,26 +336,6 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||||
|
props.onPlaceholderIndexChange?.(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
- // Handle case where user ends drag without moving their finger.
|
||||||
|
- useAnimatedReaction(
|
||||||
|
- () => {
|
||||||
|
- return isTouchActiveNative.value;
|
||||||
|
- },
|
||||||
|
- (cur, prev) => {
|
||||||
|
- if (cur !== prev && !cur) {
|
||||||
|
- const hasMoved = !!touchTranslate.value;
|
||||||
|
- if (!hasMoved && activeIndexAnim.value >= 0 && !disabled.value) {
|
||||||
|
- runOnJS(onRelease)(activeIndexAnim.value);
|
||||||
|
- runOnJS(onDragEnd)({
|
||||||
|
- from: activeIndexAnim.value,
|
||||||
|
- to: spacerIndexAnim.value,
|
||||||
|
- });
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
- },
|
||||||
|
- [isTouchActiveNative, onDragEnd, onRelease]
|
||||||
|
- );
|
||||||
|
-
|
||||||
|
useAnimatedReaction(
|
||||||
|
() => {
|
||||||
|
return spacerIndexAnim.value;
|
||||||
|
@@ -271,11 +349,19 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||||
|
);
|
||||||
|
|
||||||
|
const gestureDisabled = useSharedValue(false);
|
||||||
|
+ const touchStartX = useSharedValue(0);
|
||||||
|
+ const touchStartY = useSharedValue(0);
|
||||||
|
+ const coordinatesDragWithExternalGesture = Boolean(props.simultaneousHandlers);
|
||||||
|
|
||||||
|
- const panGesture = Gesture.Pan()
|
||||||
|
+ let panGesture = Gesture.Pan()
|
||||||
|
.onBegin((evt) => {
|
||||||
|
gestureDisabled.value = disabled.value;
|
||||||
|
if (gestureDisabled.value) return;
|
||||||
|
+ const sessionId = gestureSessionId.value + 1;
|
||||||
|
+ gestureSessionId.value = sessionId;
|
||||||
|
+ gestureSessionActive.value = true;
|
||||||
|
+ terminalHandled.value = false;
|
||||||
|
+ runOnJS(onGestureBegin)(sessionId);
|
||||||
|
panGestureState.value = evt.state;
|
||||||
|
})
|
||||||
|
.onUpdate((evt) => {
|
||||||
|
@@ -286,8 +372,15 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||||
|
: evt.translationY;
|
||||||
|
touchTranslate.value = translation;
|
||||||
|
})
|
||||||
|
- .onEnd((evt) => {
|
||||||
|
+ .onEnd((evt, success) => {
|
||||||
|
if (gestureDisabled.value) return;
|
||||||
|
+ if (
|
||||||
|
+ !success ||
|
||||||
|
+ terminalHandled.value ||
|
||||||
|
+ !gestureSessionActive.value
|
||||||
|
+ )
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
// Set touch val to current translate val
|
||||||
|
isTouchActiveNative.value = false;
|
||||||
|
const translation = horizontalAnim.value
|
||||||
|
@@ -299,6 +392,7 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||||
|
|
||||||
|
// Only call onDragEnd if actually dragging a cell
|
||||||
|
if (activeIndexAnim.value === -1 || disabled.value) return;
|
||||||
|
+ terminalHandled.value = true;
|
||||||
|
disabled.value = true;
|
||||||
|
runOnJS(onRelease)(activeIndexAnim.value);
|
||||||
|
const springTo = placeholderOffset.value - activeCellOffset.value;
|
||||||
|
@@ -309,14 +403,58 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||||
|
runOnJS(onDragEnd)({
|
||||||
|
from: activeIndexAnim.value,
|
||||||
|
to: spacerIndexAnim.value,
|
||||||
|
+ sessionId: gestureSessionId.value,
|
||||||
|
});
|
||||||
|
disabled.value = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
- .onTouchesDown(() => {
|
||||||
|
+ .onFinalize((evt) => {
|
||||||
|
+ panGestureState.value = evt.state;
|
||||||
|
+ runOnJS(onContainerTouchEnd)();
|
||||||
|
+ if (gestureDisabled.value) return;
|
||||||
|
+
|
||||||
|
+ const sessionId = gestureSessionId.value;
|
||||||
|
+ const committed = terminalHandled.value;
|
||||||
|
+ gestureSessionActive.value = false;
|
||||||
|
+ if (!committed) finalizeCancelledDrag();
|
||||||
|
+ runOnJS(onGestureFinalize)(sessionId, committed);
|
||||||
|
+ })
|
||||||
|
+ .onTouchesDown((event) => {
|
||||||
|
+ const touch = event.changedTouches[0];
|
||||||
|
+ if (touch) {
|
||||||
|
+ touchStartX.value = touch.absoluteX;
|
||||||
|
+ touchStartY.value = touch.absoluteY;
|
||||||
|
+ }
|
||||||
|
runOnJS(onContainerTouchStart)();
|
||||||
|
})
|
||||||
|
+ .onTouchesMove((event, stateManager) => {
|
||||||
|
+ if (!coordinatesDragWithExternalGesture) return;
|
||||||
|
+
|
||||||
|
+ const touch = event.changedTouches[0];
|
||||||
|
+ if (!touch || event.numberOfTouches !== 1) {
|
||||||
|
+ stateManager.fail();
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ const deltaX = touch.absoluteX - touchStartX.value;
|
||||||
|
+ const deltaY = touch.absoluteY - touchStartY.value;
|
||||||
|
+ const absDeltaX = Math.abs(deltaX);
|
||||||
|
+ const absDeltaY = Math.abs(deltaY);
|
||||||
|
+
|
||||||
|
+ if (activeIndexAnim.value < 0) {
|
||||||
|
+ if (absDeltaX > 6 || absDeltaY > 6) {
|
||||||
|
+ stateManager.fail();
|
||||||
|
+ }
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (absDeltaX > 10 && absDeltaX > absDeltaY) {
|
||||||
|
+ stateManager.fail();
|
||||||
|
+ } else if (absDeltaY >= activationDistanceProp && absDeltaY > absDeltaX) {
|
||||||
|
+ stateManager.activate();
|
||||||
|
+ }
|
||||||
|
+ })
|
||||||
|
.onTouchesUp(() => {
|
||||||
|
// Turning this into a worklet causes timing issues. We want it to run
|
||||||
|
// just after the finger lifts.
|
||||||
|
@@ -324,7 +462,9 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dragHitSlop) panGesture.hitSlop(dragHitSlop);
|
||||||
|
- if (activationDistanceProp) {
|
||||||
|
+ if (coordinatesDragWithExternalGesture) {
|
||||||
|
+ panGesture = panGesture.manualActivation(true);
|
||||||
|
+ } else if (activationDistanceProp) {
|
||||||
|
const activeOffset = [-activationDistanceProp, activationDistanceProp];
|
||||||
|
if (props.horizontal) {
|
||||||
|
panGesture.activeOffsetX(activeOffset);
|
||||||
|
@@ -332,6 +472,21 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||||
panGesture.activeOffsetY(activeOffset);
|
panGesture.activeOffsetY(activeOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,11 +365,30 @@ index 7c88afc..b630940 100644
|
|||||||
+ : [props.simultaneousHandlers])
|
+ : [props.simultaneousHandlers])
|
||||||
+ );
|
+ );
|
||||||
+ }
|
+ }
|
||||||
|
+
|
||||||
|
+ // GestureDetector only updates handler config when a retained host reappears
|
||||||
|
+ // with the same native tag. Changing the gesture count forces a native
|
||||||
|
+ // reattachment without remounting the list subtree.
|
||||||
|
+ const detectorGesture =
|
||||||
|
+ props.dragGestureHostPresented === false
|
||||||
|
+ ? Gesture.Simultaneous(panGesture, Gesture.Manual().enabled(false))
|
||||||
|
+ : panGesture;
|
||||||
|
|
||||||
const onScroll = useStableCallback((scrollOffset: number) => {
|
const onScroll = useStableCallback((scrollOffset: number) => {
|
||||||
props.onScrollOffsetChange?.(scrollOffset);
|
props.onScrollOffsetChange?.(scrollOffset);
|
||||||
|
@@ -373,8 +528,9 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||||
|
horizontal={!!props.horizontal}
|
||||||
|
layoutAnimationDisabled={layoutAnimationDisabled}
|
||||||
|
>
|
||||||
|
- <GestureDetector gesture={panGesture}>
|
||||||
|
+ <GestureDetector gesture={detectorGesture}>
|
||||||
|
<Animated.View
|
||||||
|
+ collapsable={false}
|
||||||
|
style={props.containerStyle}
|
||||||
|
ref={containerRef}
|
||||||
|
onLayout={onContainerLayout}
|
||||||
diff --git a/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx b/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
diff --git a/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx b/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
||||||
index 1559352..99d7a9b 100644
|
index 1559352..33eb5de 100644
|
||||||
--- a/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
--- a/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
||||||
+++ b/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
+++ b/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
||||||
@@ -1,5 +1,5 @@
|
@@ -1,5 +1,5 @@
|
||||||
@@ -36,3 +407,43 @@ index 1559352..99d7a9b 100644
|
|||||||
|
|
||||||
const onSuccess = (_x: number, y: number) => {
|
const onSuccess = (_x: number, y: number) => {
|
||||||
listVerticalOffset.value = y;
|
listVerticalOffset.value = y;
|
||||||
|
@@ -74,6 +74,10 @@ function NestableDraggableFlatListInner<T>(
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
+ const onDragTerminate = useStableCallback(() => {
|
||||||
|
+ setOuterScrollEnabled(true);
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
const onAnimValInit: DraggableFlatListProps<T>["onAnimValInit"] = useStableCallback(
|
||||||
|
(params) => {
|
||||||
|
setListHoverOffset(params.hoverOffset);
|
||||||
|
@@ -95,6 +99,7 @@ function NestableDraggableFlatListInner<T>(
|
||||||
|
outerScrollOffset={outerScrollOffset}
|
||||||
|
onDragBegin={onDragBegin}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
+ onDragTerminate={onDragTerminate}
|
||||||
|
onAnimValInit={onAnimValInit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
diff --git a/node_modules/react-native-draggable-flatlist/src/types.ts b/node_modules/react-native-draggable-flatlist/src/types.ts
|
||||||
|
index d6755c8..1b244a6 100644
|
||||||
|
--- a/node_modules/react-native-draggable-flatlist/src/types.ts
|
||||||
|
+++ b/node_modules/react-native-draggable-flatlist/src/types.ts
|
||||||
|
@@ -36,12 +36,16 @@ export type DraggableFlatListProps<T> = Modify<
|
||||||
|
keyExtractor: (item: T, index: number) => string;
|
||||||
|
onDragBegin?: (index: number) => void;
|
||||||
|
onDragEnd?: (params: DragEndParams<T>) => void;
|
||||||
|
+ /** @internal Used by the nestable wrapper to release its outer-scroll lock on cancellation. */
|
||||||
|
+ onDragTerminate?: () => void;
|
||||||
|
onPlaceholderIndexChange?: (placeholderIndex: number) => void;
|
||||||
|
onRelease?: (index: number) => void;
|
||||||
|
onScrollOffsetChange?: (scrollOffset: number) => void;
|
||||||
|
renderItem: RenderItem<T>;
|
||||||
|
renderPlaceholder?: RenderPlaceholder<T>;
|
||||||
|
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[];
|
||||||
|
+ /** @internal Forces handler reattachment when a retained native host is presented. */
|
||||||
|
+ dragGestureHostPresented?: boolean;
|
||||||
|
outerScrollOffset?: Animated.SharedValue<number>;
|
||||||
|
onAnimValInit?: (animVals: ReturnType<typeof useAnimatedValues>) => void;
|
||||||
|
itemEnteringAnimation?: AnimateProps<Animated.View>["entering"];
|
||||||
|
|||||||
@@ -1,5 +1,171 @@
|
|||||||
|
diff --git a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
|
||||||
|
index a532cf6..7b66222 100644
|
||||||
|
--- a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
|
||||||
|
+++ b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
|
||||||
|
@@ -2,6 +2,7 @@ package com.swmansion.gesturehandler.core
|
||||||
|
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.graphics.PointF
|
||||||
|
+import android.util.SparseArray
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
@@ -24,6 +25,10 @@ class GestureHandlerOrchestrator(
|
||||||
|
private val awaitingHandlers = arrayListOf<GestureHandler>()
|
||||||
|
private val preparedHandlers = arrayListOf<GestureHandler>()
|
||||||
|
|
||||||
|
+ // Used by `cancelTouchesInInterceptedViews`.
|
||||||
|
+ private val viewsToCancel = arrayListOf<View>()
|
||||||
|
+ private val pointerDownPoints = SparseArray<PointF>()
|
||||||
|
+
|
||||||
|
// In `onHandlerStateChange` method we iterate through `awaitingHandlers`, but calling `tryActivate` may modify this list.
|
||||||
|
// To avoid `ConcurrentModificationException` we iterate through copy. There is one more problem though - if handler was
|
||||||
|
// removed from `awaitingHandlers`, it was still present in copy of original list. This hashset helps us identify which handlers
|
||||||
|
@@ -42,6 +47,7 @@ class GestureHandlerOrchestrator(
|
||||||
|
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
isHandlingTouch = true
|
||||||
|
val action = event.actionMasked
|
||||||
|
+ trackPointerDownPoints(event)
|
||||||
|
if (action == MotionEvent.ACTION_DOWN ||
|
||||||
|
action == MotionEvent.ACTION_POINTER_DOWN ||
|
||||||
|
action == MotionEvent.ACTION_HOVER_MOVE
|
||||||
|
@@ -562,6 +568,107 @@ class GestureHandlerOrchestrator(
|
||||||
|
extractGestureHandlers(wrapperView, tempCoords, pointerId, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
+ private fun trackPointerDownPoints(event: MotionEvent) {
|
||||||
|
+ val index = event.actionIndex
|
||||||
|
+ when (event.actionMasked) {
|
||||||
|
+ MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN ->
|
||||||
|
+ pointerDownPoints.put(event.getPointerId(index), PointF(event.getX(index), event.getY(index)))
|
||||||
|
+ MotionEvent.ACTION_POINTER_UP ->
|
||||||
|
+ pointerDownPoints.remove(event.getPointerId(index))
|
||||||
|
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL ->
|
||||||
|
+ pointerDownPoints.clear()
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fun cancelTouchesInInterceptedViews(event: MotionEvent) {
|
||||||
|
+ viewsToCancel.clear()
|
||||||
|
+ for (i in 0 until pointerDownPoints.size()) {
|
||||||
|
+ val point = pointerDownPoints.valueAt(i)
|
||||||
|
+ tempCoords[0] = point.x
|
||||||
|
+ tempCoords[1] = point.y
|
||||||
|
+ collectViewsAtPoint(wrapperView, tempCoords, viewsToCancel)
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (viewsToCancel.isEmpty()) {
|
||||||
|
+ return
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ val activeHandlers = gestureHandlers.filter { it.isActive }
|
||||||
|
+ val cancelEvent = MotionEvent.obtain(event).apply { action = MotionEvent.ACTION_CANCEL }
|
||||||
|
+
|
||||||
|
+ for (view in viewsToCancel) {
|
||||||
|
+ if (view === wrapperView || isViewDrivenByActiveNativeGesture(view, activeHandlers)) {
|
||||||
|
+ continue
|
||||||
|
+ }
|
||||||
|
+ view.onTouchEvent(cancelEvent)
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ cancelEvent.recycle()
|
||||||
|
+ viewsToCancel.clear()
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ // Whether the view's touch is still owned by a NativeViewGestureHandler that survived arbitration.
|
||||||
|
+ // Only those are fed through `onTouchEvent`, so only those break if cancelled. Other handlers are
|
||||||
|
+ // orchestrator-driven and unaffected.
|
||||||
|
+ private fun isViewDrivenByActiveNativeGesture(view: View, activeHandlers: List<GestureHandler>) =
|
||||||
|
+ handlerRegistry.getHandlersForView(view)?.let { handlers ->
|
||||||
|
+ synchronized(handlers) {
|
||||||
|
+ handlers.any { nativeGestureSurvivesArbitration(it, activeHandlers) }
|
||||||
|
+ }
|
||||||
|
+ } ?: false
|
||||||
|
+
|
||||||
|
+ // A native handler survives arbitration if it is active, or it does not conflict with any active handler.
|
||||||
|
+ private fun nativeGestureSurvivesArbitration(handler: GestureHandler, activeHandlers: List<GestureHandler>) =
|
||||||
|
+ handler is NativeViewGestureHandler &&
|
||||||
|
+ (handler.isActive || activeHandlers.none { shouldHandlerBeCancelledBy(handler, it) })
|
||||||
|
+
|
||||||
|
+ // Collects the view path under the point (topmost child first, like touch dispatch), leaf to root.
|
||||||
|
+ private fun collectViewsAtPoint(view: View, coords: FloatArray, out: MutableList<View>): Boolean {
|
||||||
|
+ val pointerEvents = viewConfigHelper.getPointerEventsConfigForView(view)
|
||||||
|
+ if (pointerEvents == PointerEventsConfig.NONE) {
|
||||||
|
+ return false
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ var found = false
|
||||||
|
+ if (view is ViewGroup && pointerEvents != PointerEventsConfig.BOX_ONLY) {
|
||||||
|
+ for (i in view.childCount - 1 downTo 0) {
|
||||||
|
+ val child = viewConfigHelper.getChildInDrawingOrderAtIndex(view, i)
|
||||||
|
+ if (!canReceiveEvents(child)) {
|
||||||
|
+ continue
|
||||||
|
+ }
|
||||||
|
+ val childPoint = tempPoint
|
||||||
|
+ transformPointToChildViewCoords(coords[0], coords[1], view, child, childPoint)
|
||||||
|
+ if (isClipping(child) && !isTransformedTouchPointInView(childPoint.x, childPoint.y, child)) {
|
||||||
|
+ continue
|
||||||
|
+ }
|
||||||
|
+ val restoreX = coords[0]
|
||||||
|
+ val restoreY = coords[1]
|
||||||
|
+ coords[0] = childPoint.x
|
||||||
|
+ coords[1] = childPoint.y
|
||||||
|
+ found = collectViewsAtPoint(child, coords, out)
|
||||||
|
+ coords[0] = restoreX
|
||||||
|
+ coords[1] = restoreY
|
||||||
|
+
|
||||||
|
+ if (found) {
|
||||||
|
+ break
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ // BOX_NONE views can't be the target themselves, only their children can.
|
||||||
|
+ val selfIsTarget = pointerEvents != PointerEventsConfig.BOX_NONE &&
|
||||||
|
+ isTransformedTouchPointInView(coords[0], coords[1], view)
|
||||||
|
+
|
||||||
|
+ if (found || selfIsTarget) {
|
||||||
|
+ // `out` may already contain this view when several pointers share part of their path.
|
||||||
|
+ if (!out.contains(view)) {
|
||||||
|
+ out.add(view)
|
||||||
|
+ }
|
||||||
|
+ return true
|
||||||
|
+ }
|
||||||
|
+ return false
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
private fun extractGestureHandlers(
|
||||||
|
viewGroup: ViewGroup,
|
||||||
|
coords: FloatArray,
|
||||||
|
diff --git a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
|
||||||
|
index d8f8fd6..ed101b9 100644
|
||||||
|
--- a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
|
||||||
|
+++ b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
|
||||||
|
@@ -19,6 +19,7 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
|
||||||
|
private val jsGestureHandler: GestureHandler?
|
||||||
|
val rootView: ViewGroup
|
||||||
|
private var shouldIntercept = false
|
||||||
|
+ private var wasIntercepting = false
|
||||||
|
private var passingTouch = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
@@ -117,6 +118,14 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
|
||||||
|
passingTouch = true
|
||||||
|
orchestrator!!.onTouchEvent(ev)
|
||||||
|
passingTouch = false
|
||||||
|
+
|
||||||
|
+ // On the transition into interception, cancel the native views the pointers landed on - the
|
||||||
|
+ // framework's ACTION_CANCEL never reaches them since RNGH ignores `onInterceptTouchEvent`.
|
||||||
|
+ if (shouldIntercept && !wasIntercepting) {
|
||||||
|
+ orchestrator!!.cancelTouchesInInterceptedViews(ev)
|
||||||
|
+ }
|
||||||
|
+ wasIntercepting = shouldIntercept
|
||||||
|
+
|
||||||
|
return shouldIntercept
|
||||||
|
}
|
||||||
|
|
||||||
diff --git a/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js b/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js
|
diff --git a/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js b/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js
|
||||||
index 76eb808..0debe83 100644
|
index 76eb808..7c40526 100644
|
||||||
--- a/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js
|
--- a/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js
|
||||||
+++ b/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js
|
+++ b/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js
|
||||||
@@ -58,7 +58,14 @@ class PointerEventManager extends _EventManager.default {
|
@@ -58,7 +58,14 @@ class PointerEventManager extends _EventManager.default {
|
||||||
@@ -19,7 +185,7 @@ index 76eb808..0debe83 100644
|
|||||||
this.markAsOutOfBounds(adaptedEvent.pointerId);
|
this.markAsOutOfBounds(adaptedEvent.pointerId);
|
||||||
this.trackedPointers.delete(adaptedEvent.pointerId);
|
this.trackedPointers.delete(adaptedEvent.pointerId);
|
||||||
diff --git a/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js b/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js
|
diff --git a/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js b/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js
|
||||||
index 6ce3b8f..3976f3f 100644
|
index 6ce3b8f..8fe28bc 100644
|
||||||
--- a/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js
|
--- a/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js
|
||||||
+++ b/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js
|
+++ b/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js
|
||||||
@@ -53,7 +53,14 @@ export default class PointerEventManager extends EventManager {
|
@@ -53,7 +53,14 @@ export default class PointerEventManager extends EventManager {
|
||||||
|
|||||||
Reference in New Issue
Block a user