mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 12:35:35 +02:00
Revert "fix(resize): keep drag cursor consistent for table & chat resize (MUL…" (#5830)
This reverts commit 975cd49c3f.
This commit is contained in:
@@ -126,29 +126,27 @@ export function DataTable<TData>({
|
||||
setResizingColumnId(header.column.id);
|
||||
setColumnWidth(header, startWidth);
|
||||
|
||||
// Lock the resize cursor globally so it survives the pointer leaving the
|
||||
// narrow handle; the matching rule lives in packages/ui/styles/base.css.
|
||||
document.documentElement.setAttribute("data-table-resizing", "true");
|
||||
const originalCursor = document.body.style.cursor;
|
||||
const originalUserSelect = document.body.style.userSelect;
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
|
||||
const handlePointerMove = (pointerEvent: PointerEvent) => {
|
||||
setColumnWidth(header, startWidth + pointerEvent.clientX - startX);
|
||||
};
|
||||
|
||||
// stopResize is idempotent; wire it to every drag-end path (including a
|
||||
// window blur mid-drag) so the global resize-cursor lock can never strand.
|
||||
const stopResize = () => {
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", stopResize);
|
||||
window.removeEventListener("pointercancel", stopResize);
|
||||
window.removeEventListener("blur", stopResize);
|
||||
document.documentElement.removeAttribute("data-table-resizing");
|
||||
document.body.style.cursor = originalCursor;
|
||||
document.body.style.userSelect = originalUserSelect;
|
||||
setResizingColumnId(null);
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", stopResize);
|
||||
window.addEventListener("pointercancel", stopResize);
|
||||
window.addEventListener("blur", stopResize);
|
||||
},
|
||||
[setColumnWidth],
|
||||
);
|
||||
|
||||
@@ -246,30 +246,6 @@ html:has(
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
html[data-table-resizing="true"],
|
||||
html[data-table-resizing="true"] * {
|
||||
cursor: col-resize !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
html[data-chat-resizing="left"],
|
||||
html[data-chat-resizing="left"] * {
|
||||
cursor: col-resize !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
html[data-chat-resizing="top"],
|
||||
html[data-chat-resizing="top"] * {
|
||||
cursor: row-resize !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
html[data-chat-resizing="corner"],
|
||||
html[data-chat-resizing="corner"] * {
|
||||
cursor: nw-resize !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
[data-sidebar-resizing="true"] [data-slot="sidebar-gap"],
|
||||
[data-sidebar-resizing="true"] [data-slot="sidebar-container"] {
|
||||
transition: none !important;
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { useRef } from "react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ChatResizeHandles } from "./chat-resize-handles";
|
||||
import { useChatResize } from "./use-chat-resize";
|
||||
|
||||
// useChatResize only needs the size fields and setters from the chat store.
|
||||
vi.mock("@multica/core/chat", () => {
|
||||
const state = {
|
||||
chatWidth: 360,
|
||||
chatHeight: 480,
|
||||
isExpanded: false,
|
||||
setChatSize: vi.fn(),
|
||||
setExpanded: vi.fn(),
|
||||
};
|
||||
const useChatStore = Object.assign(
|
||||
(selector?: (s: typeof state) => unknown) =>
|
||||
selector ? selector(state) : state,
|
||||
{ getState: () => state },
|
||||
);
|
||||
return { CHAT_MIN_W: 320, CHAT_MIN_H: 400, useChatStore };
|
||||
});
|
||||
|
||||
function Harness() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { startDrag } = useChatResize(ref);
|
||||
return (
|
||||
<div ref={ref} style={{ position: "relative" }}>
|
||||
<ChatResizeHandles onDragStart={startDrag} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const POINTER_ID = 11;
|
||||
|
||||
function startLeftDrag(container: HTMLElement) {
|
||||
// The left edge handle is the one with the col-resize cursor.
|
||||
const handle = container.querySelector<HTMLElement>(".cursor-col-resize")!;
|
||||
const setPointerCapture = vi.fn();
|
||||
const releasePointerCapture = vi.fn();
|
||||
// jsdom elements don't implement the pointer-capture API.
|
||||
handle.setPointerCapture = setPointerCapture;
|
||||
handle.hasPointerCapture = vi.fn(() => true);
|
||||
handle.releasePointerCapture = releasePointerCapture;
|
||||
|
||||
fireEvent.pointerDown(handle, {
|
||||
button: 0,
|
||||
isPrimary: true,
|
||||
pointerId: POINTER_ID,
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
});
|
||||
|
||||
return { handle, setPointerCapture, releasePointerCapture };
|
||||
}
|
||||
|
||||
describe("chat resize cursor lock cleanup", () => {
|
||||
beforeEach(() => {
|
||||
document.documentElement.removeAttribute("data-chat-resizing");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
document.documentElement.removeAttribute("data-chat-resizing");
|
||||
});
|
||||
|
||||
it("locks the resize cursor on drag start and captures the pointer", () => {
|
||||
const { container } = render(<Harness />);
|
||||
const { setPointerCapture } = startLeftDrag(container);
|
||||
|
||||
expect(setPointerCapture).toHaveBeenCalledWith(POINTER_ID);
|
||||
expect(document.documentElement).toHaveAttribute(
|
||||
"data-chat-resizing",
|
||||
"left",
|
||||
);
|
||||
});
|
||||
|
||||
it("clears the lock and releases capture on pointerup", () => {
|
||||
const { container } = render(<Harness />);
|
||||
const { releasePointerCapture } = startLeftDrag(container);
|
||||
|
||||
fireEvent.pointerUp(document, { pointerId: POINTER_ID });
|
||||
|
||||
expect(document.documentElement).not.toHaveAttribute("data-chat-resizing");
|
||||
expect(releasePointerCapture).toHaveBeenCalledWith(POINTER_ID);
|
||||
});
|
||||
|
||||
it("clears the lock on pointercancel (no pointerup)", () => {
|
||||
const { container } = render(<Harness />);
|
||||
startLeftDrag(container);
|
||||
|
||||
fireEvent.pointerCancel(document, { pointerId: POINTER_ID });
|
||||
|
||||
expect(document.documentElement).not.toHaveAttribute("data-chat-resizing");
|
||||
});
|
||||
|
||||
it("clears the lock on lostpointercapture (no pointerup)", () => {
|
||||
const { container } = render(<Harness />);
|
||||
const { handle } = startLeftDrag(container);
|
||||
|
||||
fireEvent.lostPointerCapture(handle, { pointerId: POINTER_ID });
|
||||
|
||||
expect(document.documentElement).not.toHaveAttribute("data-chat-resizing");
|
||||
});
|
||||
|
||||
it("clears the lock when the window loses focus mid-drag", () => {
|
||||
const { container } = render(<Harness />);
|
||||
startLeftDrag(container);
|
||||
|
||||
fireEvent.blur(window);
|
||||
|
||||
expect(document.documentElement).not.toHaveAttribute("data-chat-resizing");
|
||||
});
|
||||
});
|
||||
@@ -84,9 +84,7 @@ export function useChatResize(
|
||||
const startDrag = useCallback(
|
||||
(e: React.PointerEvent, dir: DragDir) => {
|
||||
e.preventDefault();
|
||||
const captureEl = e.currentTarget as HTMLElement;
|
||||
const { pointerId } = e;
|
||||
captureEl.setPointerCapture(pointerId);
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
|
||||
dragRef.current = {
|
||||
startX: e.clientX,
|
||||
@@ -97,38 +95,9 @@ export function useChatResize(
|
||||
};
|
||||
setIsDragging(true);
|
||||
|
||||
// Lock the resize cursor globally so it survives the pointer leaving the
|
||||
// narrow handle; per-direction rules live in packages/ui/styles/base.css.
|
||||
document.documentElement.setAttribute("data-chat-resizing", dir);
|
||||
|
||||
// One idempotent cleanup wired to every way a pointer-capture drag can
|
||||
// end. A drag started with setPointerCapture may terminate via
|
||||
// pointercancel / lostpointercapture / window blur without a pointerup;
|
||||
// missing any of those would strand data-chat-resizing and freeze the
|
||||
// whole page in the resize cursor with text selection disabled.
|
||||
let finished = false;
|
||||
const finishDrag = () => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
document.removeEventListener("pointercancel", onPointerCancel);
|
||||
captureEl.removeEventListener("lostpointercapture", onLostPointerCapture);
|
||||
window.removeEventListener("blur", finishDrag);
|
||||
|
||||
dragRef.current = null;
|
||||
setIsDragging(false);
|
||||
document.documentElement.removeAttribute("data-chat-resizing");
|
||||
|
||||
if (captureEl.hasPointerCapture?.(pointerId)) {
|
||||
captureEl.releasePointerCapture?.(pointerId);
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerMove = (ev: PointerEvent) => {
|
||||
const d = dragRef.current;
|
||||
if (!d || ev.pointerId !== pointerId) return;
|
||||
if (!d) return;
|
||||
|
||||
const { maxW: mw, maxH: mh } = boundsRef.current;
|
||||
|
||||
@@ -143,21 +112,26 @@ export function useChatResize(
|
||||
|
||||
setChatSize(clamp(rawW, CHAT_MIN_W, mw), clamp(rawH, CHAT_MIN_H, mh));
|
||||
};
|
||||
const onPointerUp = (ev: PointerEvent) => {
|
||||
if (ev.pointerId === pointerId) finishDrag();
|
||||
};
|
||||
const onPointerCancel = (ev: PointerEvent) => {
|
||||
if (ev.pointerId === pointerId) finishDrag();
|
||||
};
|
||||
const onLostPointerCapture = (ev: PointerEvent) => {
|
||||
if (ev.pointerId === pointerId) finishDrag();
|
||||
|
||||
const onPointerUp = () => {
|
||||
dragRef.current = null;
|
||||
setIsDragging(false);
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
document.addEventListener("pointercancel", onPointerCancel);
|
||||
captureEl.addEventListener("lostpointercapture", onLostPointerCapture);
|
||||
window.addEventListener("blur", finishDrag);
|
||||
|
||||
const cursorMap: Record<DragDir, string> = {
|
||||
left: "col-resize",
|
||||
top: "row-resize",
|
||||
corner: "nw-resize",
|
||||
};
|
||||
document.body.style.cursor = cursorMap[dir];
|
||||
document.body.style.userSelect = "none";
|
||||
},
|
||||
[renderWidth, renderHeight, setChatSize],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user