Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add touch pan blocker to gesture handling for touch devices #11116

Merged
merged 26 commits into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5b5c2d0
initial implementation of touch pan blocker of gesture handler
Oct 4, 2021
294331a
add default locale to touch pan message -- needs refactoring (some re…
Oct 4, 2021
2afcb0d
Merge branch 'main' into avpeery/two-fingers-to-move
Oct 8, 2021
43c6b06
allow tap zoom with gesture handler implemented, fix flickering alert
Oct 12, 2021
b77f80d
Removed unneeded tap references in touch pan
Oct 12, 2021
5c8819a
added touch_pan gesture handling unit tests
Oct 12, 2021
2e64d7d
increased timeout for less flickering
Oct 12, 2021
56a3a06
added release testing page for gesture handling
Oct 12, 2021
42a093e
updated debug page for gesture handling
Oct 12, 2021
e8592d2
updated documentation for gesture handling to incoporate touch pan
Oct 13, 2021
e1d0c6c
git removed file name change not using
Oct 13, 2021
cb1cf61
fix scrollable page issue by toggling touch-action css property
Oct 13, 2021
8fd5561
Merge branch 'main' into avpeery/two-fingers-to-move
Oct 13, 2021
73d838a
override touch-action with specificity, next step - not showing alert…
Oct 14, 2021
78eb8f7
added in removing extra class
Oct 14, 2021
fc85d8b
use mapTouches and add comments
Oct 14, 2021
c77995d
add comments
Oct 14, 2021
6d4c404
fixed touch pitch handler to require three fingers if gesture handlin…
Oct 18, 2021
7399cac
remove removing override class while gesture handler still active
Oct 19, 2021
e1a12ac
fixed typos in unit test
Oct 19, 2021
8d3f030
Switched term from gestureHandling to cooperativeGestures
Oct 19, 2021
b7f0b63
forgot this name change
Oct 19, 2021
f5b2144
added clearTimeout to disable methods
Oct 19, 2021
686c5fc
addressed issue with css override class when touch zoom rotate is ena…
Oct 19, 2021
376da3e
changed override css class to pan-x pan-y
Oct 19, 2021
14395a0
add cooperative to comment
avpeery Oct 19, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions debug/scroll_zoom_blocker.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Scroll Zoom Blocker Control</title>
<title>Gesture Handling</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
Expand All @@ -12,7 +12,7 @@
</head>

<body>
<div id='map' style='width: 250px; height: 250px;'></div>
<div id='map' style='width: 400px; height: 400px;'></div>

<div style='width: 500px; height: 500px;'></div>
<script src='../dist/mapbox-gl-dev.js'></script>
Expand Down
7 changes: 7 additions & 0 deletions src/css/mapbox-gl.css
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ a.mapboxgl-ctrl-logo.mapboxgl-compact {
}
}

.mapboxgl-touch-pan-blocker,
.mapboxgl-scroll-zoom-blocker {
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
Expand All @@ -789,7 +790,13 @@ a.mapboxgl-ctrl-logo.mapboxgl-compact {
transition-delay: 1s;
}

.mapboxgl-touch-pan-blocker-show,
.mapboxgl-scroll-zoom-blocker-show {
opacity: 1;
transition: opacity 0.1s ease-in-out;
}

.mapboxgl-canvas-container.mapboxgl-touch-pan-blocker-override.mapboxgl-scrollable-page,
.mapboxgl-canvas-container.mapboxgl-touch-pan-blocker-override.mapboxgl-scrollable-page .mapboxgl-canvas {
touch-action: auto;
ansis marked this conversation as resolved.
Show resolved Hide resolved
}
3 changes: 2 additions & 1 deletion src/ui/default_locale.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const defaultLocale = {
'ScaleControl.Miles': 'mi',
'ScaleControl.NauticalMiles': 'nm',
'ScrollZoomBlocker.CtrlMessage': 'Use ctrl + scroll to zoom the map',
'ScrollZoomBlocker.CmdMessage': 'Use ⌘ + scroll to zoom the map'
'ScrollZoomBlocker.CmdMessage': 'Use ⌘ + scroll to zoom the map',
'TouchPanBlocker.Message': 'Use two fingers to move the map'
};

export default defaultLocale;
59 changes: 58 additions & 1 deletion src/ui/handler/touch_pan.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
// @flow

import Point from '@mapbox/point-geometry';
import type Map from '../map.js';
import {indexTouches} from './handler_util.js';
import {bindAll} from '../../util/util.js';
import DOM from '../../util/dom.js';

export default class TouchPanHandler {

_map: Map;
_el: HTMLElement;
_enabled: boolean;
_active: boolean;
_touches: { [string | number]: Point };
_minTouches: number;
_clickTolerance: number;
_sum: Point;
_alertContainer: HTMLElement;
_alertTimer: TimeoutID;

constructor(options: { clickTolerance: number }) {
constructor(map: Map, options: { clickTolerance: number }) {
this._map = map;
this._el = map.getCanvasContainer();
this._minTouches = 1;
this._clickTolerance = options.clickTolerance || 1;
this.reset();
bindAll(['_addTouchPanBlocker', '_showTouchPanBlockerAlert'], this);
}

reset() {
Expand All @@ -30,7 +40,21 @@ export default class TouchPanHandler {

touchmove(e: TouchEvent, points: Array<Point>, mapTouches: Array<Touch>) {
if (!this._active || mapTouches.length < this._minTouches) return;

// if gesture handling is set to true, require two fingers to touch pan
avpeery marked this conversation as resolved.
Show resolved Hide resolved
if (this._map._gestureHandling && !this._map.isMoving()) {
if (mapTouches.length === 1) {
this._showTouchPanBlockerAlert();
return;
} else if (this._alertContainer.style.visibility !== 'hidden') {
// immediately hide alert if it is visible when two fingers are used to pan.
this._alertContainer.style.visibility = 'hidden';
clearTimeout(this._alertTimer);
}
}

e.preventDefault();

return this._calculateTransform(e, points, mapTouches);
}

Expand Down Expand Up @@ -84,10 +108,19 @@ export default class TouchPanHandler {

enable() {
this._enabled = true;
if (this._map._gestureHandling) {
// override touch-action css property to enable scrolling page over map
this._el.classList.add('mapboxgl-touch-pan-blocker-override', 'mapboxgl-scrollable-page');
this._addTouchPanBlocker();
}
}

disable() {
this._enabled = false;
avpeery marked this conversation as resolved.
Show resolved Hide resolved
if (this._map._gestureHandling) {
this._el.classList.remove('mapboxgl-touch-pan-blocker-override', 'mapboxgl-scrollable-page');
this._alertContainer.remove();
}
this.reset();
}

Expand All @@ -98,4 +131,28 @@ export default class TouchPanHandler {
isActive() {
return this._active;
}

_addTouchPanBlocker() {
if (this._map && !this._alertContainer) {
this._alertContainer = DOM.create('div', 'mapboxgl-touch-pan-blocker', this._map._container);

this._alertContainer.textContent = this._map._getUIString('TouchPanBlocker.Message');

// dynamically set the font size of the touch pan blocker alert message
this._alertContainer.style.fontSize = `${Math.max(10, Math.min(24, Math.floor(this._el.clientWidth * 0.05)))}px`;
}
}

_showTouchPanBlockerAlert() {
if (this._alertContainer.style.visibility === 'hidden') this._alertContainer.style.visibility = 'visible';

this._alertContainer.classList.add('mapboxgl-touch-pan-blocker-show');

clearTimeout(this._alertTimer);

this._alertTimer = setTimeout(() => {
this._alertContainer.classList.remove('mapboxgl-touch-pan-blocker-show');
}, 500);
}

}
11 changes: 11 additions & 0 deletions src/ui/handler/touch_zoom_rotate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import Point from '@mapbox/point-geometry';
import DOM from '../../util/dom.js';
import type Map from '../map.js';

class TwoTouchHandler {

Expand Down Expand Up @@ -205,6 +206,12 @@ export class TouchPitchHandler extends TwoTouchHandler {
_valid: boolean | void;
_firstMove: number;
_lastPoints: [Point, Point];
_map: Map;

constructor(map: Map) {
super();
this._map = map;
}

reset() {
super.reset();
Expand All @@ -220,13 +227,17 @@ export class TouchPitchHandler extends TwoTouchHandler {
this._valid = false;

}

}

_move(points: [Point, Point], center: Point, e: TouchEvent) {
const vectorA = points[0].sub(this._lastPoints[0]);
const vectorB = points[1].sub(this._lastPoints[1]);

if (this._map._gestureHandling && e.touches.length < 3) return;

this._valid = this.gestureBeginsVertically(vectorA, vectorB, e.timeStamp);

if (!this._valid) return;

this._lastPoints = points;
Expand Down
5 changes: 3 additions & 2 deletions src/ui/handler_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class HandlerManager {
const tapDragZoom = new TapDragZoomHandler();
this._add('tapDragZoom', tapDragZoom);

const touchPitch = map.touchPitch = new TouchPitchHandler();
const touchPitch = map.touchPitch = new TouchPitchHandler(map);
this._add('touchPitch', touchPitch);

const mouseRotate = new MouseRotateHandler(options);
Expand All @@ -256,7 +256,7 @@ class HandlerManager {
this._add('mousePitch', mousePitch, ['mouseRotate']);

const mousePan = new MousePanHandler(options);
const touchPan = new TouchPanHandler(options);
const touchPan = new TouchPanHandler(map, options);
map.dragPan = new DragPanHandler(el, mousePan, touchPan);
this._add('mousePan', mousePan);
this._add('touchPan', touchPan, ['touchZoom', 'touchRotate']);
Expand Down Expand Up @@ -309,6 +309,7 @@ class HandlerManager {
isZooming() {
return !!this._eventsInProgress.zoom || this._map.scrollZoom.isZooming();
}

isRotating() {
return !!this._eventsInProgress.rotate;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ui/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,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 TouchZoomRotateHandler#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 TouchPitchHandler#enable}.
* @param {boolean} [options.gestureHandling=false] If `true`, scroll zoom will require pressing the ctrl or ⌘ key while scrolling to zoom map.
* @param {boolean} [options.gestureHandling=false] If `true`, scroll zoom will require pressing the ctrl or ⌘ key while scrolling to zoom map, and touch pan will require using two fingers while panning to move the map. Touch pitch will require three fingers to activate if enabled.
* @param {boolean} [options.trackResize=true] If `true`, the map will automatically resize when the browser window resizes.
* @param {LngLatLike} [options.center=[0, 0]] The inital geographical centerpoint of the map. If `center` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `[0, 0]` Note: Mapbox GL uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match GeoJSON.
* @param {number} [options.zoom=0] The initial zoom level of the map. If `zoom` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`.
Expand Down
1 change: 1 addition & 0 deletions test/release/scroll_zoom_blocker.html
64 changes: 64 additions & 0 deletions test/unit/ui/handler/touch_pan.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {test} from '../../../util/test.js';
import window from '../../../../src/util/window.js';
import Map from '../../../../src/ui/map.js';
import DOM from '../../../../src/util/dom.js';
import simulate from '../../../util/simulate_interaction.js';

function createMapWithGestureHandling(t) {
t.stub(Map.prototype, '_detectMissingCSS');
t.stub(Map.prototype, '_authenticate');
return new Map({
container: DOM.create('div', '', window.document.body),
gestureHandling: true
});
}

test('If gestureHandling option is set to true, a .mapboxgl-touch-pan-blocker element is added to map', (t) => {
const map = createMapWithGestureHandling(t);

t.equal(map.getContainer().querySelectorAll('.mapboxgl-touch-pan-blocker').length, 1);
t.end();
});

test('If gestureHandling option is set to true, touch pan is prevented when one finger is used to pan', (t) => {
const map = createMapWithGestureHandling(t);
const target = map.getCanvas();

const moveSpy = t.spy();
map.on('move', moveSpy);

simulate.touchstart(map.getCanvas(), {touches: [{target, identifier: 1, clientX: 0, clientY: -50}]});
map._renderTaskQueue.run();

simulate.touchmove(map.getCanvas(), {touches: [{target, identifier: 1, clientX: 0, clientY: -40}]});
map._renderTaskQueue.run();

t.equal(moveSpy.callCount, 0);
t.end();
});

test('If gestureHandling option is set to true, touch pan is triggered when two fingers are used to pan', (t) => {
const map = createMapWithGestureHandling(t);
const target = map.getCanvas();

const moveSpy = t.spy();
map.on('move', moveSpy);

simulate.touchstart(map.getCanvas(), {touches: [{target, identifier: 1, clientX: 0, clientY: -40}, {target, identifier: 2, clientX: 0, clientY: -30}]});
map._renderTaskQueue.run();

simulate.touchmove(map.getCanvas(), {touches: [{target, identifier: 1, clientX: 0, clientY: -50}, {target, identifier: 2, clientX: 0, clientY: -40}]});
map._renderTaskQueue.run();

t.equal(moveSpy.callCount, 1);
t.end();
});

test('Disabling touch pan removes .mapboxgl-touch-pan-blocker element', (t) => {
const map = createMapWithGestureHandling(t);

map.handlers._handlersById.touchPan.disable();

t.equal(map.getContainer().querySelectorAll('.mapboxgl-touch-pan-blocker').length, 0);
t.end();
});