Skip to content

Commit 6684a5b

Browse files
Fix top-left resizing - account for new position when calling back onResize (#136)
* Adjust deltas based on updated handle positions * Reduce type check to any html el * Persist reference to element, always measure by it * Add standalone omnidirectional resize * Position my example in line with others * Extra padding at bottom for absolute example * Adjust positioning adjustment by transformScale * Adjust deltas based on updated handle positions * Reduce type check to any html el * Persist reference to element, always measure by it * Add standalone omnidirectional resize * Position my example in line with others * Extra padding at bottom for absolute example * Adjust positioning adjustment by transformScale * More declarative axis checks * Replace individual handle values with one object * Replace individual handle values with one object * Update webpack entry file + exact ClientRect * Adding full absolute-positioning layout example * Set up test case to verify modified position * Test callback modifications with delta position
1 parent a1fff41 commit 6684a5b

6 files changed

Lines changed: 241 additions & 8 deletions

File tree

__tests__/Resizable.test.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,111 @@ describe('render Resizable', () => {
6363
expect(element.find('.custom-component-se')).toHaveLength(1);
6464
});
6565
});
66+
67+
describe('onResize callback with modified position', () => {
68+
const customProps = {
69+
...props,
70+
resizeHandles: ['nw', 'sw' ,'ne', 'se', 'n', 's', 'w', 'e'],
71+
};
72+
const mockClientRect = {
73+
left: 0,
74+
top: 0,
75+
};
76+
const eventTarget = document.createElement('div');
77+
// $FlowIgnore need to override to have control over dummy dom element
78+
eventTarget.getBoundingClientRect = () => ({ ...mockClientRect });
79+
const mockEvent = { target: eventTarget };
80+
const element = shallow(<Resizable {...customProps}>{resizableBoxChildren}</Resizable>);
81+
const nwHandle = element.find('DraggableCore').first();
82+
83+
test('Gradual resizing without movement between does not modify callback', () => {
84+
expect(props.onResize).not.toHaveBeenCalled();
85+
nwHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 5, deltaY: 10 });
86+
expect(props.onResize).lastCalledWith(
87+
mockEvent,
88+
expect.objectContaining({
89+
size: {
90+
height: 40,
91+
width: 45,
92+
},
93+
})
94+
);
95+
});
96+
97+
test('Movement between callbacks modifies response values', () => {
98+
expect(props.onResize).not.toHaveBeenCalled();
99+
100+
mockClientRect.top = -10; // Object moves between callbacks
101+
nwHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 5, deltaY: 10 });
102+
expect(props.onResize).lastCalledWith(
103+
mockEvent,
104+
expect.objectContaining({
105+
size: {
106+
height: 50, // No height change since deltaY is caused by clientRect moving vertically
107+
width: 45,
108+
},
109+
})
110+
);
111+
112+
mockClientRect.left = 20; // Object moves between callbacks
113+
nwHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 5, deltaY: 10 });
114+
expect(props.onResize).lastCalledWith(
115+
mockEvent,
116+
expect.objectContaining({
117+
size: {
118+
height: 40, // Height decreased as deltaY increases - no further top position change since last
119+
width: 25, // Width decreased 25 - 5 from deltaX and 20 from changing position
120+
},
121+
})
122+
);
123+
124+
props.onResize.mockClear();
125+
mockClientRect.left -= 10; // Object moves between callbacks
126+
mockClientRect.top -= 10; // Object moves between callbacks
127+
nwHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 10, deltaY: 10 });
128+
expect(props.onResize).not.toHaveBeenCalled();
129+
130+
mockClientRect.left -= 10; // Object moves between callbacks
131+
mockClientRect.top -= 10; // Object moves between callbacks
132+
const swHandle = element.find('DraggableCore').at(1);
133+
swHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 10, deltaY: 10 });
134+
expect(props.onResize).lastCalledWith(
135+
mockEvent,
136+
expect.objectContaining({
137+
size: {
138+
height: 60, // Changed since resizing from bottom doesn't cause position change
139+
width: 50, // No change - movement has caused entire delta
140+
},
141+
})
142+
);
143+
144+
mockClientRect.left -= 10; // Object moves between callbacks
145+
mockClientRect.top -= 10; // Object moves between callbacks
146+
const neHandle = element.find('DraggableCore').at(2);
147+
neHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 10, deltaY: 10 });
148+
expect(props.onResize).lastCalledWith(
149+
mockEvent,
150+
expect.objectContaining({
151+
size: {
152+
height: 50, // No change - movement has caused entire delta
153+
width: 60, // Changed since resizing from right doesn't cause position change
154+
},
155+
})
156+
);
157+
158+
mockClientRect.left -= 10; // Object moves between callbacks
159+
mockClientRect.top -= 10; // Object moves between callbacks
160+
const seHandle = element.find('DraggableCore').at(3);
161+
seHandle.prop('onDrag')(mockEvent, { node: null, deltaX: 10, deltaY: 10 });
162+
expect(props.onResize).lastCalledWith(
163+
mockEvent,
164+
expect.objectContaining({
165+
size: {
166+
height: 60, // Changed since resizing from right doesn't cause position change
167+
width: 60, // Changed since resizing from right doesn't cause position change
168+
},
169+
})
170+
);
171+
});
172+
});
66173
});

examples/1.html

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@
77
}
88
#content {
99
width: 100%;
10-
background: #eee;
11-
padding-bottom: 200px;
10+
padding-bottom: 250px;
1211
}
1312
.layoutRoot {
1413
display: flex;
14+
background: #eee;
15+
margin-bottom: 20px;
1516
flex-wrap: wrap;
1617
}
18+
.absoluteLayout {
19+
height: 600px;
20+
position: relative;
21+
justify-content: center;
22+
align-items: center
23+
}
1724
.box {
1825
display: inline-block;
1926
background: #ccc;
@@ -42,6 +49,24 @@
4249
.box3:hover .react-resizable-handle {
4350
display: block;
4451
}
52+
.absolutely-positioned {
53+
position: absolute !important;
54+
}
55+
.center-aligned {
56+
margin: auto;
57+
}
58+
.left-aligned {
59+
left: 0;
60+
}
61+
.right-aligned {
62+
right: 0;
63+
}
64+
.top-aligned {
65+
top: 0;
66+
}
67+
.bottom-aligned {
68+
bottom: 0;
69+
}
4570
</style>
4671
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.min.js"></script>
4772
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react-dom.min.js"></script>

examples/ExampleLayout.js

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,50 @@ import React from 'react';
22
import Resizable from '../lib/Resizable';
33
import ResizableBox from '../lib/ResizableBox';
44
import 'style-loader!css-loader!../css/styles.css';
5-
import 'style-loader!css-loader!./test.css';
5+
import 'style-loader!css-loader!./example.css';
66

77
export default class ExampleLayout extends React.Component<{}, {width: number, height: number}> {
8-
state = {width: 200, height: 200};
8+
state = {
9+
width: 200,
10+
height: 200,
11+
absoluteWidth: 200,
12+
absoluteHeight: 200,
13+
absoluteLeft: 0,
14+
absoluteTop: 0,
15+
};
916

1017
onClick = () => {
11-
this.setState({width: 200, height: 200});
18+
this.setState({ width: 200, height: 200, absoluteWidth: 200, absoluteHeight: 200 });
1219
};
1320

1421
onResize = (event, {element, size, handle}) => {
1522
this.setState({width: size.width, height: size.height});
1623
};
24+
onResizeAbsolute = (event, {element, size, handle}) => {
25+
this.setState((state) => {
26+
let newLeft = state.absoluteLeft;
27+
let newTop = state.absoluteTop;
28+
const deltaHeight = size.height - state.absoluteHeight;
29+
const deltaWidth = size.width - state.absoluteWidth;
30+
if (handle[0] === 'n') {
31+
newTop -= deltaHeight / 2;
32+
} else if (handle[0] === 's') {
33+
newTop += deltaHeight / 2;
34+
}
35+
if (handle[handle.length - 1] === 'w') {
36+
newLeft -= deltaWidth / 2;
37+
} else if (handle[handle.length - 1] === 'e') {
38+
newLeft += deltaWidth / 2;
39+
}
40+
41+
return {
42+
absoluteWidth: size.width,
43+
absoluteHeight: size.height,
44+
absoluteLeft: newLeft,
45+
absoluteTop: newTop,
46+
};
47+
});
48+
};
1749

1850
render() {
1951
return (
@@ -73,6 +105,38 @@ export default class ExampleLayout extends React.Component<{}, {width: number, h
73105
<span className="text">Not resizable ("none" axis).</span>
74106
</ResizableBox>
75107
</div>
108+
<div className="layoutRoot absoluteLayout">
109+
<ResizableBox className="box absolutely-positioned top-aligned left-aligned" height={200} width={200} resizeHandles={['se', 'e', 's']}>
110+
<span className="text">{"Top-left Aligned"}</span>
111+
</ResizableBox>
112+
<ResizableBox className="box absolutely-positioned bottom-aligned left-aligned" height={200} width={200} resizeHandles={['ne', 'e', 'n']}>
113+
<span className="text">{"Bottom-left Aligned"}</span>
114+
</ResizableBox>
115+
<Resizable
116+
className="box absolutely-positioned center-aligned"
117+
height={this.state.absoluteHeight}
118+
width={this.state.absoluteWidth}
119+
onResize={this.onResizeAbsolute}
120+
resizeHandles={['sw', 'se', 'nw', 'ne', 'w', 'e', 'n', 's']}
121+
>
122+
<div
123+
className="box"
124+
style={{
125+
width: this.state.absoluteWidth,
126+
height: this.state.absoluteHeight,
127+
margin: `${this.state.absoluteTop} 0 0 ${this.state.absoluteLeft}`,
128+
}}
129+
>
130+
<span className="text">{"Raw use of <Resizable> element with controlled position. Resize and reposition in all directions"}</span>
131+
</div>
132+
</Resizable>
133+
<ResizableBox className="box absolutely-positioned top-aligned right-aligned" height={200} width={200} resizeHandles={['sw', 'w', 's']}>
134+
<span className="text">{"Top-right Aligned"}</span>
135+
</ResizableBox>
136+
<ResizableBox className="box absolutely-positioned bottom-aligned right-aligned" height={200} width={200} resizeHandles={['nw', 'w', 'n']}>
137+
<span className="text">{"Bottom-right Aligned"}</span>
138+
</ResizableBox>
139+
</div>
76140
</div>
77141
);
78142
}

lib/Resizable.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
import React from 'react';
33
import type {Node as ReactNode} from 'react';
44
import {DraggableCore} from 'react-draggable';
5-
65
import {cloneElement} from './utils';
76
import {resizableProps} from "./propTypes";
8-
import type {ResizeHandleAxis, Props, ResizableState, DragCallbackData} from './propTypes';
7+
import type {ResizeHandleAxis, Props, ResizableState, DragCallbackData, ClientRect} from './propTypes';
98

109
export default class Resizable extends React.Component<Props, ResizableState> {
1110
static propTypes = resizableProps;
@@ -24,6 +23,9 @@ export default class Resizable extends React.Component<Props, ResizableState> {
2423
slackW: 0, slackH: 0,
2524
};
2625

26+
lastHandleRect: ?ClientRect = null;
27+
draggingNode: ?HTMLElement = null;
28+
2729
lockAspectRatio(width: number, height: number, aspectRatio: number): [number, number] {
2830
height = width / aspectRatio;
2931
width = height * aspectRatio;
@@ -93,6 +95,36 @@ export default class Resizable extends React.Component<Props, ResizableState> {
9395
const canDragX = (this.props.axis === 'both' || this.props.axis === 'x') && ['n', 's'].indexOf(axis) === -1;
9496
const canDragY = (this.props.axis === 'both' || this.props.axis === 'y') && ['e', 'w'].indexOf(axis) === -1;
9597

98+
/*
99+
Track the element being dragged to account for changes in position.
100+
If a handle's position is changed between callbacks, we need to factor this in to the next callback
101+
*/
102+
if (this.draggingNode == null && e.target instanceof HTMLElement) {
103+
this.draggingNode = e.target;
104+
}
105+
if (this.draggingNode instanceof HTMLElement) {
106+
const handleRect = this.draggingNode.getBoundingClientRect();
107+
if (this.lastHandleRect != null) {
108+
// Find how much the handle has moved since the last callback
109+
const deltaLeftSinceLast = handleRect.left - this.lastHandleRect.left;
110+
const deltaTopSinceLast = handleRect.top - this.lastHandleRect.top;
111+
112+
// If the handle has repositioned on either axis since last render,
113+
// we need to increase our callback values by this much.
114+
// Only checking 'n', 'w' since resizing by 's', 'w' won't affect the overall position on page
115+
if (canDragX && axis[axis.length - 1] === 'w') {
116+
deltaX += deltaLeftSinceLast / this.props.transformScale;
117+
}
118+
if(canDragY && axis[0] === 'n') {
119+
deltaY += deltaTopSinceLast / this.props.transformScale;
120+
}
121+
}
122+
this.lastHandleRect = {
123+
top: handleRect.top,
124+
left: handleRect.left,
125+
};
126+
}
127+
96128
// reverse delta if using top or left drag handles
97129
if (canDragX && axis[axis.length - 1] === 'w') {
98130
deltaX = -deltaX;
@@ -117,6 +149,7 @@ export default class Resizable extends React.Component<Props, ResizableState> {
117149
// nothing
118150
} else if (handlerName === 'onResizeStop') {
119151
newState.slackW = newState.slackH = 0;
152+
this.lastHandleRect = this.draggingNode = null;
120153
} else {
121154
// Early return if no change after constraints
122155
if (width === this.props.width && height === this.props.height) return;

lib/propTypes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export type ResizeCallbackData = {|
2323
size: {|width: number, height: number|},
2424
handle: ResizeHandleAxis
2525
|};
26+
export type ClientRect = {|
27+
left: number;
28+
top: number;
29+
|};
2630

2731
// <Resizable>
2832
export type Props = {|

webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const path = require('path');
44
module.exports = {
55
context: __dirname,
66
entry: {
7-
test: "./test/test.js",
7+
test: "./examples/example.js",
88
},
99
output: {
1010
path: path.join(__dirname, "dist"),

0 commit comments

Comments
 (0)