Skip to content

Commit 4a8aa4b

Browse files
fix: remove toolbar when mouse leaves the canvas
1 parent 41e9e00 commit 4a8aa4b

3 files changed

Lines changed: 164 additions & 2 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
2+
import { addEventListeners, removeEventListeners } from "../index";
3+
import * as mouseHoverModule from "../mouseHover";
4+
import * as generateToolbarModule from "../../generators/generateToolbar";
5+
import { VisualBuilder } from "../../index";
6+
7+
// Mock dependencies
8+
vi.mock("../mouseClick", () => ({
9+
default: vi.fn(),
10+
}));
11+
12+
vi.mock("../mouseHover", () => ({
13+
default: vi.fn(),
14+
cancelPendingMouseHover: vi.fn(),
15+
cancelPendingHoverToolbar: vi.fn(),
16+
cancelPendingAddOutline: vi.fn(),
17+
hideCustomCursor: vi.fn(),
18+
hideHoverOutline: vi.fn(),
19+
showCustomCursor: vi.fn(),
20+
}));
21+
22+
vi.mock("../../generators/generateToolbar", () => ({
23+
removeFieldToolbar: vi.fn(),
24+
}));
25+
26+
vi.mock("../../index", () => ({
27+
VisualBuilder: {
28+
VisualBuilderGlobalState: {
29+
value: {
30+
previousSelectedEditableDOM: null,
31+
isFocussed: false,
32+
},
33+
},
34+
},
35+
}));
36+
37+
vi.mock("lodash-es", async () => ({
38+
...(await import("lodash-es")),
39+
throttle: vi.fn((fn) => fn),
40+
debounce: vi.fn((fn) => fn),
41+
}));
42+
43+
describe("mouseleave handler changes", () => {
44+
let overlayWrapper: HTMLDivElement;
45+
let visualBuilderContainer: HTMLDivElement;
46+
let focusedToolbar: HTMLDivElement;
47+
let customCursor: HTMLDivElement;
48+
let resizeObserver: ResizeObserver;
49+
50+
beforeEach(() => {
51+
overlayWrapper = document.createElement("div");
52+
visualBuilderContainer = document.createElement("div");
53+
focusedToolbar = document.createElement("div");
54+
customCursor = document.createElement("div");
55+
resizeObserver = new ResizeObserver(() => {});
56+
57+
vi.clearAllMocks();
58+
});
59+
60+
afterEach(() => {
61+
removeEventListeners({
62+
overlayWrapper,
63+
visualBuilderContainer,
64+
previousSelectedEditableDOM: null,
65+
focusedToolbar,
66+
resizeObserver,
67+
customCursor,
68+
});
69+
vi.clearAllMocks();
70+
});
71+
72+
test("should cancel pending operations on mouseleave", () => {
73+
addEventListeners({
74+
overlayWrapper,
75+
visualBuilderContainer,
76+
previousSelectedEditableDOM: null,
77+
focusedToolbar,
78+
resizeObserver,
79+
customCursor,
80+
});
81+
82+
const mouseleaveEvent = new Event("mouseleave", { bubbles: true });
83+
document.documentElement.dispatchEvent(mouseleaveEvent);
84+
85+
expect(mouseHoverModule.cancelPendingMouseHover).toHaveBeenCalled();
86+
expect(mouseHoverModule.cancelPendingHoverToolbar).toHaveBeenCalled();
87+
expect(mouseHoverModule.cancelPendingAddOutline).toHaveBeenCalled();
88+
});
89+
90+
test("should remove field toolbar on mouseleave when not focused", () => {
91+
VisualBuilder.VisualBuilderGlobalState.value.isFocussed = false;
92+
93+
addEventListeners({
94+
overlayWrapper,
95+
visualBuilderContainer,
96+
previousSelectedEditableDOM: null,
97+
focusedToolbar,
98+
resizeObserver,
99+
customCursor,
100+
});
101+
102+
const mouseleaveEvent = new Event("mouseleave", { bubbles: true });
103+
document.documentElement.dispatchEvent(mouseleaveEvent);
104+
105+
expect(generateToolbarModule.removeFieldToolbar).toHaveBeenCalledWith(
106+
focusedToolbar
107+
);
108+
});
109+
110+
test("should not remove field toolbar on mouseleave when focused", () => {
111+
VisualBuilder.VisualBuilderGlobalState.value.isFocussed = true;
112+
113+
addEventListeners({
114+
overlayWrapper,
115+
visualBuilderContainer,
116+
previousSelectedEditableDOM: null,
117+
focusedToolbar,
118+
resizeObserver,
119+
customCursor,
120+
});
121+
122+
const mouseleaveEvent = new Event("mouseleave", { bubbles: true });
123+
document.documentElement.dispatchEvent(mouseleaveEvent);
124+
125+
expect(generateToolbarModule.removeFieldToolbar).not.toHaveBeenCalled();
126+
});
127+
128+
test("should not remove field toolbar when focusedToolbar is null", () => {
129+
VisualBuilder.VisualBuilderGlobalState.value.isFocussed = false;
130+
131+
addEventListeners({
132+
overlayWrapper,
133+
visualBuilderContainer,
134+
previousSelectedEditableDOM: null,
135+
focusedToolbar: null,
136+
resizeObserver,
137+
customCursor,
138+
});
139+
140+
const mouseleaveEvent = new Event("mouseleave", { bubbles: true });
141+
document.documentElement.dispatchEvent(mouseleaveEvent);
142+
143+
expect(generateToolbarModule.removeFieldToolbar).not.toHaveBeenCalled();
144+
});
145+
});

src/visualBuilder/listeners/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { VisualBuilder } from "..";
2+
import { removeFieldToolbar } from "../generators/generateToolbar";
23
import handleBuilderInteraction from "./mouseClick";
34
import handleMouseHover, {
5+
cancelPendingAddOutline,
6+
cancelPendingHoverToolbar,
7+
cancelPendingMouseHover,
48
hideCustomCursor,
59
hideHoverOutline,
610
showCustomCursor,
7-
showHoverToolbar,
811
} from "./mouseHover";
912
import EventListenerHandlerParams from "./types";
1013

@@ -41,8 +44,15 @@ const eventHandlers = {
4144
});
4245
},
4346
mouseleave: (params: AddEventListenersParams) => () => {
47+
cancelPendingMouseHover();
48+
cancelPendingHoverToolbar();
49+
cancelPendingAddOutline();
50+
4451
hideCustomCursor(params.customCursor);
4552
hideHoverOutline(params.visualBuilderContainer);
53+
if(!VisualBuilder?.VisualBuilderGlobalState?.value?.isFocussed && params?.focusedToolbar) {
54+
removeFieldToolbar(params.focusedToolbar);
55+
}
4656
},
4757
mouseenter: (params: AddEventListenersParams) => () => {
4858
showCustomCursor(params.customCursor);

src/visualBuilder/listeners/mouseHover.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ async function addOutline(params?: AddOutlineParams): Promise<void> {
105105
}
106106

107107
const debouncedAddOutline = debounce(addOutline, 50, { trailing: true });
108+
export const cancelPendingAddOutline = () => debouncedAddOutline.cancel();
108109
const showOutline = (params?: AddOutlineParams): Promise<void> | undefined => debouncedAddOutline(params);
109110

110111
function hideDefaultCursor(): void {
@@ -181,6 +182,8 @@ const debouncedRenderHoverToolbar = debounce(async (params: HandleBuilderInterac
181182

182183
export const showHoverToolbar = async (params: HandleBuilderInteractionParams) => await debouncedRenderHoverToolbar(params);
183184

185+
export const cancelPendingHoverToolbar = () => debouncedRenderHoverToolbar.cancel();
186+
184187
function isOverlay(target: HTMLElement): boolean {
185188
return target.classList.contains("visual-builder__overlay");
186189
}
@@ -222,7 +225,9 @@ const throttledMouseHover = throttle(async (params: HandleMouseHoverParams) => {
222225
eventTarget &&
223226
(isFieldPathDropdown(eventTarget) || isFieldPathParent(eventTarget))
224227
) {
225-
params.customCursor && hideCustomCursor(params.customCursor);
228+
if (params.customCursor) {
229+
hideCustomCursor(params.customCursor);
230+
}
226231
showOutline();
227232
showHoverToolbar({
228233
event: params.event,
@@ -399,4 +404,6 @@ const handleMouseHover = async (
399404
params: HandleMouseHoverParams
400405
): Promise<void> => await throttledMouseHover(params);
401406

407+
export const cancelPendingMouseHover = () => throttledMouseHover.cancel();
408+
402409
export default handleMouseHover;

0 commit comments

Comments
 (0)