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

Format Library: Assign Popover anchorRect to update selection position #14938

Merged
merged 8 commits into from
Apr 12, 2019

Conversation

aduth
Copy link
Member

@aduth aduth commented Apr 11, 2019

Fixes #14872
Fixes #11092
Alternative to #14921
Cherry-picks fc1fec7 from #14851

This pull request seeks to resolve an issue where repeated mounting of the link URLPopover component would cause excessive animations to occur in response to changes of selection or content within the formatted link. In doing so, it updates the behavior of link Popover to anchor to the link element itself, rather than follow the position of the caret.

Implementation Notes:

This reimplements the positioning to behave similar to Autocomplete in passing getAnchorRect to the rendered Popover, computed from the current selection. This replaces use of PositionedAtSelection.

#14872 is partly caused by the fact that to update PositionedAtSelection, we must assign a new key value, which would unmount and remount the component, causing the animation to restart.

It may be simpler to review using GitHub's "Hide whitespace changes" diff options, given the changes in inline.js are largely indentation changes.

Testing instructions:

Repeat Steps to Reproduce from #14872, verifying that the animation only occurs once when within the link formatting.

Avoids need to explicitly support onClickOutside as deprecated pass-through prop, instead leveraging pass-through nature of spread props.
gziolo
gziolo previously requested changes Apr 12, 2019
Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

It doesn't work for me as expected. The popover moves on the screen...:

link-test

macOS, Chrome 73

position = 'bottom center',
focusOnMount = 'firstElement',
...popoverProps
Copy link
Member

Choose a reason for hiding this comment

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

I think I reviewed it already earlier this week 😃

@gziolo gziolo added the [Type] Bug An existing feature does not function as intended label Apr 12, 2019
@aduth
Copy link
Member Author

aduth commented Apr 12, 2019

@gziolo Good catch. It's not reliable to continuously update the caret positioning when link interactions (add, edit) will place selection into the editing popover.

@ellatrix I've pushed some updates which converge a bit more with your #14921, but still opts to render URLPopover with the getAnchorRect than to update PositionedAtSelection. It works well, though I did note the one case of pressing ArrowRight into the anchor node, it will not show the popover when first entering. I noticed this is what you'd accounted for with considering nextElementSibling, but I was a bit worried about the possibility of elements within the link yielding an unexpected value (the bolded element, for example, instead of the anchor). Although, now that I type this, I guess if it eventually uses Element#closest( 'a' ) anyways, it would match the anchor node.

@ellatrix
Copy link
Member

I noticed this is what you'd accounted for with considering nextElementSibling, but I was a bit worried about the possibility of elements within the link yielding an unexpected value (the bolded element, for example, instead of the anchor). Although, now that I type this, I guess if it eventually uses Element#closest( 'a' ) anyways, it would match the anchor node.

Yes, it's an edge case. I think it could work reliably though, you'd always end up with the right element.

@@ -123,6 +123,20 @@ Opt-in prop to show popovers fullscreen on mobile, pass `false` in this prop to
- Required: No
- Default: `false`

### getAnchorRect
Copy link
Member

Choose a reason for hiding this comment

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

Are we certain about the future of getAnchorRect before documenting it? Perhaps we could work towards only supporting anchorRect.

Copy link
Member Author

Choose a reason for hiding this comment

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

Are we certain about the future of getAnchorRect before documenting it? Perhaps we could work towards only supporting anchorRect.

I don't mind to remove the documentation, if even just to keep the changes well-contained. I don't have much a problem with this prop, at least in that it's a useful way to also base the computed anchor rect relative the passed default anchor argument. I'm also not entirely comfortable with the prospect of removing functionality based on the absence of it from documentation either.

}

const hasAnchorRect = this.props.hasOwnProperty( 'anchorRect' );
if ( hasAnchorRect !== prevProps.hasOwnProperty( 'anchorRect' ) ) {
Copy link
Member

Choose a reason for hiding this comment

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

Why use hasOwnProperty?

Copy link
Member Author

@aduth aduth Apr 12, 2019

Choose a reason for hiding this comment

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

Why use hasOwnProperty?

Because we expect the value could change over time, but scheduling the interval doesn't depend on the value, it just depends on whether a value exists.

i.e. if the alternative is this.props.anchorRect !== prevProps.anchorRect, it would have unintended consequences on the auto-refresh.

Copy link
Member

@ellatrix ellatrix left a comment

Choose a reason for hiding this comment

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

This works really well in my testing!

@@ -77,6 +77,39 @@ const LinkViewerUrl = ( { url } ) => {
);
};

const URLPopoverAtLink = ( { isActive, addingLink, value, ...props } ) => {
const anchorRect = useMemo( () => {
Copy link
Member

Choose a reason for hiding this comment

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

Just wondering... Does this have a limit on cache size? Sounds like it should have a limit of exactly one.

Copy link
Member Author

@aduth aduth Apr 12, 2019

Choose a reason for hiding this comment

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

Just wondering... Does this have a limit on cache size? Sounds like it should have a limit of exactly one.

It's not entirely clear, but it seems like something they would have considered as being the default behavior, but I also recall some similar discussion with @gziolo recently where he was showing a separate project which, by its name, might imply a different behavior 🤷‍♂️

https://reactjs.org/docs/hooks-reference.html#usememo

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

https://github.com/alexreardon/use-memo-one#readme

useMemo and useCallback cache the most recent result. However, this cache can be destroyed by React when it wants to:

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, to me it sounded like it was not the intention of useMemo to limit the cache to one by default. So I would consider swapping it. I'm generally wary of things that create cache without any limit. We're just taking more and more space from memory for no reason.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, to me it sounded like it was not the intention of useMemo to limit the cache to one by default. So I would consider swapping it. I'm generally wary of things that create cache without any limit. We're just taking more and more space from memory for no reason.

Oh, I was thinking the opposite by my interpretation; that yes, it would only store the one latest result in memory. I'll plan to double-check, though.

Copy link
Member Author

Choose a reason for hiding this comment

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

@aduth aduth dismissed gziolo’s stale review April 12, 2019 18:40

Feedback addressed, confirmed via DM to dismiss

@aduth aduth changed the title Format Library: Assign Popover getAnchorRect to update selection position Format Library: Assign Popover anchorRect to update selection position Apr 12, 2019
@aduth aduth merged commit 6a26e36 into master Apr 12, 2019
@aduth aduth deleted the fix/14872-link-url-popover-popover-rect branch April 12, 2019 18:43
mchowning pushed a commit to mchowning/gutenberg that referenced this pull request Apr 15, 2019
WordPress#14938)

* Block Editor: Pass spread props from URLPopover to Popover

Avoids need to explicitly support onClickOutside as deprecated pass-through prop, instead leveraging pass-through nature of spread props.

* Format Library: Assign Popover getAnchorRect to update selection position

* Format Library: Compute memoized anchor from caret point or active element

* Components: Add anchorRect prop to Popover component

* Format Library: Provide direct reference to Popover anchorRect

* Format Library: Consider next element sibling for initial caret placement

Co-Authored-By: Ella van Durpe <ellatrix@users.noreply.github.com>

* Components: Mark PositionedAtSelection as unstable

* Components: Remove getAnchorRect mention from Popover README
@youknowriad youknowriad added this to the 5.5 (Gutenberg) milestone Apr 15, 2019
aduth added a commit that referenced this pull request Apr 16, 2019
#14938)

* Block Editor: Pass spread props from URLPopover to Popover

Avoids need to explicitly support onClickOutside as deprecated pass-through prop, instead leveraging pass-through nature of spread props.

* Format Library: Assign Popover getAnchorRect to update selection position

* Format Library: Compute memoized anchor from caret point or active element

* Components: Add anchorRect prop to Popover component

* Format Library: Provide direct reference to Popover anchorRect

* Format Library: Consider next element sibling for initial caret placement

Co-Authored-By: Ella van Durpe <ellatrix@users.noreply.github.com>

* Components: Mark PositionedAtSelection as unstable

* Components: Remove getAnchorRect mention from Popover README
aduth added a commit that referenced this pull request Apr 16, 2019
#14938)

* Block Editor: Pass spread props from URLPopover to Popover

Avoids need to explicitly support onClickOutside as deprecated pass-through prop, instead leveraging pass-through nature of spread props.

* Format Library: Assign Popover getAnchorRect to update selection position

* Format Library: Compute memoized anchor from caret point or active element

* Components: Add anchorRect prop to Popover component

* Format Library: Provide direct reference to Popover anchorRect

* Format Library: Consider next element sibling for initial caret placement

Co-Authored-By: Ella van Durpe <ellatrix@users.noreply.github.com>

* Components: Mark PositionedAtSelection as unstable

* Components: Remove getAnchorRect mention from Popover README
This was referenced Apr 17, 2019
if ( closest ) {
return closest.getBoundingClientRect();
}
}, [ isActive, addingLink, value.start, value.end ] );
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know that isActive needed to be listed as a dependency. It's not ever used in the useMemo callback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Format library /packages/format-library [Type] Bug An existing feature does not function as intended
Projects
None yet
4 participants