Skip to content

Commit 2c8cd1d

Browse files
authored
APP-6535: only fire clickOutside if click starts and ends outside (#616)
1 parent dbe2e5f commit 2c8cd1d

File tree

3 files changed

+69
-15
lines changed

3 files changed

+69
-15
lines changed

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@viamrobotics/prime-core",
3-
"version": "0.0.172",
3+
"version": "0.0.173",
44
"repository": {
55
"type": "git",
66
"url": "https://github.com/viamrobotics/prime.git",

packages/core/src/lib/__tests__/click-outside.spec.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ describe('use:clickOutside', () => {
88
const onClickOutside = vi.fn();
99

1010
it('should trigger a callback only when clicked outside', async () => {
11+
const user = userEvent.setup();
12+
1113
render(Subject, { onClickOutside });
1214

13-
const user = userEvent.setup();
1415
const subject = screen.getByTestId('subject');
1516
const insideButton = screen.getByTestId('inside');
1617
const outsideButton = screen.getByTestId('outside');
@@ -26,9 +27,9 @@ describe('use:clickOutside', () => {
2627
});
2728

2829
it('should not trigger if clicked element gets removed from the DOM', async () => {
30+
const user = userEvent.setup();
2931
render(Subject, { onClickOutside });
3032

31-
const user = userEvent.setup();
3233
const insideButton = screen.getByTestId('inside');
3334

3435
insideButton.addEventListener('click', () => {
@@ -39,4 +40,38 @@ describe('use:clickOutside', () => {
3940

4041
expect(onClickOutside).not.toHaveBeenCalled();
4142
});
43+
44+
it('should not trigger if click starts inside element and moves out', async () => {
45+
const user = userEvent.setup();
46+
render(Subject, { onClickOutside });
47+
48+
const insideButton = screen.getByTestId('inside');
49+
const outsideButton = screen.getByTestId('outside');
50+
51+
// 1. press the left mouse button on the inside button
52+
// 2. release the left mouse button on the outside button
53+
await user.pointer([
54+
{ keys: '[MouseLeft>]', target: insideButton },
55+
{ keys: '[/MouseLeft]', target: outsideButton },
56+
]);
57+
58+
expect(onClickOutside).not.toHaveBeenCalled();
59+
});
60+
61+
it('should not trigger if click starts outside element and moves in', async () => {
62+
const user = userEvent.setup();
63+
render(Subject, { onClickOutside });
64+
65+
const insideButton = screen.getByTestId('inside');
66+
const outsideButton = screen.getByTestId('outside');
67+
68+
// 1. press the left mouse button on the inside button
69+
// 2. release the left mouse button on the outside button
70+
await user.pointer([
71+
{ keys: '[MouseLeft>]', target: outsideButton },
72+
{ keys: '[/MouseLeft]', target: insideButton },
73+
]);
74+
75+
expect(onClickOutside).not.toHaveBeenCalled();
76+
});
4277
});

packages/core/src/lib/click-outside.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,31 +33,50 @@ export const clickOutside: Action<
3333
ClickOutsideHandler | undefined
3434
> = (node, handler) => {
3535
let handleClickOutside = handler;
36+
let isMouseDown = false;
3637

37-
const handleWindowClick = (event: MouseEvent): void => {
38-
if (!node || !handleClickOutside) {
39-
return;
38+
const getOutsideTarget = (event: MouseEvent): Element | undefined => {
39+
const { target } = event;
40+
41+
return node &&
42+
target instanceof Element &&
43+
window.document.contains(target) &&
44+
!node.contains(target)
45+
? target
46+
: undefined;
47+
};
48+
49+
const handleMouseDown = (event: MouseEvent): void => {
50+
const target = getOutsideTarget(event);
51+
52+
if (target) {
53+
isMouseDown = true;
4054
}
55+
};
4156

42-
const target = event.target as Element;
57+
const handleMouseUp = (event: MouseEvent): void => {
58+
const previousIsMouseDown = isMouseDown;
59+
const target = getOutsideTarget(event);
60+
isMouseDown = false;
4361

44-
if (
45-
window.document.contains(target) &&
46-
!node.contains(target) &&
47-
!event.defaultPrevented
48-
) {
49-
handleClickOutside(target);
62+
if (target && previousIsMouseDown) {
63+
handleClickOutside?.(target);
5064
}
5165
};
5266

53-
window.document.addEventListener('click', handleWindowClick, true);
67+
// Listen to mousedown and mouseup rather than click
68+
// so don't trigger if the click starts inside the element and moves out.
69+
// TODO(mc, 2025-01-14): investigate whether these need to be in the capture phase
70+
window.document.addEventListener('mousedown', handleMouseDown, true);
71+
window.document.addEventListener('mouseup', handleMouseUp, true);
5472

5573
return {
5674
update: (nextHandler: ClickOutsideHandler | undefined) => {
5775
handleClickOutside = nextHandler;
5876
},
5977
destroy: () => {
60-
window.document.removeEventListener('click', handleWindowClick, true);
78+
window.document.removeEventListener('mousedown', handleMouseDown, true);
79+
window.document.removeEventListener('mouseup', handleMouseUp, true);
6180
},
6281
};
6382
};

0 commit comments

Comments
 (0)