Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
107 changes: 107 additions & 0 deletions __tests__/Resizable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Resizable {...customProps}>{resizableBoxChildren}</Resizable>);
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
},
})
);
});
});
});
29 changes: 27 additions & 2 deletions examples/1.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react-dom.min.js"></script>
Expand Down
70 changes: 67 additions & 3 deletions examples/ExampleLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -73,6 +105,38 @@ export default class ExampleLayout extends React.Component<{}, {width: number, h
<span className="text">Not resizable ("none" axis).</span>
</ResizableBox>
</div>
<div className="layoutRoot absoluteLayout">
<ResizableBox className="box absolutely-positioned top-aligned left-aligned" height={200} width={200} resizeHandles={['se', 'e', 's']}>
<span className="text">{"Top-left Aligned"}</span>
</ResizableBox>
<ResizableBox className="box absolutely-positioned bottom-aligned left-aligned" height={200} width={200} resizeHandles={['ne', 'e', 'n']}>
<span className="text">{"Bottom-left Aligned"}</span>
</ResizableBox>
<Resizable
className="box absolutely-positioned center-aligned"
height={this.state.absoluteHeight}
width={this.state.absoluteWidth}
onResize={this.onResizeAbsolute}
resizeHandles={['sw', 'se', 'nw', 'ne', 'w', 'e', 'n', 's']}
>
<div
className="box"
style={{
width: this.state.absoluteWidth,
height: this.state.absoluteHeight,
margin: `${this.state.absoluteTop} 0 0 ${this.state.absoluteLeft}`,
}}
>
<span className="text">{"Raw use of <Resizable> element with controlled position. Resize and reposition in all directions"}</span>
</div>
</Resizable>
<ResizableBox className="box absolutely-positioned top-aligned right-aligned" height={200} width={200} resizeHandles={['sw', 'w', 's']}>
<span className="text">{"Top-right Aligned"}</span>
</ResizableBox>
<ResizableBox className="box absolutely-positioned bottom-aligned right-aligned" height={200} width={200} resizeHandles={['nw', 'w', 'n']}>
<span className="text">{"Bottom-right Aligned"}</span>
</ResizableBox>
</div>
</div>
);
}
Expand Down
37 changes: 35 additions & 2 deletions lib/Resizable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props, ResizableState> {
static propTypes = resizableProps;
Expand All @@ -24,6 +23,9 @@ export default class Resizable extends React.Component<Props, ResizableState> {
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;
Expand Down Expand Up @@ -93,6 +95,36 @@ export default class Resizable extends React.Component<Props, ResizableState> {
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) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is saving this ref necessary (rather than simply using e.target)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally had it just using e.target on each callback but because the listener is calling back to any movement in the document body, e.target will not always be the same value during a drag. It will be whatever element the mouse is currently over. As you can see, using this value directly causes jumpy behaviour when the mouse enters different elements:
Image from Gyazo,

Hence the need to persist a reference to the same element through multiple callbacks

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or an even worse demo:
Image from Gyazo

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;
Expand All @@ -117,6 +149,7 @@ export default class Resizable extends React.Component<Props, ResizableState> {
// 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;
Expand Down
4 changes: 4 additions & 0 deletions lib/propTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export type ResizeCallbackData = {|
size: {|width: number, height: number|},
handle: ResizeHandleAxis
|};
export type ClientRect = {|
left: number;
top: number;
|};

// <Resizable>
export type Props = {|
Expand Down
2 changes: 1 addition & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down