diff --git a/__tests__/Resizable.test.js b/__tests__/Resizable.test.js index 7b12e5ca..1dd52528 100644 --- a/__tests__/Resizable.test.js +++ b/__tests__/Resizable.test.js @@ -63,4 +63,111 @@ describe('render Resizable', () => { expect(element.find('.custom-component-se')).toHaveLength(1); }); }); + + describe('onResize callback with modified position', () => { + const customProps = { + ...props, + resizeHandles: ['nw', 'sw' ,'ne', 'se', 'n', 's', 'w', 'e'], + }; + const mockClientRect = { + left: 0, + top: 0, + }; + const eventTarget = document.createElement('div'); + // $FlowIgnore need to override to have control over dummy dom element + eventTarget.getBoundingClientRect = () => ({ ...mockClientRect }); + const mockEvent = { target: eventTarget }; + const element = shallow({resizableBoxChildren}); + const nwHandle = element.find('DraggableCore').first(); + + test('Gradual resizing without movement between does not modify callback', () => { + expect(props.onResize).not.toHaveBeenCalled(); + nwHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 5, deltaY: 10 }); + expect(props.onResize).lastCalledWith( + mockEvent, + expect.objectContaining({ + size: { + height: 40, + width: 45, + }, + }) + ); + }); + + test('Movement between callbacks modifies response values', () => { + expect(props.onResize).not.toHaveBeenCalled(); + + mockClientRect.top = -10; // Object moves between callbacks + nwHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 5, deltaY: 10 }); + expect(props.onResize).lastCalledWith( + mockEvent, + expect.objectContaining({ + size: { + height: 50, // No height change since deltaY is caused by clientRect moving vertically + width: 45, + }, + }) + ); + + mockClientRect.left = 20; // Object moves between callbacks + nwHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 5, deltaY: 10 }); + expect(props.onResize).lastCalledWith( + mockEvent, + expect.objectContaining({ + size: { + height: 40, // Height decreased as deltaY increases - no further top position change since last + width: 25, // Width decreased 25 - 5 from deltaX and 20 from changing position + }, + }) + ); + + props.onResize.mockClear(); + mockClientRect.left -= 10; // Object moves between callbacks + mockClientRect.top -= 10; // Object moves between callbacks + nwHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 10, deltaY: 10 }); + expect(props.onResize).not.toHaveBeenCalled(); + + mockClientRect.left -= 10; // Object moves between callbacks + mockClientRect.top -= 10; // Object moves between callbacks + const swHandle = element.find('DraggableCore').at(1); + swHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 10, deltaY: 10 }); + expect(props.onResize).lastCalledWith( + mockEvent, + expect.objectContaining({ + size: { + height: 60, // Changed since resizing from bottom doesn't cause position change + width: 50, // No change - movement has caused entire delta + }, + }) + ); + + mockClientRect.left -= 10; // Object moves between callbacks + mockClientRect.top -= 10; // Object moves between callbacks + const neHandle = element.find('DraggableCore').at(2); + neHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 10, deltaY: 10 }); + expect(props.onResize).lastCalledWith( + mockEvent, + expect.objectContaining({ + size: { + height: 50, // No change - movement has caused entire delta + width: 60, // Changed since resizing from right doesn't cause position change + }, + }) + ); + + mockClientRect.left -= 10; // Object moves between callbacks + mockClientRect.top -= 10; // Object moves between callbacks + const seHandle = element.find('DraggableCore').at(3); + seHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 10, deltaY: 10 }); + expect(props.onResize).lastCalledWith( + mockEvent, + expect.objectContaining({ + size: { + height: 60, // Changed since resizing from right doesn't cause position change + width: 60, // Changed since resizing from right doesn't cause position change + }, + }) + ); + }); + }); }); diff --git a/examples/1.html b/examples/1.html index c63def0b..70e3a600 100644 --- a/examples/1.html +++ b/examples/1.html @@ -7,13 +7,20 @@ } #content { width: 100%; - background: #eee; - padding-bottom: 200px; + padding-bottom: 250px; } .layoutRoot { display: flex; + background: #eee; + margin-bottom: 20px; flex-wrap: wrap; } + .absoluteLayout { + height: 600px; + position: relative; + justify-content: center; + align-items: center + } .box { display: inline-block; background: #ccc; @@ -42,6 +49,24 @@ .box3:hover .react-resizable-handle { display: block; } + .absolutely-positioned { + position: absolute !important; + } + .center-aligned { + margin: auto; + } + .left-aligned { + left: 0; + } + .right-aligned { + right: 0; + } + .top-aligned { + top: 0; + } + .bottom-aligned { + bottom: 0; + } diff --git a/examples/ExampleLayout.js b/examples/ExampleLayout.js index 22666fcd..1e3ee089 100644 --- a/examples/ExampleLayout.js +++ b/examples/ExampleLayout.js @@ -2,18 +2,50 @@ import React from 'react'; import Resizable from '../lib/Resizable'; import ResizableBox from '../lib/ResizableBox'; import 'style-loader!css-loader!../css/styles.css'; -import 'style-loader!css-loader!./test.css'; +import 'style-loader!css-loader!./example.css'; export default class ExampleLayout extends React.Component<{}, {width: number, height: number}> { - state = {width: 200, height: 200}; + state = { + width: 200, + height: 200, + absoluteWidth: 200, + absoluteHeight: 200, + absoluteLeft: 0, + absoluteTop: 0, + }; onClick = () => { - this.setState({width: 200, height: 200}); + this.setState({ width: 200, height: 200, absoluteWidth: 200, absoluteHeight: 200 }); }; onResize = (event, {element, size, handle}) => { this.setState({width: size.width, height: size.height}); }; + onResizeAbsolute = (event, {element, size, handle}) => { + this.setState((state) => { + let newLeft = state.absoluteLeft; + let newTop = state.absoluteTop; + const deltaHeight = size.height - state.absoluteHeight; + const deltaWidth = size.width - state.absoluteWidth; + if (handle[0] === 'n') { + newTop -= deltaHeight / 2; + } else if (handle[0] === 's') { + newTop += deltaHeight / 2; + } + if (handle[handle.length - 1] === 'w') { + newLeft -= deltaWidth / 2; + } else if (handle[handle.length - 1] === 'e') { + newLeft += deltaWidth / 2; + } + + return { + absoluteWidth: size.width, + absoluteHeight: size.height, + absoluteLeft: newLeft, + absoluteTop: newTop, + }; + }); + }; render() { return ( @@ -73,6 +105,38 @@ export default class ExampleLayout extends React.Component<{}, {width: number, h Not resizable ("none" axis). +
+ + {"Top-left Aligned"} + + + {"Bottom-left Aligned"} + + +
+ {"Raw use of element with controlled position. Resize and reposition in all directions"} +
+
+ + {"Top-right Aligned"} + + + {"Bottom-right Aligned"} + +
); } diff --git a/lib/Resizable.js b/lib/Resizable.js index 83353d25..d5561881 100644 --- a/lib/Resizable.js +++ b/lib/Resizable.js @@ -2,10 +2,9 @@ import React from 'react'; import type {Node as ReactNode} from 'react'; import {DraggableCore} from 'react-draggable'; - import {cloneElement} from './utils'; import {resizableProps} from "./propTypes"; -import type {ResizeHandleAxis, Props, ResizableState, DragCallbackData} from './propTypes'; +import type {ResizeHandleAxis, Props, ResizableState, DragCallbackData, ClientRect} from './propTypes'; export default class Resizable extends React.Component { static propTypes = resizableProps; @@ -24,6 +23,9 @@ export default class Resizable extends React.Component { slackW: 0, slackH: 0, }; + lastHandleRect: ?ClientRect = null; + draggingNode: ?HTMLElement = null; + lockAspectRatio(width: number, height: number, aspectRatio: number): [number, number] { height = width / aspectRatio; width = height * aspectRatio; @@ -93,6 +95,36 @@ export default class Resizable extends React.Component { const canDragX = (this.props.axis === 'both' || this.props.axis === 'x') && ['n', 's'].indexOf(axis) === -1; const canDragY = (this.props.axis === 'both' || this.props.axis === 'y') && ['e', 'w'].indexOf(axis) === -1; + /* + Track the element being dragged to account for changes in position. + If a handle's position is changed between callbacks, we need to factor this in to the next callback + */ + if (this.draggingNode == null && e.target instanceof HTMLElement) { + this.draggingNode = e.target; + } + if (this.draggingNode instanceof HTMLElement) { + const handleRect = this.draggingNode.getBoundingClientRect(); + if (this.lastHandleRect != null) { + // Find how much the handle has moved since the last callback + const deltaLeftSinceLast = handleRect.left - this.lastHandleRect.left; + const deltaTopSinceLast = handleRect.top - this.lastHandleRect.top; + + // If the handle has repositioned on either axis since last render, + // we need to increase our callback values by this much. + // Only checking 'n', 'w' since resizing by 's', 'w' won't affect the overall position on page + if (canDragX && axis[axis.length - 1] === 'w') { + deltaX += deltaLeftSinceLast / this.props.transformScale; + } + if(canDragY && axis[0] === 'n') { + deltaY += deltaTopSinceLast / this.props.transformScale; + } + } + this.lastHandleRect = { + top: handleRect.top, + left: handleRect.left, + }; + } + // reverse delta if using top or left drag handles if (canDragX && axis[axis.length - 1] === 'w') { deltaX = -deltaX; @@ -117,6 +149,7 @@ export default class Resizable extends React.Component { // nothing } else if (handlerName === 'onResizeStop') { newState.slackW = newState.slackH = 0; + this.lastHandleRect = this.draggingNode = null; } else { // Early return if no change after constraints if (width === this.props.width && height === this.props.height) return; diff --git a/lib/propTypes.js b/lib/propTypes.js index 6aef0c1a..6ec135ff 100644 --- a/lib/propTypes.js +++ b/lib/propTypes.js @@ -23,6 +23,10 @@ export type ResizeCallbackData = {| size: {|width: number, height: number|}, handle: ResizeHandleAxis |}; +export type ClientRect = {| + left: number; + top: number; +|}; // export type Props = {| diff --git a/webpack.config.js b/webpack.config.js index e60e4cae..9954e050 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,7 @@ const path = require('path'); module.exports = { context: __dirname, entry: { - test: "./test/test.js", + test: "./examples/example.js", }, output: { path: path.join(__dirname, "dist"),