Skip to content

Commit

Permalink
Merge branch 'realize-last-focused' of https://github.com/NickGerlema…
Browse files Browse the repository at this point in the history
…n/react-native into realize-last-focused
  • Loading branch information
NickGerleman committed Jan 12, 2022
2 parents 85d0afd + b8a64f0 commit 1bd3342
Show file tree
Hide file tree
Showing 3 changed files with 599 additions and 157 deletions.
92 changes: 71 additions & 21 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 @@ -1847,7 +1842,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
// Mark as high priority if we're close to the end of the last item
// But only if there are items after the last rendered item
if (last < itemCount - 1) {
if (last > 0 && last < itemCount - 1) {
const distBottom =
this._getFrameMetricsApprox(last).offset - (offset + visibleLength);
hiPri =
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 @@ -1959,7 +1954,11 @@ class VirtualizedList extends React.PureComponent<Props, State> {
// check for invalid frames due to row re-ordering
return frame;
} else {
const {getItemLayout} = this.props;
const {data, getItemCount, getItemLayout} = this.props;
invariant(
index >= 0 && index < getItemCount(data),
'Tried to get frame for out of range index ' + index,
);
invariant(
!getItemLayout,
'Should not have to estimate frames when a measurement metrics function is provided',
Expand All @@ -1982,7 +1981,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
} => {
const {data, getItem, getItemCount, getItemLayout} = this.props;
invariant(
getItemCount(data) > index,
index >= 0 && index < getItemCount(data),
'Tried to get frame for out of range index ' + index,
);
const item = getItem(data, index);
Expand All @@ -1998,6 +1997,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 1bd3342

Please sign in to comment.