Skip to content

Commit 4b46cba

Browse files
authored
feat(touch): more robust solution for touch body scrolling (#470)
We now simply cancel the 'touchstart' event. As of Chrome >= 56, this has to be done on event handlers that explicitly specify `{passive: false}`. Fixes #227, #435, #351, #406, #412, #279, redux #165
1 parent 458706c commit 4b46cba

File tree

3 files changed

+61
-17
lines changed

3 files changed

+61
-17
lines changed

lib/DraggableCore.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import PropTypes from 'prop-types';
44
import ReactDOM from 'react-dom';
55
import {matchesSelectorAndParentsTo, addEvent, removeEvent, addUserSelectStyles, getTouchIdentifier,
6-
removeUserSelectStyles, styleHacks} from './utils/domFns';
6+
removeUserSelectStyles} from './utils/domFns';
77
import {createCoreData, getControlPosition, snapToGrid} from './utils/positionFns';
88
import {dontSetMe} from './utils/shims';
99
import log from './utils/log';
@@ -226,6 +226,12 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
226226

227227
componentDidMount() {
228228
this.mounted = true;
229+
// Touch handlers must be added with {passive: false} to be cancelable.
230+
// https://developers.google.com/web/updates/2017/01/scrolling-intervention
231+
const thisNode = ReactDOM.findDOMNode(this);
232+
if (thisNode) {
233+
addEvent(thisNode, eventsFor.touch.start, this.onTouchStart, {passive: false});
234+
}
229235
}
230236

231237
componentWillUnmount() {
@@ -239,6 +245,7 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
239245
removeEvent(ownerDocument, eventsFor.touch.move, this.handleDrag);
240246
removeEvent(ownerDocument, eventsFor.mouse.stop, this.handleDragStop);
241247
removeEvent(ownerDocument, eventsFor.touch.stop, this.handleDragStop);
248+
removeEvent(thisNode, eventsFor.touch.start, this.onTouchStart, {passive: false});
242249
if (this.props.enableUserSelectHack) removeUserSelectStyles(ownerDocument);
243250
}
244251
}
@@ -265,6 +272,10 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
265272
return;
266273
}
267274

275+
// Prevent scrolling on mobile devices, like ipad/iphone.
276+
// Important that this is after handle/cancel.
277+
if (e.type === 'touchstart') e.preventDefault();
278+
268279
// Set touch identifier in component state if this is a touch event. This allows us to
269280
// distinguish between individual touches on multitouch screens by identifying which
270281
// touchpoint was set to this element.
@@ -309,9 +320,6 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
309320

310321
handleDrag: EventHandler<MouseTouchEvent> = (e) => {
311322

312-
// Prevent scrolling on mobile devices, like ipad/iphone.
313-
if (e.type === 'touchmove') e.preventDefault();
314-
315323
// Get the current drag point from the event. This is used as the offset.
316324
const position = getControlPosition(e, this.state.touchIdentifier, this);
317325
if (position == null) return;
@@ -418,13 +426,15 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
418426
// Reuse the child provided
419427
// This makes it flexible to use whatever element is wanted (div, ul, etc)
420428
return React.cloneElement(React.Children.only(this.props.children), {
421-
style: styleHacks(this.props.children.props.style),
429+
style: this.props.children.props.style,
422430

423431
// Note: mouseMove handler is attached to document so it will still function
424432
// when the user drags quickly and leaves the bounds of the element.
425433
onMouseDown: this.onMouseDown,
426-
onTouchStart: this.onTouchStart,
427434
onMouseUp: this.onMouseUp,
435+
// onTouchStart is added on `componentDidMount` so they can be added with
436+
// {passive: false}, which allows it to cancel. See
437+
// https://developers.google.com/web/updates/2017/01/scrolling-intervention
428438
onTouchEnd: this.onTouchEnd
429439
});
430440
}

lib/utils/domFns.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -180,15 +180,6 @@ export function removeUserSelectStyles(doc: ?Document) {
180180
}
181181
}
182182
183-
export function styleHacks(childStyle: Object = {}): Object {
184-
// Workaround IE pointer events; see #51
185-
// https://github.com/mzabriskie/react-draggable/issues/51#issuecomment-103488278
186-
return {
187-
touchAction: 'none',
188-
...childStyle
189-
};
190-
}
191-
192183
export function addClassName(el: HTMLElement, className: string) {
193184
if (el.classList) {
194185
el.classList.add(className);

specs/draggable.spec.jsx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('react-draggable', function () {
3131
afterEach(function() {
3232
try {
3333
TestUtils.Simulate.mouseUp(ReactDOM.findDOMNode(drag)); // reset user-select
34-
React.unmountComponentAtNode(ReactDOM.findDOMNode(drag).parentNode);
34+
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(drag).parentNode);
3535
} catch(e) { return; }
3636
});
3737

@@ -52,8 +52,9 @@ describe('react-draggable', function () {
5252
drag = (<Draggable><div className="foo" style={{color: 'black'}}/></Draggable>);
5353

5454
const node = renderToNode(drag);
55+
// Touch-action hack has been removed
5556
if ('touchAction' in document.body.style) {
56-
assert(node.getAttribute('style').indexOf('touch-action: none') >= 0);
57+
assert(node.getAttribute('style').indexOf('touch-action: none') === -1);
5758
}
5859
assert(node.getAttribute('style').indexOf('color: black') >= 0);
5960
assert(new RegExp(transformStyle + ': translate\\(0px(?:, 0px)?\\)').test(node.getAttribute('style')));
@@ -600,6 +601,48 @@ describe('react-draggable', function () {
600601
resetDragging(drag);
601602
});
602603

604+
it('should initialize dragging ontouchstart', function () {
605+
drag = TestUtils.renderIntoDocument(<Draggable><div/></Draggable>);
606+
607+
// Need to dispatch this ourselves as there is no onTouchStart handler (due to passive)
608+
// so TestUtils.Simulate will not work
609+
const e = new Event('touchstart');
610+
ReactDOM.findDOMNode(drag).dispatchEvent(e);
611+
assert(drag.state.dragging === true);
612+
});
613+
614+
it('should call preventDefault on touchStart event', function () {
615+
drag = TestUtils.renderIntoDocument(<Draggable><div/></Draggable>);
616+
617+
const e = new Event('touchstart');
618+
// Oddly `e.defaultPrevented` is not changing here. Maybe because we're not mounted to a real doc?
619+
let pdCalled = false;
620+
e.preventDefault = function() { pdCalled = true; };
621+
ReactDOM.findDOMNode(drag).dispatchEvent(e);
622+
assert(pdCalled);
623+
assert(drag.state.dragging === true);
624+
});
625+
626+
it('should not call preventDefault on touchStart event if not on handle', function () {
627+
drag = TestUtils.renderIntoDocument(
628+
<Draggable handle=".handle">
629+
<div>
630+
<div className="handle">
631+
<div><span><div className="deep">Handle</div></span></div>
632+
</div>
633+
<div className="content">Lorem ipsum...</div>
634+
</div>
635+
</Draggable>
636+
);
637+
638+
const e = new Event('touchstart');
639+
let pdCalled = false;
640+
e.preventDefault = function() { pdCalled = true; };
641+
ReactDOM.findDOMNode(drag).querySelector('.content').dispatchEvent(e);
642+
assert(!pdCalled);
643+
assert(drag.state.dragging !== true);
644+
});
645+
603646
it('should modulate position on scroll', function (done) {
604647
let dragCalled = false;
605648

0 commit comments

Comments
 (0)