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

feat: create virtual scroller class #70

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

samrichardsontylertech
Copy link
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

  • Tests for the changes have been added: N
  • Docs have been added / updated: N
  • Does this PR introduce a breaking change? N
  • I have linked any related GitHub issues to be closed when this PR is merged? N

Describe the new behavior?

A virtual scroller class was created to append and remove elements as they come into view. A container element, data array, element builder callback, and fixed item height are required to instantiate it. To maintain semantic relationships, no intermediate elements are created but an aria-hidden sibling to the items is added to set the scroll height.

Additional information

This is a draft pull request. The virtual scroller's name, location in the code, and exposure as part of the public API can all be changed. The main intention for this at the moment is to be used internally as part of list dropdowns.

@samrichardsontylertech samrichardsontylertech added enhancement New feature or request skip-release Preserve the current version when merged labels Jun 8, 2022
Copy link
Collaborator

@DRiFTy17 DRiFTy17 left a comment

Choose a reason for hiding this comment

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

This is looking great so far! Nice work. I don't have a lot of feedback on the logic itself yet, as I more or less skimmed the calculations and flow of the more complex parts. I was focusing more on architectural stuff, APIs, performance concerns, potential APIs, and code organization for this first pass.

I think from a users perspective the functionality works as expected, and adjusting the buffer worked really to get the desired result. For a POC this is in a really good place.

I know some of my comments are a little out of context in regards to out goal of keeping this scoped to internal use within our dropdowns, but I wanted to express them while they came to mind so feel free to ignore certain things.

I say the next steps should be to:

  1. cleanup and organize code
  2. finish todo items
  3. apply this to the list-dropdown and iterate as necessary (this will help reviewers play with it in storybook as well)
  4. write some unit tests around it, as well as test smaller utilities in isolation (as noted in my code organization comments), and that should help flush out any remaining issues once the code is more finalized

Comment on lines 97 to 312
private _createChildElement(index: number): HTMLElement {
const element = this._childBuilder(this._data[index], index);
element.style.position = 'absolute';
element.style.left = '0';
element.style.top = this._getChildTop(index);
if (!this._skipAccessibility) {
toggleAttribute(element, true, 'aria-setsize', this._setSize.toString());
toggleAttribute(element, true, 'aria-posinset', (index + 1).toString());
}
return element;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

We may want to implement a rolling cache here to avoid having to always call the builder. We could use index by default for the cache key to start for our dropdown use case since the order doesn't change. In the future it may also be worth it to implement an alternative API for returning results from the builder callback to allow devs to not only provide the element itself, but also a "tracking" key that maps the element to a specific key to avoid ordering issues.

Copy link
Contributor

Choose a reason for hiding this comment

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

Caching/reusing the created elements might also help for some of the problems we've seen with other virtualization libraries for something like a checkbox list, where if you check an item, scroll it off screen and then scroll back to it, the box appears unchecked. That's maybe a bad example because that checkbox state should probably be tracked somewhere beside the element itself and referenced by the builder, but I can't recall the exact scenario we ran into. Just that in addition to performance, it might also avert a whole class of virtualization-specific problems for developers.

src/lib/virtual-scroller/virtual-scroller.ts Outdated Show resolved Hide resolved
src/lib/virtual-scroller/virtual-scroller.ts Outdated Show resolved Hide resolved
src/lib/virtual-scroller/virtual-scroller.ts Outdated Show resolved Hide resolved
src/lib/virtual-scroller/virtual-scroller.ts Outdated Show resolved Hide resolved
this._applyContainer(container);
this._data = [...data];
this._childBuilder = childBuilder;
this._childHeight = childHeight;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should potentially adjust the builder callback to expect a result object in the following form:

// Adjust builder type to allow for also returning string and IVirtualScrollerChildBuilderResult
type VirtualScrollerChildBuilder<T> = (data: T, index: number) => HTMLElement | string | IVirtualScrollerChildBuilderResult;

// Accept a new result from the builder with more (optional) info
interface IVirtualScrollerChildBuilderResult { 
  element: HTMLElement | string; // The element instance 
  height?: number; // To allow for dynamic height that overrides global `childHeight`
  trackBy?: () => unknown; // An optional 
}

This could allow us to use dynamic height elements that the developer can calculate/provide to use to avoid having to calculate it on the fly.

Copy link
Contributor

@MikeMatusz MikeMatusz left a comment

Choose a reason for hiding this comment

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

Looking pretty good! I like all of Kieran's suggestions, but the API is straightforward and I think this implementation will be flexible enough to provide value in a number of places.

src/lib/virtual-scroller/virtual-scroller.ts Outdated Show resolved Hide resolved
Comment on lines 97 to 312
private _createChildElement(index: number): HTMLElement {
const element = this._childBuilder(this._data[index], index);
element.style.position = 'absolute';
element.style.left = '0';
element.style.top = this._getChildTop(index);
if (!this._skipAccessibility) {
toggleAttribute(element, true, 'aria-setsize', this._setSize.toString());
toggleAttribute(element, true, 'aria-posinset', (index + 1).toString());
}
return element;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Caching/reusing the created elements might also help for some of the problems we've seen with other virtualization libraries for something like a checkbox list, where if you check an item, scroll it off screen and then scroll back to it, the box appears unchecked. That's maybe a bad example because that checkbox state should probably be tracked somewhere beside the element itself and referenced by the builder, but I can't recall the exact scenario we ran into. Just that in addition to performance, it might also avert a whole class of virtualization-specific problems for developers.

@samrichardsontylertech
Copy link
Contributor Author

It's been a while since my last update on this. I addressed most of the points raised here in the latest commit, cleaned up and commented the code and enabled a couple of properties that were stubbed out earlier. There are still some cross-browser performance issues that need to be addressed but I think we have a decent first iteration as far as functionality goes.

Next up we can think about:

  • How to handle dynamically sized items
  • Tracking items separately from their indices
  • How containers are laid out and the impact this has on styling and accessibility — the inset properties are slightly difficult to work with as is
  • Horizontal scrolling?

But in the meantime we can work on tests and implementing this into a few components.

@samrichardsontylertech samrichardsontylertech marked this pull request as ready for review July 12, 2022 20:41
@samrichardsontylertech samrichardsontylertech requested a review from a team as a code owner July 12, 2022 20:41
Copy link
Collaborator

@DRiFTy17 DRiFTy17 left a comment

Choose a reason for hiding this comment

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

Overall things are really coming along nicely. I like the configuration and comments that were added to the implementation. Much easier to follow now and reason about.

I noticed that when trying to scroll the container by dragging the mouse in Safari didn't work, but I'm sure you're aware of that one?

Otherwise, I think we should update the list-dropdown next to use this and that should help us uncover any other items we need to take into account.

If you'd like to get us together to discuss your remaining questions from the previous comment feel free to put something on the calendar.

src/lib/virtual-scroller/virtual-scroller.ts Show resolved Hide resolved
src/lib/virtual-scroller/virtual-scroller.ts Outdated Show resolved Hide resolved
src/lib/virtual-scroller/virtual-scroller.ts Show resolved Hide resolved
src/lib/virtual-scroller/virtual-scroller.ts Outdated Show resolved Hide resolved
@samrichardsontylertech
Copy link
Contributor Author

samrichardsontylertech commented Oct 7, 2022

Using VoiceOver in Safari and Chrome, only the elements that were in the scroller when it was navigated to are announced with their positions and the set size (e.g. "4 of 20"). It also seems to be ignoring the aria-setsize and aria-posinset attributes and only counting the elements that are currently in the DOM. Does anyone else see this with VoiceOver or another screen reader?

Probably related to this.

@samrichardsontylertech
Copy link
Contributor Author

Using VoiceOver in Safari and Chrome, only the elements that were in the scroller when it was navigated to are announced with their positions and the set size (e.g. "4 of 20"). It also seems to be ignoring the aria-setsize and aria-posinset attributes and only counting the elements that are currently in the DOM. Does anyone else see this with VoiceOver or another screen reader?

Probably related to this.

This seems to be an issue with VoiceOver specifically. I submitted a bug report to Apple so we'll see if/when it gets addressed. In the meantime there isn't much to do about it, the accessibility features here are already up to spec.

@DRiFTy17 DRiFTy17 marked this pull request as draft June 2, 2023 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request skip-release Preserve the current version when merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants