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

[labs/virtualizer] RangeChangedEvent and VisibilityChangedEvent no longer bubble up #3609

Merged
merged 1 commit into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wild-turtles-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/virtualizer': minor
---

RangeChangedEvent and VisibilityChangedEvent both no longer bubble up. Listeners for these events must be placed on the lit-virtualizer or virtualize directive's host element.
4 changes: 2 additions & 2 deletions packages/labs/virtualizer/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class RangeChangedEvent extends Event {
last: number;

constructor(range: Range) {
super(RangeChangedEvent.eventName, {bubbles: true});
super(RangeChangedEvent.eventName, {bubbles: false});
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not for this PR, but can we get code comments on the event class and on first and last to document to users and maintainers what they're for? It can just be what's in the readme here: https://github.com/lit/lit/tree/main/packages/labs/virtualizer#visibilitychanged-event

this.first = range.first;
this.last = range.last;
}
Expand All @@ -24,7 +24,7 @@ export class VisibilityChangedEvent extends Event {
last: number;

constructor(range: Range) {
super(VisibilityChangedEvent.eventName, {bubbles: true});
super(VisibilityChangedEvent.eventName, {bubbles: false});
this.first = range.first;
this.last = range.last;
}
Expand Down
26 changes: 13 additions & 13 deletions packages/labs/virtualizer/src/test/layouts/flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('flow layout', () => {
)) as LitVirtualizer;
expect(virtualizer).to.be.instanceof(LitVirtualizer);
await until(() => getVisibleItems(virtualizer).length === 4);
return {container, virtualizer};
return virtualizer;
}

function getVisibleItems(virtualizer: LitVirtualizer) {
Expand All @@ -67,14 +67,14 @@ describe('flow layout', () => {

describe('item resizing', () => {
it('emits VisibilityChanged event due to item resizing', async () => {
const {container, virtualizer} = await createVirtualizer({
const virtualizer = await createVirtualizer({
items: array(1000),
});
const visibilityChangedEvents: VisibilityChangedEvent[] = [];

await until(() => getVisibleItems(virtualizer).length == 4);

container.addEventListener('visibilityChanged', (e) => {
virtualizer.addEventListener('visibilityChanged', (e) => {
visibilityChangedEvents.push(e as VisibilityChangedEvent);
});

Expand Down Expand Up @@ -105,7 +105,7 @@ describe('flow layout', () => {

describe('element(<index>).scrollIntoView', () => {
it('shows the correct items when scrolling to start position', async () => {
const {virtualizer} = await createVirtualizer({items: array(1000)});
const virtualizer = await createVirtualizer({items: array(1000)});

virtualizer.element(5)!.scrollIntoView({block: 'start'});

Expand All @@ -120,7 +120,7 @@ describe('flow layout', () => {
});

it('shows leading items when scrolling to last item in start position', async () => {
const {virtualizer} = await createVirtualizer({items: array(1000)});
const virtualizer = await createVirtualizer({items: array(1000)});

virtualizer.element(999)!.scrollIntoView({block: 'start'});

Expand All @@ -135,7 +135,7 @@ describe('flow layout', () => {
});

it('shows the correct items when scrolling to center position', async () => {
const {virtualizer} = await createVirtualizer({items: array(1000)});
const virtualizer = await createVirtualizer({items: array(1000)});

virtualizer.element(200)!.scrollIntoView({block: 'center'});

Expand All @@ -151,7 +151,7 @@ describe('flow layout', () => {
});

it('shows trailing items when scrolling to first item in end position', async () => {
const {virtualizer} = await createVirtualizer({items: array(1000)});
const virtualizer = await createVirtualizer({items: array(1000)});
virtualizer.element(0)!.scrollIntoView({block: 'end'});

await until(() =>
Expand All @@ -165,7 +165,7 @@ describe('flow layout', () => {
});

it('shows the correct items when scrolling to nearest position', async () => {
const {virtualizer} = await createVirtualizer({items: array(1000)});
const virtualizer = await createVirtualizer({items: array(1000)});

// The nearest position for item 500 will be at the end.
virtualizer.element(500)!.scrollIntoView({block: 'nearest'});
Expand Down Expand Up @@ -208,7 +208,7 @@ describe('flow layout', () => {

describe('scrollToIndex', () => {
it('shows the correct items when scrolling to start position', async () => {
const {virtualizer} = await createVirtualizer({items: array(1000)});
const virtualizer = await createVirtualizer({items: array(1000)});

virtualizer.scrollToIndex(5, 'start');

Expand All @@ -223,7 +223,7 @@ describe('flow layout', () => {
});

it('shows leading items when scrolling to last item in start position', async () => {
const {virtualizer} = await createVirtualizer({items: array(1000)});
const virtualizer = await createVirtualizer({items: array(1000)});

virtualizer.scrollToIndex(999, 'start');

Expand All @@ -238,7 +238,7 @@ describe('flow layout', () => {
});

it('shows the correct items when scrolling to center position', async () => {
const {virtualizer} = await createVirtualizer({items: array(1000)});
const virtualizer = await createVirtualizer({items: array(1000)});

virtualizer.scrollToIndex(200, 'center');

Expand All @@ -254,7 +254,7 @@ describe('flow layout', () => {
});

it('shows trailing items when scrolling to first item in end position', async () => {
const {virtualizer} = await createVirtualizer({items: array(1000)});
const virtualizer = await createVirtualizer({items: array(1000)});
virtualizer.scrollToIndex(0, 'end');

await until(() =>
Expand All @@ -268,7 +268,7 @@ describe('flow layout', () => {
});

it('shows the correct items when scrolling to nearest position', async () => {
const {virtualizer} = await createVirtualizer({items: array(1000)});
const virtualizer = await createVirtualizer({items: array(1000)});

// The nearest position for item 500 will be at the end.
virtualizer.scrollToIndex(500, 'nearest');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
import {array, ignoreBenignErrors, until} from '../helpers.js';
import {LitVirtualizer} from '../../lit-virtualizer.js';
import {virtualize} from '../../virtualize.js';
import {RangeChangedEvent, VisibilityChangedEvent} from '../../events.js';
import {css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {expect, html, fixture} from '@open-wc/testing';

abstract class TestElement extends LitElement {
static styles = css`
:host {
display: block;
height: 200px;
.item {
height: 50px;
margin: 0;
padding: 0;
}
`;

Expand All @@ -24,29 +26,52 @@ abstract class TestElement extends LitElement {

@property({type: Array, attribute: false})
public items: Array<number> = [];

@property({type: Array, attribute: false})
Copy link
Collaborator

Choose a reason for hiding this comment

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

type is only used for attribute converters (it would be more accurately named typeHint), so if a property has attribute: false it doesn't need a type at all. For test elements, it's academic though and you could just leave all properties off. The converter would only kick in if reflecting to an attribute (which is off) or setting from an attribute, which you're not doing.

public rangeChangedEvents: RangeChangedEvent[] = [];

@property({type: Array, attribute: false})
public visibilityChangedEvents: VisibilityChangedEvent[] = [];

recordRangeChangedEvent(event: RangeChangedEvent) {
this.rangeChangedEvents.push(event);
}

recordVisibilityChangedEvent(event: VisibilityChangedEvent) {
this.visibilityChangedEvents.push(event);
}
}

@customElement('using-lit-virtualizer')
class UsingLitVirtualizer extends TestElement {
render() {
return html` <lit-virtualizer
@rangeChanged=${this.recordRangeChangedEvent}
@visibilityChanged=${this.recordVisibilityChangedEvent}
scroller
.items=${this.items}
.renderItem=${(n: number) =>
html`<div>${n}${this.selected.has(n) ? ' selected' : ''}</div>`}
html`<div class="item">
[${n}${this.selected.has(n) ? ' selected' : ''}]
</div>`}
></lit-virtualizer>`;
}
}

@customElement('using-virtualize-directive')
class UsingVirtualizeDirective extends TestElement {
render() {
return html` <div>
return html` <div
@rangeChanged=${this.recordRangeChangedEvent}
@visibilityChanged=${this.recordVisibilityChangedEvent}
>
${virtualize({
scroller: true,
items: this.items,
renderItem: (n) =>
html`<div>${n}${this.selected.has(n) ? ' selected' : ''}</div>`,
html`<div class="item">
[${n}${this.selected.has(n) ? ' selected' : ''}]
</div>`,
})}
</div>`;
}
Expand Down Expand Up @@ -81,6 +106,14 @@ describe('lit-virtualizer and virtualize directive', () => {
<using-lit-virtualizer></using-lit-virtualizer>
<using-virtualize-directive></using-virtualize-directive>
</div>
<style>
using-lit-virtualizer {
height: 500px;
}
using-virtualize-directive {
height: 500px;
}
</style>
`);
await until(
() =>
Expand All @@ -96,45 +129,67 @@ describe('lit-virtualizer and virtualize directive', () => {
const ulv: UsingLitVirtualizer = example.querySelector(
'using-lit-virtualizer'
)!;
const uvd: UsingVirtualizeDirective = example.querySelector(
'using-virtualize-directive'
Copy link
Collaborator

Choose a reason for hiding this comment

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

For my edification, do you know why we need to wait for these elements first with the until() call above? The elements are defined in the module, and before the fixture call, which itself is awaited. I wouldn't think the until()s are needed at all. but if there was a need to wait for an upgrade, customElements.whenDefined('using-virtualize-directive') should work too.

I'm asking because the test is so long that any reduction in boilerplate will help new people read it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah this was how I was watching for upgrades from the outside. I didn't want to select the element until after the upgrade occurred, but I'll see if I can either eliminate the need for those, write a helper, or comment why it's necessary to explicitly await the upgrades for the elements when I do test clean ups in #2788

)!;

ulv.items = [...items];
uvd.items = [...items];

ulv.items = items;
ulv.selected = selected;
uvd.selected = selected;

await until(() => ulv.shadowRoot?.textContent?.includes('2 selected'));
await until(() => ulv.shadowRoot?.textContent?.includes('[5 selected]'));
await until(() => uvd.shadowRoot?.textContent?.includes('[5 selected]'));

expect(ulv.shadowRoot?.textContent).to.include('2 selected');
expect(ulv.shadowRoot?.textContent).to.include('5 selected');
expect(ulv.shadowRoot?.textContent).to.include('[2 selected]');
expect(uvd.shadowRoot?.textContent).to.include('[2 selected]');

const uvd: UsingVirtualizeDirective = example.querySelector(
'using-virtualize-directive'
)!;
expect(ulv.shadowRoot?.textContent).to.include('[5 selected]');
expect(uvd.shadowRoot?.textContent).to.include('[5 selected]');

uvd.items = items;
uvd.selected = selected;
// Changing selection doesn't trigger visibility changed or range changed events.
ulv.selected = new Set([1, 3]);
uvd.selected = new Set([1, 3]);

await until(() => ulv.shadowRoot?.textContent?.includes('[3 selected]'));
await until(() => uvd.shadowRoot?.textContent?.includes('[3 selected]'));

expect(ulv.shadowRoot?.textContent).to.include('[1 selected]');
expect(uvd.shadowRoot?.textContent).to.include('[1 selected]');

await until(() => uvd.shadowRoot?.textContent?.includes('2 selected'));
expect(ulv.shadowRoot?.textContent).to.include('[3 selected]');
expect(uvd.shadowRoot?.textContent).to.include('[3 selected]');

expect(uvd.shadowRoot?.textContent).to.include('2 selected');
expect(uvd.shadowRoot?.textContent).to.include('5 selected');
expect(ulv.shadowRoot?.textContent).not.to.include('[2 selected]');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe not now, but since the test is so long, it'd be great to see some comments break it up and describe the scenario, what the setup is doing, what the interaction is, expectations are checking, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair points. I'll add this when I do some overall test sprucing up in #2788

expect(uvd.shadowRoot?.textContent).not.to.include('[2 selected]');

const newSelected = new Set([1, 3]);
expect(ulv.shadowRoot?.textContent).not.to.include('[5 selected]');
expect(uvd.shadowRoot?.textContent).not.to.include('[5 selected]');

ulv.selected = newSelected;
// Clearing event arrays so we can watch for specific future events.
ulv.rangeChangedEvents.splice(0);
uvd.rangeChangedEvents.splice(0);
ulv.visibilityChangedEvents.splice(0);
uvd.visibilityChangedEvents.splice(0);

await until(() => ulv.shadowRoot?.textContent?.includes('1 selected'));
// Adding an item to the start of the list to trigger rangechanged and
// visibilitychanged events.
ulv.items = [-1, ...items];
uvd.items = [-1, ...items];

expect(ulv.shadowRoot?.textContent).to.include('1 selected');
expect(ulv.shadowRoot?.textContent).to.include('3 selected');
expect(ulv.shadowRoot?.textContent).not.to.include('2 selected');
expect(ulv.shadowRoot?.textContent).not.to.include('5 selected');
await until(() => ulv.shadowRoot?.textContent?.includes('[-1]'));
await until(() => uvd.shadowRoot?.textContent?.includes('[-1]'));

uvd.selected = newSelected;
await until(() => ulv.rangeChangedEvents.length > 0);
await until(() => uvd.rangeChangedEvents.length > 0);

await until(() => uvd.shadowRoot?.textContent?.includes('1 selected'));
expect(ulv.rangeChangedEvents.length).to.equal(1);
expect(uvd.rangeChangedEvents.length).to.equal(1);

expect(uvd.shadowRoot?.textContent).to.include('1 selected');
expect(uvd.shadowRoot?.textContent).to.include('3 selected');
expect(uvd.shadowRoot?.textContent).not.to.include('2 selected');
expect(uvd.shadowRoot?.textContent).not.to.include('5 selected');
// The indexes of visible items have not changed even though new item was
// added to head of the array. So no visibilitychanged events are expected.
expect(ulv.visibilityChangedEvents.length).to.equal(0);
expect(uvd.visibilityChangedEvents.length).to.equal(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,13 @@ describe('VisibilityChanged event', () => {

first(getVisibleItems(virtualizer)).style.height = '10px';

await until(() => containerEvents.length === 1);

expect(last(containerEvents).first).to.equal(0);
expect(last(containerEvents).last).to.equal(4);

await until(() => virtualizerEvents.length === 1);

expect(last(virtualizerEvents).first).to.equal(0);
expect(last(virtualizerEvents).last).to.equal(4);

// The visibilitychanged event should not bubble up to its containing element.
expect(containerEvents.length).to.equal(0);
});

it('should fire when item moves out of view', async () => {
Expand All @@ -99,14 +97,12 @@ describe('VisibilityChanged event', () => {

first(getVisibleItems(virtualizer)).style.height = '100px';

await until(() => containerEvents.length === 1);

expect(last(containerEvents).first).to.equal(0);
expect(last(containerEvents).last).to.equal(2);

await until(() => virtualizerEvents.length === 1);

expect(last(virtualizerEvents).first).to.equal(0);
expect(last(virtualizerEvents).last).to.equal(2);

// The visibilitychanged event should not bubble up to its containing element.
expect(containerEvents.length).to.equal(0);
});
});
1 change: 0 additions & 1 deletion packages/labs/virtualizer/src/virtualize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ class VirtualizeDirective<T = unknown> extends AsyncDirective {
const hostElement = part.parentNode as HTMLElement;
if (hostElement && hostElement.nodeType === 1) {
hostElement.addEventListener('rangeChanged', (e: RangeChangedEvent) => {
e.stopPropagation();
this._first = e.first;
this._last = e.last;
this.setValue(this.render());
Expand Down