fix(app): make sidebar reordering respond immediately
Mouse input inherited the touch hold delay. Split activation by input so mouse dragging begins after deliberate movement while touch retains long-press arbitration.
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
import type { Locator } from "@playwright/test";
|
||||||
|
import { expect, test } from "./fixtures";
|
||||||
|
import { gotoAppShell } from "./helpers/app";
|
||||||
|
import { getServerId } from "./helpers/server-id";
|
||||||
|
import { seedWorkspace } from "./helpers/seed-client";
|
||||||
|
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
||||||
|
|
||||||
|
async function rowTestIds(rows: Locator) {
|
||||||
|
return rows.evaluateAll((elements) =>
|
||||||
|
elements.map((element) => element.getAttribute("data-testid")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function visibleBoundingBox(row: Locator) {
|
||||||
|
const box = await row.boundingBox();
|
||||||
|
if (!box) throw new Error("Expected a visible draggable row");
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quickDragFirstRowAfterSecond(rows: Locator) {
|
||||||
|
await expect(rows).toHaveCount(2);
|
||||||
|
const before = await rowTestIds(rows);
|
||||||
|
const sourceBox = await visibleBoundingBox(rows.nth(0));
|
||||||
|
const targetBox = await visibleBoundingBox(rows.nth(1));
|
||||||
|
|
||||||
|
const page = rows.page();
|
||||||
|
const source = { x: sourceBox.x + sourceBox.width / 2, y: sourceBox.y + sourceBox.height / 2 };
|
||||||
|
const target = { x: targetBox.x + targetBox.width / 2, y: targetBox.y + targetBox.height / 2 };
|
||||||
|
|
||||||
|
await page.mouse.move(source.x, source.y);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(source.x, source.y + 7);
|
||||||
|
await page.mouse.move(target.x, target.y, { steps: 4 });
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
await expect.poll(() => rowTestIds(rows)).toEqual([before[1], before[0]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("projects and workspaces reorder with an immediate mouse drag", async ({ page }) => {
|
||||||
|
const firstProject = await seedWorkspace({ repoPrefix: "sidebar-reorder-first-" });
|
||||||
|
const secondProject = await seedWorkspace({ repoPrefix: "sidebar-reorder-second-" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const secondWorkspace = await firstProject.client.createWorkspace({
|
||||||
|
source: {
|
||||||
|
kind: "directory",
|
||||||
|
path: firstProject.repoPath,
|
||||||
|
projectId: firstProject.projectId,
|
||||||
|
},
|
||||||
|
title: "Second workspace",
|
||||||
|
});
|
||||||
|
if (!secondWorkspace.workspace) {
|
||||||
|
throw new Error(secondWorkspace.error ?? "Failed to seed a second workspace");
|
||||||
|
}
|
||||||
|
|
||||||
|
await gotoAppShell(page);
|
||||||
|
await waitForSidebarHydration(page);
|
||||||
|
|
||||||
|
await quickDragFirstRowAfterSecond(page.locator('[data-testid^="sidebar-project-row-"]'));
|
||||||
|
const firstWorkspaceTestId = `sidebar-workspace-row-${getServerId()}:${firstProject.workspaceId}`;
|
||||||
|
const secondWorkspaceTestId = `sidebar-workspace-row-${getServerId()}:${secondWorkspace.workspace.id}`;
|
||||||
|
await quickDragFirstRowAfterSecond(
|
||||||
|
page.locator(
|
||||||
|
`[data-testid="${firstWorkspaceTestId}"], [data-testid="${secondWorkspaceTestId}"]`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await firstProject.cleanup();
|
||||||
|
await secondProject.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
export { reorderItemsOnDragEnd } from "./reorder-items";
|
export { reorderItemsOnDragEnd } from "./reorder-items";
|
||||||
export type { DragEndInput } from "./reorder-items";
|
export type { DragEndInput } from "./reorder-items";
|
||||||
export {
|
export {
|
||||||
getPointerActivationConstraint,
|
getDragActivationConstraints,
|
||||||
type PointerActivationConfig,
|
type DragActivationConfig,
|
||||||
|
type DragActivationConstraints,
|
||||||
type PointerActivationConstraint,
|
type PointerActivationConstraint,
|
||||||
} from "./pointer-activation";
|
} from "./pointer-activation";
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { getPointerActivationConstraint } from "./pointer-activation";
|
import { getDragActivationConstraints } from "./pointer-activation";
|
||||||
|
|
||||||
const config = { defaultDistance: 6, holdDelayMs: 250, holdTolerance: 8 };
|
const config = { movementDistance: 6, touchHoldDelayMs: 180, touchHoldTolerance: 8 };
|
||||||
|
|
||||||
describe("getPointerActivationConstraint", () => {
|
describe("getDragActivationConstraints", () => {
|
||||||
it("uses distance activation for default draggable rows", () => {
|
it("starts mouse drags after deliberate pointer movement", () => {
|
||||||
expect(getPointerActivationConstraint(false, config)).toEqual({ distance: 6 });
|
expect(getDragActivationConstraints(true, config).mouse).toEqual({ distance: 6 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("requires a held pointer before activating handle-based drags", () => {
|
it("requires a short hold before starting touch drags", () => {
|
||||||
expect(getPointerActivationConstraint(true, config)).toEqual({ delay: 250, tolerance: 8 });
|
expect(getDragActivationConstraints(true, config).touch).toEqual({
|
||||||
|
delay: 180,
|
||||||
|
tolerance: 8,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts ordinary touch rows after deliberate movement", () => {
|
||||||
|
expect(getDragActivationConstraints(false, config).touch).toEqual({ distance: 6 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,18 +2,25 @@ export type PointerActivationConstraint =
|
|||||||
| { distance: number }
|
| { distance: number }
|
||||||
| { delay: number; tolerance: number };
|
| { delay: number; tolerance: number };
|
||||||
|
|
||||||
export interface PointerActivationConfig {
|
export interface DragActivationConfig {
|
||||||
defaultDistance: number;
|
movementDistance: number;
|
||||||
holdDelayMs: number;
|
touchHoldDelayMs: number;
|
||||||
holdTolerance: number;
|
touchHoldTolerance: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPointerActivationConstraint(
|
export interface DragActivationConstraints {
|
||||||
|
mouse: PointerActivationConstraint;
|
||||||
|
touch: PointerActivationConstraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDragActivationConstraints(
|
||||||
useDragHandle: boolean,
|
useDragHandle: boolean,
|
||||||
config: PointerActivationConfig,
|
config: DragActivationConfig,
|
||||||
): PointerActivationConstraint {
|
): DragActivationConstraints {
|
||||||
if (useDragHandle) {
|
const movement = { distance: config.movementDistance };
|
||||||
return { delay: config.holdDelayMs, tolerance: config.holdTolerance };
|
const touch = useDragHandle
|
||||||
}
|
? { delay: config.touchHoldDelayMs, tolerance: config.touchHoldTolerance }
|
||||||
return { distance: config.defaultDistance };
|
: movement;
|
||||||
|
|
||||||
|
return { mouse: movement, touch };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import {
|
|||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
KeyboardSensor,
|
KeyboardSensor,
|
||||||
PointerSensor,
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
type Modifier,
|
type Modifier,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
@@ -17,7 +18,7 @@ import {
|
|||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import type { DraggableListProps, DraggableRenderItemInfo } from "./draggable-list.types";
|
import type { DraggableListProps, DraggableRenderItemInfo } from "./draggable-list.types";
|
||||||
import { getPointerActivationConstraint, useDragReorderState } from "./drag-reorder";
|
import { getDragActivationConstraints, useDragReorderState } from "./drag-reorder";
|
||||||
|
|
||||||
export type { DraggableListProps, DraggableRenderItemInfo };
|
export type { DraggableListProps, DraggableRenderItemInfo };
|
||||||
|
|
||||||
@@ -27,10 +28,10 @@ const restrictToVerticalAxis: Modifier = ({ transform }) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const DND_MODIFIERS = [restrictToVerticalAxis];
|
const DND_MODIFIERS = [restrictToVerticalAxis];
|
||||||
const POINTER_ACTIVATION_CONFIG = {
|
const DRAG_ACTIVATION_CONFIG = {
|
||||||
defaultDistance: 6,
|
movementDistance: 6,
|
||||||
holdDelayMs: 250,
|
touchHoldDelayMs: 180,
|
||||||
holdTolerance: 8,
|
touchHoldTolerance: 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SortableItemProps<T> {
|
interface SortableItemProps<T> {
|
||||||
@@ -145,14 +146,14 @@ export function DraggableList<T>({
|
|||||||
onDragEnd,
|
onDragEnd,
|
||||||
onDragBegin,
|
onDragBegin,
|
||||||
});
|
});
|
||||||
const pointerActivationConstraint = getPointerActivationConstraint(
|
const activationConstraints = getDragActivationConstraints(useDragHandle, DRAG_ACTIVATION_CONFIG);
|
||||||
useDragHandle,
|
|
||||||
POINTER_ACTIVATION_CONFIG,
|
|
||||||
);
|
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(MouseSensor, {
|
||||||
activationConstraint: pointerActivationConstraint,
|
activationConstraint: activationConstraints.mouse,
|
||||||
|
}),
|
||||||
|
useSensor(TouchSensor, {
|
||||||
|
activationConstraint: activationConstraints.touch,
|
||||||
}),
|
}),
|
||||||
useSensor(KeyboardSensor, {
|
useSensor(KeyboardSensor, {
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
|||||||
@@ -2,15 +2,12 @@ import {
|
|||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
Pressable,
|
Pressable,
|
||||||
Platform,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
StatusBar,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
type GestureResponderEvent,
|
type GestureResponderEvent,
|
||||||
type PressableStateCallbackType,
|
type PressableStateCallbackType,
|
||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import * as Haptics from "expo-haptics";
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { ProjectIconView } from "@/components/project-icon-view";
|
import { ProjectIconView } from "@/components/project-icon-view";
|
||||||
import { AdaptiveRenameModal } from "@/components/rename-modal";
|
import { AdaptiveRenameModal } from "@/components/rename-modal";
|
||||||
@@ -87,7 +84,6 @@ import { useToast } from "@/contexts/toast-context";
|
|||||||
import { getForgePresentation, normalizeForge } from "@/git/forge";
|
import { getForgePresentation, normalizeForge } from "@/git/forge";
|
||||||
import { toWorktreeArchiveRisk } from "@/git/worktree-archive-warning";
|
import { toWorktreeArchiveRisk } from "@/git/worktree-archive-warning";
|
||||||
import { hasVisibleOrderChanged, mergeWithRemainder } from "@/utils/sidebar-reorder";
|
import { hasVisibleOrderChanged, mergeWithRemainder } from "@/utils/sidebar-reorder";
|
||||||
import { decideLongPressMove } from "@/utils/sidebar-gesture-arbitration";
|
|
||||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||||
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
|
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
|
||||||
import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
|
import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
|
||||||
@@ -96,6 +92,7 @@ import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
|||||||
import { SidebarStatusWorkspaceList } from "@/components/sidebar/sidebar-status-list";
|
import { SidebarStatusWorkspaceList } from "@/components/sidebar/sidebar-status-list";
|
||||||
import type { StatusGroup } from "@/hooks/sidebar-status-view-model";
|
import type { StatusGroup } from "@/hooks/sidebar-status-view-model";
|
||||||
import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu";
|
import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu";
|
||||||
|
import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction";
|
||||||
import { PinnedSectionHeader } from "@/components/sidebar/pinned-section-header";
|
import { PinnedSectionHeader } from "@/components/sidebar/pinned-section-header";
|
||||||
import {
|
import {
|
||||||
SidebarWorkspaceRowFrame,
|
SidebarWorkspaceRowFrame,
|
||||||
@@ -948,223 +945,6 @@ function NewWorkspaceGhostRow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useLongPressDragInteraction(input: {
|
|
||||||
drag: () => void;
|
|
||||||
menuController: ReturnType<typeof useContextMenu> | null;
|
|
||||||
}) {
|
|
||||||
const didLongPressRef = useRef(false);
|
|
||||||
const dragArmedRef = useRef(false);
|
|
||||||
const dragActivatedRef = useRef(false);
|
|
||||||
const didStartDragRef = useRef(false);
|
|
||||||
const scrollIntentRef = useRef(false);
|
|
||||||
const menuOpenedRef = useRef(false);
|
|
||||||
const touchStartRef = useRef<{ x: number; y: number } | null>(null);
|
|
||||||
const touchCurrentRef = useRef<{ x: number; y: number } | null>(null);
|
|
||||||
const dragArmTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
const contextMenuTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
|
|
||||||
const clearTimers = useCallback(() => {
|
|
||||||
if (dragArmTimerRef.current) {
|
|
||||||
clearTimeout(dragArmTimerRef.current);
|
|
||||||
dragArmTimerRef.current = null;
|
|
||||||
}
|
|
||||||
if (contextMenuTimerRef.current) {
|
|
||||||
clearTimeout(contextMenuTimerRef.current);
|
|
||||||
contextMenuTimerRef.current = null;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const openContextMenuAtStartPoint = useCallback(() => {
|
|
||||||
if (!input.menuController || !touchStartRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
|
|
||||||
input.menuController.setAnchorRect({
|
|
||||||
x: touchStartRef.current.x,
|
|
||||||
y: touchStartRef.current.y + statusBarHeight,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
});
|
|
||||||
input.menuController.setOpen(true);
|
|
||||||
menuOpenedRef.current = true;
|
|
||||||
didLongPressRef.current = true;
|
|
||||||
}, [input.menuController]);
|
|
||||||
|
|
||||||
const handleLongPress = useCallback(() => {
|
|
||||||
// Manual timers own long-press behavior on mobile.
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
clearTimers();
|
|
||||||
};
|
|
||||||
}, [clearTimers]);
|
|
||||||
|
|
||||||
const armTimers = useCallback(() => {
|
|
||||||
clearTimers();
|
|
||||||
|
|
||||||
const DRAG_ARM_DELAY_MS = 180;
|
|
||||||
const DRAG_ARM_STATIONARY_SLOP_PX = 4;
|
|
||||||
const CONTEXT_MENU_DELAY_MS = 450;
|
|
||||||
const CONTEXT_MENU_STATIONARY_SLOP_PX = 6;
|
|
||||||
|
|
||||||
dragArmTimerRef.current = setTimeout(() => {
|
|
||||||
if (scrollIntentRef.current || didStartDragRef.current || menuOpenedRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const start = touchStartRef.current;
|
|
||||||
const current = touchCurrentRef.current ?? start;
|
|
||||||
if (!start || !current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dx = current.x - start.x;
|
|
||||||
const dy = current.y - start.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
if (distance > DRAG_ARM_STATIONARY_SLOP_PX) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dragArmedRef.current = true;
|
|
||||||
dragActivatedRef.current = true;
|
|
||||||
didLongPressRef.current = true;
|
|
||||||
void Haptics.selectionAsync().catch(() => {});
|
|
||||||
input.drag();
|
|
||||||
}, DRAG_ARM_DELAY_MS);
|
|
||||||
|
|
||||||
if (!input.menuController || platformIsWeb) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
contextMenuTimerRef.current = setTimeout(() => {
|
|
||||||
if (scrollIntentRef.current || didStartDragRef.current || menuOpenedRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const start = touchStartRef.current;
|
|
||||||
const current = touchCurrentRef.current ?? start;
|
|
||||||
if (!start || !current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dx = current.x - start.x;
|
|
||||||
const dy = current.y - start.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
if (distance > CONTEXT_MENU_STATIONARY_SLOP_PX) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void Haptics.selectionAsync().catch(() => {});
|
|
||||||
openContextMenuAtStartPoint();
|
|
||||||
}, CONTEXT_MENU_DELAY_MS);
|
|
||||||
}, [clearTimers, input, openContextMenuAtStartPoint]);
|
|
||||||
|
|
||||||
const handleDragIntent = useCallback(
|
|
||||||
(_details: { dx: number; dy: number; distance: number }) => {
|
|
||||||
if (!dragActivatedRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
didStartDragRef.current = true;
|
|
||||||
didLongPressRef.current = true;
|
|
||||||
clearTimers();
|
|
||||||
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
|
|
||||||
},
|
|
||||||
[clearTimers],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleScrollIntent = useCallback(
|
|
||||||
(_details: { dx: number; dy: number; distance: number }) => {
|
|
||||||
scrollIntentRef.current = true;
|
|
||||||
didLongPressRef.current = true;
|
|
||||||
clearTimers();
|
|
||||||
},
|
|
||||||
[clearTimers],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSwipeIntent = useCallback(
|
|
||||||
(_details: { dx: number; dy: number; distance: number }) => {
|
|
||||||
didLongPressRef.current = true;
|
|
||||||
clearTimers();
|
|
||||||
},
|
|
||||||
[clearTimers],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePressIn = useCallback(
|
|
||||||
(event: GestureResponderEvent) => {
|
|
||||||
didLongPressRef.current = false;
|
|
||||||
dragArmedRef.current = false;
|
|
||||||
dragActivatedRef.current = false;
|
|
||||||
didStartDragRef.current = false;
|
|
||||||
scrollIntentRef.current = false;
|
|
||||||
menuOpenedRef.current = false;
|
|
||||||
touchStartRef.current = {
|
|
||||||
x: event.nativeEvent.pageX,
|
|
||||||
y: event.nativeEvent.pageY,
|
|
||||||
};
|
|
||||||
touchCurrentRef.current = {
|
|
||||||
x: event.nativeEvent.pageX,
|
|
||||||
y: event.nativeEvent.pageY,
|
|
||||||
};
|
|
||||||
armTimers();
|
|
||||||
},
|
|
||||||
[armTimers],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTouchMove = useCallback(
|
|
||||||
(event: GestureResponderEvent) => {
|
|
||||||
const start = touchStartRef.current;
|
|
||||||
if (!start || didStartDragRef.current || menuOpenedRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const touch = event?.nativeEvent?.touches?.[0] ?? event?.nativeEvent;
|
|
||||||
const x = touch?.pageX;
|
|
||||||
const y = touch?.pageY;
|
|
||||||
if (typeof x !== "number" || typeof y !== "number") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = { x, y };
|
|
||||||
touchCurrentRef.current = current;
|
|
||||||
const dx = current.x - start.x;
|
|
||||||
const dy = current.y - start.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
const decision = decideLongPressMove({
|
|
||||||
dragArmed: dragArmedRef.current,
|
|
||||||
didStartDrag: didStartDragRef.current,
|
|
||||||
startPoint: start,
|
|
||||||
currentPoint: current,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (decision === "vertical_scroll") {
|
|
||||||
handleScrollIntent({ dx, dy, distance });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decision === "horizontal_swipe" || decision === "cancel_long_press") {
|
|
||||||
handleSwipeIntent({ dx, dy, distance });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decision === "start_drag") {
|
|
||||||
handleDragIntent({ dx, dy, distance });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[handleDragIntent, handleScrollIntent, handleSwipeIntent],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePressOut = useCallback(() => {
|
|
||||||
clearTimers();
|
|
||||||
dragArmedRef.current = false;
|
|
||||||
dragActivatedRef.current = false;
|
|
||||||
touchStartRef.current = null;
|
|
||||||
touchCurrentRef.current = null;
|
|
||||||
}, [clearTimers]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
didLongPressRef,
|
|
||||||
handleLongPress,
|
|
||||||
handlePressIn,
|
|
||||||
handleTouchMove,
|
|
||||||
handlePressOut,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProjectHeaderRow({
|
function ProjectHeaderRow({
|
||||||
project,
|
project,
|
||||||
displayName,
|
displayName,
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ export function useLongPressDragInteraction(input: {
|
|||||||
|
|
||||||
const armTimers = useCallback(() => {
|
const armTimers = useCallback(() => {
|
||||||
clearTimers();
|
clearTimers();
|
||||||
|
if (platformIsWeb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const DRAG_ARM_DELAY_MS = 180;
|
const DRAG_ARM_DELAY_MS = 180;
|
||||||
const DRAG_ARM_STATIONARY_SLOP_PX = 4;
|
const DRAG_ARM_STATIONARY_SLOP_PX = 4;
|
||||||
@@ -87,7 +90,7 @@ export function useLongPressDragInteraction(input: {
|
|||||||
input.drag();
|
input.drag();
|
||||||
}, DRAG_ARM_DELAY_MS);
|
}, DRAG_ARM_DELAY_MS);
|
||||||
|
|
||||||
if (!input.menuController || platformIsWeb) {
|
if (!input.menuController) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user