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
96 changes: 81 additions & 15 deletions src/features/layout/hooks/useWindowDrag.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/** @vitest-environment jsdom */
import { renderHook } from "@testing-library/react";
import { cleanup, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const isTauriMock = vi.hoisted(() => vi.fn());
const getCurrentWindowMock = vi.hoisted(() => vi.fn());
const isWindowsPlatformMock = vi.hoisted(() => vi.fn());

vi.mock("@tauri-apps/api/core", () => ({
isTauri: isTauriMock,
Expand All @@ -14,10 +13,6 @@ vi.mock("@tauri-apps/api/window", () => ({
getCurrentWindow: getCurrentWindowMock,
}));

vi.mock("@utils/platformPaths", () => ({
isWindowsPlatform: isWindowsPlatformMock,
}));

import { useWindowDrag } from "./useWindowDrag";

function setRect(el: Element, rect: Pick<DOMRect, "left" | "right" | "top" | "bottom">) {
Expand All @@ -43,12 +38,11 @@ describe("useWindowDrag", () => {
});

afterEach(() => {
cleanup();
document.body.innerHTML = "";
});

it("starts dragging on Windows when click is inside a drag zone", () => {
isWindowsPlatformMock.mockReturnValue(true);

const titlebar = document.createElement("div");
titlebar.id = "titlebar";
document.body.appendChild(titlebar);
Expand All @@ -70,9 +64,82 @@ describe("useWindowDrag", () => {
expect(startDragging).toHaveBeenCalledTimes(1);
});

it("does not start dragging when clicking an interactive role target", () => {
isWindowsPlatformMock.mockReturnValue(true);
it("starts dragging on Windows when click is inside the main topbar", () => {
const topbar = document.createElement("div");
topbar.className = "main-topbar";
document.body.appendChild(topbar);
setRect(topbar, { left: 0, top: 0, right: 800, bottom: 44 });

renderHook(() => useWindowDrag("titlebar"));

const target = document.createElement("span");
topbar.appendChild(target);
target.dispatchEvent(
new MouseEvent("mousedown", {
bubbles: true,
button: 0,
clientX: 120,
clientY: 20,
}),
);

expect(startDragging).toHaveBeenCalledTimes(1);
});

it("starts dragging on Windows when mousedown target is a text node in topbar", () => {
const topbar = document.createElement("div");
topbar.className = "main-topbar";
document.body.appendChild(topbar);
setRect(topbar, { left: 0, top: 0, right: 800, bottom: 44 });

const label = document.createElement("span");
label.textContent = "Project Name";
topbar.appendChild(label);

renderHook(() => useWindowDrag("titlebar"));

const textNode = label.firstChild;
expect(textNode).toBeTruthy();
textNode?.dispatchEvent(
new MouseEvent("mousedown", {
bubbles: true,
button: 0,
clientX: 140,
clientY: 20,
}),
);

expect(startDragging).toHaveBeenCalledTimes(1);
});

it("does not start dragging when text node is inside an interactive target", () => {
const topbar = document.createElement("div");
topbar.className = "main-topbar";
document.body.appendChild(topbar);
setRect(topbar, { left: 0, top: 0, right: 800, bottom: 44 });

const button = document.createElement("button");
button.type = "button";
button.textContent = "Terminal";
topbar.appendChild(button);

renderHook(() => useWindowDrag("titlebar"));

const textNode = button.firstChild;
expect(textNode).toBeTruthy();
textNode?.dispatchEvent(
new MouseEvent("mousedown", {
bubbles: true,
button: 0,
clientX: 200,
clientY: 20,
}),
);

expect(startDragging).not.toHaveBeenCalled();
});

it("does not start dragging when clicking an interactive role target", () => {
const sidebarDragStrip = document.createElement("div");
sidebarDragStrip.className = "sidebar-drag-strip";
document.body.appendChild(sidebarDragStrip);
Expand All @@ -96,8 +163,6 @@ describe("useWindowDrag", () => {
});

it("does not start dragging when click is outside all drag zones", () => {
isWindowsPlatformMock.mockReturnValue(true);

const titlebar = document.createElement("div");
titlebar.id = "titlebar";
document.body.appendChild(titlebar);
Expand All @@ -119,19 +184,20 @@ describe("useWindowDrag", () => {
expect(startDragging).not.toHaveBeenCalled();
});

it("starts dragging on non-Windows via titlebar listener", () => {
isWindowsPlatformMock.mockReturnValue(false);

it("starts dragging via titlebar drag zone", () => {
const titlebar = document.createElement("div");
titlebar.id = "titlebar";
document.body.appendChild(titlebar);
setRect(titlebar, { left: 0, top: 0, right: 300, bottom: 44 });

renderHook(() => useWindowDrag("titlebar"));

titlebar.dispatchEvent(
new MouseEvent("mousedown", {
bubbles: true,
button: 0,
clientX: 12,
clientY: 12,
}),
);

Expand Down
48 changes: 22 additions & 26 deletions src/features/layout/hooks/useWindowDrag.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect } from "react";
import { isTauri } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { isWindowsPlatform } from "@utils/platformPaths";

const NEVER_DRAG_TARGET_SELECTOR = [
"button",
Expand All @@ -26,8 +25,6 @@ const NEVER_DRAG_TARGET_SELECTOR = [
".right-panel-divider",
].join(",");

const DRAG_ZONE_SELECTORS = ["#titlebar", ".sidebar-drag-strip", ".right-panel-drag-strip"];

function startDraggingSafe() {
try {
void getCurrentWindow().startDragging();
Expand All @@ -40,8 +37,14 @@ function isNeverDragTarget(event: MouseEvent) {
if (event.button !== 0) {
return true;
}
const target = event.target;
if (!(target instanceof Element)) {
const targetNode = event.target;
const target =
targetNode instanceof Element
? targetNode
: targetNode instanceof Node
? targetNode.parentElement
: null;
if (!target) {
return true;
}
return Boolean(target.closest(NEVER_DRAG_TARGET_SELECTOR));
Expand All @@ -56,8 +59,12 @@ function isInsideRect(clientX: number, clientY: number, rect: DOMRect) {
);
}

function isInsideAnyDragZone(clientX: number, clientY: number) {
for (const selector of DRAG_ZONE_SELECTORS) {
function isInsideAnyDragZone(
clientX: number,
clientY: number,
dragZoneSelectors: readonly string[],
) {
for (const selector of dragZoneSelectors) {
const zoneElements = document.querySelectorAll<HTMLElement>(selector);
for (const zone of zoneElements) {
const rect = zone.getBoundingClientRect();
Expand All @@ -78,32 +85,21 @@ export function useWindowDrag(targetId: string) {
return;
}

const el = document.getElementById(targetId);

const handler = (event: MouseEvent) => {
if (isNeverDragTarget(event)) {
return;
}
startDraggingSafe();
};

if (!isWindowsPlatform()) {
if (!el) {
return;
}
el.addEventListener("mousedown", handler);
return () => {
el.removeEventListener("mousedown", handler);
};
}
const dragZoneSelectors = [
`#${targetId}`,
".main-topbar",
".sidebar-drag-strip",
".right-panel-drag-strip",
] as const;

const handleMouseDown = (event: MouseEvent) => {
if (isNeverDragTarget(event)) {
return;
}
if (!isInsideAnyDragZone(event.clientX, event.clientY)) {
if (!isInsideAnyDragZone(event.clientX, event.clientY, dragZoneSelectors)) {
return;
}
event.preventDefault();
startDraggingSafe();
};

Expand Down
Loading