Skip to content

Commit 74e31fa

Browse files
ktaborsLFDanLu
andauthored
Reducing unnecessary Virtualizer onLoadMore calls impacting ComboBox
* adding tests for ComboBox onLoadMore not being called on every open * updating a cardview onloadmore test fix * getting the correct number of onLoadMore calls to happen Co-authored-by: Daniel Lu <[email protected]>
1 parent bd8c099 commit 74e31fa

File tree

4 files changed

+115
-9
lines changed

4 files changed

+115
-9
lines changed

packages/@react-aria/virtualizer/src/Virtualizer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function Virtualizer<T extends object, V>(props: VirtualizerProps<T, V>, ref: Re
8585

8686
useLayoutEffect(() => {
8787
if (!isLoading && onLoadMore && !state.isAnimating) {
88-
if (state.contentSize.height <= state.virtualizer.visibleRect.height) {
88+
if (state.contentSize.height > 0 && state.contentSize.height <= state.virtualizer.visibleRect.height) {
8989
onLoadMore();
9090
}
9191
}

packages/@react-spectrum/card/test/CardView.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,7 +1076,7 @@ describe('CardView', function () {
10761076
let cards = tree.getAllByRole('gridcell');
10771077
expect(cards).toBeTruthy();
10781078
// Virtualizer calls onLoadMore twice due to initial layout
1079-
expect(onLoadMore).toHaveBeenCalledTimes(2);
1079+
expect(onLoadMore).toHaveBeenCalledTimes(1);
10801080
triggerPress(cards[1]);
10811081

10821082
act(() => {
@@ -1088,7 +1088,7 @@ describe('CardView', function () {
10881088
let grid = tree.getByRole('grid');
10891089
grid.scrollTop = 3000;
10901090
fireEvent.scroll(grid);
1091-
expect(onLoadMore).toHaveBeenCalledTimes(3);
1091+
expect(onLoadMore).toHaveBeenCalledTimes(2);
10921092
});
10931093

10941094
it.each`

packages/@react-spectrum/combobox/stories/ComboBox.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Add from '@spectrum-icons/workflow/Add';
1616
import Alert from '@spectrum-icons/workflow/Alert';
1717
import Bell from '@spectrum-icons/workflow/Bell';
1818
import {ButtonGroup} from '@react-spectrum/buttongroup';
19+
import {chain} from '@react-aria/utils';
1920
import {ComboBox, Item, Section} from '../';
2021
import Copy from '@spectrum-icons/workflow/Copy';
2122
import Draw from '@spectrum-icons/workflow/Draw';
@@ -562,7 +563,7 @@ function AsyncLoadingExample() {
562563
inputValue={list.filterText}
563564
onInputChange={list.setFilterText}
564565
loadingState={list.loadingState}
565-
onLoadMore={list.loadMore}
566+
onLoadMore={chain(action('onLoadMore'), list.loadMore)}
566567
onOpenChange={action('onOpenChange')}>
567568
{item => <Item key={item.name}>{item.name}</Item>}
568569
</ComboBox>

packages/@react-spectrum/combobox/test/ComboBox.test.js

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ jest.mock('@react-aria/live-announcer');
1414
import {act, fireEvent, render, screen, waitFor, within} from '@testing-library/react';
1515
import {announce} from '@react-aria/live-announcer';
1616
import {Button} from '@react-spectrum/button';
17+
import {chain} from '@react-aria/utils';
1718
import {ComboBox, Item, Section} from '../';
1819
import {Provider} from '@react-spectrum/provider';
1920
import React from 'react';
@@ -37,6 +38,7 @@ let onInputChange = jest.fn();
3738
let outerBlur = jest.fn();
3839
let onFocus = jest.fn();
3940
let onBlur = jest.fn();
41+
let onLoadMore = jest.fn();
4042

4143
let defaultProps = {
4244
label: 'Test',
@@ -193,7 +195,8 @@ let AsyncComboBox = () => {
193195
inputValue={list.filterText}
194196
onInputChange={list.setFilterText}
195197
loadingState={list.loadingState}
196-
onLoadMore={list.loadMore}>
198+
onLoadMore={chain(list.loadMore, onLoadMore)}
199+
onOpenChange={onOpenChange}>
197200
{(item) => <Item>{item.name}</Item>}
198201
</ComboBox>
199202
);
@@ -1975,6 +1978,104 @@ describe('ComboBox', function () {
19751978
});
19761979
});
19771980

1981+
describe('async', function () {
1982+
let clientHeightSpy;
1983+
let scrollHeightSpy;
1984+
beforeEach(() => {
1985+
// clientHeight is needed for ScrollView's updateSize()
1986+
clientHeightSpy = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementationOnce(() => 0).mockImplementation(() => 40);
1987+
// scrollHeight is for useVirutalizerItem to mock its getSize()
1988+
scrollHeightSpy = jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(() => 32);
1989+
});
1990+
afterEach(() => {
1991+
clientHeightSpy.mockRestore();
1992+
scrollHeightSpy.mockRestore();
1993+
// This returns this to the value used by all the other tests
1994+
jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 1000);
1995+
});
1996+
it('onLoadMore is called on initial open', async () => {
1997+
let {getByRole} = render(
1998+
<Provider theme={theme}>
1999+
<AsyncComboBox />
2000+
</Provider>
2001+
);
2002+
2003+
let button = getByRole('button');
2004+
2005+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
2006+
expect(onOpenChange).toHaveBeenCalledTimes(0);
2007+
expect(onLoadMore).toHaveBeenCalledTimes(0);
2008+
2009+
await act(async () => {
2010+
triggerPress(button);
2011+
jest.runAllTimers();
2012+
});
2013+
2014+
let listbox = getByRole('listbox');
2015+
expect(listbox).toBeVisible();
2016+
2017+
expect(onOpenChange).toHaveBeenCalledTimes(1);
2018+
expect(onOpenChange).toHaveBeenCalledWith(true, 'manual');
2019+
expect(onLoadMore).toHaveBeenCalledTimes(1);
2020+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
2021+
});
2022+
2023+
it('onLoadMore is not called on when previously opened', async () => {
2024+
let {getByRole} = render(
2025+
<Provider theme={theme}>
2026+
<AsyncComboBox />
2027+
</Provider>
2028+
);
2029+
2030+
let button = getByRole('button');
2031+
let combobox = getByRole('combobox');
2032+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
2033+
expect(onOpenChange).toHaveBeenCalledTimes(0);
2034+
expect(onLoadMore).toHaveBeenCalledTimes(0);
2035+
2036+
// this call and the one below are more correct for how the code should
2037+
// behave, the inital call would have a height of zero and after that a measureable height
2038+
clientHeightSpy.mockRestore();
2039+
clientHeightSpy = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementationOnce(() => 0).mockImplementation(() => 40);
2040+
// open menu
2041+
await act(async () => {
2042+
triggerPress(button);
2043+
jest.runAllTimers();
2044+
});
2045+
2046+
expect(onOpenChange).toHaveBeenCalledTimes(1);
2047+
expect(onOpenChange).toHaveBeenCalledWith(true, 'manual');
2048+
expect(onLoadMore).toHaveBeenCalledTimes(1);
2049+
2050+
// close menu
2051+
act(() => {
2052+
combobox.blur();
2053+
jest.runAllTimers();
2054+
});
2055+
2056+
expect(onOpenChange).toHaveBeenCalledTimes(2);
2057+
expect(onOpenChange).toHaveBeenLastCalledWith(false, undefined);
2058+
expect(onLoadMore).toHaveBeenCalledTimes(1);
2059+
2060+
clientHeightSpy.mockRestore();
2061+
clientHeightSpy = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementationOnce(() => 0).mockImplementation(() => 40);
2062+
// reopen menu
2063+
await act(async () => {
2064+
triggerPress(button);
2065+
jest.runAllTimers();
2066+
});
2067+
2068+
expect(onOpenChange).toHaveBeenCalledTimes(3);
2069+
expect(onOpenChange).toHaveBeenLastCalledWith(true, 'manual');
2070+
// Please test this in storybook.
2071+
// In virtualizer.tsx the onVisibleRectChange() causes this to be called twice
2072+
// because the browser limits the popover height via calculatePosition(),
2073+
// while the test doesn't, causing virtualizer to try to load more
2074+
expect(onLoadMore).toHaveBeenCalledTimes(2);
2075+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
2076+
});
2077+
});
2078+
19782079
describe('controlled combobox', function () {
19792080
describe('controlled by both selectedKey and inputValue', function () {
19802081
it('does not update state', function () {
@@ -3387,7 +3488,7 @@ describe('ComboBox', function () {
33873488
</ComboBox>
33883489
</Provider>
33893490
);
3390-
3491+
33913492
let button = getByRole('button');
33923493

33933494
act(() => {
@@ -4472,9 +4573,13 @@ describe('ComboBox', function () {
44724573
trayInput.focus();
44734574
fireEvent.change(trayInput, {target: {value: 'aard'}});
44744575
jest.advanceTimersByTime(500);
4475-
let trayInputProgress = within(tray).getByRole('progressbar', {hidden: true});
4476-
expect(trayInputProgress).toBeTruthy();
4477-
expect(within(listbox).queryByRole('progressbar')).toBeNull();
4576+
});
4577+
4578+
let trayInputProgress = within(tray).getByRole('progressbar', {hidden: true});
4579+
expect(trayInputProgress).toBeTruthy();
4580+
expect(within(listbox).queryByRole('progressbar')).toBeNull();
4581+
4582+
await act(async () => {
44784583
jest.runAllTimers();
44794584
});
44804585

0 commit comments

Comments
 (0)