Skip to content

Commit 7312e58

Browse files
authored
fix DragRotate when mouseup occurs outside window or iframe (#9512)
fix #4622
1 parent d9696a0 commit 7312e58

File tree

4 files changed

+107
-18
lines changed

4 files changed

+107
-18
lines changed

src/ui/handler/mouse.js

+23
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ import type Point from '@mapbox/point-geometry';
66
const LEFT_BUTTON = 0;
77
const RIGHT_BUTTON = 2;
88

9+
// the values for each button in MouseEvent.buttons
10+
const BUTTONS_FLAGS = {
11+
[LEFT_BUTTON]: 1,
12+
[RIGHT_BUTTON]: 2
13+
};
14+
15+
function buttonStillPressed(e: MouseEvent, button: number) {
16+
const flag = BUTTONS_FLAGS[button];
17+
return e.buttons === undefined || (e.buttons & flag) !== flag;
18+
}
19+
920
class MouseHandler {
1021

1122
_enabled: boolean;
@@ -50,6 +61,17 @@ class MouseHandler {
5061
if (!lastPoint) return;
5162
e.preventDefault();
5263

64+
if (buttonStillPressed(e, this._eventButton)) {
65+
// Some browsers don't fire a `mouseup` when the mouseup occurs outside
66+
// the window or iframe:
67+
// https://github.com/mapbox/mapbox-gl-js/issues/4622
68+
//
69+
// If the button is no longer pressed during this `mousemove` it may have
70+
// been released outside of the window or iframe.
71+
this.reset();
72+
return;
73+
}
74+
5375
if (!this._moved && point.dist(lastPoint) < this._clickTolerance) return;
5476
this._moved = true;
5577
this._lastPoint = point;
@@ -59,6 +81,7 @@ class MouseHandler {
5981
}
6082

6183
mouseupWindow(e: MouseEvent) {
84+
if (!this._lastPoint) return;
6285
const eventButton = DOM.mouseButton(e);
6386
if (eventButton !== this._eventButton) return;
6487
if (this._moved) DOM.suppressClick();

test/unit/ui/handler/drag_pan.test.js

+19-16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ function createMap(t, clickTolerance, dragPan) {
1313
});
1414
}
1515

16+
// MouseEvent.buttons = 1 // left button
17+
const buttons = 1;
18+
1619
test('DragPanHandler fires dragstart, drag, and dragend events at appropriate times in response to a mouse-triggered drag', (t) => {
1720
const map = createMap(t);
1821

@@ -30,7 +33,7 @@ test('DragPanHandler fires dragstart, drag, and dragend events at appropriate ti
3033
t.equal(drag.callCount, 0);
3134
t.equal(dragend.callCount, 0);
3235

33-
simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10});
36+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10});
3437
map._renderTaskQueue.run();
3538
t.equal(dragstart.callCount, 1);
3639
t.equal(drag.callCount, 1);
@@ -63,7 +66,7 @@ test('DragPanHandler captures mousemove events during a mouse-triggered drag (re
6366
t.equal(drag.callCount, 0);
6467
t.equal(dragend.callCount, 0);
6568

66-
simulate.mousemove(window.document.body, {clientX: 10, clientY: 10});
69+
simulate.mousemove(window.document.body, {buttons, clientX: 10, clientY: 10});
6770
map._renderTaskQueue.run();
6871
t.equal(dragstart.callCount, 1);
6972
t.equal(drag.callCount, 1);
@@ -121,7 +124,7 @@ test('DragPanHandler prevents mousemove events from firing during a drag (#1555)
121124
simulate.mousedown(map.getCanvasContainer());
122125
map._renderTaskQueue.run();
123126

124-
simulate.mousemove(map.getCanvasContainer(), {clientX: 10, clientY: 10});
127+
simulate.mousemove(map.getCanvasContainer(), {buttons, clientX: 10, clientY: 10});
125128
map._renderTaskQueue.run();
126129

127130
simulate.mouseup(map.getCanvasContainer());
@@ -142,7 +145,7 @@ test('DragPanHandler ends a mouse-triggered drag if the window blurs', (t) => {
142145
simulate.mousedown(map.getCanvas());
143146
map._renderTaskQueue.run();
144147

145-
simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10});
148+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10});
146149
map._renderTaskQueue.run();
147150

148151
simulate.blur(window);
@@ -176,14 +179,14 @@ test('DragPanHandler requests a new render frame after each mousemove event', (t
176179
const requestFrame = t.spy(map, '_requestRenderFrame');
177180

178181
simulate.mousedown(map.getCanvas());
179-
simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10});
182+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10});
180183
t.ok(requestFrame.callCount > 0);
181184

182185
map._renderTaskQueue.run();
183186

184187
// https://github.com/mapbox/mapbox-gl-js/issues/6063
185188
requestFrame.resetHistory();
186-
simulate.mousemove(map.getCanvas(), {clientX: 20, clientY: 20});
189+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 20, clientY: 20});
187190
t.equal(requestFrame.callCount, 1);
188191

189192
map.remove();
@@ -208,7 +211,7 @@ test('DragPanHandler can interleave with another handler', (t) => {
208211
t.equal(drag.callCount, 0);
209212
t.equal(dragend.callCount, 0);
210213

211-
simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10});
214+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10});
212215
map._renderTaskQueue.run();
213216
t.equal(dragstart.callCount, 1);
214217
t.equal(drag.callCount, 1);
@@ -221,7 +224,7 @@ test('DragPanHandler can interleave with another handler', (t) => {
221224
t.equal(drag.callCount, 1);
222225
t.equal(dragend.callCount, 0);
223226

224-
simulate.mousemove(map.getCanvas(), {clientX: 20, clientY: 20});
227+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 20, clientY: 20});
225228
map._renderTaskQueue.run();
226229
t.equal(dragstart.callCount, 1);
227230
t.equal(drag.callCount, 2);
@@ -250,13 +253,13 @@ test('DragPanHandler can interleave with another handler', (t) => {
250253
map.on('drag', drag);
251254
map.on('dragend', dragend);
252255

253-
simulate.mousedown(map.getCanvas(), {[`${modifier}Key`]: true});
256+
simulate.mousedown(map.getCanvas(), {buttons, [`${modifier}Key`]: true});
254257
map._renderTaskQueue.run();
255258
t.equal(dragstart.callCount, 0);
256259
t.equal(drag.callCount, 0);
257260
t.equal(dragend.callCount, 0);
258261

259-
simulate.mousemove(map.getCanvas(), {[`${modifier}Key`]: true, clientX: 10, clientY: 10});
262+
simulate.mousemove(map.getCanvas(), {buttons, [`${modifier}Key`]: true, clientX: 10, clientY: 10});
260263
map._renderTaskQueue.run();
261264
t.equal(dragstart.callCount, 0);
262265
t.equal(drag.callCount, 0);
@@ -296,7 +299,7 @@ test('DragPanHandler can interleave with another handler', (t) => {
296299
t.equal(drag.callCount, 0);
297300
t.equal(dragend.callCount, 0);
298301

299-
simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10});
302+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10});
300303
map._renderTaskQueue.run();
301304
t.equal(dragstart.callCount, 0);
302305
t.equal(drag.callCount, 0);
@@ -359,25 +362,25 @@ test('DragPanHandler does not end a drag on right button mouseup', (t) => {
359362
t.equal(drag.callCount, 0);
360363
t.equal(dragend.callCount, 0);
361364

362-
simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10});
365+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10});
363366
map._renderTaskQueue.run();
364367
t.equal(dragstart.callCount, 1);
365368
t.equal(drag.callCount, 1);
366369
t.equal(dragend.callCount, 0);
367370

368-
simulate.mousedown(map.getCanvas(), {buttons: 2, button: 2});
371+
simulate.mousedown(map.getCanvas(), {buttons: buttons + 2, button: 2});
369372
map._renderTaskQueue.run();
370373
t.equal(dragstart.callCount, 1);
371374
t.equal(drag.callCount, 1);
372375
t.equal(dragend.callCount, 0);
373376

374-
simulate.mouseup(map.getCanvas(), {buttons: 0, button: 2});
377+
simulate.mouseup(map.getCanvas(), {buttons, button: 2});
375378
map._renderTaskQueue.run();
376379
t.equal(dragstart.callCount, 1);
377380
t.equal(drag.callCount, 1);
378381
t.equal(dragend.callCount, 0);
379382

380-
simulate.mousemove(map.getCanvas(), {clientX: 20, clientY: 20});
383+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 20, clientY: 20});
381384
map._renderTaskQueue.run();
382385
t.equal(dragstart.callCount, 1);
383386
t.equal(drag.callCount, 2);
@@ -409,7 +412,7 @@ test('DragPanHandler does not begin a drag if preventDefault is called on the mo
409412
simulate.mousedown(map.getCanvas());
410413
map._renderTaskQueue.run();
411414

412-
simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10});
415+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10});
413416
map._renderTaskQueue.run();
414417

415418
simulate.mouseup(map.getCanvas());
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {test} from '../../../util/test';
2+
import {extend} from '../../../../src/util/util';
3+
import window from '../../../../src/util/window';
4+
import Map from '../../../../src/ui/map';
5+
import DOM from '../../../../src/util/dom';
6+
import simulate from '../../../util/simulate_interaction';
7+
import browser from '../../../../src/util/browser';
8+
9+
function createMap(t, options) {
10+
t.stub(Map.prototype, '_detectMissingCSS');
11+
return new Map(extend({container: DOM.create('div', '', window.document.body)}, options));
12+
}
13+
14+
test('MouseRotateHandler#isActive', (t) => {
15+
const map = createMap(t);
16+
const mouseRotate = map.handlers._handlersById.mouseRotate;
17+
18+
// Prevent inertial rotation.
19+
t.stub(browser, 'now').returns(0);
20+
t.equal(mouseRotate.isActive(), false);
21+
22+
simulate.mousedown(map.getCanvas(), {buttons: 2, button: 2});
23+
map._renderTaskQueue.run();
24+
t.equal(mouseRotate.isActive(), false);
25+
26+
simulate.mousemove(map.getCanvas(), {buttons: 2, clientX: 10, clientY: 10});
27+
map._renderTaskQueue.run();
28+
t.equal(mouseRotate.isActive(), true);
29+
30+
simulate.mouseup(map.getCanvas(), {buttons: 0, button: 2});
31+
map._renderTaskQueue.run();
32+
t.equal(mouseRotate.isActive(), false);
33+
34+
map.remove();
35+
t.end();
36+
});
37+
38+
test('MouseRotateHandler#isActive #4622 regression test', (t) => {
39+
const map = createMap(t);
40+
const mouseRotate = map.handlers._handlersById.mouseRotate;
41+
42+
// Prevent inertial rotation.
43+
simulate.mousedown(map.getCanvas(), {buttons: 2, button: 2});
44+
map._renderTaskQueue.run();
45+
t.equal(mouseRotate.isActive(), false);
46+
47+
simulate.mousemove(map.getCanvas(), {buttons: 2, clientX: 10, clientY: 10});
48+
map._renderTaskQueue.run();
49+
t.equal(mouseRotate.isActive(), true);
50+
51+
// Some browsers don't fire mouseup when it happens outside the window.
52+
// Make the handler in active when it encounters a mousemove without the button pressed.
53+
54+
simulate.mousemove(map.getCanvas(), {buttons: 0, clientX: 10, clientY: 10});
55+
map._renderTaskQueue.run();
56+
t.equal(mouseRotate.isActive(), false);
57+
58+
map.remove();
59+
t.end();
60+
});

test/unit/ui/map/isMoving.test.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ function createMap(t) {
1010
return new Map({container: DOM.create('div', '', window.document.body)});
1111
}
1212

13+
// MouseEvent.buttons
14+
const buttons = 1;
15+
1316
test('Map#isMoving returns false by default', (t) => {
1417
const map = createMap(t);
1518
t.equal(map.isMoving(), false);
@@ -49,7 +52,7 @@ test('Map#isMoving returns true when drag panning', (t) => {
4952
simulate.mousedown(map.getCanvas());
5053
map._renderTaskQueue.run();
5154

52-
simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10});
55+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10});
5356
map._renderTaskQueue.run();
5457

5558
simulate.mouseup(map.getCanvas());
@@ -139,7 +142,7 @@ test('Map#isMoving returns true when drag panning and scroll zooming interleave'
139142
simulate.mousedown(map.getCanvas());
140143
map._renderTaskQueue.run();
141144

142-
simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10});
145+
simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10});
143146
map._renderTaskQueue.run();
144147

145148
const browserNow = t.stub(browser, 'now');

0 commit comments

Comments
 (0)