Skip to content

Commit ddb63ae

Browse files
authored
Fix Virtualizer infinite loop for height 0 (#6497)
* Fix Virtualizer infinite loop for height 0
1 parent bd458c1 commit ddb63ae

File tree

4 files changed

+59
-6
lines changed

4 files changed

+59
-6
lines changed

packages/@react-spectrum/list/stories/ListView.stories.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,3 +517,32 @@ function Demo(props) {
517517
</ListView>
518518
);
519519
}
520+
521+
const manyItems = [];
522+
for (let i = 0; i < 500; i++) {manyItems.push({key: i, name: `item ${i}`});}
523+
524+
function DisplayNoneComponent(args) {
525+
const [isDisplay, setIsDisplay] = useState(false);
526+
527+
return (
528+
<>
529+
<Button variant="primary" onPress={() => setIsDisplay(prev => !prev)}>Toggle ListView display</Button>
530+
<div style={!isDisplay ? {display: 'none'} : null}>
531+
<ListView aria-label="Many items" items={manyItems} width="300px" height="200px" {...args}>
532+
{(item: any) => (
533+
<Item key={item.key} textValue={item.name}>
534+
<Text>
535+
{item.name}
536+
</Text>
537+
</Item>
538+
)}
539+
</ListView>
540+
</div>
541+
</>
542+
);
543+
}
544+
545+
export const DisplayNone: ListViewStory = {
546+
render: (args) => <DisplayNoneComponent {...args} />,
547+
name: 'display: none with many items'
548+
};

packages/@react-spectrum/list/test/ListView.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1664,4 +1664,28 @@ describe('ListView', function () {
16641664
});
16651665
});
16661666
});
1667+
1668+
describe('height 0', () => {
1669+
1670+
it('should render and not infinite loop', function () {
1671+
offsetWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 0);
1672+
offsetHeight = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 0);
1673+
scrollHeight = jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(() => 0);
1674+
let tree = render(
1675+
<ListView
1676+
width="250px"
1677+
height="60px"
1678+
aria-label="List"
1679+
data-testid="test"
1680+
items={[...Array(20).keys()].map(k => ({key: k, name: `Item ${k}`}))}>
1681+
{item => <Item>{item.name}</Item>}
1682+
</ListView>
1683+
);
1684+
let grid = tree.getByRole('grid');
1685+
act(() => {
1686+
jest.runAllTimers();
1687+
});
1688+
expect(grid.firstChild).toBeEmptyDOMElement();
1689+
});
1690+
});
16671691
});

packages/@react-stately/virtualizer/src/OverscanManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class OverscanManager {
1717
private startTime = 0;
1818
private velocity = new Point(0, 0);
1919
private visibleRect = new Rect();
20-
20+
2121
setVisibleRect(rect: Rect) {
2222
let time = performance.now() - this.startTime;
2323
if (time < 500) {

packages/@react-stately/virtualizer/src/Virtualizer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import {Size} from './Size';
2323

2424
/**
2525
* The Virtualizer class renders a scrollable collection of data using customizable layouts.
26-
* It supports very large collections by only rendering visible views to the DOM, reusing
27-
* them as you scroll. Virtualizer can present any type of view, including non-item views
26+
* It supports very large collections by only rendering visible views to the DOM, reusing
27+
* them as you scroll. Virtualizer can present any type of view, including non-item views
2828
* such as section headers and footers.
2929
*
3030
* Virtualizer uses {@link Layout} objects to compute what views should be visible, and how
@@ -133,7 +133,7 @@ export class Virtualizer<T extends object, V, W> {
133133
*/
134134
keyAtPoint(point: Point): Key | null {
135135
let rect = new Rect(point.x, point.y, 1, 1);
136-
let layoutInfos = this.layout.getVisibleLayoutInfos(rect);
136+
let layoutInfos = rect.area === 0 ? [] : this.layout.getVisibleLayoutInfos(rect);
137137

138138
// Layout may return multiple layout infos in the case of
139139
// persisted keys, so find the first one that actually intersects.
@@ -180,7 +180,7 @@ export class Virtualizer<T extends object, V, W> {
180180
rect = this._overscanManager.getOverscannedRect();
181181
}
182182

183-
let layoutInfos = this.layout.getVisibleLayoutInfos(rect);
183+
let layoutInfos = rect.area === 0 ? [] : this.layout.getVisibleLayoutInfos(rect);
184184
let map = new Map;
185185
for (let layoutInfo of layoutInfos) {
186186
map.set(layoutInfo.key, layoutInfo);
@@ -273,7 +273,7 @@ export class Virtualizer<T extends object, V, W> {
273273
if (!this.visibleRect.equals(opts.visibleRect)) {
274274
this._overscanManager.setVisibleRect(opts.visibleRect);
275275
let shouldInvalidate = this.layout.shouldInvalidate(opts.visibleRect, this.visibleRect);
276-
276+
277277
if (shouldInvalidate) {
278278
offsetChanged = !opts.visibleRect.pointEquals(this.visibleRect);
279279
sizeChanged = !opts.visibleRect.sizeEquals(this.visibleRect);

0 commit comments

Comments
 (0)