-
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
Changes from all commits
89c80b7
e353f1a
008135b
9056f4f
6a7dc7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@lit-labs/virtualizer': patch | ||
| --- | ||
|
|
||
| Fix bug affecting position: fixed scrollers (#4125) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2022 Google LLC | ||
| * SPDX-License-Identifier: BSD-3-Clause | ||
| */ | ||
|
|
||
| import {array, ignoreBenignErrors} from '../helpers.js'; | ||
| import {LitVirtualizer} from '../../lit-virtualizer.js'; | ||
| import {grid} from '../../layouts/grid.js'; | ||
| import {styleMap} from 'lit/directives/style-map.js'; | ||
| import {expect, html, fixture} from '@open-wc/testing'; | ||
|
|
||
| interface NamedItem { | ||
| name: string; | ||
| } | ||
|
|
||
| const _100Items = array(100).map((_, i) => ({name: `Item ${i}`})); | ||
|
|
||
| describe('Virtualizer behaves properly when it has a position: fixed ancestor', () => { | ||
| ignoreBenignErrors(beforeEach, afterEach); | ||
|
|
||
| // Regression test for https://github.com/lit/lit/issues/3904 | ||
| it('should calculate its viewport correctly', async () => { | ||
| // Our outer container is overflow: hidden, so would normally be | ||
| // a "clipping ancestor" of our virtualizer. Since it has a | ||
| // width of zero, it would cause the virtualizer to be invisible. | ||
| const containerStyles = { | ||
| overflow: 'hidden', | ||
| width: '0', | ||
| }; | ||
|
|
||
| // But since this scrolling element is position: fixed, it will | ||
| // be positioned relative to the document, essentially "popping out" | ||
| // of its zero-width parent. This means the virtualizer will be | ||
| // visible and have some space to work with, after all. | ||
| const scrollerStyles = { | ||
| position: 'fixed', | ||
| width: '200px', | ||
| height: '200px', | ||
| }; | ||
|
|
||
| // We use the grid layout here because it conveniently will not | ||
| // render any items if it calculates that it doesn't have sufficient | ||
| // space, making our testing job easier. | ||
| const container = await fixture(html` | ||
| <div id="container" style=${styleMap(containerStyles)}> | ||
| <div id="scroller" style=${styleMap(scrollerStyles)}> | ||
| <lit-virtualizer | ||
| .layout=${grid({ | ||
| itemSize: '100px', | ||
| })} | ||
| .items=${_100Items} | ||
| .renderItem=${({name}: NamedItem) => html`<div>${name}</div>`} | ||
| ></lit-virtualizer> | ||
| </div> | ||
| </div> | ||
| `); | ||
|
|
||
| const scroller = container.querySelector('#scroller')!; | ||
| expect(scroller).to.be.instanceOf(HTMLDivElement); | ||
| const virtualizer = container.querySelector('lit-virtualizer')!; | ||
| expect(virtualizer).to.be.instanceOf(LitVirtualizer); | ||
|
|
||
| // If virtualizer has properly ignored the zero-width ancestor of our | ||
| // fixed-position scroller, some children will be rendered; otherwise, not. | ||
| // | ||
| // 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'); | ||
| }); | ||
|
|
||
| // Regression test for https://github.com/lit/lit/issues/4125 | ||
| it('should respond to scroll events from a fixed-position scroller', async () => { | ||
| const containerStyles = { | ||
| overflow: 'hidden', | ||
| }; | ||
|
|
||
| // Our scroller is position: fixed. While its ancestors (if any) should | ||
| // therefore be ignored for the purpose of calculating virtualizer's | ||
| // viewport, the scroller itself should be considered a clipping ancestor, | ||
| // and virtualizer should listen to any `scroll` events it emits. | ||
| const scrollerStyles = { | ||
| position: 'fixed', | ||
| width: '200px', | ||
| height: '200px', | ||
| overflow: 'auto', | ||
| }; | ||
|
|
||
| const container = await fixture(html` | ||
| <div id="container" style=${styleMap(containerStyles)}> | ||
| <div id="scroller" style=${styleMap(scrollerStyles)}> | ||
| <lit-virtualizer | ||
| .items=${_100Items} | ||
| .renderItem=${({name}: NamedItem) => html`<p>${name}</p>`} | ||
| ></lit-virtualizer> | ||
| </div> | ||
| </div> | ||
| `); | ||
|
|
||
| const scroller = container.querySelector('#scroller')!; | ||
| expect(scroller).to.be.instanceOf(HTMLDivElement); | ||
| const virtualizer = container.querySelector('lit-virtualizer')!; | ||
| expect(virtualizer).to.be.instanceOf(LitVirtualizer); | ||
|
|
||
| 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'); | ||
|
Comment on lines
+106
to
+117
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interestingly replacing both of the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 In my updated version of this test, I added some ad hoc logic to race 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.
Considering a failure's a failure so I'm okay either way, but what are your thoughts on using
passhere since the failure message would be more explicit?Uh oh!
There was an error while loading. Please reload this page.
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
passexcept in cases where we really can't avoid it; instead, I'd prefer to make surelayoutCompleteis 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
layoutCompleteimplementation 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
layoutCompleteis worth discussing but probably better to be split from this PR just so we can land theposition: fixedscroller 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
layoutCompletechanges into a separate PR.