Skip to content

Commit 1526ce1

Browse files
SamigosDimillian
andauthored
fix(layout): improve topbar window dragging (#508)
Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
1 parent f423ba3 commit 1526ce1

2 files changed

Lines changed: 103 additions & 41 deletions

File tree

src/features/layout/hooks/useWindowDrag.test.tsx

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
/** @vitest-environment jsdom */
2-
import { renderHook } from "@testing-library/react";
2+
import { cleanup, renderHook } from "@testing-library/react";
33
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
44

55
const isTauriMock = vi.hoisted(() => vi.fn());
66
const getCurrentWindowMock = vi.hoisted(() => vi.fn());
7-
const isWindowsPlatformMock = vi.hoisted(() => vi.fn());
87

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

17-
vi.mock("@utils/platformPaths", () => ({
18-
isWindowsPlatform: isWindowsPlatformMock,
19-
}));
20-
2116
import { useWindowDrag } from "./useWindowDrag";
2217

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

4540
afterEach(() => {
41+
cleanup();
4642
document.body.innerHTML = "";
4743
});
4844

4945
it("starts dragging on Windows when click is inside a drag zone", () => {
50-
isWindowsPlatformMock.mockReturnValue(true);
51-
5246
const titlebar = document.createElement("div");
5347
titlebar.id = "titlebar";
5448
document.body.appendChild(titlebar);
@@ -70,9 +64,82 @@ describe("useWindowDrag", () => {
7064
expect(startDragging).toHaveBeenCalledTimes(1);
7165
});
7266

73-
it("does not start dragging when clicking an interactive role target", () => {
74-
isWindowsPlatformMock.mockReturnValue(true);
67+
it("starts dragging on Windows when click is inside the main topbar", () => {
68+
const topbar = document.createElement("div");
69+
topbar.className = "main-topbar";
70+
document.body.appendChild(topbar);
71+
setRect(topbar, { left: 0, top: 0, right: 800, bottom: 44 });
72+
73+
renderHook(() => useWindowDrag("titlebar"));
74+
75+
const target = document.createElement("span");
76+
topbar.appendChild(target);
77+
target.dispatchEvent(
78+
new MouseEvent("mousedown", {
79+
bubbles: true,
80+
button: 0,
81+
clientX: 120,
82+
clientY: 20,
83+
}),
84+
);
85+
86+
expect(startDragging).toHaveBeenCalledTimes(1);
87+
});
7588

89+
it("starts dragging on Windows when mousedown target is a text node in topbar", () => {
90+
const topbar = document.createElement("div");
91+
topbar.className = "main-topbar";
92+
document.body.appendChild(topbar);
93+
setRect(topbar, { left: 0, top: 0, right: 800, bottom: 44 });
94+
95+
const label = document.createElement("span");
96+
label.textContent = "Project Name";
97+
topbar.appendChild(label);
98+
99+
renderHook(() => useWindowDrag("titlebar"));
100+
101+
const textNode = label.firstChild;
102+
expect(textNode).toBeTruthy();
103+
textNode?.dispatchEvent(
104+
new MouseEvent("mousedown", {
105+
bubbles: true,
106+
button: 0,
107+
clientX: 140,
108+
clientY: 20,
109+
}),
110+
);
111+
112+
expect(startDragging).toHaveBeenCalledTimes(1);
113+
});
114+
115+
it("does not start dragging when text node is inside an interactive target", () => {
116+
const topbar = document.createElement("div");
117+
topbar.className = "main-topbar";
118+
document.body.appendChild(topbar);
119+
setRect(topbar, { left: 0, top: 0, right: 800, bottom: 44 });
120+
121+
const button = document.createElement("button");
122+
button.type = "button";
123+
button.textContent = "Terminal";
124+
topbar.appendChild(button);
125+
126+
renderHook(() => useWindowDrag("titlebar"));
127+
128+
const textNode = button.firstChild;
129+
expect(textNode).toBeTruthy();
130+
textNode?.dispatchEvent(
131+
new MouseEvent("mousedown", {
132+
bubbles: true,
133+
button: 0,
134+
clientX: 200,
135+
clientY: 20,
136+
}),
137+
);
138+
139+
expect(startDragging).not.toHaveBeenCalled();
140+
});
141+
142+
it("does not start dragging when clicking an interactive role target", () => {
76143
const sidebarDragStrip = document.createElement("div");
77144
sidebarDragStrip.className = "sidebar-drag-strip";
78145
document.body.appendChild(sidebarDragStrip);
@@ -96,8 +163,6 @@ describe("useWindowDrag", () => {
96163
});
97164

98165
it("does not start dragging when click is outside all drag zones", () => {
99-
isWindowsPlatformMock.mockReturnValue(true);
100-
101166
const titlebar = document.createElement("div");
102167
titlebar.id = "titlebar";
103168
document.body.appendChild(titlebar);
@@ -119,19 +184,20 @@ describe("useWindowDrag", () => {
119184
expect(startDragging).not.toHaveBeenCalled();
120185
});
121186

122-
it("starts dragging on non-Windows via titlebar listener", () => {
123-
isWindowsPlatformMock.mockReturnValue(false);
124-
187+
it("starts dragging via titlebar drag zone", () => {
125188
const titlebar = document.createElement("div");
126189
titlebar.id = "titlebar";
127190
document.body.appendChild(titlebar);
191+
setRect(titlebar, { left: 0, top: 0, right: 300, bottom: 44 });
128192

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

131195
titlebar.dispatchEvent(
132196
new MouseEvent("mousedown", {
133197
bubbles: true,
134198
button: 0,
199+
clientX: 12,
200+
clientY: 12,
135201
}),
136202
);
137203

src/features/layout/hooks/useWindowDrag.ts

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useEffect } from "react";
22
import { isTauri } from "@tauri-apps/api/core";
33
import { getCurrentWindow } from "@tauri-apps/api/window";
4-
import { isWindowsPlatform } from "@utils/platformPaths";
54

65
const NEVER_DRAG_TARGET_SELECTOR = [
76
"button",
@@ -26,8 +25,6 @@ const NEVER_DRAG_TARGET_SELECTOR = [
2625
".right-panel-divider",
2726
].join(",");
2827

29-
const DRAG_ZONE_SELECTORS = ["#titlebar", ".sidebar-drag-strip", ".right-panel-drag-strip"];
30-
3128
function startDraggingSafe() {
3229
try {
3330
void getCurrentWindow().startDragging();
@@ -40,8 +37,14 @@ function isNeverDragTarget(event: MouseEvent) {
4037
if (event.button !== 0) {
4138
return true;
4239
}
43-
const target = event.target;
44-
if (!(target instanceof Element)) {
40+
const targetNode = event.target;
41+
const target =
42+
targetNode instanceof Element
43+
? targetNode
44+
: targetNode instanceof Node
45+
? targetNode.parentElement
46+
: null;
47+
if (!target) {
4548
return true;
4649
}
4750
return Boolean(target.closest(NEVER_DRAG_TARGET_SELECTOR));
@@ -56,8 +59,12 @@ function isInsideRect(clientX: number, clientY: number, rect: DOMRect) {
5659
);
5760
}
5861

59-
function isInsideAnyDragZone(clientX: number, clientY: number) {
60-
for (const selector of DRAG_ZONE_SELECTORS) {
62+
function isInsideAnyDragZone(
63+
clientX: number,
64+
clientY: number,
65+
dragZoneSelectors: readonly string[],
66+
) {
67+
for (const selector of dragZoneSelectors) {
6168
const zoneElements = document.querySelectorAll<HTMLElement>(selector);
6269
for (const zone of zoneElements) {
6370
const rect = zone.getBoundingClientRect();
@@ -78,32 +85,21 @@ export function useWindowDrag(targetId: string) {
7885
return;
7986
}
8087

81-
const el = document.getElementById(targetId);
82-
83-
const handler = (event: MouseEvent) => {
84-
if (isNeverDragTarget(event)) {
85-
return;
86-
}
87-
startDraggingSafe();
88-
};
89-
90-
if (!isWindowsPlatform()) {
91-
if (!el) {
92-
return;
93-
}
94-
el.addEventListener("mousedown", handler);
95-
return () => {
96-
el.removeEventListener("mousedown", handler);
97-
};
98-
}
88+
const dragZoneSelectors = [
89+
`#${targetId}`,
90+
".main-topbar",
91+
".sidebar-drag-strip",
92+
".right-panel-drag-strip",
93+
] as const;
9994

10095
const handleMouseDown = (event: MouseEvent) => {
10196
if (isNeverDragTarget(event)) {
10297
return;
10398
}
104-
if (!isInsideAnyDragZone(event.clientX, event.clientY)) {
99+
if (!isInsideAnyDragZone(event.clientX, event.clientY, dragZoneSelectors)) {
105100
return;
106101
}
102+
event.preventDefault();
107103
startDraggingSafe();
108104
};
109105

0 commit comments

Comments
 (0)