Fix mobile web gestures, DnD activation, iOS zoom, and Enter behavior (#1048)

* fix(app): require long-press for sidebar dnd on mobile

* fix(app): right-edge open gesture for explorer sidebar

* fix(app): left-edge open gesture for workspace sidebar

* fix(app): allow gesture children to scroll on web

* fix(app): lock viewport zoom on compact web

* fix(app): don't submit on enter in compact web composer

---------

Co-authored-by: jon <nikuscs@gmail.com>
This commit is contained in:
Mohamed Boudra
2026-05-16 14:20:24 +08:00
committed by GitHub
co-authored by jon
parent 5762055213
commit 667f441cc0
7 changed files with 125 additions and 18 deletions
@@ -13,6 +13,9 @@ interface DndContextProps {
}
let latestDndContextProps: DndContextProps | null = null;
const dndKitMocks = vi.hoisted(() => ({
useSensor: vi.fn(() => ({})),
}));
vi.mock("@dnd-kit/core", () => ({
DndContext: ({ children, ...props }: React.PropsWithChildren<DndContextProps>) => {
@@ -22,7 +25,7 @@ vi.mock("@dnd-kit/core", () => ({
closestCenter: vi.fn(),
KeyboardSensor: vi.fn(),
PointerSensor: vi.fn(),
useSensor: vi.fn(() => ({})),
useSensor: dndKitMocks.useSensor,
useSensors: vi.fn(() => []),
}));
@@ -104,7 +107,7 @@ function renderItem({ item, isActive }: { item: string; isActive: boolean }) {
);
}
function renderList(): void {
function renderList({ useDragHandle = false }: { useDragHandle?: boolean } = {}): void {
act(() => {
root?.render(
<DraggableList
@@ -113,6 +116,7 @@ function renderList(): void {
onDragEnd={vi.fn()}
renderItem={renderItem}
scrollEnabled={false}
useDragHandle={useDragHandle}
/>,
);
});
@@ -125,6 +129,28 @@ function getItemActiveState(item: string): string | null {
}
describe("DraggableList web", () => {
it("uses distance activation for default draggable rows", () => {
renderList();
expect(dndKitMocks.useSensor).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({
activationConstraint: { distance: 6 },
}),
);
});
it("requires a held pointer before activating handle-based drags", () => {
renderList({ useDragHandle: true });
expect(dndKitMocks.useSensor).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({
activationConstraint: { delay: 250, tolerance: 8 },
}),
);
});
it("clears active drag state when a drag is cancelled", () => {
renderList();
@@ -30,6 +30,8 @@ const restrictToVerticalAxis: Modifier = ({ transform }) => ({
});
const DND_MODIFIERS = [restrictToVerticalAxis];
const DEFAULT_POINTER_ACTIVATION_CONSTRAINT = { distance: 6 };
const HANDLE_POINTER_ACTIVATION_CONSTRAINT = { delay: 250, tolerance: 8 };
interface SortableItemProps<T> {
id: string;
@@ -89,12 +91,6 @@ function SortableItem<T>({
}),
[combinedTransform, transition, isDragging],
);
const {
role: _dragRole,
tabIndex: _dragTabIndex,
"aria-roledescription": _dragRoleDescription,
...dragHandleAttributes
} = attributes as unknown as Record<string, unknown>;
const info: DraggableRenderItemInfo<T> = {
item,
@@ -103,7 +99,7 @@ function SortableItem<T>({
isActive: activeId === id,
dragHandleProps: useDragHandle
? {
attributes: dragHandleAttributes,
attributes: attributes as unknown as Record<string, unknown>,
listeners: listeners as unknown as Record<string, unknown>,
setActivatorNodeRef: setActivatorNodeRef as unknown as (node: unknown) => void,
}
@@ -149,12 +145,13 @@ export function DraggableList<T>({
const scrollbar = useWebScrollViewScrollbar(scrollViewRef, {
enabled: showCustomScrollbar,
});
const pointerActivationConstraint = useDragHandle
? HANDLE_POINTER_ACTIVATION_CONSTRAINT
: DEFAULT_POINTER_ACTIVATION_CONSTRAINT;
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 6,
},
activationConstraint: pointerActivationConstraint,
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
@@ -53,6 +53,7 @@ import { getShortcutOs } from "@/utils/shortcut-platform";
import type { MessageInputKeyboardActionKind } from "@/keyboard/actions";
import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime";
import { isWeb } from "@/constants/platform";
import { useIsCompactFormFactor } from "@/constants/layout";
import { useComposerHeightMirror } from "./composer-height-mirror";
import { computeCanStartDictation } from "./message-input-state";
@@ -365,6 +366,7 @@ function resolveSendTooltipLabel(input: {
interface DesktopKeyPressContext {
onKeyPressCallback: ((event: { key: string; preventDefault: () => void }) => boolean) | undefined;
submitOnEnter: boolean;
isAgentRunning: boolean;
onQueue: ((payload: MessagePayload) => void) | undefined;
isSubmitDisabled: boolean;
@@ -391,6 +393,7 @@ function handleDesktopKeyPressImpl(
const { shiftKey, metaKey, ctrlKey } = event.nativeEvent;
if (event.nativeEvent.key !== "Enter") return;
if (!ctx.submitOnEnter) return;
if (shiftKey) return;
if ((metaKey || ctrlKey) && ctx.isAgentRunning && ctx.onQueue) {
@@ -1187,6 +1190,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
inputWrapperStyle,
attachmentSlot,
} = resolveMessageInputProps(props);
const isCompact = useIsCompactFormFactor();
const buttonIconSize = isWeb ? ICON_SIZE.md : ICON_SIZE.lg;
const toast = useToast();
const voice = useVoiceOptional();
@@ -1567,12 +1571,14 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
[onSelectionChangeCallback],
);
const shouldHandleDesktopSubmit = isWeb;
const shouldHandleWebKeyPress = isWeb;
const shouldSubmitOnEnter = isWeb && !isCompact;
function handleDesktopKeyPress(event: WebTextInputKeyPressEvent) {
if (!shouldHandleDesktopSubmit) return;
if (!shouldHandleWebKeyPress) return;
handleDesktopKeyPressImpl(event, {
onKeyPressCallback,
submitOnEnter: shouldSubmitOnEnter,
isAgentRunning,
onQueue,
isSubmitDisabled,
@@ -1724,7 +1730,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
scrollEnabled={isWeb ? inputHeight >= MAX_INPUT_HEIGHT : true}
onContentSizeChange={handleContentSizeChange}
editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled}
onKeyPress={shouldHandleDesktopSubmit ? handleDesktopKeyPress : undefined}
onKeyPress={shouldHandleWebKeyPress ? handleDesktopKeyPress : undefined}
onSelectionChange={handleSelectionChange}
autoFocus={isWeb && autoFocus}
/>