-
Notifications
You must be signed in to change notification settings - Fork 1k
[labs/virtualizer] Fix issue with position: fixed scrollers #4130
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
Conversation
🦋 Changeset detectedLatest commit: 6a7dc7f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📊 Tachometer Benchmark ResultsSummarynop-update
render
update
update-reflect
Resultsthis-change
render
update
update-reflect
this-change, tip-of-tree, previous-release
render
update
nop-update
this-change, tip-of-tree, previous-release
render
update
this-change, tip-of-tree, previous-release
render
update
update-reflect
|
augustjk
left a comment
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.
Excellent fix and test additions!
I had a some questions about whether we might not need the awaiting of layoutComplete but seems like it is indeed needed at least when we want to test scrolling.
| // In practice, we'll time out if we fail here because the `layoutComplete` | ||
| // promise will never be fulfilled. | ||
| await virtualizer.layoutComplete; | ||
| expect(virtualizer.textContent).to.contain('Item 0'); |
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.
Considering a failure's a failure so I'm okay either way, but what are your thoughts on using pass here since the failure message would be more explicit?
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 prefer to avoid using pass except in cases where we really can't avoid it; instead, I'd prefer to make sure layoutComplete is as reliable as possible and use that for cases where we want to check the state of things after a virtualizer renders.
That said, I was also bothered by the fact that this test failed with a timeout rather than something more akin to the failure you'd observe in manual testing, so I did a bit of digging. It turns out that there were two quirks in the layoutComplete implementation that resulted in the timeout in this case:
-
When you provided a layout explicitly, rather than relying on the default layout to be dynamically loaded, the initial layout pass would complete before you had a chance to request the
layoutCompletepromise. -
If a layout pass resulted in no children being rendered (as in the failure case for this test),
layoutCompletewould never resolve.
I have made tweaks to address both of these quirks and pushed a new commit to this branch. As a result, this test now fails not with a timeout but with a bad render, as you'd expect.
However, there is a decent chance that releasing this change will require users to update some existing tests; specifically, if they are rendering a virtualizer and immediately checking the state of its initial render (without any delay or retry logic), they will likely need to insert an await virtualizer.layoutComplete. (I had to do this to two of our existing tests.) So we need to decide whether we want to go ahead (I'm inclined to) and, if so, whether we call this a breaking change for the Virtualizer release (I'm open to discussion).
Edit: If we do go ahead, we'll want to edit the change log entry for this release to separately call out the change to layoutComplete.
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 torn. I think the change to layoutComplete is worth discussing but probably better to be split from this PR just so we can land the position: fixed scroller fix first.
Like the comment you added, it feels bad to introduce a delay purely for the sake of a promise whose main use is for testing.
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.
Yeah, that makes sense. I'll roll back that commit here and make the layoutComplete changes into a separate PR.
| await virtualizer.layoutComplete; | ||
| expect(virtualizer.textContent).to.contain('Item 0'); | ||
|
|
||
| // If the position: fixed scroller has properly been recognized as | ||
| // a clipping ancestor, then virtualizer will re-render as scrolling | ||
| // occurs; otherwise, not. | ||
| // | ||
| // In practice, we'll time out if we fail here because the `layoutComplete` | ||
| // promise will never be fulfilled. | ||
| scroller.scrollTo(0, scroller.scrollHeight); | ||
| await virtualizer.layoutComplete; | ||
| expect(virtualizer.textContent).to.contain('Item 99'); |
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.
Interestingly replacing both of the await virtualizer.layoutComplete here with pass() makes this fail. There must be something about starting a scroller.scrollTo() specifically after awaiting layoutComplete that looking for 'Item 0' alone isn't enough.
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.
Yeah, it is possible in a testing context to initiate a scroll while virtualizer is still in the middle of a layout pass, and this can cause unpredictable results. Waiting for layoutComplete avoids this situation, which is another reason why I'd like to lean on it more heavily in our tests.
In my updated version of this test, I added some ad hoc logic to race the layoutComplete promise against a short timeout so that we don't fail with a timeout; instead, we fail because the rendered state of the virtualizer is not correct (which aligns nicely with how you'd observe this failure in manual testing).
|
@augustjk Thanks for your review! I made some changes in response to your comments, though a bit different from the ones you proposed. See my detailed comments above. It's not obvious whether we should proceed with the changes in my last commit, so please take a look and let's discuss. |
This reverts commit 9056f4f.
Fixes #4125, which was a regression caused by #3976 (a fix for #3904).
When connected, a virtualizer walks up the DOM tree, visiting its ancestor elements and determining which of them are "clipping ancestors" (ancestor elements whose scroll positions may affect the visibility of the virtualizer itself). In principle, virtualizer should stop looking for clipping ancestors as soon as it comes to a
position: fixedancestor, since fixed positioning is always relative to the document, making any higher-level ancestors irrelevant.Prior to #3976, we didn't pay attention to whether ancestors were
position: fixed, causing the issue described in #3904.However, the logic in #3976 wasn't quite right; it excluded the
position: fixedancestor itself, along with its higher-level ancestors. In the case where theposition: fixedancestor was itself a scroller, this caused virtualizer not to listen forscrollevents and therefore not re-render children when scrolling occurred (as described in #4125).Fixed the logic and added regression tests for both issues.