diff --git a/src/ui/handler/mouse.js b/src/ui/handler/mouse.js index a2d4f41eeed..3cf5c532816 100644 --- a/src/ui/handler/mouse.js +++ b/src/ui/handler/mouse.js @@ -6,6 +6,17 @@ import type Point from '@mapbox/point-geometry'; const LEFT_BUTTON = 0; const RIGHT_BUTTON = 2; +// the values for each button in MouseEvent.buttons +const BUTTONS_FLAGS = { + [LEFT_BUTTON]: 1, + [RIGHT_BUTTON]: 2 +}; + +function buttonStillPressed(e: MouseEvent, button: number) { + const flag = BUTTONS_FLAGS[button]; + return e.buttons === undefined || (e.buttons & flag) !== flag; +} + class MouseHandler { _enabled: boolean; @@ -50,6 +61,17 @@ class MouseHandler { if (!lastPoint) return; e.preventDefault(); + if (buttonStillPressed(e, this._eventButton)) { + // Some browsers don't fire a `mouseup` when the mouseup occurs outside + // the window or iframe: + // https://github.com/mapbox/mapbox-gl-js/issues/4622 + // + // If the button is no longer pressed during this `mousemove` it may have + // been released outside of the window or iframe. + this.reset(); + return; + } + if (!this._moved && point.dist(lastPoint) < this._clickTolerance) return; this._moved = true; this._lastPoint = point; @@ -59,6 +81,7 @@ class MouseHandler { } mouseupWindow(e: MouseEvent) { + if (!this._lastPoint) return; const eventButton = DOM.mouseButton(e); if (eventButton !== this._eventButton) return; if (this._moved) DOM.suppressClick(); diff --git a/test/unit/ui/handler/drag_pan.test.js b/test/unit/ui/handler/drag_pan.test.js index 971c981186c..6f364ae3e8e 100644 --- a/test/unit/ui/handler/drag_pan.test.js +++ b/test/unit/ui/handler/drag_pan.test.js @@ -13,6 +13,9 @@ function createMap(t, clickTolerance, dragPan) { }); } +// MouseEvent.buttons = 1 // left button +const buttons = 1; + test('DragPanHandler fires dragstart, drag, and dragend events at appropriate times in response to a mouse-triggered drag', (t) => { const map = createMap(t); @@ -30,7 +33,7 @@ test('DragPanHandler fires dragstart, drag, and dragend events at appropriate ti t.equal(drag.callCount, 0); t.equal(dragend.callCount, 0); - simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10}); map._renderTaskQueue.run(); t.equal(dragstart.callCount, 1); t.equal(drag.callCount, 1); @@ -63,7 +66,7 @@ test('DragPanHandler captures mousemove events during a mouse-triggered drag (re t.equal(drag.callCount, 0); t.equal(dragend.callCount, 0); - simulate.mousemove(window.document.body, {clientX: 10, clientY: 10}); + simulate.mousemove(window.document.body, {buttons, clientX: 10, clientY: 10}); map._renderTaskQueue.run(); t.equal(dragstart.callCount, 1); t.equal(drag.callCount, 1); @@ -121,7 +124,7 @@ test('DragPanHandler prevents mousemove events from firing during a drag (#1555) simulate.mousedown(map.getCanvasContainer()); map._renderTaskQueue.run(); - simulate.mousemove(map.getCanvasContainer(), {clientX: 10, clientY: 10}); + simulate.mousemove(map.getCanvasContainer(), {buttons, clientX: 10, clientY: 10}); map._renderTaskQueue.run(); simulate.mouseup(map.getCanvasContainer()); @@ -142,7 +145,7 @@ test('DragPanHandler ends a mouse-triggered drag if the window blurs', (t) => { simulate.mousedown(map.getCanvas()); map._renderTaskQueue.run(); - simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10}); map._renderTaskQueue.run(); simulate.blur(window); @@ -176,14 +179,14 @@ test('DragPanHandler requests a new render frame after each mousemove event', (t const requestFrame = t.spy(map, '_requestRenderFrame'); simulate.mousedown(map.getCanvas()); - simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10}); t.ok(requestFrame.callCount > 0); map._renderTaskQueue.run(); // https://github.com/mapbox/mapbox-gl-js/issues/6063 requestFrame.resetHistory(); - simulate.mousemove(map.getCanvas(), {clientX: 20, clientY: 20}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 20, clientY: 20}); t.equal(requestFrame.callCount, 1); map.remove(); @@ -208,7 +211,7 @@ test('DragPanHandler can interleave with another handler', (t) => { t.equal(drag.callCount, 0); t.equal(dragend.callCount, 0); - simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10}); map._renderTaskQueue.run(); t.equal(dragstart.callCount, 1); t.equal(drag.callCount, 1); @@ -221,7 +224,7 @@ test('DragPanHandler can interleave with another handler', (t) => { t.equal(drag.callCount, 1); t.equal(dragend.callCount, 0); - simulate.mousemove(map.getCanvas(), {clientX: 20, clientY: 20}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 20, clientY: 20}); map._renderTaskQueue.run(); t.equal(dragstart.callCount, 1); t.equal(drag.callCount, 2); @@ -250,13 +253,13 @@ test('DragPanHandler can interleave with another handler', (t) => { map.on('drag', drag); map.on('dragend', dragend); - simulate.mousedown(map.getCanvas(), {[`${modifier}Key`]: true}); + simulate.mousedown(map.getCanvas(), {buttons, [`${modifier}Key`]: true}); map._renderTaskQueue.run(); t.equal(dragstart.callCount, 0); t.equal(drag.callCount, 0); t.equal(dragend.callCount, 0); - simulate.mousemove(map.getCanvas(), {[`${modifier}Key`]: true, clientX: 10, clientY: 10}); + simulate.mousemove(map.getCanvas(), {buttons, [`${modifier}Key`]: true, clientX: 10, clientY: 10}); map._renderTaskQueue.run(); t.equal(dragstart.callCount, 0); t.equal(drag.callCount, 0); @@ -296,7 +299,7 @@ test('DragPanHandler can interleave with another handler', (t) => { t.equal(drag.callCount, 0); t.equal(dragend.callCount, 0); - simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10}); map._renderTaskQueue.run(); t.equal(dragstart.callCount, 0); t.equal(drag.callCount, 0); @@ -359,25 +362,25 @@ test('DragPanHandler does not end a drag on right button mouseup', (t) => { t.equal(drag.callCount, 0); t.equal(dragend.callCount, 0); - simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10}); map._renderTaskQueue.run(); t.equal(dragstart.callCount, 1); t.equal(drag.callCount, 1); t.equal(dragend.callCount, 0); - simulate.mousedown(map.getCanvas(), {buttons: 2, button: 2}); + simulate.mousedown(map.getCanvas(), {buttons: buttons + 2, button: 2}); map._renderTaskQueue.run(); t.equal(dragstart.callCount, 1); t.equal(drag.callCount, 1); t.equal(dragend.callCount, 0); - simulate.mouseup(map.getCanvas(), {buttons: 0, button: 2}); + simulate.mouseup(map.getCanvas(), {buttons, button: 2}); map._renderTaskQueue.run(); t.equal(dragstart.callCount, 1); t.equal(drag.callCount, 1); t.equal(dragend.callCount, 0); - simulate.mousemove(map.getCanvas(), {clientX: 20, clientY: 20}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 20, clientY: 20}); map._renderTaskQueue.run(); t.equal(dragstart.callCount, 1); t.equal(drag.callCount, 2); @@ -409,7 +412,7 @@ test('DragPanHandler does not begin a drag if preventDefault is called on the mo simulate.mousedown(map.getCanvas()); map._renderTaskQueue.run(); - simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10}); map._renderTaskQueue.run(); simulate.mouseup(map.getCanvas()); diff --git a/test/unit/ui/handler/mouse_rotate.test.js b/test/unit/ui/handler/mouse_rotate.test.js new file mode 100644 index 00000000000..0a587b7a0dd --- /dev/null +++ b/test/unit/ui/handler/mouse_rotate.test.js @@ -0,0 +1,60 @@ +import {test} from '../../../util/test'; +import {extend} from '../../../../src/util/util'; +import window from '../../../../src/util/window'; +import Map from '../../../../src/ui/map'; +import DOM from '../../../../src/util/dom'; +import simulate from '../../../util/simulate_interaction'; +import browser from '../../../../src/util/browser'; + +function createMap(t, options) { + t.stub(Map.prototype, '_detectMissingCSS'); + return new Map(extend({container: DOM.create('div', '', window.document.body)}, options)); +} + +test('MouseRotateHandler#isActive', (t) => { + const map = createMap(t); + const mouseRotate = map.handlers._handlersById.mouseRotate; + + // Prevent inertial rotation. + t.stub(browser, 'now').returns(0); + t.equal(mouseRotate.isActive(), false); + + simulate.mousedown(map.getCanvas(), {buttons: 2, button: 2}); + map._renderTaskQueue.run(); + t.equal(mouseRotate.isActive(), false); + + simulate.mousemove(map.getCanvas(), {buttons: 2, clientX: 10, clientY: 10}); + map._renderTaskQueue.run(); + t.equal(mouseRotate.isActive(), true); + + simulate.mouseup(map.getCanvas(), {buttons: 0, button: 2}); + map._renderTaskQueue.run(); + t.equal(mouseRotate.isActive(), false); + + map.remove(); + t.end(); +}); + +test('MouseRotateHandler#isActive #4622 regression test', (t) => { + const map = createMap(t); + const mouseRotate = map.handlers._handlersById.mouseRotate; + + // Prevent inertial rotation. + simulate.mousedown(map.getCanvas(), {buttons: 2, button: 2}); + map._renderTaskQueue.run(); + t.equal(mouseRotate.isActive(), false); + + simulate.mousemove(map.getCanvas(), {buttons: 2, clientX: 10, clientY: 10}); + map._renderTaskQueue.run(); + t.equal(mouseRotate.isActive(), true); + + // Some browsers don't fire mouseup when it happens outside the window. + // Make the handler in active when it encounters a mousemove without the button pressed. + + simulate.mousemove(map.getCanvas(), {buttons: 0, clientX: 10, clientY: 10}); + map._renderTaskQueue.run(); + t.equal(mouseRotate.isActive(), false); + + map.remove(); + t.end(); +}); diff --git a/test/unit/ui/map/isMoving.test.js b/test/unit/ui/map/isMoving.test.js index ffc7d1ad324..0cf88158a63 100644 --- a/test/unit/ui/map/isMoving.test.js +++ b/test/unit/ui/map/isMoving.test.js @@ -10,6 +10,9 @@ function createMap(t) { return new Map({container: DOM.create('div', '', window.document.body)}); } +// MouseEvent.buttons +const buttons = 1; + test('Map#isMoving returns false by default', (t) => { const map = createMap(t); t.equal(map.isMoving(), false); @@ -49,7 +52,7 @@ test('Map#isMoving returns true when drag panning', (t) => { simulate.mousedown(map.getCanvas()); map._renderTaskQueue.run(); - simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10}); map._renderTaskQueue.run(); simulate.mouseup(map.getCanvas()); @@ -139,7 +142,7 @@ test('Map#isMoving returns true when drag panning and scroll zooming interleave' simulate.mousedown(map.getCanvas()); map._renderTaskQueue.run(); - simulate.mousemove(map.getCanvas(), {clientX: 10, clientY: 10}); + simulate.mousemove(map.getCanvas(), {buttons, clientX: 10, clientY: 10}); map._renderTaskQueue.run(); const browserNow = t.stub(browser, 'now');