feat: Added virtualization component - #2222
Conversation
- Use a Binary Indexed Tree for efficient range calculations in the virtualization engine. - Update the virtualization component to utilize the new engine and optimize DOM queries by using part selectors instead of class selectors. - Adjust unit tests to reflect changes in the component structure and ensure they target the correct elements.
Updated the combo test suite.
There was a problem hiding this comment.
Pull request overview
Introduces a new igc-virtual-scroll component to provide in-house list virtualization and wires it into the Combo component to replace the previous @lit-labs/virtualizer-based implementation.
Changes:
- Added a new virtualization engine +
igc-virtual-scrollcomponent, along with Storybook stories and unit tests. - Updated Combo to use
igc-virtual-scrollfor its dropdown list (types, navigation behavior, styles, and tests updated accordingly). - Improved test stability/logging by centralizing ResizeObserver-loop suppression and filtering benign
nullbrowser logs in web-test-runner output.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| web-test-runner.config.mjs | Adds a custom reporter to filter benign null browser logs before the default reporter runs. |
| stories/virtual-scroll.stories.ts | New Storybook stories demonstrating vertical/horizontal, variable size, and remote-data virtualization scenarios. |
| src/index.ts | Exports the new virtual scroll component and its public types/events. |
| src/components/virtualization/virtualization.ts | New igc-virtual-scroll Lit component integrating the engine, measurements, scrolling API, and events. |
| src/components/virtualization/virtualization.spec.ts | New tests for accessibility, events, public API, engine integration, and RTL behavior. |
| src/components/virtualization/types.ts | Defines public types: item context, state snapshot, and data request payload. |
| src/components/virtualization/engine.ts | New virtualization math engine (Fenwick tree/BIT-based sizing + visible-range computations). |
| src/components/common/utils.spec.ts | Adds shared suppressResizeObserverLoopError() helper for tests. |
| src/components/combo/types.ts | Switches Combo render-function typing to use the new virtual scroll item template type. |
| src/components/combo/themes/combo.base.scss | Adjusts list sizing styles to work with the new virtual scroll list element. |
| src/components/combo/controllers/navigation.ts | Updates scrolling behavior to use scrollToIndex() instead of scrollIntoView(). |
| src/components/combo/combo.ts | Replaces igc-combo-list with igc-virtual-scroll and adapts rendering/theming integration. |
| src/components/combo/combo.spec.ts | Updates Combo tests to await virtual scroll layoutComplete and query the new list element. |
| src/components/combo/combo-list.ts | Removes the old LitVirtualizer-based igc-combo-list component. |
| src/components/chat/chat.spec.ts | Uses the shared ResizeObserver-loop suppression helper. |
|
@rkaraivanov , I am noticing a potentially faulty behavior. So I added a virtualization combo story (with opus, we can remove it later) in the storybook with some controls. When navigating by index, the item, while in the rendered frame is actually not in the view frame. You can reproduce it by either scrolling to the middle with the button or by searching for a specific item. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
src/components/virtualization/virtualization.ts:309
aria-hidden="true"on the track element hides all rendered items from the accessibility tree (including when the host is used as arole="listbox"in combo), which can make options undiscoverable to screen readers.
<div part="igc-vs-track" style=${styleMap(trackStyle)} aria-hidden="true">
src/components/virtualization/virtualization.ts:473
scrollToIndex()acceptsScrollIntoViewOptions, but it forwards those options intoElement.scrollTo(), which expectsScrollToOptions. This is already causing callers to passblock: 'center', which has no effect and is misleading.
/** Programmatically scrolls to the specified item index. */
public scrollToIndex(index: number, options?: ScrollIntoViewOptions): void {
const offset = this._engine.getScrollOffsetForIndex(index);
…eUI/igniteui-webcomponents into rkaraivanov/virtualization
…rollToIndex Fix VirtualScrollEngine's BIT.findIndexAtOffset returning idx - 1, which could resolve to the wrong item near large scroll offsets. scrollToIndex now iteratively re-aligns the scroll position after waiting for the scroll to settle and layout to complete, correcting for the gap between estimated and measured item sizes until the offset stabilizes. This fixes scrolling landing past the requested index in large lists (e.g. combo's virtualized dropdown) before all items in between have been measured. Adds a scroll request id so overlapping scrollToIndex calls no longer race each other, and removes a stray aria-hidden on the track element. Add tests covering index alignment with under-estimated item sizes, including a large-list smooth-scroll scenario.
|
@ChronosSF |
Removed the lit-virtualizer dependency since it is no longer used in the project. The virtual scroll functionality has been implemented using the new virtual-scroll component, eliminating the need for the external library.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/components/virtualization/virtualization.ts:261
- When
orientationchanges,_scrollPositionkeeps the previous axis value until the next scroll event fires. This can render the wrong range immediately after switching orientation (e.g. a scrolled vertical list switched to horizontal still uses the oldscrollTopvalue as if it werescrollLeft). Consider syncing_scrollPositionto the current axis right after updating the listener.
if (changed.has('orientation')) {
this._measureViewport();
this._setupScrollListener();
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
web-test-runner.config.mjs:15
- The comment claims this reporter filters only the benign ResizeObserver-loop noise “without hiding other, legitimate console.* output”, but the filter condition removes any single-argument
nulllog entry — including an intentionalconsole.log(null)from a test. Either narrow the filter further (if possible with the available log metadata) or adjust the comment to reflect the trade-off.
* but web-test-runner's browser log collection picks the (error-less)
* exception up independently of that, printing it as a bare `null` line.
* This reporter strips those specific single-`null`-argument log entries
* before the default reporter prints anything, without hiding other,
* legitimate `console.*` output from tests.
src/components/virtualization/virtualization.ts:425
_scrollAndWaitForEnd()attaches ascrollendlistener but if the timeout wins thePromise.race, the listener is never removed. In browsers/environments wherescrollenddoesn't fire (unsupported, disconnected mid-scroll, etc.), repeated calls can leak listeners and later resolve stale promises unexpectedly. Consider removing the listener in afinallyblock (or using an AbortController signal) so the timeout path always cleans up.
const settled = new Promise<void>((resolve) => {
this.addEventListener('scrollend', () => resolve(), { once: true });
});
this._applyScroll(offset, behavior);
return Promise.race([settled, this._timeout(SCROLL_END_TIMEOUT_MS)]);
src/components/combo/combo.ts:249
aria-activedescendantcan become stale when the active index is reset to-1:_activeDescendantis never cleared, so the listbox can keep pointing to an element ID that no longer exists in the DOM. Clear_activeDescendantwhen deactivating to avoid invalid ARIA references.
@state()
private set _activeIndex(index: number) {
this._navigation.active = index;
this._listRef.value?.requestUpdate();
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/components/combo/controllers/navigation.ts:220
_scrollToActive()now callsscrollToIndex(this.active, ...)unconditionally. Whenactiveis-1(the initial value),scrollToIndex()clamps it to0, so pressing ArrowUp as the first navigation key can unexpectedly scroll the list to the top even though no item became active (previouslyelement(-1)was undefined so no scroll occurred). Guard against negativeactivebefore scrolling.
private _scrollToActive(behavior?: ScrollBehavior): void {
this._list?.scrollToIndex(this.active, {
block: 'center',
behavior: behavior ?? 'auto',
});
Description
Added virtualization component
Type of Change
Checklist