Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -564,10 +564,13 @@ export const ResizableSidebar: Story = {

const sidebarWidth = () =>
sidebar.style.getPropertyValue("--agents-left-sidebar-width");
// Synthetic pointer events default isPrimary to false; a real mouse
// always reports true, and the handle ignores non-primary pointers.
const pointer = { pointerId: 1, isPrimary: true };
const dragSidebar = (fromX: number, toX: number) => {
fireEvent.pointerDown(handle, { clientX: fromX, pointerId: 1 });
fireEvent.pointerMove(handle, { clientX: toX, pointerId: 1 });
fireEvent.pointerUp(handle, { clientX: toX, pointerId: 1 });
fireEvent.pointerDown(handle, { ...pointer, clientX: fromX });
fireEvent.pointerMove(handle, { ...pointer, clientX: toX });
fireEvent.pointerUp(handle, { ...pointer, clientX: toX });
};

const initialWidth = clampLeftSidebarWidth(LEFT_SIDEBAR_DEFAULT_WIDTH);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ResizableChatsSidebarFrame } from "./ResizableChatsSidebarFrame";
import {
LEFT_SIDEBAR_DEFAULT_WIDTH,
LEFT_SIDEBAR_STORAGE_KEY,
} from "./sidebarWidth";

// Pointer events are dispatched with fireEvent because userEvent cannot emit
// lostpointercapture or pointercancel, set isPrimary, or drive a second
// pointer ID.
const PRIMARY = { pointerId: 1, button: 0, isPrimary: true };
const SECONDARY_POINTER = { pointerId: 2, button: 0, isPrimary: false };
const RIGHT_BUTTON = { pointerId: 1, button: 2, isPrimary: true };

const persistedWidth = () =>
Number(localStorage.getItem(LEFT_SIDEBAR_STORAGE_KEY));

const renderHandle = () => {
render(
<ResizableChatsSidebarFrame>
<div>sidebar</div>
</ResizableChatsSidebarFrame>,
);
return screen.getByTestId("agents-sidebar-resize-handle");
};

describe("ResizableChatsSidebarFrame", () => {
beforeEach(() => {
vi.stubGlobal("innerWidth", 1440);
localStorage.clear();
});

afterEach(() => {
vi.unstubAllGlobals();
});

it("persists the width while a primary drag is in progress", () => {
const handle = renderHandle();

fireEvent.pointerDown(handle, { ...PRIMARY, clientX: 0 });
fireEvent.pointerMove(handle, { ...PRIMARY, clientX: 40 });

expect(persistedWidth()).toBe(LEFT_SIDEBAR_DEFAULT_WIDTH + 40);
});

it("stops tracking after lostpointercapture without pointerup", () => {
const handle = renderHandle();

fireEvent.pointerDown(handle, { ...PRIMARY, clientX: 0 });
fireEvent.pointerMove(handle, { ...PRIMARY, clientX: 40 });
fireEvent.lostPointerCapture(handle, PRIMARY);
fireEvent.pointerMove(handle, { ...PRIMARY, clientX: 200 });

expect(persistedWidth()).toBe(LEFT_SIDEBAR_DEFAULT_WIDTH + 40);
});

it("stops tracking after pointercancel", () => {
const handle = renderHandle();

fireEvent.pointerDown(handle, { ...PRIMARY, clientX: 0 });
fireEvent.pointerMove(handle, { ...PRIMARY, clientX: 40 });
fireEvent.pointerCancel(handle, PRIMARY);
fireEvent.pointerMove(handle, { ...PRIMARY, clientX: 200 });

expect(persistedWidth()).toBe(LEFT_SIDEBAR_DEFAULT_WIDTH + 40);
});

it("ignores a secondary-button pointerdown", () => {
const handle = renderHandle();

fireEvent.pointerDown(handle, { ...RIGHT_BUTTON, clientX: 0 });
fireEvent.pointerMove(handle, { ...RIGHT_BUTTON, clientX: 40 });

expect(localStorage.getItem(LEFT_SIDEBAR_STORAGE_KEY)).toBeNull();
});

it("ignores a non-primary pointer", () => {
const handle = renderHandle();

fireEvent.pointerDown(handle, { ...SECONDARY_POINTER, clientX: 0 });
fireEvent.pointerMove(handle, { ...SECONDARY_POINTER, clientX: 40 });

expect(localStorage.getItem(LEFT_SIDEBAR_STORAGE_KEY)).toBeNull();
});

it("ignores a second pointer while a drag is in progress", () => {
const handle = renderHandle();

fireEvent.pointerDown(handle, { ...PRIMARY, clientX: 0 });
fireEvent.pointerDown(handle, { ...SECONDARY_POINTER, clientX: 0 });
fireEvent.pointerMove(handle, { ...SECONDARY_POINTER, clientX: 200 });
fireEvent.pointerUp(handle, SECONDARY_POINTER);
fireEvent.pointerMove(handle, { ...PRIMARY, clientX: 40 });

expect(persistedWidth()).toBe(LEFT_SIDEBAR_DEFAULT_WIDTH + 40);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const ResizableChatsSidebarFrame = ({
const [width, setWidth] = useState(loadPersistedLeftSidebarWidth);
const maxWidth = getLeftSidebarMaxWidth();
const isDragging = useRef(false);
const activePointerId = useRef<number | null>(null);
const startX = useRef(0);
const startWidth = useRef(0);

Expand All @@ -54,28 +55,39 @@ export const ResizableChatsSidebarFrame = ({
}, []);

const handlePointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
// Only a primary left-button pointer starts a drag; a second pointer
// cannot take over one that is already in progress.
if (isDragging.current || e.button !== 0 || !e.isPrimary) {
return;
}
e.preventDefault();
isDragging.current = true;
activePointerId.current = e.pointerId;
startX.current = e.clientX;
startWidth.current = width;
e.currentTarget.setPointerCapture?.(e.pointerId);
};

const handlePointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
if (!isDragging.current) {
if (!isDragging.current || e.pointerId !== activePointerId.current) {
return;
}

const rawWidth = startWidth.current + (e.clientX - startX.current);
setUserWidth(rawWidth);
};

// Ends the drag on pointerup, pointercancel, and lostpointercapture. The
// last two fire without pointerup when the browser claims the gesture or
// capture is lost (window deactivation, context menu), so all three must
// reset the drag state.
const handlePointerEnd = (e: ReactPointerEvent<HTMLDivElement>) => {
if (!isDragging.current) {
if (!isDragging.current || e.pointerId !== activePointerId.current) {
return;
}

isDragging.current = false;
activePointerId.current = null;
if (e.currentTarget.hasPointerCapture?.(e.pointerId)) {
e.currentTarget.releasePointerCapture?.(e.pointerId);
}
Expand Down Expand Up @@ -129,6 +141,7 @@ export const ResizableChatsSidebarFrame = ({
onPointerMove={handlePointerMove}
onPointerUp={handlePointerEnd}
onPointerCancel={handlePointerEnd}
onLostPointerCapture={handlePointerEnd}
onKeyDown={handleKeyDown}
className="absolute top-0 right-0 z-20 hidden h-full w-1 touch-none cursor-col-resize select-none transition-colors hover:bg-content-link focus-visible:bg-content-link focus-visible:outline-hidden sm:block"
/>
Expand Down
Loading
Loading