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
|
||||
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
|
||||
|
||||
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,
|
||||
extraData,
|
||||
simultaneousGestureRef,
|
||||
gestureHostPresented,
|
||||
waitFor,
|
||||
onDragBegin: onDragBeginProp,
|
||||
nestable = false,
|
||||
@@ -118,6 +119,7 @@ export function DraggableList<T>({
|
||||
scrollEnabled={scrollEnabled}
|
||||
extraData={extraData}
|
||||
simultaneousHandlers={simultaneousHandlers}
|
||||
dragGestureHostPresented={gestureHostPresented}
|
||||
// Higher activation distance reduces accidental drag capture while nested
|
||||
// lists are inside a scroll container.
|
||||
activationDistance={20}
|
||||
|
||||
@@ -49,6 +49,8 @@ export interface DraggableListProps<T> {
|
||||
extraData?: unknown;
|
||||
/** Gesture ref for simultaneous handling with parent gestures (e.g., sidebar close) */
|
||||
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 */
|
||||
waitFor?: MutableRefObject<GestureType | undefined> | MutableRefObject<GestureType | undefined>[];
|
||||
/** 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 { useCloseAgentListGesture } from "@/mobile-panels/gestures";
|
||||
import { MobilePanelOverlay } from "@/mobile-panels/presentation";
|
||||
import { useIsMobilePanelPresented } from "@/mobile-panels/provider";
|
||||
import {
|
||||
buildOpenProjectRoute,
|
||||
buildNewWorkspaceRoute,
|
||||
@@ -619,6 +620,7 @@ function MobileSidebar({
|
||||
const isSessionsActive = pathname.includes("/sessions");
|
||||
const isSchedulesActive = pathname.includes("/schedules");
|
||||
const { gesture: closeGesture, gestureRef: closeGestureRef } = useCloseAgentListGesture();
|
||||
const dragGestureHostPresented = useIsMobilePanelPresented("agent-list");
|
||||
|
||||
const handleViewMore = useCallback(() => {
|
||||
closeSidebar();
|
||||
@@ -714,6 +716,7 @@ function MobileSidebar({
|
||||
onWorkspacePress={handleWorkspacePress}
|
||||
onAddProject={handleOpenProject}
|
||||
parentGestureRef={closeGestureRef}
|
||||
dragGestureHostPresented={dragGestureHostPresented}
|
||||
listHeaderComponent={workspacesSectionHeaderElement}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -249,6 +249,7 @@ interface SidebarWorkspaceListProps {
|
||||
listHeaderComponent?: ReactElement | null;
|
||||
/** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */
|
||||
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||
dragGestureHostPresented?: boolean;
|
||||
}
|
||||
|
||||
interface ProjectHeaderRowProps {
|
||||
@@ -1605,6 +1606,7 @@ function ProjectBlock({
|
||||
isDragging,
|
||||
dragHandleProps,
|
||||
useNestable,
|
||||
dragGestureHostPresented,
|
||||
creatingWorkspaceIds,
|
||||
activeWorkspaceSelection,
|
||||
hostLabelByServerId,
|
||||
@@ -1630,6 +1632,7 @@ function ProjectBlock({
|
||||
isDragging: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
useNestable: boolean;
|
||||
dragGestureHostPresented?: boolean;
|
||||
creatingWorkspaceIds: ReadonlySet<string>;
|
||||
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
||||
hostLabelByServerId: ReadonlyMap<string, string>;
|
||||
@@ -1806,6 +1809,7 @@ function ProjectBlock({
|
||||
useDragHandle
|
||||
nestable={useNestable}
|
||||
simultaneousGestureRef={parentGestureRef}
|
||||
gestureHostPresented={dragGestureHostPresented}
|
||||
containerStyle={styles.workspaceListContainer}
|
||||
/>
|
||||
{canToggleWorkspaces ? (
|
||||
@@ -1886,6 +1890,7 @@ function areProjectBlockPropsEqual(previous: ProjectBlockProps, next: ProjectBlo
|
||||
previous.isDragging === next.isDragging &&
|
||||
previous.dragHandleProps === next.dragHandleProps &&
|
||||
previous.useNestable === next.useNestable &&
|
||||
previous.dragGestureHostPresented === next.dragGestureHostPresented &&
|
||||
previous.creatingWorkspaceIds === next.creatingWorkspaceIds &&
|
||||
areProjectBlockSelectionsEqual(previous, next)
|
||||
);
|
||||
@@ -1936,6 +1941,7 @@ export function SidebarWorkspaceList({
|
||||
listFooterComponent,
|
||||
listHeaderComponent,
|
||||
parentGestureRef,
|
||||
dragGestureHostPresented,
|
||||
}: SidebarWorkspaceListProps) {
|
||||
const pathname = usePathname();
|
||||
const hosts = useHosts();
|
||||
@@ -1991,6 +1997,7 @@ export function SidebarWorkspaceList({
|
||||
listFooterComponent={listFooterComponent}
|
||||
listHeaderComponent={listHeaderComponent}
|
||||
parentGestureRef={parentGestureRef}
|
||||
dragGestureHostPresented={dragGestureHostPresented}
|
||||
pathname={pathname}
|
||||
hostLabelByServerId={hostLabelByServerId}
|
||||
showHostLabels={showHostLabels}
|
||||
@@ -2065,6 +2072,7 @@ function ProjectModeList({
|
||||
listFooterComponent,
|
||||
listHeaderComponent,
|
||||
parentGestureRef,
|
||||
dragGestureHostPresented,
|
||||
pathname,
|
||||
hostLabelByServerId,
|
||||
showHostLabels,
|
||||
@@ -2279,6 +2287,7 @@ function ProjectModeList({
|
||||
isDragging={dragState.isDragging}
|
||||
dragHandleProps={dragState.dragHandleProps}
|
||||
useNestable={platformIsNative}
|
||||
dragGestureHostPresented={dragGestureHostPresented}
|
||||
creatingWorkspaceIds={creatingWorkspaceIds}
|
||||
activeWorkspaceSelection={activeWorkspaceSelection}
|
||||
hostLabelByServerId={hostLabelByServerId}
|
||||
@@ -2302,6 +2311,7 @@ function ProjectModeList({
|
||||
onWorkspacePress,
|
||||
onToggleProjectCollapsed,
|
||||
parentGestureRef,
|
||||
dragGestureHostPresented,
|
||||
projectIconByProjectViewKey,
|
||||
selectionEnabled,
|
||||
shortcutIndexByWorkspaceKey,
|
||||
@@ -2399,6 +2409,7 @@ function ProjectModeList({
|
||||
useDragHandle
|
||||
nestable={platformIsNative}
|
||||
simultaneousGestureRef={parentGestureRef}
|
||||
gestureHostPresented={dragGestureHostPresented}
|
||||
containerStyle={styles.projectListContainer}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -69,24 +69,33 @@ export function MobilePanelOverlay({
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={overlayStyle} pointerEvents={overlayPointerEvents}>
|
||||
<Pressable
|
||||
accessibilityElementsHidden
|
||||
importantForAccessibility="no-hide-descendants"
|
||||
onPress={showMobileAgent}
|
||||
pointerEvents={isOpen ? "auto" : "none"}
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
testID={`${panel}-backdrop`}
|
||||
<GestureDetector gesture={closeGesture} touchAction="pan-y">
|
||||
{/* Fabric needs an always-mounted native host to attach the close handler while the
|
||||
retained panel content is hidden. nativeID keeps that host registered. */}
|
||||
<View
|
||||
collapsable={false}
|
||||
nativeID={`${panel}-gesture-host`}
|
||||
pointerEvents={overlayPointerEvents}
|
||||
style={styles.overlay}
|
||||
>
|
||||
<Animated.View pointerEvents="none" style={backdropStyle} />
|
||||
</Pressable>
|
||||
<View style={overlayStyle} pointerEvents={overlayPointerEvents}>
|
||||
<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}>
|
||||
<WindowChromeRootRegion corners="both">{children}</WindowChromeRootRegion>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
<Animated.View pointerEvents={isOpen ? "auto" : "none"} style={combinedPanelStyle}>
|
||||
<WindowChromeRootRegion corners="both">{children}</WindowChromeRootRegion>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</View>
|
||||
</GestureDetector>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user