Skip to content

Commit

Permalink
Add more sophisticated logic for "last focused" render region
Browse files Browse the repository at this point in the history
  • Loading branch information
NickGerleman committed Dec 8, 2021
1 parent 8fd72a4 commit 3ab1de1
Show file tree
Hide file tree
Showing 3 changed files with 592 additions and 154 deletions.
82 changes: 64 additions & 18 deletions Libraries/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
static _createRenderMask(
props: Props,
cellsAroundViewport: {first: number, last: number},
lastFocusedItem: ?number,
additionalRegions?: ?$ReadOnlyArray<{first: number, last: number}>,
): CellRenderMask {
const itemCount = props.getItemCount(props.data);

Expand All @@ -768,17 +768,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {

const renderMask = new CellRenderMask(itemCount);

// Keep the items around the last focused rendered, to allow for keyboard
// navigation
if (lastFocusedItem) {
const first = Math.max(0, lastFocusedItem - 1);
const last = Math.min(itemCount - 1, lastFocusedItem + 1);
renderMask.addCells({first, last});
}

if (itemCount > 0) {
if (cellsAroundViewport.last >= cellsAroundViewport.first) {
renderMask.addCells(cellsAroundViewport);
const allRegions = [cellsAroundViewport, ...(additionalRegions ?? [])];
for (const region of allRegions) {
if (region.last >= region.first) {
renderMask.addCells(region);
}
}

// The initially rendered cells are retained as part of the
Expand Down Expand Up @@ -1016,7 +1011,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
prevCellKey={prevCellKey}
onUpdateSeparators={this._onUpdateSeparators}
onLayout={e => this._onCellLayout(e, key, ii)}
onFocusCapture={e => this._onCellFocusCapture(ii)}
onFocusCapture={e => this._onCellFocusCapture(key)}
onUnmount={this._onCellUnmount}
parentProps={this.props}
ref={ref => {
Expand Down Expand Up @@ -1364,7 +1359,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
_averageCellLength = 0;
// Maps a cell key to the set of keys for all outermost child lists within that cell
_cellKeysToChildListKeys: Map<string, Set<string>> = new Map();
_cellRefs = {};
_cellRefs: {[string]: ?CellRenderer} = {};
_fillRateHelper: FillRateHelper;
_frames = {};
_footerLength = 0;
Expand All @@ -1376,7 +1371,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
_hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update
_highestMeasuredFrameIndex = 0;
_indicesToKeys: Map<number, string> = new Map();
_lastFocusedItem: ?number = null;
_lastFocusedCellKey: ?string = null;
_nestedChildLists: Map<
string,
{
Expand Down Expand Up @@ -1485,12 +1480,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
this._updateViewableItems(this.props.data);
}

_onCellFocusCapture(itemIndex: number) {
this._lastFocusedItem = itemIndex;
_onCellFocusCapture(cellKey: string) {
this._lastFocusedCellKey = cellKey;
const renderMask = VirtualizedList._createRenderMask(
this.props,
this.state.cellsAroundViewport,
this._lastFocusedItem,
this._getNonViewportRenderRegions(),
);

if (!renderMask.equals(this.state.renderMask)) {
Expand Down Expand Up @@ -1926,7 +1921,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
const renderMask = VirtualizedList._createRenderMask(
props,
cellsAroundViewport,
this._lastFocusedItem,
this._getNonViewportRenderRegions(),
);

if (
Expand Down Expand Up @@ -1998,6 +1993,57 @@ class VirtualizedList extends React.PureComponent<Props, State> {
return frame;
};

_getNonViewportRenderRegions = (): $ReadOnlyArray<{
first: number,
last: number,
}> => {
// Keep a viewport's worth of content around the last focused cell to allow
// random navigation around it without any blanking. E.g. tabbing from one
// focused item out of viewport to another.
if (
!(this._lastFocusedCellKey && this._cellRefs[this._lastFocusedCellKey])
) {
return [];
}

const lastFocusedCellRenderer = this._cellRefs[this._lastFocusedCellKey];
const focusedCellIndex = lastFocusedCellRenderer.props.index;
const itemCount = this.props.getItemCount(this.props.data);

// The cell may have been unmounted and have a stale index
if (
focusedCellIndex >= itemCount ||
this._indicesToKeys.get(focusedCellIndex) !== this._lastFocusedCellKey
) {
return [];
}

let first = focusedCellIndex;
let heightOfCellsBeforeFocused = 0;
for (
let i = first - 1;
i >= 0 && heightOfCellsBeforeFocused < this._scrollMetrics.visibleLength;
i--
) {
first--;
heightOfCellsBeforeFocused += this._getFrameMetricsApprox(i).length;
}

let last = focusedCellIndex;
let heightOfCellsAfterFocused = 0;
for (
let i = last + 1;
i < itemCount &&
heightOfCellsAfterFocused < this._scrollMetrics.visibleLength;
i++
) {
last++;
heightOfCellsAfterFocused += this._getFrameMetricsApprox(i).length;
}

return [{first, last}];
};

_updateViewableItems(data: any) {
const {getItemCount} = this.props;

Expand Down
133 changes: 128 additions & 5 deletions Libraries/Lists/__tests__/VirtualizedList-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1445,7 +1445,7 @@ it('renders windowSize derived region at bottom', () => {
expect(component).toMatchSnapshot();
});

it('keeps last focused item rendered', () => {
it('keeps viewport below last focused rendered', () => {
const items = generateItems(20);
const ITEM_HEIGHT = 10;

Expand Down Expand Up @@ -1479,24 +1479,147 @@ it('keeps last focused item rendered', () => {
performAllBatches();
});

// Cells 1-4 should remain rendered after scrolling to the bottom of the list
// Cells 1-8 should remain rendered after scrolling to the bottom of the list
expect(component).toMatchSnapshot();
});

it('virtualizes away last focused item if focus changes to a new cell', () => {
const items = generateItems(20);
const ITEM_HEIGHT = 10;

let component;
ReactTestRenderer.act(() => {
component = ReactTestRenderer.create(
<VirtualizedList
initialNumToRender={1}
windowSize={1}
{...baseItemProps(items)}
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
/>,
);
});

ReactTestRenderer.act(() => {
simulateLayout(component, {
viewport: {width: 10, height: 50},
content: {width: 10, height: 200},
});

performAllBatches();
});

ReactTestRenderer.act(() => {
component.getInstance()._onCellFocusCapture(3);
});

ReactTestRenderer.act(() => {
simulateScroll(component, {x: 0, y: 150});
performAllBatches();
});

ReactTestRenderer.act(() => {
component.getInstance()._onCellFocusCapture(17);
});

// Cells 2-4 should no longer be rendered after focus is moved to the end of
// Cells 1-8 should no longer be rendered after focus is moved to the end of
// the list
expect(component).toMatchSnapshot();
});

it('keeps viewport above last focused rendered', () => {
const items = generateItems(20);
const ITEM_HEIGHT = 10;

let component;
ReactTestRenderer.act(() => {
component = ReactTestRenderer.create(
<VirtualizedList
initialNumToRender={1}
windowSize={1}
{...baseItemProps(items)}
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
/>,
);
});

ReactTestRenderer.act(() => {
simulateLayout(component, {
viewport: {width: 10, height: 50},
content: {width: 10, height: 200},
});

performAllBatches();
});

ReactTestRenderer.act(() => {
component.getInstance()._onCellFocusCapture(3);
});

ReactTestRenderer.act(() => {
simulateScroll(component, {x: 0, y: 150});
performAllBatches();
});

ReactTestRenderer.act(() => {
component.getInstance()._onCellFocusCapture(17);
});

ReactTestRenderer.act(() => {
simulateScroll(component, {x: 0, y: 0});
performAllBatches();
});

// Cells 16-18 should remain rendered after scrolling back to the top of the
// list
// Cells 12-19 should remain rendered after scrolling to the top of the list
expect(component).toMatchSnapshot();
});

it('virtualizes away last focused index if item removed', () => {
const items = generateItems(20);
const ITEM_HEIGHT = 10;

let component;
ReactTestRenderer.act(() => {
component = ReactTestRenderer.create(
<VirtualizedList
initialNumToRender={1}
windowSize={1}
{...baseItemProps(items)}
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
/>,
);
});

ReactTestRenderer.act(() => {
simulateLayout(component, {
viewport: {width: 10, height: 50},
content: {width: 10, height: 200},
});

performAllBatches();
});

ReactTestRenderer.act(() => {
component.getInstance()._onCellFocusCapture(3);
});

ReactTestRenderer.act(() => {
simulateScroll(component, {x: 0, y: 150});
performAllBatches();
});

const itemsWithoutFocused = [...items.slice(0, 3), ...items.slice(4)];
ReactTestRenderer.act(() => {
component.update(
<VirtualizedList
initialNumToRender={1}
windowSize={1}
{...baseItemProps(itemsWithoutFocused)}
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
/>,
);
});

// Cells 1-8 should no longer be rendered
expect(component).toMatchSnapshot();
});

Expand Down
Loading

0 comments on commit 3ab1de1

Please sign in to comment.