Skip to content
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

Fix top-left resizing - account for new position when calling back onResize #136

Merged
merged 22 commits into from
Aug 28, 2020

Conversation

ConorKelleher
Copy link
Contributor

What's wrong?

Hey, love the library. Works really well for the most part but, as a few different issues have mentioned, it has some serious issues when resizing from the left or the top, using anything other than top-left positioning. The following open issues are just some that have documented the issue:

I've put together a minimal codesandbox to demonstrate the issue as well:
https://codesandbox.io/s/react-playground-vwlpk?file=/index.js

Samples from the above codesandbox:

  • Dragging to the right: smooth
    Image from Gyazo

  • Dragging to the left: jumpy/flickering
    Image from Gyazo

The Cause?

The problem is that on subsequent calls of the resizeHandler function, it's reporting a resize based on the deltaX and deltaY of the movement. However, if calling back through the onResize prop results in a position change (for instance, if you are using right-based absolute positioning as in my example above, or if you are manually updating the top/left positions based on the resize), then the drag handle itself might be moved by the re-render, so the next call to resizeHandler will report that the handle has moved, even though this is a prop-driven movement rather than a mouse-event driven one.

The solution

I've added a few lines to track a reference to the drag handle element in the document and in every call of resizeHandler, I'm comparing the new top and left of the element on the page with the previous values. A difference in these values will be a result of a positioning change, and so we want to account for this in our deltaX/deltaY values.

If dragging any handle with an 'n' in it and its position has changed, then our event's reported deltaY value will either be larger or smaller than we expect it to be (because this value will include the position change). Adding this y-position differential to the delta value accounts for this and keeps the callback only reporting on movement that was caused by the mouse event. The same goes for the x-axis and any handle with a 'w' in it.

Demo

I've added a new example to the examples demo page which features a new box with omni-directional movement. This can be used to demonstrate a before and after and also shows much more power behind this library.

e.g.

  • Old - omni-directional box stuttering dragging top and left
    ezgif-3-9726531015d8

  • New - omni-directional box smoothly resizing in all directions
    ezgif-3-8d801f0a91eb

Let me know of any issues you come across with it and have a good day!

@ConorKelleher
Copy link
Contributor Author

I've just pushed a small fix to scale the amount we're accounting for inversely by the transformScale property, since to match the client's screen, we need to counteract the current scale value.

Before:
Image from Gyazo
After:
Image from Gyazo

@STRML
Copy link
Collaborator

STRML commented Jun 9, 2020

Hi @conor-kelleher. Thanks a lot for this contribution!

I'd like for this to make it into 2.0. #133 contributed a test suite into the application, it would be really great if you could rebase on master and add a few cases as part of this PR. If you can, I'll wrap it up in a bow & release as part of 2.0!

Copy link
Collaborator

@STRML STRML left a comment

Choose a reason for hiding this comment

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

Implementation works, just some minor bits on the code & comments. Thanks!

@@ -182,6 +186,27 @@ export default class Resizable extends React.Component<Props, State> {
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;

// Check if handle's position has moved since last resizeHandler call - adjust deltas accordingly
if (this.draggingNode == null && e.target instanceof HTMLElement) {
Copy link
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
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
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

lib/Resizable.js Outdated
deltaY += deltaTopSinceLast / this.props.transformScale;
}
}
this.lastTop = handleRect.top;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this be a single property (this.lastHandleRect = handleRect)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, it can. I've rolled that in now

lib/Resizable.js Outdated
const deltaLeftSinceLast = handleRect.left - this.lastLeft;
const deltaTopSinceLast = handleRect.top - this.lastTop;

if (canDragX && axis.indexOf('w') > -1) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The allowable values for axis are:

['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne']

So this could be more clear as axis[axis.length - 1] === 'w', similar to https://github.com/STRML/react-resizable/blob/a1fff416339df13b15b23044b026ebbe376a2b9d/lib/Resizable.js#L97.

This of course would be a great use case for String#endsWith() but we are trying to avoid prototype methods that aren't supported in IE.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I've made this change now

lib/Resizable.js Outdated
if (canDragX && axis.indexOf('w') > -1) {
deltaX += deltaLeftSinceLast / this.props.transformScale;
}
if(canDragY && axis.indexOf('n') > -1) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

See above comment, axis[0] === 'n' will work here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Very true, much cleaner this way 👌

lib/Resizable.js Outdated
@@ -182,6 +186,27 @@ export default class Resizable extends React.Component<Props, State> {
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;

// Check if handle's position has moved since last resizeHandler call - adjust deltas accordingly
Copy link
Collaborator

Choose a reason for hiding this comment

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

It'd be great if you could expand upon this comment with some of the detail in this PR: that is, what happens, why it only affects n and w directions, and how this fixes it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fleshed these comments out a good bit. Hopefully they're clear enough now 🤞

@ConorKelleher ConorKelleher changed the base branch from master to draggable-v1 June 14, 2020 11:52
@ConorKelleher ConorKelleher changed the base branch from draggable-v1 to master June 14, 2020 11:52
@ConorKelleher
Copy link
Contributor Author

ConorKelleher commented Jun 14, 2020

I've made the requested changes to the code. Need to add some tests before re-requesting a review

@satendra02
Copy link

Hey @conor-kelleher

Great PR 🎉 . Really looking forward to its release.

@ConorKelleher
Copy link
Contributor Author

Hey @STRML, sorry for the delay getting this fixed up. I've made the suggested changes, added a suite of tests for the new functionality and added a new set of examples to the demo, keeping the absolutely positioned ones away from the inline ones:
Image from Gyazo

Just note: after pulling the changes you pushed to master preparing for v2, I couldn't get the example to run since the webpack config was referencing a non-existent file test/test.js. I've updated it to point to the new examples/example.js. Hope that's right!

lib/Resizable.js Outdated
@@ -119,7 +119,10 @@ export default class Resizable extends React.Component<Props, ResizableState> {
deltaY += deltaTopSinceLast / this.props.transformScale;
}
}
this.lastHandleRect = handleRect;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not that it matters much, but why not just save the whole rect?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I hit an issue with flow upon trying to cast an exact object to an inexact type. I'm new to flow so couldnt find a proper way around this, just got it working by taking the exact data I needed. I'm open to changes though

@sghoweri
Copy link

Any updates on this fix? Ran into this same issue!

@Ashalbulk
Copy link

I have the same issue. @STRML would this fix available soon?

@STRML
Copy link
Collaborator

STRML commented Aug 28, 2020

This works great. Thanks for the contribution!

@STRML STRML merged commit 6684a5b into react-grid-layout:master Aug 28, 2020
@vipulbhj
Copy link

vipulbhj commented Feb 1, 2021

Is there some code sandbox for the final solution ?

@vipulbhj
Copy link

vipulbhj commented Feb 1, 2021

Any plans on updating the official demo to demonstrate this.

Anyone have any codesandbox for this, I tried the examples.js and it does seem to work for me. Anyone have any examples I can look at ?

@gabrielvf1
Copy link

gabrielvf1 commented May 17, 2021

Can someone help me? Why when positioning my resize handles on top/left/top-left the behavior is so weird like i show at the GIF? Is something im doing wrong or i need to implement the right behavior by my self?

ezgif com-gif-maker

I expected that the grid would follow my mouse but it expand to the other side...

Thanks for the attention

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants