Skip to content

Commit 5600f9d

Browse files
author
Akos Kitta
committed
Virtualized list item rendering.
Signed-off-by: Akos Kitta <[email protected]>
1 parent 168bbf1 commit 5600f9d

File tree

4 files changed

+116
-42
lines changed

4 files changed

+116
-42
lines changed

arduino-ide-extension/src/browser/widgets/component-list/component-list-item.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class ComponentListItem<
3939
await this.props.uninstall(item);
4040
}
4141

42-
protected onVersionChange(version: Installable.Version) {
42+
protected onVersionChange(version: Installable.Version): void {
4343
this.setState({ selectedVersion: version });
4444
}
4545

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,129 @@
11
import * as React from '@theia/core/shared/react';
2-
import { Installable } from '../../../common/protocol/installable';
2+
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
3+
import {
4+
CellMeasurer,
5+
CellMeasurerCache,
6+
} from 'react-virtualized/dist/commonjs/CellMeasurer';
7+
import type {
8+
ListRowProps,
9+
ListRowRenderer,
10+
} from 'react-virtualized/dist/commonjs/List';
11+
import List from 'react-virtualized/dist/commonjs/List';
312
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
13+
import { Installable } from '../../../common/protocol/installable';
414
import { ComponentListItem } from './component-list-item';
515
import { ListItemRenderer } from './list-item-renderer';
616

717
export class ComponentList<T extends ArduinoComponent> extends React.Component<
8-
ComponentList.Props<T>
18+
ComponentList.Props<T>,
19+
ComponentList.State
920
> {
10-
protected container?: HTMLElement;
21+
private readonly cache: CellMeasurerCache;
22+
private resizeAllFlag: boolean;
23+
private list: List | undefined;
24+
private mostRecentWidth: number | undefined;
25+
26+
constructor(props: ComponentList.Props<T>) {
27+
super(props);
28+
this.state = { focusIndex: 'none' };
29+
this.cache = new CellMeasurerCache({
30+
defaultHeight: 200,
31+
fixedWidth: true,
32+
});
33+
}
1134

1235
override render(): React.ReactNode {
1336
return (
14-
<div className={'items-container'} ref={this.setRef}>
15-
{this.props.items.map((item) => this.createItem(item))}
16-
</div>
37+
<AutoSizer>
38+
{({ width, height }) => {
39+
if (this.mostRecentWidth && this.mostRecentWidth !== width) {
40+
this.resizeAllFlag = true;
41+
setTimeout(this.clearAll, 0);
42+
}
43+
this.mostRecentWidth = width;
44+
return (
45+
<List
46+
rowRenderer={this.createItem}
47+
overscanRowCount={100}
48+
height={height}
49+
width={width}
50+
rowCount={this.props.items.length}
51+
rowHeight={this.cache.rowHeight}
52+
deferredMeasurementCache={this.cache}
53+
ref={this.setListRef}
54+
/>
55+
);
56+
}}
57+
</AutoSizer>
1758
);
1859
}
1960

20-
override componentDidMount(): void {
21-
if (this.container && this.props.resolveContainer) {
22-
this.props.resolveContainer(this.container);
61+
override componentDidUpdate(
62+
prevProps: ComponentList.Props<T>,
63+
prevState: ComponentList.State
64+
): void {
65+
if (this.resizeAllFlag || this.props.items !== prevProps.items) {
66+
this.clearAll();
67+
} else if (this.state.focusIndex !== prevState.focusIndex) {
68+
if (typeof this.state.focusIndex === 'number') {
69+
this.clear(this.state.focusIndex);
70+
}
71+
if (typeof prevState.focusIndex === 'number') {
72+
this.clear(prevState.focusIndex);
73+
}
2374
}
2475
}
2576

26-
protected setRef = (element: HTMLElement | null) => {
27-
this.container = element || undefined;
77+
private setListRef = (ref: List | null): void => {
78+
this.list = ref || undefined;
2879
};
2980

30-
protected createItem(item: T): React.ReactNode {
81+
private clearAll(): void {
82+
this.resizeAllFlag = false;
83+
this.cache.clearAll();
84+
if (this.list) {
85+
this.list.recomputeRowHeights();
86+
}
87+
}
88+
89+
private clear(index: number): void {
90+
this.cache.clear(index, 0);
91+
if (this.list) {
92+
this.list.recomputeRowHeights(index);
93+
}
94+
}
95+
96+
private createItem: ListRowRenderer = ({
97+
index,
98+
parent,
99+
key,
100+
style,
101+
}: ListRowProps): React.ReactNode => {
102+
const item = this.props.items[index];
31103
return (
32-
<ComponentListItem<T>
33-
key={this.props.itemLabel(item)}
34-
item={item}
35-
itemRenderer={this.props.itemRenderer}
36-
install={this.props.install}
37-
uninstall={this.props.uninstall}
38-
/>
104+
<CellMeasurer
105+
cache={this.cache}
106+
columnIndex={0}
107+
key={key}
108+
rowIndex={index}
109+
parent={parent}
110+
>
111+
<div
112+
style={style}
113+
onMouseEnter={() => this.setState({ focusIndex: index })}
114+
onMouseLeave={() => this.setState({ focusIndex: 'none' })}
115+
>
116+
<ComponentListItem<T>
117+
key={this.props.itemLabel(item)}
118+
item={item}
119+
itemRenderer={this.props.itemRenderer}
120+
install={this.props.install}
121+
uninstall={this.props.uninstall}
122+
/>
123+
</div>
124+
</CellMeasurer>
39125
);
40-
}
126+
};
41127
}
42128

43129
export namespace ComponentList {
@@ -48,6 +134,8 @@ export namespace ComponentList {
48134
readonly itemRenderer: ListItemRenderer<T>;
49135
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
50136
readonly uninstall: (item: T) => Promise<void>;
51-
readonly resolveContainer: (element: HTMLElement) => void;
137+
}
138+
export interface State {
139+
focusIndex: number | 'none';
52140
}
53141
}

arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ export class FilterableListContainer<
6666
}
6767

6868
protected renderComponentList(): React.ReactNode {
69-
const { itemLabel, itemDeprecated, resolveContainer, itemRenderer } =
70-
this.props;
69+
const { itemLabel, itemDeprecated, itemRenderer } = this.props;
7170
return (
7271
<ComponentList<T>
7372
items={this.state.items}
@@ -76,14 +75,13 @@ export class FilterableListContainer<
7675
itemRenderer={itemRenderer}
7776
install={this.install.bind(this)}
7877
uninstall={this.uninstall.bind(this)}
79-
resolveContainer={resolveContainer}
8078
/>
8179
);
8280
}
8381

8482
protected handleFilterTextChange = (
8583
filterText: string = this.state.filterText
86-
) => {
84+
): void => {
8785
this.setState({ filterText });
8886
this.search(filterText);
8987
};
@@ -159,7 +157,7 @@ export namespace FilterableListContainer {
159157
readonly itemLabel: (item: T) => string;
160158
readonly itemDeprecated: (item: T) => boolean;
161159
readonly itemRenderer: ListItemRenderer<T>;
162-
readonly resolveContainer: (element: HTMLElement) => void;
160+
// readonly resolveContainer: (element: HTMLElement) => void;
163161
readonly resolveFocus: (element: HTMLElement | undefined) => void;
164162
readonly filterTextChangeEvent: Event<string | undefined>;
165163
readonly messageService: MessageService;

arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ import {
66
} from '@theia/core/shared/inversify';
77
import { Widget } from '@theia/core/shared/@phosphor/widgets';
88
import { Message } from '@theia/core/shared/@phosphor/messaging';
9-
import { Deferred } from '@theia/core/lib/common/promise-util';
109
import { Emitter } from '@theia/core/lib/common/event';
11-
import { MaybePromise } from '@theia/core/lib/common/types';
1210
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
1311
import { CommandService } from '@theia/core/lib/common/command';
1412
import { MessageService } from '@theia/core/lib/common/message-service';
@@ -42,7 +40,7 @@ export abstract class ListWidget<
4240
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
4341
*/
4442
protected focusNode: HTMLElement | undefined;
45-
protected readonly deferredContainer = new Deferred<HTMLElement>();
43+
// protected readonly deferredContainer = new Deferred<HTMLElement>();
4644
protected readonly filterTextChangeEmitter = new Emitter<
4745
string | undefined
4846
>();
@@ -62,9 +60,6 @@ export abstract class ListWidget<
6260
this.title.closable = true;
6361
this.addClass('arduino-list-widget');
6462
this.node.tabIndex = 0; // To be able to set the focus on the widget.
65-
this.scrollOptions = {
66-
suppressScrollX: true,
67-
};
6863
this.toDispose.push(this.filterTextChangeEmitter);
6964
}
7065

@@ -77,10 +72,6 @@ export abstract class ListWidget<
7772
]);
7873
}
7974

80-
protected override getScrollContainer(): MaybePromise<HTMLElement> {
81-
return this.deferredContainer.promise;
82-
}
83-
8475
protected override onAfterShow(message: Message): void {
8576
this.maybeUpdateOnFirstRender();
8677
super.onAfterShow(message);
@@ -109,7 +100,7 @@ export abstract class ListWidget<
109100
this.updateScrollBar();
110101
}
111102

112-
protected onFocusResolved = (element: HTMLElement | undefined) => {
103+
protected onFocusResolved = (element: HTMLElement | undefined): void => {
113104
this.focusNode = element;
114105
};
115106

@@ -139,7 +130,6 @@ export abstract class ListWidget<
139130
return (
140131
<FilterableListContainer<T>
141132
container={this}
142-
resolveContainer={this.deferredContainer.resolve}
143133
resolveFocus={this.onFocusResolved}
144134
searchable={this.options.searchable}
145135
install={this.install.bind(this)}
@@ -160,9 +150,7 @@ export abstract class ListWidget<
160150
* If it is `undefined`, updates the view state by re-running the search with the current `filterText` term.
161151
*/
162152
refresh(filterText: string | undefined): void {
163-
this.deferredContainer.promise.then(() =>
164-
this.filterTextChangeEmitter.fire(filterText)
165-
);
153+
this.filterTextChangeEmitter.fire(filterText);
166154
}
167155

168156
updateScrollBar(): void {

0 commit comments

Comments
 (0)