-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
FocalPointPicker: Allow updating its value while dragging. #38247
FocalPointPicker: Allow updating its value while dragging. #38247
Conversation
…is interacting with it
👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @razwan! In case you missed it, we'd love to have you join us in our Slack community, where we hold regularly weekly meetings open to anyone to coordinate with each other. If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information. |
Hi @razwan, This is indeed how the logic was before #28676. I believe that I made that change thinking it would be a minor optimization and can't recall any critical function of it. I didn't imagine it could have been a breaking change but your case proves otherwise. I think changing it back (as in 8705154) should be fine. Though perhaps it would be nicer to intentionally support such use cases. For example, if the Here's my quick take on showing what I mean with the doDrag( event ) {
// Prevents text-selection when dragging.
event.preventDefault();
const value = this.getValueFromPoint(
{ x: event.pageX, y: event.pageY },
event.shiftKey
);
const modifiedValue = this.props.onDrag?.( value, event );
this.updateValue( modifiedValue ?? value );
} |
Hi @stokesman! Thanks for your feedback! Even though it feels safer, I believe that the change you're proposing is somewhat obscure rather than expected behaviour. It's nonetheless a way of accessing the
|
I've updated the pull request. @ajitbohra could you take a look at this? Thanks! |
@stokesman any idea on why this test fails? |
That test failure is due the the implicit return of the event handlers in the test. This line:
Needs to add brackets around the function body: handlers[ name ] = ( ...all ) => {
eventLogger( name, all );
} That does indicate this could be a breaking change for some consuming code inadvertently returning a value. I feel that is so unlikely that we don't need to worry about it but, if we do, guarding against it should be simple enough. If this approach is deemed favorable, we'll want to update the docs for @ciampo, given this is component territory, I'll add you as a reviewer. |
Thank you for the ping, @stokesman ! @mirka or I will try to give this PR a look in the next days :) |
Hey @stokesman and @razwan, I had a quick look at this PR (and #28676) and, if I understand correctly:
If what I understand is correct (from my limited knowledge of the
Please do let me know if what I'm proposing makes sense at all, or if I missed something while going through this PR! |
Hi @ciampo, thanks for having a look. Your last solution suggestion makes sense but it seems the root of this issue wasn't clear. I'll try and clarify/summarize. The change from #28676 that broke @razwan’s component was the addition to gutenberg/packages/components/src/focal-point-picker/index.js Lines 84 to 86 in ea79ef6
As the title of this PR still reflects, @razwan’s first commit 8705154 merely removed that condition. I agreed that it should be a safe change. Though I had the idea that we could ease the implementation of such a use case and suggested #38247 (comment) an alternate approach. As I see it we have the following options (last one from @ciampo):
I favor either of the latter which are the same general idea. @ciampo’s suggestion is probably a bit safer and more explicit. |
Ah, I see! Thank you for the clarification. As for the suggested approaches:
Wouldn't this cause a potential perf regression by calling
It looks like this solution requires the
This solution looks clear enough
Same for me. Allowing On the other hand, it'd be nice if |
Hi @ciampo! Thanks for tuning in! I agree that implementing a snapping functionality directly into the FocalPointPicker would be favourable in some cases. But this is just one use case. There are plenty of ways in which the component could be extended. For instance you may want to restrict (for whatever reason) the area in which the focal point can be dragged. Also, the snapping functionality requires not only a snapGrid prop, but also some sort of threshold etc. To me, these are the kind of features that developers should be able to develop on top of the core component. After giving it a second thought I favor dropping the I don't see how if focal point is snapped (or sticky) while dragging then the performance would suffer. The performance is rather greatly impacted when you update the block attributes while dragging but that's a whole another issue. Am I missing anything? |
I imagined that such a While there really should be no performance concern for the first option. I simply prefer that we facilitate such use cases. As @razwan wrote #38247 (comment)
|
This is definitely an interesting angle on the matter — allowing consumers to extend the core component is definitely a powerful pattern When referring at options highlighted in this comment, if I understand correctly:
If that's the case, my preference would be for @stokesman 's solution, which seems cleaner and still enables the extension of the component like @razwan 's wishes for. |
…eturning new value from onDrag callback
There's a fine line there but I understand that under such heavy development most of the times the focus is on adding the features needed by the core blocks. However I enjoy components that are very little opinionated and highly extensible.
The |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a fine line there but I understand that under such heavy development most of the times the focus is on adding the features needed by the core blocks. However I enjoy components that are very little opinionated and highly extensible.
Makes sense. It would be actually cool if you could add a Storybook example in this PR implementing snapping through the new resolvePoint
function!
The resolvePoint solution seems more than reasonable to me. I've made changes to the PR accordingly. Have you can have a look @ciampo.
Sure thing! Apart from the inline comment:
- could you add an entry to the CHANGELOG ?
- could you add a unit test for the
resolvePoint
function? A simple way would be to pass, as theresolvePoint
prop, a spy function (and then check that it was called correctly). We could add multiple tests to verify that it gets called also while interacting with the keyboard
Thank you!
this.updateValue( value ); | ||
this.props.onDrag?.( value, event ); | ||
const resolvedValue = this.props.resolvePoint?.( value ) ?? value; | ||
this.updateValue( resolvedValue ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like @stokesman suggested earlier, should we call resolvePoint
inside updateValue
instead?
…t controls or keyboard ar affected
Done. @ciampo |
Awesome! I'll try to give a proper look later today |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @razwan , thank you for working on this.
The code looks good and tests well — I just left a couple of comments, but we're quite close to being able to merge this!
resolvePoint={ () => { | ||
spy(); | ||
return { x: 0.91, y: 0.42 }; | ||
} } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: when checking this test on my local machine, I tried to make the test fail by modifying the resolvePoint
function to
resolvePoint={ ( value ) => {
spy();
return value;
} }
The test fails (as expected), but the values received by the onChange
spy are not x: 0.25, y: 0.25
, but instead are x: 0, y: 0
. I wasn't expecting this behaviour, do you know why would this happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My guess is that the main reason is that the picker doesn't have width and height props. Also, fireEvent is called without any other parameters but am not sure if the case would be the same if the picker had set dimensions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so I haven't been able to use mouseMove
to create a proper emulation of a user interaction. However, I managed that by using fireEvent.keyDown( dragArea, { charCode: 0, keyCode: 38 } );
which triggers onChange with an "expected" value of x: 0.25, y: 0.24
making my test fail. That was because onChange was called with the same value updateValue
would received.
I added a second parameter to the updateValue
method which is a callback that is passed to the setState call. Moving the onChange
call inside that callback fixed the issue and made the test pass. Everything looks fine now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the explanation, makes sense!
I suggest we add another unit test to check this onChange
baheviour:
diff --git a/packages/components/src/focal-point-picker/test/index.js b/packages/components/src/focal-point-picker/test/index.js
index 145ca0d019..ac4f1af0e3 100644
--- a/packages/components/src/focal-point-picker/test/index.js
+++ b/packages/components/src/focal-point-picker/test/index.js
@@ -76,6 +76,8 @@ describe( 'FocalPointPicker', () => {
} }
/>
);
+
+ // Click and press arrow up
const dragArea = getByRole( 'button' );
act( () => {
fireEvent.mouseDown( dragArea );
@@ -98,5 +100,23 @@ describe( 'FocalPointPicker', () => {
rerender( <Picker value={ { x: 0.93, y: 0.5 } } /> );
expect( xInput.value ).toBe( '93' );
} );
+
+ it( 'call onChange with the expected values', async () => {
+ const spyChange = jest.fn();
+ const { getByRole } = render(
+ <Picker value={ { x: 0.14, y: 0.62 } } onChange={ spyChange } />
+ );
+
+ // Click and press arrow up
+ const dragArea = getByRole( 'button' );
+ act( () => {
+ fireEvent.mouseDown( dragArea );
+ fireEvent.keyDown( dragArea, { charCode: 0, keyCode: 38 } );
+ } );
+ expect( spyChange ).toHaveBeenCalledWith( {
+ x: '0.14',
+ y: '0.61',
+ } );
+ } );
} );
} );
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand what you're asking for. Doesn't the initial test cover this behaviour? Do you want me to remove the onChange part from that test and then make another one in the controllability
section that only tests the onChange part?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm proposing to add another unit test aimed at testing the behaviour that I highlighted earlier and you fixed in your latest commits by using the setState
callback to call onChange
.
Earlier, when removing resolvePoint
from this test, the onChange
spy was receiving x: 0, y: 0
instead of the expected value. Thanks to your fix, the onChange
would now receive the correct values. My proposal is to add an additional unit test to specifically test that (note that, in the new test, the resolvePoint
prop is not specified)
Hope I managed to explain myself better :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add the unit test but I want to make sure we're on the same page.
By using mouseDown
and mouseMove
we still receive { x: 0.00, y: 0.00 } instead of the values with which the picker is initialised.
The bug that was fixed by moving the onChange inside the setState callback was related to the resolvePoint
prop.
this.updateValue( next ); // next was being passed to resolvePoint here
this.props.onChange( next ); // but not here
This unit test becomes a test to future proof the keyboard interaction (rather for the UP key press). Am I right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By using mouseDown and mouseMove we still receive { x: 0.00, y: 0.00 } instead of the values with which the picker is initialised.
I see, I missed that detail. Do you think it's worth investigating this in a follow-up PR?
…ue and unit test passes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @razwan and @stokesman for the excellent collaboration effort!
I left a couple of minor comments, but as soon as they are addressed we can merge this PR 🚀
resolvePoint={ () => { | ||
spy(); | ||
return { x: 0.91, y: 0.42 }; | ||
} } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the explanation, makes sense!
I suggest we add another unit test to check this onChange
baheviour:
diff --git a/packages/components/src/focal-point-picker/test/index.js b/packages/components/src/focal-point-picker/test/index.js
index 145ca0d019..ac4f1af0e3 100644
--- a/packages/components/src/focal-point-picker/test/index.js
+++ b/packages/components/src/focal-point-picker/test/index.js
@@ -76,6 +76,8 @@ describe( 'FocalPointPicker', () => {
} }
/>
);
+
+ // Click and press arrow up
const dragArea = getByRole( 'button' );
act( () => {
fireEvent.mouseDown( dragArea );
@@ -98,5 +100,23 @@ describe( 'FocalPointPicker', () => {
rerender( <Picker value={ { x: 0.93, y: 0.5 } } /> );
expect( xInput.value ).toBe( '93' );
} );
+
+ it( 'call onChange with the expected values', async () => {
+ const spyChange = jest.fn();
+ const { getByRole } = render(
+ <Picker value={ { x: 0.14, y: 0.62 } } onChange={ spyChange } />
+ );
+
+ // Click and press arrow up
+ const dragArea = getByRole( 'button' );
+ act( () => {
+ fireEvent.mouseDown( dragArea );
+ fireEvent.keyDown( dragArea, { charCode: 0, keyCode: 38 } );
+ } );
+ expect( spyChange ).toHaveBeenCalledWith( {
+ x: '0.14',
+ y: '0.61',
+ } );
+ } );
} );
} );
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @razwan for you work! We can merge this PR as soon as conflicts are solved and CI is green 🚀
Congratulations on your first merged pull request, @razwan! We'd like to credit you for your contribution in the post announcing the next WordPress release, but we can't find a WordPress.org profile associated with your GitHub account. When you have a moment, visit the following URL and click "link your GitHub account" under "GitHub Username" to link your accounts: https://profiles.wordpress.org/me/profile/edit/ And if you don't have a WordPress.org account, you can create one on this page: https://login.wordpress.org/register Kudos! |
Thanks @ciampo and @stokesman for your help! On to the next one. See you around! |
It's been great to collaborate! Please feel free to ping me or @mirka for any |
In one of my projects I've created a HOC that adds a snapping functionality to the FocalPointPicker component but after #28676 it stopped working.
The onDrag event is a nice perk that should make possible most of the things that were accomplishable before. However, there's a slight change that doesn't allow me to do what I want and that's checking to see if the user is dragging when the componentDidUpdate runs before updating state.
I can't really see why that's the better thing to do or is it that we could go ahead without it without any worries.
@stokesman, would you mind dropping some knowledge on me?
I would really appreciate any feedback. Thanks!