Skip to content

Commit 176ed20

Browse files
authored
Add method to enable/disable cooperative gestures (#2152)
* Change how meta key is detected for coop. gestures UIEvents like 'wheel' include properties for whether some keys are currently pressed, including ctrl and meta. [0] This should be less prone to error, specifically when the user presses or depresses one of these keys while the browser document is not active. Adds a debug page for cooperative gestures because this feature cannot be tested with a fullscreen map, and the only other test page is a fullscreen map. [0] https://w3c.github.io/uievents/#dom-mouseevent-ctrlkey * Add method to enable/disable cooperativeGestures Fixes #2057 Also disable cooperative gestures in fullscreen, using these new methods (Fixes #1488) * Changelog * Combine imports from map The syntax is a bit odd. Background here: tc39/proposal-type-annotations#16 * Remove debug page * Specify event type * Add Map._getMetaKey() * Add cooperative gestures class * Add return value to _getMetaKey() * Add _metaKey property, initialize in constructor * Update expectedBytes to size of bundle on main
1 parent 4a8832a commit 176ed20

File tree

9 files changed

+280
-31
lines changed

9 files changed

+280
-31
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- Improve performance by sending style layers to worker thread before processing it on main thread to allow parallel processing ([#2131](https://github.com/maplibre/maplibre-gl-js/pull/2131))
55
- [Breaking] Resize map when container element is resized. the resize related events now has different data associated with it ([#2157](https://github.com/maplibre/maplibre-gl-js/pull/2157))
66
- Add Map.getImage() to retrieve previously-loaded images. ([#2168](https://github.com/maplibre/maplibre-gl-js/pull/2168))
7+
- Add method to enable/disable cooperative gestures
78
- *...Add new stuff here...*
89

910
### 🐞 Bug fixes
@@ -22,6 +23,7 @@
2223

2324
### 🐞 Bug fixes
2425
- Fix the worker been terminated on setting new style ([#2123](https://github.com/maplibre/maplibre-gl-js/pull/2123))
26+
- Change how meta key is detected for cooperative gestures
2527

2628

2729
## 3.0.0-pre.3

src/css/maplibre-gl.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
touch-action: none;
5454
}
5555

56+
.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures,
57+
.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures .maplibregl-canvas {
58+
touch-action: pan-x pan-y;
59+
}
60+
5661
.maplibregl-ctrl-top-left,
5762
.maplibregl-ctrl-top-right,
5863
.maplibregl-ctrl-bottom-left,

src/ui/control/fullscreen_control.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,61 @@ describe('FullscreenControl', () => {
7676
fullscreen._fullscreenButton.dispatchEvent(click);
7777
expect(fullscreenend).toHaveBeenCalled();
7878
});
79+
80+
test('disables cooperative gestures when fullscreen becomes active', () => {
81+
const cooperativeGestures = true;
82+
const map = createMap({cooperativeGestures});
83+
const fullscreen = new FullscreenControl({});
84+
85+
map.addControl(fullscreen);
86+
87+
const click = new window.Event('click');
88+
89+
// Simulate a click to the fullscreen button
90+
fullscreen._fullscreenButton.dispatchEvent(click);
91+
expect(map.getCooperativeGestures()).toBeFalsy();
92+
93+
// Second simulated click would exit fullscreen mode
94+
fullscreen._fullscreenButton.dispatchEvent(click);
95+
expect(map.getCooperativeGestures()).toBe(cooperativeGestures);
96+
});
97+
98+
test('reenables cooperative gestures custom options when fullscreen exits', () => {
99+
const cooperativeGestures = {
100+
'windowsHelpText': 'Custom message',
101+
'macHelpText': 'Custom message',
102+
'mobileHelpText': 'Custom message',
103+
};
104+
const map = createMap({cooperativeGestures});
105+
const fullscreen = new FullscreenControl({});
106+
107+
map.addControl(fullscreen);
108+
109+
const click = new window.Event('click');
110+
111+
// Simulate a click to the fullscreen button
112+
fullscreen._fullscreenButton.dispatchEvent(click);
113+
expect(map.getCooperativeGestures()).toBeFalsy();
114+
115+
// Second simulated click would exit fullscreen mode
116+
fullscreen._fullscreenButton.dispatchEvent(click);
117+
expect(map.getCooperativeGestures()).toEqual(cooperativeGestures);
118+
});
119+
120+
test('if never set, cooperative gestures remain disabled when fullscreen exits', () => {
121+
const map = createMap({cooperativeGestures: false});
122+
const fullscreen = new FullscreenControl({});
123+
124+
map.addControl(fullscreen);
125+
126+
const click = new window.Event('click');
127+
128+
// Simulate a click to the fullscreen button
129+
fullscreen._fullscreenButton.dispatchEvent(click);
130+
expect(map.getCooperativeGestures()).toBeFalsy();
131+
132+
// Second simulated click would exit fullscreen mode
133+
fullscreen._fullscreenButton.dispatchEvent(click);
134+
expect(map.getCooperativeGestures()).toBeFalsy();
135+
});
79136
});

src/ui/control/fullscreen_control.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import DOM from '../../util/dom';
33
import {warnOnce} from '../../util/util';
44

55
import {Event, Evented} from '../../util/evented';
6-
import type Map from '../map';
6+
import type {default as Map, GestureOptions} from '../map';
77
import type {IControl} from './control';
88

99
type FullscreenOptions = {
@@ -13,6 +13,8 @@ type FullscreenOptions = {
1313
/**
1414
* A `FullscreenControl` control contains a button for toggling the map in and out of fullscreen mode.
1515
* When [requestFullscreen](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen) is not supported, fullscreen is handled via CSS properties.
16+
* The map's `cooperativeGestures` option is temporarily disabled while the map
17+
* is in fullscreen mode, and is restored when the map exist fullscreen mode.
1618
*
1719
* @implements {IControl}
1820
* @param {Object} [options]
@@ -46,6 +48,7 @@ class FullscreenControl extends Evented implements IControl {
4648
_fullscreenchange: string;
4749
_fullscreenButton: HTMLButtonElement;
4850
_container: HTMLElement;
51+
_prevCooperativeGestures: boolean | GestureOptions;
4952

5053
constructor(options: FullscreenOptions = {}) {
5154
super();
@@ -127,8 +130,16 @@ class FullscreenControl extends Evented implements IControl {
127130

128131
if (this._fullscreen) {
129132
this.fire(new Event('fullscreenstart'));
133+
if (this._map._cooperativeGestures) {
134+
this._prevCooperativeGestures = this._map._cooperativeGestures;
135+
this._map.setCooperativeGestures();
136+
}
130137
} else {
131138
this.fire(new Event('fullscreenend'));
139+
if (this._prevCooperativeGestures) {
140+
this._map.setCooperativeGestures(this._prevCooperativeGestures);
141+
delete this._prevCooperativeGestures;
142+
}
132143
}
133144
}
134145

src/ui/handler/cooperative_gestures.test.ts

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import DOM from '../../util/dom';
44
import simulate from '../../../test/unit/lib/simulate_interaction';
55
import {beforeMapTest} from '../../util/test/util';
66

7-
function createMap() {
7+
function createMap(cooperativeGestures) {
88
return new Map({
99
container: DOM.create('div', '', window.document.body),
1010
style: {
1111
'version': 8,
1212
'sources': {},
1313
'layers': []
1414
},
15-
cooperativeGestures: true
15+
cooperativeGestures
1616
});
1717
}
1818

@@ -27,7 +27,7 @@ describe('CoopGesturesHandler', () => {
2727
let now = 1555555555555;
2828
browserNow.mockReturnValue(now);
2929

30-
const map = createMap();
30+
const map = createMap(true);
3131
map._renderTaskQueue.run();
3232

3333
const startZoom = map.getZoom();
@@ -45,21 +45,42 @@ describe('CoopGesturesHandler', () => {
4545
map.remove();
4646
});
4747

48-
test('Zooms on wheel if control key is down', () => {
49-
// NOTE: This should pass regardless of whether cooperativeGestures is enabled or not
48+
test('Zooms on wheel if no key is down after disabling cooperative gestures', () => {
5049
const browserNow = jest.spyOn(browser, 'now');
5150
let now = 1555555555555;
5251
browserNow.mockReturnValue(now);
5352

54-
const map = createMap();
53+
const map = createMap(true);
54+
map.setCooperativeGestures(false);
5555
map._renderTaskQueue.run();
5656

57-
simulate.keydown(document, {type: 'keydown', key: 'Control'});
57+
const startZoom = map.getZoom();
58+
// simulate a single 'wheel' event
59+
simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta});
60+
map._renderTaskQueue.run();
61+
62+
now += 400;
63+
browserNow.mockReturnValue(now);
64+
map._renderTaskQueue.run();
65+
66+
const endZoom = map.getZoom();
67+
expect(endZoom - startZoom).toBeCloseTo(0.0285, 3);
68+
69+
map.remove();
70+
});
71+
72+
test('Zooms on wheel if control key is down', () => {
73+
// NOTE: This should pass regardless of whether cooperativeGestures is enabled or not
74+
const browserNow = jest.spyOn(browser, 'now');
75+
let now = 1555555555555;
76+
browserNow.mockReturnValue(now);
77+
78+
const map = createMap(true);
5879
map._renderTaskQueue.run();
5980

6081
const startZoom = map.getZoom();
6182
// simulate a single 'wheel' event
62-
simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta});
83+
simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta, ctrlKey: true});
6384
map._renderTaskQueue.run();
6485

6586
now += 400;
@@ -73,7 +94,7 @@ describe('CoopGesturesHandler', () => {
7394
});
7495

7596
test('Does not pan on touchmove with a single touch', () => {
76-
const map = createMap();
97+
const map = createMap(true);
7798
const target = map.getCanvas();
7899
const startCenter = map.getCenter();
79100
map._renderTaskQueue.run();
@@ -102,9 +123,40 @@ describe('CoopGesturesHandler', () => {
102123
map.remove();
103124
});
104125

126+
test('Pans on touchmove with a single touch after disabling cooperative gestures', () => {
127+
const map = createMap(true);
128+
map.setCooperativeGestures(false);
129+
const target = map.getCanvas();
130+
const startCenter = map.getCenter();
131+
map._renderTaskQueue.run();
132+
133+
const dragstart = jest.fn();
134+
const drag = jest.fn();
135+
const dragend = jest.fn();
136+
137+
map.on('dragstart', dragstart);
138+
map.on('drag', drag);
139+
map.on('dragend', dragend);
140+
141+
simulate.touchstart(target, {touches: [{target, clientX: 0, clientY: 0}, {target, clientX: 1, clientY: 1}]});
142+
map._renderTaskQueue.run();
143+
144+
simulate.touchmove(target, {touches: [{target, clientX: 10, clientY: 10}, {target, clientX: 11, clientY: 11}]});
145+
map._renderTaskQueue.run();
146+
147+
simulate.touchend(target);
148+
map._renderTaskQueue.run();
149+
150+
const endCenter = map.getCenter();
151+
expect(endCenter.lng).toBeGreaterThan(startCenter.lng);
152+
expect(endCenter.lat).toBeGreaterThan(startCenter.lat);
153+
154+
map.remove();
155+
});
156+
105157
test('Does pan on touchmove with a double touch', () => {
106158
// NOTE: This should pass regardless of whether cooperativeGestures is enabled or not
107-
const map = createMap();
159+
const map = createMap(true);
108160
const target = map.getCanvas();
109161
const startCenter = map.getCenter();
110162
map._renderTaskQueue.run();
@@ -135,7 +187,7 @@ describe('CoopGesturesHandler', () => {
135187

136188
test('Drag pitch works with 3 fingers', () => {
137189
// NOTE: This should pass regardless of whether cooperativeGestures is enabled or not
138-
const map = createMap();
190+
const map = createMap(true);
139191
const target = map.getCanvas();
140192
const startPitch = map.getPitch();
141193
map._renderTaskQueue.run();
@@ -162,4 +214,42 @@ describe('CoopGesturesHandler', () => {
162214

163215
map.remove();
164216
});
217+
218+
test('Initially disabled cooperative gestures can be later enabled', () => {
219+
const browserNow = jest.spyOn(browser, 'now');
220+
let now = 1555555555555;
221+
browserNow.mockReturnValue(now);
222+
223+
const map = createMap(false);
224+
map._renderTaskQueue.run();
225+
226+
const startZoom = map.getZoom();
227+
// simulate a single 'wheel' event
228+
simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta});
229+
map._renderTaskQueue.run();
230+
231+
now += 400;
232+
browserNow.mockReturnValue(now);
233+
map._renderTaskQueue.run();
234+
235+
const midZoom = map.getZoom();
236+
expect(midZoom - startZoom).toBeCloseTo(0.0285, 3);
237+
238+
// Enable cooperative gestures
239+
map.setCooperativeGestures(true);
240+
241+
// This 'wheel' event should not zoom
242+
simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta});
243+
map._renderTaskQueue.run();
244+
245+
now += 400;
246+
browserNow.mockReturnValue(now);
247+
map._renderTaskQueue.run();
248+
249+
const endZoom = map.getZoom();
250+
expect(endZoom).toBeCloseTo(midZoom);
251+
252+
map.remove();
253+
});
254+
165255
});

src/ui/handler/scroll_zoom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ class ScrollZoomHandler {
158158
wheel(e: WheelEvent) {
159159
if (!this.isEnabled()) return;
160160
if (this._map._cooperativeGestures) {
161-
if (this._map._metaPress) {
161+
if (e[this._map._metaKey]) {
162162
e.preventDefault();
163163
} else {
164164
return;

src/ui/map.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2401,6 +2401,62 @@ describe('Map', () => {
24012401
await sourcePromise;
24022402
});
24032403

2404+
describe('#setCooperativeGestures', () => {
2405+
test('returns self', () => {
2406+
const map = createMap();
2407+
expect(map.setCooperativeGestures(true)).toBe(map);
2408+
});
2409+
2410+
test('can be called more than once', () => {
2411+
const map = createMap();
2412+
map.setCooperativeGestures(true);
2413+
map.setCooperativeGestures(true);
2414+
});
2415+
2416+
test('calling set with no arguments turns cooperative gestures off', done => {
2417+
const map = createMap({cooperativeGestures: true});
2418+
map.on('load', () => {
2419+
map.setCooperativeGestures();
2420+
expect(map.getCooperativeGestures()).toBeFalsy();
2421+
done();
2422+
});
2423+
});
2424+
});
2425+
2426+
describe('#getCooperativeGestures', () => {
2427+
test('returns the cooperative gestures option', done => {
2428+
const map = createMap({cooperativeGestures: true});
2429+
2430+
map.on('load', () => {
2431+
expect(map.getCooperativeGestures()).toBe(true);
2432+
done();
2433+
});
2434+
});
2435+
2436+
test('returns falsy if cooperative gestures option is not specified', done => {
2437+
const map = createMap();
2438+
2439+
map.on('load', () => {
2440+
expect(map.getCooperativeGestures()).toBeFalsy();
2441+
done();
2442+
});
2443+
});
2444+
2445+
test('returns the cooperative gestures option with custom messages', done => {
2446+
const option = {
2447+
'windowsHelpText': 'Custom message',
2448+
'macHelpText': 'Custom message',
2449+
'mobileHelpText': 'Custom message',
2450+
};
2451+
const map = createMap({cooperativeGestures: option});
2452+
2453+
map.on('load', () => {
2454+
expect(map.getCooperativeGestures()).toEqual(option);
2455+
done();
2456+
});
2457+
});
2458+
});
2459+
24042460
describe('getCameraTargetElevation', () => {
24052461
test('Elevation is zero without terrain, and matches any given terrain', () => {
24062462
const map = createMap();

0 commit comments

Comments
 (0)