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

Declunkify focal point picker dragging #28676

Merged
merged 12 commits into from
Feb 15, 2021

Conversation

stokesman
Copy link
Contributor

@stokesman stokesman commented Feb 2, 2021

This PR stemmed from realizing that maybe it wasn't just me and my modest computer experiencing clunky dragging in the focal point picker.
#28487 (comment)

Setting out to improve that, it didn't take much tinkering to figure out that the bulk of the poor performance comes from setting block attributes during the drag operation. It's super intensive because it's driven by onMouseMove and causes quite the render cascade.

While I'd hoped to find the solution with changes only to FocalPointPicker, the consuming blocks need some changes as well. The Cover and Media & Text blocks have been updated to set their attributes only at the end of a drag operation and to preview the focal point with an imperative (render-free) approach while dragging.

A complication ensued in updating the Media & Text block. To create an imperative routine for focal point preview, a ref to a ResizableBox was needed, and being that it was also wrapped by withNotices, both components needed updates to support ref forwarding. To support that in withNotices it seemed best to refactor it to a function component. Touching that component looks fraught with peril since I see no unit tests for it, but I'm hoping there may be some E2E coverage.

UPDATE: Unit tests have been added for withNotices. A few new unit tests have also been added for FocalPointPicker.

How has this been tested?

Making posts with Cover and Media & Text blocks and dragging their focal points like mad.

Screen recordings (as specific to my machine as they may be)

Before:

focal-point-drag-before.mp4

After

focal-point-drag-after.mp4

Types of changes

  • Enhancement
  • Breaking change: FocalPointPicker calls onChange only at the end of a drag operation, whereas before it was called for each mousemove event. So it still works, just not as rapidly as it used to. (I wasn't totally sure it qualifies as breaking but it's how I've classified it in the changelog).
  • New feature: onDrag added as an event for FocalPointPicker. This is now what's called each mousemove
  • Fix: Focus of the FocalPointPicker draggable area and adjustment with arrow keys. (This was added Cover: Video - Add position controls #22531 but was no longer working)
  • Fix: FocalPointPicker fires onDragStart and onDragEnd (they were in the code already but did not actually fire).
  • New feature:ResizeableBox is now a forwardRef*
  • New feature: withNotices returns a forwardRef if passed a forwardRef*

*React docs suggest to treat changing a component to a forwardRef as breaking, but I believe that's for a context of changing a class component with an ad-hoc prop to support ref forwarding. In the cases of ResizableBox and withNotices they had no support for ref forwarding before this otherwise have no changes. So it seems only an unbreaking added feature.

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR.

Also:
- fix focus and keyboard input on media container
- fix onDragStart and onDragEnd to actually fire
Due to the need to forward a ref through a few components they are
also updated. The HOC withNotices is refactored to a function
component and made accept and return forwardRef components.
ResizableBox is updated to a forwardRef.
@gziolo gziolo added [Type] Enhancement A suggestion for improvement. [Package] Components /packages/components labels Feb 2, 2021
@gziolo gziolo requested review from ItsJonQ and jasmussen February 2, 2021 22:14
@jasmussen
Copy link
Contributor

Wow, this is a night and day improvement. You keep making amazing PRs to the project, thank you so much! Big fan of this work — I'll trust the developers to help review the details, but in testing this, the experience is vastly improved.

Copy link

@ItsJonQ ItsJonQ left a comment

Choose a reason for hiding this comment

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

🚀 from me! This is great @stokesman :).

I think offering an onDrag as an "in-between" of start/stop (onChange) works well.

Like you've demonstrated, it provides the flexibility to handle updates/rendering in alternate ways. In this case, manipulating the DOM directly, by-passing React's render cycle (which is always faster).

The trade-off is, we'd need to replicate this setup every time - which I believe is uncommon, but may happen.

It's a 👍 from me.


(Below are my thoughts on Gutenberg / UI / performance. Not directly related to this PR :D)

I feel like this touches upon a deeper problem - one that I've encountered often in my work exploring improving Design Tools for Gutenberg. That is... how do we effectively add rich interactions without degrading performance? @stokesman from what I've observed of your PRs, this is probably something you're mindful of as well.

Rich interactions often involve actions (usually drag) updating many times a second. These updates need to move up to the data store and back down, and ripple React's render cycles. From my research, the best pattern to resolve these issues (at scale) is to liberally use (data) selectors + (component) memoization to control re-renders. The trick is to establish the necessary patterns (perhaps helper functions?) to reduce the verbosity of this "rigging" work.

@stokesman
Copy link
Contributor Author

Thanks for the quick test drive @jasmussen and review @ItsJonQ!

I've pushed a bunch of mostly small commits to patch up a few things, add tests, and update docs + changelog. With that, I'll take this out of draft status.

@stokesman stokesman marked this pull request as ready for review February 8, 2021 06:11
@jasmussen jasmussen requested a review from a team February 8, 2021 08:24
@ItsJonQ
Copy link

ItsJonQ commented Feb 9, 2021

That's great @stokesman ! I'm 👍 for this to merge if other folks are

Copy link
Member

@aristath aristath left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@draganescu draganescu merged commit c5ae463 into WordPress:master Feb 15, 2021
@github-actions github-actions bot added this to the Gutenberg 10.1 milestone Feb 15, 2021
@stokesman stokesman deleted the update/focal-point-unclunk branch February 15, 2021 17:17
@dcalhoun
Copy link
Member

@stokesman @ItsJonQ @aristath these changes appear to have introduced a breakage for mobile Gutenberg in the form of wordpress-mobile/gutenberg-mobile#3149. Specifically, it appears it may be related to the changes to the withNotice HOC.

This breakage is causing an app crash and is blocking the next release of the mobile apps this week, which is a time-sensitive matter. 😞 Would you be able to help us identify a fix or evaluate the possibility of reverting these changes until a fix is identified please? Thank you!

dcalhoun added a commit to dcalhoun/gutenberg that referenced this pull request Feb 15, 2021
It is unknown why relying upon function declaration hoisting causes an
error in React Native, but it does occur. Recent changes with WordPress#28676
refactored a class to a functional component, including leveraging
function declaration hoisting. I.e. the function was referenced
prior to its declaration in terms of the order of the lines of code.

Re-ordering the code so that the function declaration occurs prior to
referencing it appears to resolve the error in React Native.

Related: https://git.io/JtXMm
@dcalhoun
Copy link
Member

To provide an update on #28676 (comment), I believe I have identified a fix and am awaiting review in #29013.

gziolo pushed a commit that referenced this pull request Feb 16, 2021
It is unknown why relying upon function declaration hoisting causes an
error in React Native, but it does occur. Recent changes with #28676
refactored a class to a functional component, including leveraging
function declaration hoisting. I.e. the function was referenced
prior to its declaration in terms of the order of the lines of code.

Re-ordering the code so that the function declaration occurs prior to
referencing it appears to resolve the error in React Native.

Related: https://git.io/JtXMm
pablinos added a commit to pablinos/gutenberg that referenced this pull request Mar 5, 2021
The refactor of withNotices in WordPress#28676 introduced a bug where the
functions within `noticeOperations` were changing on every render. That
meant that if they were present in the dependency array of a useEffect,
then the effect would trigger causing an infinite loop.

This was partially fixed with WordPress#29491, but there are other instances
where the noticeOperations object itself is being listed as a
dependency, and so the problem persists.

As suggested by @stokesman
WordPress#29491 (comment)
this change memoizes the whole object.
ntsekouras pushed a commit that referenced this pull request Mar 5, 2021
The refactor of withNotices in #28676 introduced a bug where the
functions within `noticeOperations` were changing on every render. That
meant that if they were present in the dependency array of a useEffect,
then the effect would trigger causing an infinite loop.

This was partially fixed with #29491, but there are other instances
where the noticeOperations object itself is being listed as a
dependency, and so the problem persists.

As suggested by @stokesman
#29491 (comment)
this change memoizes the whole object.
ntsekouras pushed a commit that referenced this pull request Mar 5, 2021
The refactor of withNotices in #28676 introduced a bug where the
functions within `noticeOperations` were changing on every render. That
meant that if they were present in the dependency array of a useEffect,
then the effect would trigger causing an infinite loop.

This was partially fixed with #29491, but there are other instances
where the noticeOperations object itself is being listed as a
dependency, and so the problem persists.

As suggested by @stokesman
#29491 (comment)
this change memoizes the whole object.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Components /packages/components [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants