Improved the scrolling UX in list widgets

- Fixed scrollbar does not reach end of list widget.
 - Estimated row heights to provide better scroll UX.
 - Last item's `<select>` must be visible.

Closes #1380
Closes #1381
Closes #1387

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2022-09-02 11:21:36 +02:00 committed by Akos Kitta
parent df3a34eec6
commit d0dfc656e6
3 changed files with 38 additions and 34 deletions

View File

@ -44,10 +44,6 @@
height: 100%; /* This has top be 100% down to the `scrollContainer`. */ height: 100%; /* This has top be 100% down to the `scrollContainer`. */
} }
.filterable-list-container .items-container {
padding-bottom: calc(2 * var(--theia-statusBar-height));
}
.filterable-list-container .items-container > div > div:nth-child(odd) { .filterable-list-container .items-container > div > div:nth-child(odd) {
background-color: var(--theia-sideBar-background); background-color: var(--theia-sideBar-background);
filter: contrast(105%); filter: contrast(105%);

View File

@ -1,3 +1,4 @@
import 'react-virtualized/styles.css';
import * as React from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react';
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'; import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
import { import {
@ -14,7 +15,11 @@ import { Installable } from '../../../common/protocol/installable';
import { ComponentListItem } from './component-list-item'; import { ComponentListItem } from './component-list-item';
import { ListItemRenderer } from './list-item-renderer'; import { ListItemRenderer } from './list-item-renderer';
function sameAs<T>(left: T[], right: T[], key: (item: T) => string): boolean { function sameAs<T>(
left: T[],
right: T[],
...compareProps: (keyof T)[]
): boolean {
if (left === right) { if (left === right) {
return true; return true;
} }
@ -23,10 +28,12 @@ function sameAs<T>(left: T[], right: T[], key: (item: T) => string): boolean {
return false; return false;
} }
for (let i = 0; i < leftLength; i++) { for (let i = 0; i < leftLength; i++) {
const leftKey = key(left[i]); for (const prop of compareProps) {
const rightKey = key(right[i]); const leftValue = left[i][prop];
if (leftKey !== rightKey) { const rightValue = right[i][prop];
return false; if (leftValue !== rightValue) {
return false;
}
} }
} }
return true; return true;
@ -43,7 +50,7 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
constructor(props: ComponentList.Props<T>) { constructor(props: ComponentList.Props<T>) {
super(props); super(props);
this.cache = new CellMeasurerCache({ this.cache = new CellMeasurerCache({
defaultHeight: 300, defaultHeight: 140,
fixedWidth: true, fixedWidth: true,
}); });
} }
@ -67,6 +74,11 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
rowHeight={this.cache.rowHeight} rowHeight={this.cache.rowHeight}
deferredMeasurementCache={this.cache} deferredMeasurementCache={this.cache}
ref={this.setListRef} ref={this.setListRef}
estimatedRowSize={140}
// If default value, then `react-virtualized` will optimize and list item will not receive a `:hover` event.
// Hence, install and version `<select>` won't be visible even if the mouse cursor is over the `<div>`.
// See https://github.com/bvaughn/react-virtualized/blob/005be24a608add0344284053dae7633be86053b2/source/Grid/Grid.js#L38-L42
scrollingResetTimeInterval={0}
/> />
); );
}} }}
@ -77,13 +89,13 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
override componentDidUpdate(prevProps: ComponentList.Props<T>): void { override componentDidUpdate(prevProps: ComponentList.Props<T>): void {
if ( if (
this.resizeAllFlag || this.resizeAllFlag ||
!sameAs(this.props.items, prevProps.items, this.props.itemLabel) !sameAs(this.props.items, prevProps.items, 'name', 'installedVersion')
) { ) {
this.clearAll(true); this.clearAll(true);
} }
} }
private setListRef = (ref: List | null): void => { private readonly setListRef = (ref: List | null): void => {
this.list = ref || undefined; this.list = ref || undefined;
}; };
@ -98,17 +110,7 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
} }
} }
private clear(index: number): void { private readonly createItem: ListRowRenderer = ({
this.cache.clear(index, 0);
this.list?.recomputeRowHeights(index);
// Update the last item if the if the one before was updated
if (index === this.props.items.length - 2) {
this.cache.clear(index + 1, 0);
this.list?.recomputeRowHeights(index + 1);
}
}
private createItem: ListRowRenderer = ({
index, index,
parent, parent,
key, key,
@ -123,16 +125,20 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
rowIndex={index} rowIndex={index}
parent={parent} parent={parent}
> >
<div style={style}> {({ measure, registerChild }) => (
<ComponentListItem<T> // eslint-disable-next-line @typescript-eslint/ban-ts-comment
key={this.props.itemLabel(item)} // @ts-ignore
item={item} <div ref={registerChild} style={style}>
itemRenderer={this.props.itemRenderer} <ComponentListItem<T>
install={this.props.install} key={this.props.itemLabel(item)}
uninstall={this.props.uninstall} item={item}
onFocusDidChange={() => this.clear(index)} itemRenderer={this.props.itemRenderer}
/> install={this.props.install}
</div> uninstall={this.props.uninstall}
onFocusDidChange={() => measure()}
/>
</div>
)}
</CellMeasurer> </CellMeasurer>
); );
}; };

View File

@ -51,7 +51,9 @@ export class FilterableListContainer<
<div className={'filterable-list-container'}> <div className={'filterable-list-container'}>
{this.renderSearchBar()} {this.renderSearchBar()}
{this.renderSearchFilter()} {this.renderSearchFilter()}
{this.renderComponentList()} <div className="filterable-list-container">
{this.renderComponentList()}
</div>
</div> </div>
); );
} }