Skip to content

Commit

Permalink
feat: Support limiting dragging in a given rect
Browse files Browse the repository at this point in the history
Closes #4
  • Loading branch information
idanen committed Oct 24, 2019
1 parent 1d39989 commit cc9d2e3
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 31 deletions.
15 changes: 15 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>

<style>
html,
body {
height: 100%;
}
body {
margin: 0;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
60 changes: 45 additions & 15 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,60 @@
import React from 'react';
import React, { useRef, useEffect, useState } from 'react';
import { render } from 'react-dom';
import { useDraggable } from './lib';

function App() {
const [rect, setRect] = useState({ top: 0, left: 0, width: 0, height: 0 });
const bounding = useRef(null);
const { targetRef, handleRef, getTargetProps, delta } = useDraggable({
controlStyle: true,
limitRect: true
rectLimits: rect
});

useEffect(() => {
const {
top,
left,
right,
bottom
} = bounding.current.getBoundingClientRect();
setRect({
top,
left,
right,
bottom
});
}, []);

return (
<div
className='bounding'
ref={bounding}
style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'flex-end',
width: 640,
height: 320,
margin: '18px auto',
backgroundColor: 'hotpink'
margin: '20px 0 0 20px',
padding: '200px',
height: '80vh',
width: '75vw',
background: 'lavender'
}}
ref={targetRef}
{...getTargetProps()}
>
<span style={{ color: 'white' }}>
({delta.x}, {delta.y})
</span>
<button ref={handleRef}>Grab me</button>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'flex-end',
width: 320,
height: 180,
margin: '18px auto',
backgroundColor: 'hotpink'
}}
ref={targetRef}
{...getTargetProps()}
>
<span style={{ color: 'white' }}>
({delta.x}, {delta.y})
</span>
<button ref={handleRef}>Grab me</button>
</div>
</div>
);
}
Expand Down
55 changes: 39 additions & 16 deletions src/lib/Draggable.jsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
/* eslint-disable id-length */
import { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { func, bool, shape, number } from 'prop-types';

export function Draggable({ children, ...rest }) {
return children(useDraggable(rest));
}

Draggable.propTypes = {
children: PropTypes.func.isRequired,
controlStyle: PropTypes.bool,
viewport: PropTypes.bool
children: func.isRequired,
controlStyle: bool,
rectLimits: shape({
left: number,
right: number,
top: number,
bottom: number
}),
viewport: bool
};

export function useDraggable({ controlStyle, viewport = false } = {}) {
export function useDraggable({
controlStyle,
viewport = false,
rectLimits
} = {}) {
const targetRef = useRef(null);
const handleRef = useRef(null);
const [dragging, setDragging] = useState(null);
const [prev, setPrev] = useState({ x: 0, y: 0 });
const [delta, setDelta] = useState({ x: 0, y: 0 });
const initial = useRef({ x: 0, y: 0 });
const limits = useRef({});
const limits = useRef(null);

useEffect(() => {
const handle = handleRef.current || targetRef.current;
Expand All @@ -39,23 +49,32 @@ export function useDraggable({ controlStyle, viewport = false } = {}) {
if (controlStyle) {
targetRef.current.style.willChange = 'transform';
}
if (viewport) {
if (viewport || rectLimits) {
const {
left,
top,
width,
height
} = targetRef.current.getBoundingClientRect();

limits.current = {
minX: -left + delta.x,
maxX: window.innerWidth - width - left + delta.x,
minY: -top + delta.y,
maxY: window.innerHeight - height - top + delta.y
};
if (viewport) {
limits.current = {
minX: -left + delta.x,
maxX: window.innerWidth - width - left + delta.x,
minY: -top + delta.y,
maxY: window.innerHeight - height - top + delta.y
};
} else {
limits.current = {
minX: rectLimits.left - left + delta.x,
maxX: rectLimits.right - width - left + delta.x,
minY: rectLimits.top - top + delta.y,
maxY: rectLimits.bottom - height - top + delta.y
};
}
}
}
}, [controlStyle, viewport, delta]);
}, [controlStyle, viewport, delta, rectLimits]);

useEffect(() => {
const handle = handleRef.current || targetRef.current;
Expand Down Expand Up @@ -103,12 +122,16 @@ export function useDraggable({ controlStyle, viewport = false } = {}) {
const x = clientX - initial.current.x + prev.x;
const y = clientY - initial.current.y + prev.y;

const newDelta = calcDelta({ x, y, limits: viewport && limits.current });
const newDelta = calcDelta({
x,
y,
limits: limits.current
});
setDelta(newDelta);

return newDelta;
}
}, [dragging, prev, controlStyle, viewport]);
}, [dragging, prev, controlStyle, viewport, rectLimits]);

useEffect(() => {
if (controlStyle) {
Expand Down
60 changes: 60 additions & 0 deletions src/lib/draggable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,66 @@ describe('draggable', () => {
expect(left).to.equal(window.innerWidth - width);
});
});

describe('limit in rect', () => {
const limits = {
left: 11,
right: window.innerWidth - 11,
top: 5,
bottom: window.innerHeight - 13
};

beforeEach(() => {
cleanup();
utils = setup({
controlStyle: true,
rectLimits: limits,
style: {
...defaultStyle,
width: '180px',
left: '20px'
}
});
});

it('should not change transition beyond given rect', () => {
const { drag, getByTestId } = utils;
const targetElement = getByTestId('main');
const rect = targetElement.getBoundingClientRect();
const startAt = { clientX: rect.left + 5, clientY: rect.top + 5 };
const delta = { x: -50, y: -90 };

drag({ start: startAt, delta });

expect(targetElement.getBoundingClientRect()).to.include({
left: limits.left,
top: limits.top
});
});

it('should keep limits when dragging more than once', () => {
const { drag, getByTestId } = utils;
const targetElement = getByTestId('main');
targetElement.style.right = '50px';
targetElement.style.left = 'auto';
const rect = targetElement.getBoundingClientRect();

const startAt = { clientX: rect.left + 5, clientY: rect.top + 5 };
const delta = { x: 15, y: 1 };

drag({ start: startAt, delta });
drag({
start: {
clientX: startAt.clientX + delta.x,
clientY: startAt.clientY + delta.y
},
delta: { x: 50, y: 0 }
});

const { left, width } = targetElement.getBoundingClientRect();
expect(left).to.equal(limits.right - width);
});
});
});

function Consumer(props) {
Expand Down

0 comments on commit cc9d2e3

Please sign in to comment.