Skip to content

Commit 50c958d

Browse files
authored
Update useListData to handle select all w/ unloaded lists (#2225)
* handle removal and selection for scenarios with unloaded items * better solution + tests * Revert removeSelectedItems functionality and add extra test * remove outdated comment
1 parent 0e528a5 commit 50c958d

File tree

3 files changed

+67
-4
lines changed

3 files changed

+67
-4
lines changed

packages/@react-stately/data/src/useAsyncList.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ export function useAsyncList<T, C = string>(options: AsyncListOptions<T, C>): As
342342
sort(sortDescriptor: SortDescriptor) {
343343
dispatchFetch({type: 'sorting', sortDescriptor}, sort || load);
344344
},
345-
...createListActions({...options, getKey}, fn => {
345+
...createListActions({...options, getKey, cursor: data.cursor}, fn => {
346346
dispatch({type: 'update', updater: fn});
347347
}),
348348
setFilterText(filterText: string) {

packages/@react-stately/data/src/useListData.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ export interface ListState<T> {
128128
filterText: string
129129
}
130130

131+
interface CreateListOptions<T, C> extends ListOptions<T> {
132+
cursor?: C
133+
}
134+
131135
/**
132136
* Manages state for an immutable list data structure, and provides convenience methods to
133137
* update the data over time.
@@ -162,8 +166,8 @@ export function useListData<T>(options: ListOptions<T>): ListData<T> {
162166
};
163167
}
164168

165-
export function createListActions<T>(opts: ListOptions<T>, dispatch: (updater: (state: ListState<T>) => ListState<T>) => void): Omit<ListData<T>, 'items' | 'selectedKeys' | 'getItem' | 'filterText'> {
166-
let {getKey} = opts;
169+
export function createListActions<T, C>(opts: CreateListOptions<T, C>, dispatch: (updater: (state: ListState<T>) => ListState<T>) => void): Omit<ListData<T>, 'items' | 'selectedKeys' | 'getItem' | 'filterText'> {
170+
let {cursor, getKey} = opts;
167171
return {
168172
setSelectedKeys(selectedKeys: Selection) {
169173
dispatch(state => ({
@@ -218,7 +222,7 @@ export function createListActions<T>(opts: ListOptions<T>, dispatch: (updater: (
218222
selection.delete(key);
219223
}
220224
}
221-
if (selection === 'all' && items.length === 0) {
225+
if (cursor == null && items.length === 0) {
222226
selection = new Set();
223227
}
224228

packages/@react-stately/data/test/useAsyncList.test.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ function getItems2() {
2929
});
3030
}
3131

32+
function getItemsEnd() {
33+
return new Promise(resolve => {
34+
setTimeout(() => resolve({items: ITEMS, cursor: null}), 100);
35+
});
36+
}
37+
3238
describe('useAsyncList', () => {
3339
beforeAll(() => {
3440
jest.useFakeTimers();
@@ -870,6 +876,59 @@ describe('useAsyncList', () => {
870876
expect(result.current.selectedKeys).toEqual('all');
871877
});
872878

879+
it('should maintain all selection if last visible item removed and unloaded items still exist', async () => {
880+
let load = jest.fn()
881+
.mockImplementationOnce(getItems);
882+
let {result, waitForNextUpdate} = renderHook(
883+
() => useAsyncList({load})
884+
);
885+
await act(async () => {
886+
result.current.loadMore();
887+
jest.runAllTimers();
888+
await waitForNextUpdate();
889+
result.current.setSelectedKeys('all');
890+
result.current.remove(1);
891+
result.current.remove(2);
892+
jest.runAllTimers();
893+
});
894+
expect(result.current.selectedKeys).toEqual('all');
895+
});
896+
897+
it('should change selection to empty set if last item removed with no unloaded items left', async () => {
898+
let load = jest.fn()
899+
.mockImplementationOnce(getItemsEnd);
900+
let {result, waitForNextUpdate} = renderHook(
901+
() => useAsyncList({load})
902+
);
903+
await act(async () => {
904+
result.current.loadMore();
905+
jest.runAllTimers();
906+
await waitForNextUpdate();
907+
result.current.setSelectedKeys('all');
908+
result.current.remove(1);
909+
result.current.remove(2);
910+
jest.runAllTimers();
911+
});
912+
expect(result.current.selectedKeys).toEqual(new Set());
913+
});
914+
915+
it('should change selection to empty set if all items removed', async () => {
916+
let load = jest.fn()
917+
.mockImplementationOnce(getItemsEnd);
918+
let {result, waitForNextUpdate} = renderHook(
919+
() => useAsyncList({load})
920+
);
921+
await act(async () => {
922+
result.current.loadMore();
923+
jest.runAllTimers();
924+
await waitForNextUpdate();
925+
result.current.setSelectedKeys('all');
926+
result.current.removeSelectedItems();
927+
jest.runAllTimers();
928+
});
929+
expect(result.current.selectedKeys).toEqual(new Set());
930+
});
931+
873932
describe('filtering', function () {
874933
const filterItems = [{id: 1, name: 'Bob'}, {id: 2, name: 'Joe'}, {id: 3, name: 'Bob Joe'}];
875934
const itemsFirstCall = [{id: 1, name: 'Bob'}, {id: 3, name: 'Bob Joe'}];

0 commit comments

Comments
 (0)