diff --git a/src/ui/control/fullscreen_control.test.ts b/src/ui/control/fullscreen_control.test.ts index 2ef11075b2..55ca7e0f4e 100644 --- a/src/ui/control/fullscreen_control.test.ts +++ b/src/ui/control/fullscreen_control.test.ts @@ -77,4 +77,61 @@ describe('FullscreenControl', () => { fullscreen._fullscreenButton.dispatchEvent(click); expect(fullscreenend).toHaveBeenCalled(); }); + + test('disables cooperative gestures when fullscreen becomes active', () => { + const cooperativeGestures = true; + const map = createMap({cooperativeGestures}); + const fullscreen = new FullscreenControl({}); + + map.addControl(fullscreen); + + const click = new window.Event('click'); + + // Simulate a click to the fullscreen button + fullscreen._fullscreenButton.dispatchEvent(click); + expect(map.getCooperativeGestures()).toBeFalsy(); + + // Second simulated click would exit fullscreen mode + fullscreen._fullscreenButton.dispatchEvent(click); + expect(map.getCooperativeGestures()).toBe(cooperativeGestures); + }); + + test('reenables cooperative gestures custom options when fullscreen exits', () => { + const cooperativeGestures = { + 'windowsHelpText': 'Custom message', + 'macHelpText': 'Custom message', + 'mobileHelpText': 'Custom message', + }; + const map = createMap({cooperativeGestures}); + const fullscreen = new FullscreenControl({}); + + map.addControl(fullscreen); + + const click = new window.Event('click'); + + // Simulate a click to the fullscreen button + fullscreen._fullscreenButton.dispatchEvent(click); + expect(map.getCooperativeGestures()).toBeFalsy(); + + // Second simulated click would exit fullscreen mode + fullscreen._fullscreenButton.dispatchEvent(click); + expect(map.getCooperativeGestures()).toEqual(cooperativeGestures); + }); + + test('if never set, cooperative gestures remain disabled when fullscreen exits', () => { + const map = createMap({cooperativeGestures: false}); + const fullscreen = new FullscreenControl({}); + + map.addControl(fullscreen); + + const click = new window.Event('click'); + + // Simulate a click to the fullscreen button + fullscreen._fullscreenButton.dispatchEvent(click); + expect(map.getCooperativeGestures()).toBeFalsy(); + + // Second simulated click would exit fullscreen mode + fullscreen._fullscreenButton.dispatchEvent(click); + expect(map.getCooperativeGestures()).toBeFalsy(); + }); }); diff --git a/src/ui/control/fullscreen_control.ts b/src/ui/control/fullscreen_control.ts index 370d638b9b..91cc38c2fd 100644 --- a/src/ui/control/fullscreen_control.ts +++ b/src/ui/control/fullscreen_control.ts @@ -5,6 +5,7 @@ import {warnOnce} from '../../util/util'; import {Event, Evented} from '../../util/evented'; import type Map from '../map'; import type {IControl} from './control'; +import type {GestureOptions} from '../map'; type FullscreenOptions = { container?: HTMLElement; @@ -13,6 +14,8 @@ type FullscreenOptions = { /** * A `FullscreenControl` control contains a button for toggling the map in and out of fullscreen mode. * When [requestFullscreen](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen) is not supported, fullscreen is handled via CSS properties. + * The map's `cooperativeGestures` option is temporarily disabled while the map + * is in fullscreen mode, and is restored when the map exist fullscreen mode. * * @implements {IControl} * @param {Object} [options] @@ -46,6 +49,7 @@ class FullscreenControl extends Evented implements IControl { _fullscreenchange: string; _fullscreenButton: HTMLButtonElement; _container: HTMLElement; + _prevCooperativeGestures: boolean | GestureOptions; constructor(options: FullscreenOptions = {}) { super(); @@ -127,8 +131,16 @@ class FullscreenControl extends Evented implements IControl { if (this._fullscreen) { this.fire(new Event('fullscreenstart')); + if (this._map._cooperativeGestures) { + this._prevCooperativeGestures = this._map._cooperativeGestures; + this._map.setCooperativeGestures(); + } } else { this.fire(new Event('fullscreenend')); + if (this._prevCooperativeGestures) { + this._map.setCooperativeGestures(this._prevCooperativeGestures); + delete this._prevCooperativeGestures; + } } } diff --git a/src/ui/handler/cooperative_gestures.test.ts b/src/ui/handler/cooperative_gestures.test.ts index dccd1334f8..5e45d35a68 100644 --- a/src/ui/handler/cooperative_gestures.test.ts +++ b/src/ui/handler/cooperative_gestures.test.ts @@ -4,7 +4,7 @@ import DOM from '../../util/dom'; import simulate from '../../../test/unit/lib/simulate_interaction'; import {setMatchMedia, setPerformance, setWebGlContext} from '../../util/test/util'; -function createMap() { +function createMap(cooperativeGestures) { return new Map({ container: DOM.create('div', '', window.document.body), style: { @@ -12,7 +12,7 @@ function createMap() { 'sources': {}, 'layers': [] }, - cooperativeGestures: true + cooperativeGestures }); } @@ -29,7 +29,7 @@ describe('CoopGesturesHandler', () => { let now = 1555555555555; browserNow.mockReturnValue(now); - const map = createMap(); + const map = createMap(true); map._renderTaskQueue.run(); const startZoom = map.getZoom(); @@ -47,21 +47,42 @@ describe('CoopGesturesHandler', () => { map.remove(); }); - test('Zooms on wheel if control key is down', () => { - // NOTE: This should pass regardless of whether cooperativeGestures is enabled or not + test('Zooms on wheel if no key is down after disabling cooperative gestures', () => { const browserNow = jest.spyOn(browser, 'now'); let now = 1555555555555; browserNow.mockReturnValue(now); - const map = createMap(); + const map = createMap(true); + map.setCooperativeGestures(false); map._renderTaskQueue.run(); - simulate.keydown(document, {type: 'keydown', key: 'Control'}); + const startZoom = map.getZoom(); + // simulate a single 'wheel' event + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta}); + map._renderTaskQueue.run(); + + now += 400; + browserNow.mockReturnValue(now); + map._renderTaskQueue.run(); + + const endZoom = map.getZoom(); + expect(endZoom - startZoom).toBeCloseTo(0.0285, 3); + + map.remove(); + }); + + test('Zooms on wheel if control key is down', () => { + // NOTE: This should pass regardless of whether cooperativeGestures is enabled or not + const browserNow = jest.spyOn(browser, 'now'); + let now = 1555555555555; + browserNow.mockReturnValue(now); + + const map = createMap(true); map._renderTaskQueue.run(); const startZoom = map.getZoom(); // simulate a single 'wheel' event - simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta}); + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta, ctrlKey: true}); map._renderTaskQueue.run(); now += 400; @@ -75,7 +96,7 @@ describe('CoopGesturesHandler', () => { }); test('Does not pan on touchmove with a single touch', () => { - const map = createMap(); + const map = createMap(true); const target = map.getCanvas(); const startCenter = map.getCenter(); map._renderTaskQueue.run(); @@ -104,9 +125,40 @@ describe('CoopGesturesHandler', () => { map.remove(); }); + test('Pans on touchmove with a single touch after disabling cooperative gestures', () => { + const map = createMap(true); + map.setCooperativeGestures(false); + const target = map.getCanvas(); + const startCenter = map.getCenter(); + map._renderTaskQueue.run(); + + const dragstart = jest.fn(); + const drag = jest.fn(); + const dragend = jest.fn(); + + map.on('dragstart', dragstart); + map.on('drag', drag); + map.on('dragend', dragend); + + simulate.touchstart(target, {touches: [{target, clientX: 0, clientY: 0}, {target, clientX: 1, clientY: 1}]}); + map._renderTaskQueue.run(); + + simulate.touchmove(target, {touches: [{target, clientX: 10, clientY: 10}, {target, clientX: 11, clientY: 11}]}); + map._renderTaskQueue.run(); + + simulate.touchend(target); + map._renderTaskQueue.run(); + + const endCenter = map.getCenter(); + expect(endCenter.lng).toBeGreaterThan(startCenter.lng); + expect(endCenter.lat).toBeGreaterThan(startCenter.lat); + + map.remove(); + }); + test('Does pan on touchmove with a double touch', () => { // NOTE: This should pass regardless of whether cooperativeGestures is enabled or not - const map = createMap(); + const map = createMap(true); const target = map.getCanvas(); const startCenter = map.getCenter(); map._renderTaskQueue.run(); @@ -137,7 +189,7 @@ describe('CoopGesturesHandler', () => { test('Drag pitch works with 3 fingers', () => { // NOTE: This should pass regardless of whether cooperativeGestures is enabled or not - const map = createMap(); + const map = createMap(true); const target = map.getCanvas(); const startPitch = map.getPitch(); map._renderTaskQueue.run(); @@ -164,4 +216,42 @@ describe('CoopGesturesHandler', () => { map.remove(); }); + + test('Initially disabled cooperative gestures can be later enabled', () => { + const browserNow = jest.spyOn(browser, 'now'); + let now = 1555555555555; + browserNow.mockReturnValue(now); + + const map = createMap(false); + map._renderTaskQueue.run(); + + const startZoom = map.getZoom(); + // simulate a single 'wheel' event + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta}); + map._renderTaskQueue.run(); + + now += 400; + browserNow.mockReturnValue(now); + map._renderTaskQueue.run(); + + const midZoom = map.getZoom(); + expect(midZoom - startZoom).toBeCloseTo(0.0285, 3); + + // Enable cooperative gestures + map.setCooperativeGestures(true); + + // This 'wheel' event should not zoom + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta}); + map._renderTaskQueue.run(); + + now += 400; + browserNow.mockReturnValue(now); + map._renderTaskQueue.run(); + + const endZoom = map.getZoom(); + expect(endZoom).toBeCloseTo(midZoom); + + map.remove(); + }); + }); diff --git a/src/ui/map.test.ts b/src/ui/map.test.ts index dfd8f56b09..23918fbd2e 100755 --- a/src/ui/map.test.ts +++ b/src/ui/map.test.ts @@ -2294,6 +2294,62 @@ describe('Map', () => { await sourcePromise; }); + describe('#setCooperativeGestures', () => { + test('returns self', () => { + const map = createMap(); + expect(map.setCooperativeGestures(true)).toBe(map); + }); + + test('can be called more than once', () => { + const map = createMap(); + map.setCooperativeGestures(true); + map.setCooperativeGestures(true); + }); + + test('calling set with no arguments turns cooperative gestures off', done => { + const map = createMap({cooperativeGestures: true}); + map.on('load', () => { + map.setCooperativeGestures(); + expect(map.getCooperativeGestures()).toBeFalsy(); + done(); + }); + }); + }); + + describe('#getCooperativeGestures', () => { + test('returns the cooperative gestures option', done => { + const map = createMap({cooperativeGestures: true}); + + map.on('load', () => { + expect(map.getCooperativeGestures()).toBe(true); + done(); + }); + }); + + test('returns falsy if cooperative gestures option is not specified', done => { + const map = createMap(); + + map.on('load', () => { + expect(map.getCooperativeGestures()).toBeFalsy(); + done(); + }); + }); + + test('returns the cooperative gestures option with custom messages', done => { + const option = { + 'windowsHelpText': 'Custom message', + 'macHelpText': 'Custom message', + 'mobileHelpText': 'Custom message', + }; + const map = createMap({cooperativeGestures: option}); + + map.on('load', () => { + expect(map.getCooperativeGestures()).toEqual(option); + done(); + }); + }); + }); + describe('getCameraTargetElevation', () => { test('Elevation is zero without terrain, and matches any given terrain', () => { const map = createMap(); diff --git a/src/ui/map.ts b/src/ui/map.ts index a1965a700c..0c93ecaf1e 100644 --- a/src/ui/map.ts +++ b/src/ui/map.ts @@ -238,7 +238,7 @@ const defaultOptions = { * @param {boolean} [options.doubleClickZoom=true] If `true`, the "double click to zoom" interaction is enabled (see {@link DoubleClickZoomHandler}). * @param {boolean|Object} [options.touchZoomRotate=true] If `true`, the "pinch to rotate and zoom" interaction is enabled. An `Object` value is passed as options to {@link TwoFingersTouchZoomRotateHandler#enable}. * @param {boolean|Object} [options.touchPitch=true] If `true`, the "drag to pitch" interaction is enabled. An `Object` value is passed as options to {@link TwoFingersTouchPitchHandler#enable}. - * @param {boolean|GestureOptions} [options.cooperativeGestures=undefined] If `true` or set to an options object, map is only accessible on desktop while holding Command/Ctrl and only accessible on mobile with two fingers. Interacting with the map using normal gestures will trigger an informational screen. With this option enabled, "drag to pitch" requires a three-finger gesture. + * @param {boolean|GestureOptions} [options.cooperativeGestures=undefined] If `true` or set to an options object, map is only accessible on desktop while holding Command/Ctrl and only accessible on mobile with two fingers. Interacting with the map using normal gestures will trigger an informational screen. With this option enabled, "drag to pitch" requires a three-finger gesture. Cooperative gestures are disabled when a map enters fullscreen using {@link #FullscreenControl}. * A valid options object includes the following properties to customize the text on the informational screen. The values below are the defaults. * { * windowsHelpText: "Use Ctrl + scroll to zoom the map", @@ -459,6 +459,7 @@ class Map extends Camera { '_onWindowOnline', '_onWindowResize', '_onMapScroll', + '_cooperativeGesturesOnWheel', '_contextLost', '_contextRestored' ], this); @@ -914,6 +915,32 @@ class Map extends Camera { return this._update(); } + /** + * Gets the map's cooperativeGestures option + * + * @returns {GestureOptions} gestureOptions + */ + getCooperativeGestures() { + return this._cooperativeGestures; + } + + /** + * Sets or clears the map's cooperativeGestures option + * + * @param {GestureOptions | null | undefined} gestureOptions If `true` or set to an options object, map is only accessible on desktop while holding Command/Ctrl and only accessible on mobile with two fingers. Interacting with the map using normal gestures will trigger an informational screen. With this option enabled, "drag to pitch" requires a three-finger gesture. + * @returns {Map} `this` + */ + setCooperativeGestures(gestureOptions?: GestureOptions | boolean | null) { + this._cooperativeGestures = gestureOptions; + if (this._cooperativeGestures) { + this._setupCooperativeGestures(); + } else { + this._destroyCooperativeGestures(); + } + + return this; + } + /** * Returns a [Point](https://github.com/mapbox/point-geometry) representing pixel coordinates, relative to the map's `container`, * that correspond to the specified geographical location. @@ -2599,6 +2626,10 @@ class Map extends Camera { this._container.addEventListener('scroll', this._onMapScroll, false); } + _cooperativeGesturesOnWheel(e) { + this._onCooperativeGesture(e, e[this._metaKey], 1); + } + _setupCooperativeGestures() { const container = this._container; this._cooperativeGesturesScreen = DOM.create('div', 'maplibregl-cooperative-gesture-screen', container); @@ -2614,13 +2645,18 @@ class Map extends Camera {
${mobileMessage}
`; // Add event to canvas container since gesture container is pointer-events: none - this._canvasContainer.addEventListener('wheel', (e) => { - this._onCooperativeGesture(e, e[this._metaKey], 1); - }, false); + this._canvasContainer.addEventListener('wheel', this._cooperativeGesturesOnWheel, false); // Remove the traditional pan classes this._canvasContainer.classList.remove('maplibregl-touch-drag-pan'); } + _destroyCooperativeGestures() { + DOM.remove(this._cooperativeGesturesScreen); + this._canvasContainer.removeEventListener('wheel', this._cooperativeGesturesOnWheel, false); + // Add the traditional pan classes back + this._canvasContainer.classList.add('maplibregl-touch-drag-pan'); + } + _resizeCanvas(width: number, height: number, pixelRatio: number) { // Request the required canvas size taking the pixelratio into account. this._canvas.width = pixelRatio * width; @@ -2955,7 +2991,7 @@ class Map extends Camera { DOM.remove(this._canvasContainer); DOM.remove(this._controlContainer); if (this._cooperativeGestures) { - DOM.remove(this._cooperativeGesturesScreen); + this._destroyCooperativeGestures(); } this._container.classList.remove('maplibregl-map'); diff --git a/test/debug-pages/cooperative-gestures.html b/test/debug-pages/cooperative-gestures.html index 3fd87b2096..fa5216f9a5 100644 --- a/test/debug-pages/cooperative-gestures.html +++ b/test/debug-pages/cooperative-gestures.html @@ -30,6 +30,7 @@


+
@@ -147,6 +148,10 @@ map.dragRotate._pitchWithRotate = !!this.checked; }; +document.getElementById('cooperative-gestures-checkbox').onclick = function() { + map.setCooperativeGestures(!!this.checked); +}; +