diff --git a/examples/two-finger-pan-scroll.html b/examples/two-finger-pan-scroll.html
new file mode 100644
index 0000000000..f3faf89318
--- /dev/null
+++ b/examples/two-finger-pan-scroll.html
@@ -0,0 +1,12 @@
+---
+layout: example.html
+title: Panning and page scrolling
+shortdesc: Shows a map that allows page scrolling unless two fingers or Cmd/Ctrl are used to pan the map.
+docs: >
+ This example shows a common behavior for page scrolling: on touch devices, when one finger
+ is placed on the map, it can be used to scroll the page. Only two fingers will perform drag pan.
+ For mouse or trackpad devices, the platform modifier key (Cmd or Ctrl) will enable drag pan on
+ the map, otherwise the page will scroll.
+tags: "trackpad, mousewheel, zoom, scroll, page"
+---
+
diff --git a/examples/two-finger-pan-scroll.js b/examples/two-finger-pan-scroll.js
new file mode 100644
index 0000000000..1207d28799
--- /dev/null
+++ b/examples/two-finger-pan-scroll.js
@@ -0,0 +1,29 @@
+import Map from '../src/ol/Map.js';
+import View from '../src/ol/View.js';
+import TileLayer from '../src/ol/layer/Tile.js';
+import OSM from '../src/ol/source/OSM.js';
+import {defaults, DragPan, MouseWheelZoom} from '../src/ol/interaction.js';
+import {platformModifierKeyOnly} from '../src/ol/events/condition.js';
+
+const map = new Map({
+ interactions: defaults({dragPan: false, mouseWheelZoom: false}).extend([
+ new DragPan({
+ condition: function(event) {
+ return this.getPointerCount() === 2 || platformModifierKeyOnly(event);
+ }
+ }),
+ new MouseWheelZoom({
+ condition: platformModifierKeyOnly
+ })
+ ]),
+ layers: [
+ new TileLayer({
+ source: new OSM()
+ })
+ ],
+ target: 'map',
+ view: new View({
+ center: [0, 0],
+ zoom: 2
+ })
+});
diff --git a/src/ol/MapBrowserEventHandler.js b/src/ol/MapBrowserEventHandler.js
index e56e178488..e19b74e55e 100644
--- a/src/ol/MapBrowserEventHandler.js
+++ b/src/ol/MapBrowserEventHandler.js
@@ -3,12 +3,13 @@
*/
import 'elm-pep';
-import {DEVICE_PIXEL_RATIO} from './has.js';
+import {DEVICE_PIXEL_RATIO, PASSIVE_EVENT_LISTENERS} from './has.js';
import MapBrowserEventType from './MapBrowserEventType.js';
import MapBrowserPointerEvent from './MapBrowserPointerEvent.js';
import {listen, unlistenByKey} from './events.js';
import EventTarget from './events/Target.js';
import PointerEventType from './pointer/EventType.js';
+import EventType from './events/EventType.js';
class MapBrowserEventHandler extends EventTarget {
@@ -84,6 +85,12 @@ class MapBrowserEventHandler extends EventTarget {
PointerEventType.POINTERDOWN,
this.handlePointerDown_, this);
+ /**
+ * @type {PointerEvent}
+ * @private
+ */
+ this.originalPointerMoveEvent_;
+
/**
* @type {?import("./events.js").EventsKey}
* @private
@@ -92,6 +99,13 @@ class MapBrowserEventHandler extends EventTarget {
PointerEventType.POINTERMOVE,
this.relayEvent_, this);
+ /**
+ * @private
+ */
+ this.boundHandleTouchMove_ = this.handleTouchMove_.bind(this);
+
+ this.element_.addEventListener(EventType.TOUCHMOVE, this.boundHandleTouchMove_,
+ PASSIVE_EVENT_LISTENERS ? {passive: false} : false);
}
/**
@@ -246,11 +260,26 @@ class MapBrowserEventHandler extends EventTarget {
* @private
*/
relayEvent_(pointerEvent) {
+ this.originalPointerMoveEvent_ = pointerEvent;
const dragging = !!(this.down_ && this.isMoving_(pointerEvent));
this.dispatchEvent(new MapBrowserPointerEvent(
pointerEvent.type, this.map_, pointerEvent, dragging));
}
+ /**
+ * Flexible handling of a `touch-action: none` css equivalent: because calling
+ * `preventDefault()` on a `pointermove` event does not stop native page scrolling
+ * and zooming, we also listen for `touchmove` and call `preventDefault()` on it
+ * when an interaction (currently `DragPan` handles the event.
+ * @param {TouchEvent} event Event.
+ * @private
+ */
+ handleTouchMove_(event) {
+ if (this.originalPointerMoveEvent_.defaultPrevented) {
+ event.preventDefault();
+ }
+ }
+
/**
* @param {PointerEvent} pointerEvent Pointer
* event.
@@ -271,6 +300,8 @@ class MapBrowserEventHandler extends EventTarget {
unlistenByKey(this.relayedListenerKey_);
this.relayedListenerKey_ = null;
}
+ this.element_.removeEventListener(EventType.TOUCHMOVE, this.boundHandleTouchMove_);
+
if (this.pointerdownListenerKey_) {
unlistenByKey(this.pointerdownListenerKey_);
this.pointerdownListenerKey_ = null;
diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js
index a6e524988b..99db86f835 100644
--- a/src/ol/PluggableMap.js
+++ b/src/ol/PluggableMap.js
@@ -131,17 +131,6 @@ import {toUserCoordinate, fromUserCoordinate} from './proj.js';
*/
-/**
- * @param {HTMLElement} element Element.
- * @param {string} touchAction Value for `touch-action'.
- */
-function setTouchAction(element, touchAction) {
- element.style.msTouchAction = touchAction;
- element.style.touchAction = touchAction;
- element.setAttribute('touch-action', touchAction);
-}
-
-
/**
* @fires import("./MapBrowserEvent.js").MapBrowserEvent
* @fires import("./MapEvent.js").MapEvent
@@ -305,12 +294,6 @@ class PluggableMap extends BaseObject {
*/
this.keyHandlerKeys_ = null;
- /**
- * @private
- * @type {?Array}
- */
- this.focusHandlerKeys_ = null;
-
const handleBrowserEvent = this.handleBrowserEvent.bind(this);
this.viewport_.addEventListener(EventType.CONTEXTMENU, handleBrowserEvent, false);
this.viewport_.addEventListener(EventType.WHEEL, handleBrowserEvent,
@@ -939,6 +922,7 @@ class PluggableMap extends BaseObject {
const type = opt_type || browserEvent.type;
const mapBrowserEvent = new MapBrowserEvent(type, this, browserEvent);
this.handleMapBrowserEvent(mapBrowserEvent);
+ browserEvent.preventDefault();
}
/**
@@ -1044,12 +1028,6 @@ class PluggableMap extends BaseObject {
targetElement = this.getTargetElement();
}
- if (this.focusHandlerKeys_) {
- for (let i = 0, ii = this.focusHandlerKeys_.length; i < ii; ++i) {
- unlistenByKey(this.focusHandlerKeys_[i]);
- }
- this.focusHandlerKeys_ = null;
- }
if (this.keyHandlerKeys_) {
for (let i = 0, ii = this.keyHandlerKeys_.length; i < ii; ++i) {
unlistenByKey(this.keyHandlerKeys_[i]);
@@ -1078,15 +1056,6 @@ class PluggableMap extends BaseObject {
if (!this.renderer_) {
this.renderer_ = this.createRenderer();
}
- let hasFocus = true;
- if (targetElement.hasAttribute('tabindex')) {
- hasFocus = document.activeElement === targetElement;
- this.focusHandlerKeys_ = [
- listen(targetElement, EventType.FOCUS, setTouchAction.bind(this, this.viewport_, 'none')),
- listen(targetElement, EventType.BLUR, setTouchAction.bind(this, this.viewport_, 'auto'))
- ];
- }
- setTouchAction(this.viewport_, hasFocus ? 'none' : 'auto');
const keyboardEventTarget = !this.keyboardEventTarget_ ?
targetElement : this.keyboardEventTarget_;
diff --git a/src/ol/control/ZoomSlider.js b/src/ol/control/ZoomSlider.js
index a87da521c0..dcb8b4c223 100644
--- a/src/ol/control/ZoomSlider.js
+++ b/src/ol/control/ZoomSlider.js
@@ -136,7 +136,6 @@ class ZoomSlider extends Control {
thumbElement.setAttribute('type', 'button');
thumbElement.className = className + '-thumb ' + CLASS_UNSELECTABLE;
const containerElement = this.element;
- containerElement.setAttribute('touch-action', 'none');
containerElement.className = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL;
containerElement.appendChild(thumbElement);
diff --git a/src/ol/events/EventType.js b/src/ol/events/EventType.js
index 26594c960b..8959635887 100644
--- a/src/ol/events/EventType.js
+++ b/src/ol/events/EventType.js
@@ -34,5 +34,6 @@ export default {
KEYPRESS: 'keypress',
LOAD: 'load',
RESIZE: 'resize',
+ TOUCHMOVE: 'touchmove',
WHEEL: 'wheel'
};
diff --git a/src/ol/interaction/DragPan.js b/src/ol/interaction/DragPan.js
index 12f526548b..1c98592308 100644
--- a/src/ol/interaction/DragPan.js
+++ b/src/ol/interaction/DragPan.js
@@ -115,6 +115,7 @@ class DragPan extends PointerInteraction {
}
this.lastCentroid = centroid;
this.lastPointersCount_ = targetPointers.length;
+ mapBrowserEvent.originalEvent.preventDefault();
}
/**
diff --git a/src/ol/interaction/Pointer.js b/src/ol/interaction/Pointer.js
index 38deafb79f..8f262e350b 100644
--- a/src/ol/interaction/Pointer.js
+++ b/src/ol/interaction/Pointer.js
@@ -95,6 +95,16 @@ class PointerInteraction extends Interaction {
}
+ /**
+ * Returns the current number of pointers involved in the interaction,
+ * e.g. `2` when two fingers are used.
+ * @return {number} The number of pointers.
+ * @api
+ */
+ getPointerCount() {
+ return this.targetPointers.length;
+ }
+
/**
* Handle pointer down events.
* @param {import("../MapBrowserPointerEvent.js").default} mapBrowserEvent Event.
diff --git a/test/spec/ol/MapBrowserEventHandler.test.js b/test/spec/ol/MapBrowserEventHandler.test.js
index 15d6db9586..b8d33fd380 100644
--- a/test/spec/ol/MapBrowserEventHandler.test.js
+++ b/test/spec/ol/MapBrowserEventHandler.test.js
@@ -174,4 +174,21 @@ describe('ol/MapBrowserEventHandler', function() {
expect(moveToleranceHandler.isMoving_(pointermoveAt2)).to.be(true);
});
});
+
+ describe('handleTouchMove_', function() {
+ let handler;
+ beforeEach(function() {
+ handler = new MapBrowserEventHandler(new Map({}));
+ });
+ it('prevents default on touchmove event', function() {
+ handler.originalPointerMoveEvent_ = {
+ defaultPrevented: true
+ };
+ const event = {
+ preventDefault: sinon.spy()
+ };
+ handler.handleTouchMove_(event);
+ expect(event.preventDefault.callCount).to.be(1);
+ });
+ });
});
diff --git a/test/spec/ol/map.test.js b/test/spec/ol/map.test.js
index 974a396beb..b6202a11f8 100644
--- a/test/spec/ol/map.test.js
+++ b/test/spec/ol/map.test.js
@@ -666,21 +666,6 @@ describe('ol.Map', function() {
expect(map.handleResize_).to.be.ok();
});
- it('handles touch-action on focus and blur', function() {
- expect(map.focusHandlerKeys_).to.be(null);
- expect(map.getViewport().getAttribute('touch-action')).to.be('none');
- const target = document.createElement('div');
- target.setAttribute('tabindex', 1);
- map.setTarget(target);
- expect(Array.isArray(map.focusHandlerKeys_)).to.be(true);
- expect(map.getViewport().getAttribute('touch-action')).to.be('auto');
- target.dispatchEvent(new Event('focus'));
- expect(map.getViewport().getAttribute('touch-action')).to.be('none');
- map.setTarget(null);
- expect(map.focusHandlerKeys_).to.be(null);
- expect(map.getViewport().getAttribute('touch-action')).to.be('none');
- });
-
describe('call setTarget with null', function() {
it('unregisters the viewport resize listener', function() {
map.setTarget(null);