Skip to content

Refactor #173 #179

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

Merged
merged 2 commits into from
Jul 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ grid: [number, number],
// Example: '.handle'
handle: string,

// If desired, you can provide your own offsetParent for drag calculations.
// By default, we use the Draggable's offsetParent. This can be useful for elements
// with odd display types or floats.
offsetParent: HTMLElement,

// Called whenever the user mouses down. Called regardless of handle or
// disabled status.
onMouseDown: (e: MouseEvent) => void,
Expand Down Expand Up @@ -249,6 +254,7 @@ on itself and thus must have callbacks attached to be useful.
cancel: string,
disabled: boolean,
enableUserSelectHack: boolean,
offsetParent: HTMLElement,
grid: [number, number],
handle: string,
onStart: DraggableEventHandler,
Expand Down
11 changes: 11 additions & 0 deletions lib/DraggableCore.es6
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ export default class DraggableCore extends React.Component {
*/
enableUserSelectHack: PropTypes.bool,

/**
* `offsetParent`, if set, uses the passed DOM node to compute drag offsets
* instead of using the parent node.
*/
offsetParent: function(props, propName) {
if (process.browser && props[propName] && props[propName].nodeType !== 1) {
throw new Error('Draggable\'s offsetParent must be a DOM Node.');
}
},

/**
* `grid` specifies the x and y that dragging should snap to.
*/
Expand Down Expand Up @@ -152,6 +162,7 @@ export default class DraggableCore extends React.Component {
cancel: null,
disabled: false,
enableUserSelectHack: true,
offsetParent: null,
handle: null,
grid: null,
transform: null,
Expand Down
7 changes: 4 additions & 3 deletions lib/utils/domFns.es6
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,10 @@ export function innerWidth(node: HTMLElement): number {
}

// Get from offsetParent
export function offsetXYFromParentOf(evt: {clientX: number, clientY: number}, node: HTMLElement & {offsetParent: HTMLElement}): ControlPosition {
const offsetParent = node.offsetParent || document.body;
const offsetParentRect = node.offsetParent === document.body ? {left: 0, top: 0} : offsetParent.getBoundingClientRect();
export function offsetXYFromParent(evt: {clientX: number, clientY: number}, offsetParent: ?HTMLElement): ControlPosition {
if (!offsetParent) offsetParent = document.body;
const isBody = offsetParent === offsetParent.ownerDocument.body;
const offsetParentRect = isBody ? {left: 0, top: 0} : offsetParent.getBoundingClientRect();

const x = evt.clientX + offsetParent.scrollLeft - offsetParentRect.left;
const y = evt.clientY + offsetParent.scrollTop - offsetParentRect.top;
Expand Down
8 changes: 6 additions & 2 deletions lib/utils/positionFns.es6
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @flow
import {isNum, int} from './shims';
import ReactDOM from 'react-dom';
import {getTouch, innerWidth, innerHeight, offsetXYFromParentOf, outerWidth, outerHeight} from './domFns';
import {getTouch, innerWidth, innerHeight, offsetXYFromParent, outerWidth, outerHeight} from './domFns';

import type Draggable from '../Draggable';
import type {Bounds, ControlPosition, DraggableData} from './types';
Expand Down Expand Up @@ -66,7 +66,11 @@ export function canDragY(draggable: Draggable): boolean {
export function getControlPosition(e: MouseEvent, touchIdentifier: ?number, draggableCore: DraggableCore): ?ControlPosition {
const touchObj = typeof touchIdentifier === 'number' ? getTouch(e, touchIdentifier) : null;
if (typeof touchIdentifier === 'number' && !touchObj) return null; // not the right touch
return offsetXYFromParentOf(touchObj || e, ReactDOM.findDOMNode(draggableCore));
// User can provide an offsetParent if desired.
const offsetParent = draggableCore.props.offsetParent ||
ReactDOM.findDOMNode(draggableCore).offsetParent ||
document.body;
return offsetXYFromParent(touchObj || e, offsetParent);
}

// Create an data object exposed by <DraggableCore>'s events
Expand Down
76 changes: 65 additions & 11 deletions specs/draggable.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -456,32 +456,74 @@ describe('react-draggable', function () {
});

it('should modulate position on scroll', function (done) {
// This test fails in karma under Chrome & Firefox, positioning quirks
// FIXME: Why? Chrome reports 2x scrollTo, Phantom reports 0x, Firefox reports 1x as it should
var is_ff = navigator.userAgent.toLowerCase().indexOf('Firefox') > -1;
if (!is_ff) return done();

var dragCalled = false;
let dragCalled = false;

function onDrag(e, coreEvent) {
assert(coreEvent.deltaY === 500);
dragCalled = true;
}
drag = TestUtils.renderIntoDocument(<Draggable onDrag={onDrag}><div/></Draggable>);
var node = ReactDOM.findDOMNode(drag);
const node = ReactDOM.findDOMNode(drag);

// Create a container we can scroll. I'm doing it this way so we can still access <Draggable>.
// Enzyme (airbnb project) would make this a lot easier.
const fragment = fragmentFromString(`
<div style="overflow: auto; height: 100px;">
<div style="height: 10000px; position: relative;">
</div>
</div>
`);
transplantNodeInto(node, fragment, (f) => f.children[0]);

TestUtils.Simulate.mouseDown(node, {clientX: 0, clientY: 0});
assert(drag.state.dragging === true);

document.body.style.height = '10000px';
window.scrollTo(0, 500);
TestUtils.Simulate.mouseUp(node, {clientX: 0, clientY: 0});
// Scroll the inner container & trigger a scroll
fragment.scrollTop = 500;
mouseMove(0, 0);
TestUtils.Simulate.mouseUp(node);
setTimeout(function() {
assert(drag.state.dragging === false);
assert(dragCalled === true);
assert(drag.state.clientY === 500);
assert(drag.state.y === 500);
// Cleanup
document.body.removeChild(fragment);
done();
}, 50);

});

it('should respect offsetParent on nested div scroll', function (done) {
let dragCalled = false;
function onDrag(e, coreEvent) {
dragCalled = true;
// Because the offsetParent is the body, we technically haven't moved at all relative to it
assert(coreEvent.deltaY === 0);
}
drag = TestUtils.renderIntoDocument(<Draggable onDrag={onDrag} offsetParent={document.body}><div/></Draggable>);
const node = ReactDOM.findDOMNode(drag);

// Create a container we can scroll. I'm doing it this way so we can still access <Draggable>.
// Enzyme (airbnb project) would make this a lot easier.
const fragment = fragmentFromString(`
<div style="overflow: auto; height: 100px;">
<div style="height: 10000px; position: relative;">
</div>
</div>
`);
transplantNodeInto(node, fragment, (f) => f.children[0]);

TestUtils.Simulate.mouseDown(node, {clientX: 0, clientY: 0});
fragment.scrollTop = 500;

mouseMove(0, 0);
TestUtils.Simulate.mouseUp(node);
setTimeout(function() {
assert(dragCalled);
// Cleanup
document.body.removeChild(fragment);
done();
}, 50);
});

describe('draggable callbacks', function () {
Expand Down Expand Up @@ -597,3 +639,15 @@ function simulateMovementFromTo(drag, fromX, fromY, toX, toY) {
mouseMove(toX, toY);
TestUtils.Simulate.mouseUp(node);
}

function fragmentFromString(strHTML) {
var temp = document.createElement('div');
temp.innerHTML = strHTML;
return temp.children[0];
}

function transplantNodeInto(node, into, selector) {
node.parentNode.removeChild(node);
selector(into).appendChild(node);
document.body.appendChild(into);
}