-
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
Popover: consistently adjust position on scroll #17867
Conversation
anchorRef, | ||
anchorWithoutPadding, | ||
anchorVerticalBuffer, | ||
anchorHorizontalBuffer, |
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.
Do you think we should have a single prop instead with different options? anchor = { ref, padding, buffer }
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 don't like nesting that much for component props. That would break shallow comparison, right? Maybe we can name them differently...
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.
It does break shallow comparison. we can use things like useMemo
to avoid that but I'm not sure if it's worth it.
I like the approach of this PR, maybe we can get more thoughts on props naming here @mcsf
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 wonder how other popover libraries or design systems name/handle the positioning options. cc @ItsJonQ
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.
@youknowriad + @ellatrix 👋 Halloo!
A popular popover/positioner library folks use (under the hood) is Popper.js
Their React implementation uses the main library and share the same API.
They use nested objects, example:
offsets.popper
top, left, width, height values
data.offsets.reference
top, left, width, height values
I've used both shallow and nested approaches before. Both have their pros and cons :).
Typically component library components/primitives have lots of props. Way more compared to components created at the app level.
If this were going into@wordpress/components
to update the Popover
, I would vote for @youknowriad 's suggestion with nested.
However, since this is updating an "application" component, something that typically won't be consumed externally and used to build interfaces... I would agree with you @ellatrix, and vote for shallow.
Hope this helps! <3
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've used both shallow and nested approaches before. Both have their pros and cons :).
Could you elaborate? What are the pros of nesting? I'm glad to see that Popper
also requires an element reference, just as I'm proposing here.
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 glad to see that Popper also requires an element reference, just as I'm proposing here.
❤️
A pro of nesting would be that it keeps related props grouped together.
Also makes them easier to work with (IMO) those props are passed within a render prop.
I would only nest 1 level deep (2 max!!! if you really really need to)
An example would be react-select. The components
prop is a good one. It allows the user to pass in custom components to replace their sub-components (e.g. Menu
). If that wasn't there, they might need to define something like componentMenu
, componentInput
, etc...
It's not the end of the world. But that library has a ton of props. I think this keeps it tidier :)
That being said.. if one decides to nest, they'll need to do things like (as @youknowriad mentioned) memoize. Also to do some sort of defaultValues + userDefinedValues merging.
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.
useMemo
puts extra responsibilities on the implementor. Not sure if it's worth it for these few props.
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 apologies! I didn't realize these changes were on @wordpress/components/popover
.
(It was early for me)
useMemo puts extra responsibilities on the implementor.
I agree. These primitives should take care of this overhead.
If we were to use an object
, would exporting it like this help?
import { useRef, useState, useEffect, memo } from '@wordpress/element';
...
const PopoverContainer = memo(Popover);
I'm guessing not, since memo
only does shallow comparisons.
Not sure if it's worth it for these few props.
Given its current state, I think it might be better to keep it simple (and shallow).
My concern was maybe later the number of props + names of them may get unwieldy, but that may not happen :).
Apologies for the back and forth!
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 mean, the interface provider can always manage itself with shouldComponentUpdate
, and I get the benefit of keeping things tidy with nested props.
The question is probably how much we expect these interfaces to grow, since we aren't building an all-encompassing popover library. My first impression is that it's probably not worth the extra complexity to have nested props.
I like the approach of this PR, maybe we can get more thoughts on props naming here @mcsf
I think this PR is good. I would just double-check that the props are named in a way that conveys their type well, as pointed out in https://github.com/WordPress/gutenberg/pull/17867/files#r351238110.
if ( ! anchorRef.current ) { | ||
return; | ||
} | ||
let newAnchor = computeAnchorRect( |
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 don't see much difference betweencomputeAnchorRect
and useAnchor
. I mean we could just inline it here. What's the reasoning?
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.
It's clearer to me to make early returns than to wrap inside else
blocks. :)
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.
And to return a value to a constant instead of assigning it to a variable.
@youknowriad @ItsJonQ Anything left here? |
dda3afd
to
3af3643
Compare
* @param {Object} anchorRect Anchor Rect prop used to override the computed value. | ||
* @param {Function} getAnchorRect Function used to override the anchor value computation algorithm. | ||
* @param {Object} anchorRef Reference to the popover anchor fallback element. | ||
* @param {Object} anchorIncludePadding Whether to include the anchor padding. |
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.
Type should be boolean.
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.
The buffer props below should have their type corrected too.
Similarly, anchorRef
should be better defined, as it seems to assume many shapes: Range
, Boolean
, maybe also WPElement
? Do we need a new type definition for it, and is the "ref" portion of the name still apt?
Finally, I recommend that anchorIncludePadding
be rephrased so as to convey that it's a Boolean, with a should
prefix.
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 updated the types. anchorRef
can be an Element
or a Range
. I think the naming is fine as there are references to a live element or range.
I changed anchorIncludePadding
to shouldAnchorIncludePadding
, but I find the naming a bit strange. Maybe includeAnchorPadding
or shouldIncludeAnchorPadding
?
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 think all names will be strange :P. This is when I miss Scheme's or Ruby's use of question marks for predicates (e.g. (empty? x)
or x.empty?
) — not that this helps in the conversation.
There's also anchorShouldIncludePadding
? Honestly, as long as should
or a comparable modal is there, I'm happy with whatever. :)
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.
This is looking quite good. From the code I'm only wondering about performance in useAnchor
; specifically, I'm looking at refreshAnchorRect
, whether it should be memoised, and also at the getAnchorRect
prop — which, since it's a function, places the burden of reusing function references on the interface user.
But the only thing that really matters is overall performance impact. Our perf suite is focused entirely on typing, but it's all we have for now. Have you compared perf results against master for this branch?
@mcsf I'm not sure how the performance test would help as there are no popovers rendering during the test. I'll memoize |
@mcsf Hm, we shouldn't memoize that function or anything in it because it calculates a new position based on the bounding rectangles. So if you scroll, we need a new rect for the same element or range with the same options. |
There should be no difference in perf, but if there were that would be a smell to address. :)
👍 |
Looks like it's the opposite. Master is a tiny bit slower. Master: This branch: |
Description
Currently some popovers won't reposition when the page is scrolled. These include: the link popover, image, and autocomplete.
The problem is that the rectangle is NOT calculated by
Popover
, but by the component implementing it. In other word, theanchorRect
is no good if you want the popover to reposition on scroll. I'm not sure what the prop would be good for, so perhaps we should look into deprecating it if no core components use it.getAnchorRect
would be an option, but I believe we can provide a better API than that. Some implementation might want to adjust the rectangle: add a buffer or include the padding of the target anchor. It seems much cleaner for the implementor to just pass the anchor reference, and letPopover
compute the anchor rect and adjust it when needed.So I added a few more props for the
Popover
component. The old ones will still continue to work.anchorRef
: a reference to the anchor, which can be either anElement
or aRange
(the browser can give us aDOMRect
for both of these).anchorIncludePadding
: can be used if you want to include the anchor padding when creating a rectangle.anchorVerticalBuffer
andanchorHorizontalBuffer
can be used to add a buffer/offset between the anchor and the popover.How has this been tested?
Open the link, image, or slash command popover. Scroll the page.
Screenshots
Types of changes
Bug fix.
Checklist: