diff --git a/src/Filler.tsx b/src/Filler.tsx index 5e480e25..f52971b9 100644 --- a/src/Filler.tsx +++ b/src/Filler.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; -import ResizeObserver from 'rc-resize-observer'; import classNames from 'classnames'; +import ResizeObserver from 'rc-resize-observer'; +import * as React from 'react'; export type InnerProps = Pick, 'role' | 'id'>; @@ -69,14 +69,19 @@ const Filler = React.forwardRef( }; } + const handleResize = React.useCallback( + ({ offsetHeight }) => { + if (offsetHeight && onInnerResize) { + onInnerResize(); + } + }, + [onInnerResize], + ); + return (
{ - if (offsetHeight && onInnerResize) { - onInnerResize(); - } - }} + onResize={handleResize} >
(props: ListProps, ref: React.Ref) { setScrollMoving(false); }; - const sharedConfig: SharedConfig = { - getKey, - }; - // ================================ Scroll ================================ function syncScrollTop(newTop: number | ((prev: number) => number)) { setOffsetTop((origin) => { @@ -572,7 +568,7 @@ export function RawList(props: ListProps, ref: React.Ref) { offsetLeft, setInstanceRef, children, - sharedConfig, + getKey, ); let componentStyle: React.CSSProperties = null; diff --git a/src/hooks/useChildren.tsx b/src/hooks/useChildren.tsx index 8c4fdf6d..55f8f584 100644 --- a/src/hooks/useChildren.tsx +++ b/src/hooks/useChildren.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { RenderFunc, SharedConfig } from '../interface'; +import type { RenderFunc, GetKey } from '../interface'; import { Item } from '../Item'; export default function useChildren( @@ -10,22 +10,25 @@ export default function useChildren( offsetX: number, setNodeRef: (item: T, element: HTMLElement) => void, renderFunc: RenderFunc, - { getKey }: SharedConfig, + getKey: GetKey, ) { - return list.slice(startIndex, endIndex + 1).map((item, index) => { - const eleIndex = startIndex + index; - const node = renderFunc(item, eleIndex, { - style: { - width: scrollWidth, - }, - offsetX, - }) as React.ReactElement; + // The list reference may remain unchanged, but its internal data may change, which can result in different behavior compared to the previous implementation. + return React.useMemo(() => { + return list.slice(startIndex, endIndex + 1).map((item, index) => { + const eleIndex = startIndex + index; + const node = renderFunc(item, eleIndex, { + style: { + width: scrollWidth, + }, + offsetX, + }) as React.ReactElement; - const key = getKey(item); - return ( - setNodeRef(item, ele)}> - {node} - - ); - }); + const key = getKey(item); + return ( + setNodeRef(item, ele)}> + {node} + + ); + }); + }, [list, startIndex, endIndex, setNodeRef, renderFunc, getKey, offsetX, scrollWidth]); } diff --git a/src/hooks/useHeights.tsx b/src/hooks/useHeights.tsx index ed13de72..a7d83ccd 100644 --- a/src/hooks/useHeights.tsx +++ b/src/hooks/useHeights.tsx @@ -24,11 +24,11 @@ export default function useHeights( const promiseIdRef = useRef(0); - function cancelRaf() { + const cancelRaf = React.useCallback(function cancelRaf() { promiseIdRef.current += 1; - } + }, []); - function collectHeight(sync = false) { + const collectHeight = React.useCallback(function (sync = false) { cancelRaf(); const doCollect = () => { @@ -67,9 +67,9 @@ export default function useHeights( } }); } - } + }, [cancelRaf]); - function setInstanceRef(item: T, instance: HTMLElement) { + const setInstanceRef = React.useCallback(function setInstanceRef(item: T, instance: HTMLElement) { const key = getKey(item); const origin = instanceRef.current.get(key); @@ -88,11 +88,12 @@ export default function useHeights( onItemRemove?.(item); } } - } + }, [collectHeight, getKey, onItemAdd, onItemRemove]); useEffect(() => { return cancelRaf; }, []); + // This is somewhat confusing: when heightsRef.current.set is called, updatedMark changes, which in turn causes heightsRef.current to also change. return [setInstanceRef, collectHeight, heightsRef.current, updatedMark]; } diff --git a/src/interface.ts b/src/interface.ts index e0fd765d..3b903d54 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -4,10 +4,6 @@ export type RenderFunc = ( props: { style: React.CSSProperties; offsetX: number }, ) => React.ReactNode; -export interface SharedConfig { - getKey: (item: T) => React.Key; -} - export type GetKey = (item: T) => React.Key; export type GetSize = (startKey: React.Key, endKey?: React.Key) => { top: number; bottom: number }; diff --git a/tests/props.test.js b/tests/props.test.js index bbc41e81..d3209b20 100644 --- a/tests/props.test.js +++ b/tests/props.test.js @@ -1,5 +1,5 @@ -import React from 'react'; import { mount } from 'enzyme'; +import React from 'react'; import List from '../src'; describe('Props', () => { @@ -11,30 +11,20 @@ describe('Props', () => { } const wrapper = mount( - item.id}> + item.id}> {({ id }) => {id}} , ); - expect( - wrapper - .find('Item') - .at(0) - .key(), - ).toBe('903'); - - expect( - wrapper - .find('Item') - .at(1) - .key(), - ).toBe('1128'); + expect(wrapper.find('Item').at(0).key()).toBe('903'); + + expect(wrapper.find('Item').at(1).key()).toBe('1128'); }); it('prefixCls', () => { const wrapper = mount( - id} prefixCls="prefix"> - {id =>
{id}
} + id} prefixCls="prefix"> + {(id) =>
{id}
}
, ); @@ -44,13 +34,40 @@ describe('Props', () => { it('offsetX in renderFn', () => { let scrollLeft; mount( - id} prefixCls="prefix"> - {(id, _, { offsetX }) => { + id} prefixCls="prefix"> + {(id, _, { offsetX }) => { scrollLeft = offsetX; - return
{id}
}} + return
{id}
; + }}
, ); expect(scrollLeft).toEqual(0); }); + + it('no unnecessary re-render', () => { + const renderItem = jest.fn(); + renderItem.mockImplementation(({ id, key }) =>
{id}
); + + const data = [{ id: 1, key: 1 }]; + function Wrapper() { + const [state, setState] = React.useState(0); + + React.useEffect(() => { + setState(1); + }, []); + + return ( +
+

{state}

+ + {renderItem} + +
+ ); + } + const wrapper = mount(); + expect(wrapper.find('h1').text()).toBe('1'); + expect(renderItem).toHaveBeenCalledTimes(1); + }); }); diff --git a/tests/scroll-Firefox.test.js b/tests/scroll-Firefox.test.js index 2a9290ea..e0ecc746 100644 --- a/tests/scroll-Firefox.test.js +++ b/tests/scroll-Firefox.test.js @@ -1,9 +1,9 @@ -import React from 'react'; -import { act } from 'react-dom/test-utils'; +import { act } from '@testing-library/react'; import { mount } from 'enzyme'; -import { spyElementPrototypes } from './utils/domHook'; +import React from 'react'; import List from '../src'; import isFF from '../src/utils/isFirefox'; +import { spyElementPrototypes } from './utils/domHook'; function genData(count) { return new Array(count).fill(null).map((_, index) => ({ id: String(index) })); @@ -124,8 +124,10 @@ describe('List.Firefox-Scroll', () => { const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100), ref: listRef }); const ulElement = wrapper.find('ul').instance(); // scroll to bottom - listRef.current.scrollTo(99999); - jest.runAllTimers(); + act(() => { + listRef.current.scrollTo(99999); + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop).toEqual(1900); act(() => { diff --git a/tests/scroll.test.js b/tests/scroll.test.js index 2e30f468..2d2d6596 100644 --- a/tests/scroll.test.js +++ b/tests/scroll.test.js @@ -94,9 +94,13 @@ describe('List.Scroll', () => { jest.useFakeTimers(); const listRef = React.createRef(); const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100), ref: listRef }); - jest.runAllTimers(); - listRef.current.scrollTo(null); + act(() => { + jest.runAllTimers(); + + listRef.current.scrollTo(null); + }); + expect(wrapper.find('.rc-virtual-list-scrollbar-thumb').props().style.display).not.toEqual( 'none', ); @@ -107,8 +111,10 @@ describe('List.Scroll', () => { it('value scroll', () => { const listRef = React.createRef(); const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100), ref: listRef }); - listRef.current.scrollTo(903); - jest.runAllTimers(); + act(() => { + listRef.current.scrollTo(903); + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop).toEqual(903); wrapper.unmount(); @@ -125,9 +131,8 @@ describe('List.Scroll', () => { ...result, ref, scrollTo: (...args) => { - ref.current.scrollTo(...args); - act(() => { + ref.current.scrollTo(...args); jest.runAllTimers(); }); }, @@ -153,8 +158,10 @@ describe('List.Scroll', () => { it('scroll top should not out of range', () => { const { scrollTo, container } = presetList(); - scrollTo({ index: 0, align: 'bottom' }); - jest.runAllTimers(); + act(() => { + scrollTo({ index: 0, align: 'bottom' }); + jest.runAllTimers(); + }); expect(container.querySelector('ul').scrollTop).toEqual(0); }); @@ -389,9 +396,13 @@ describe('List.Scroll', () => { ref: listRef, direction: 'rtl', }); - jest.runAllTimers(); - listRef.current.scrollTo(null); + act(() => { + jest.runAllTimers(); + + listRef.current.scrollTo(null); + }); + expect(wrapper.find('.rc-virtual-list-scrollbar-thumb').props().style.display).not.toEqual( 'none', ); diff --git a/tests/scrollWidth.test.tsx b/tests/scrollWidth.test.tsx index 79a141c1..094f40d5 100644 --- a/tests/scrollWidth.test.tsx +++ b/tests/scrollWidth.test.tsx @@ -230,10 +230,14 @@ describe('List.scrollWidth', () => { ref: listRef, }); - listRef.current.scrollTo({ left: 135 }); + act(() => { + listRef.current.scrollTo({ left: 135 }); + }); expect(listRef.current.getScrollInfo()).toEqual({ x: 135, y: 0 }); - listRef.current.scrollTo({ left: -99 }); + act(() => { + listRef.current.scrollTo({ left: -99 }); + }); expect(listRef.current.getScrollInfo()).toEqual({ x: 0, y: 0 }); }); diff --git a/tests/touch.test.js b/tests/touch.test.js index fa62a6a2..93412697 100644 --- a/tests/touch.test.js +++ b/tests/touch.test.js @@ -71,15 +71,94 @@ describe('List.Touch', () => { return wrapper.find('.rc-virtual-list-holder').instance(); } + act(() => { + // start + const touchEvent = new Event('touchstart'); + touchEvent.touches = [{ pageY: 100 }]; + getElement().dispatchEvent(touchEvent); + + // move + const moveEvent = new Event('touchmove'); + moveEvent.touches = [{ pageY: 90 }]; + getElement().dispatchEvent(moveEvent); + + // end + const endEvent = new Event('touchend'); + getElement().dispatchEvent(endEvent); + + // smooth + jest.runAllTimers(); + }); + expect(wrapper.find('ul').instance().scrollTop > 10).toBeTruthy(); + + wrapper.unmount(); + }); + + it('origin scroll', () => { + const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) }); + + function getElement() { + return wrapper.find('.rc-virtual-list-holder').instance(); + } + + act(() => { + // start + const touchEvent = new Event('touchstart'); + touchEvent.touches = [{ pageY: 100 }]; + getElement().dispatchEvent(touchEvent); + + // move + const moveEvent1 = new Event('touchmove'); + moveEvent1.touches = [{ pageY: 110 }]; + getElement().dispatchEvent(moveEvent1); + + // move + const moveEvent2 = new Event('touchmove'); + moveEvent2.touches = [{ pageY: 150 }]; + getElement().dispatchEvent(moveEvent2); + + // end + const endEvent = new Event('touchend'); + getElement().dispatchEvent(endEvent); + + // smooth + jest.runAllTimers(); + }); + expect(wrapper.find('ul').instance().scrollTop).toBe(0); + wrapper.unmount(); + }); + + it('should handle complex touch gestures', () => { + const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) }); + + function getElement() { + return wrapper.find('.rc-virtual-list-holder').instance(); + } + // start const touchEvent = new Event('touchstart'); touchEvent.touches = [{ pageY: 100 }]; getElement().dispatchEvent(touchEvent); // move - const moveEvent = new Event('touchmove'); - moveEvent.touches = [{ pageY: 90 }]; - getElement().dispatchEvent(moveEvent); + const moveEvent1 = new Event('touchmove'); + moveEvent1.touches = [{ pageY: 110 }]; + getElement().dispatchEvent(moveEvent1); + + // move + const moveEvent2 = new Event('touchmove'); + moveEvent2.touches = [{ pageY: 150 }]; + getElement().dispatchEvent(moveEvent2); + + // move + const moveEvent3 = new Event('touchmove'); + moveEvent3.touches = [{ pageY: 20 }]; + getElement().dispatchEvent(moveEvent3); + + // move + const moveEvent4 = new Event('touchmove'); + moveEvent4.touches = [{ pageY: 100 }]; + getElement().dispatchEvent(moveEvent4); // end const endEvent = new Event('touchend'); @@ -87,8 +166,8 @@ describe('List.Touch', () => { // smooth jest.runAllTimers(); - expect(wrapper.find('ul').instance().scrollTop > 10).toBeTruthy(); + expect(wrapper.find('ul').instance().scrollTop).toBe(0); wrapper.unmount(); }); @@ -99,35 +178,39 @@ describe('List.Touch', () => { return wrapper.find('.rc-virtual-list-holder').instance(); } - // start - const touchEvent = new Event('touchstart'); - touchEvent.touches = [{ pageY: 500 }]; - getElement().dispatchEvent(touchEvent); - - // move const preventDefault = jest.fn(); - const moveEvent = new Event('touchmove'); - moveEvent.touches = [{ pageY: 0 }]; - moveEvent.preventDefault = preventDefault; - getElement().dispatchEvent(moveEvent); + act(() => { + // start + const touchEvent = new Event('touchstart'); + touchEvent.touches = [{ pageY: 500 }]; + getElement().dispatchEvent(touchEvent); + + // move + const moveEvent = new Event('touchmove'); + moveEvent.touches = [{ pageY: 0 }]; + moveEvent.preventDefault = preventDefault; + getElement().dispatchEvent(moveEvent); + }); // Call preventDefault expect(preventDefault).toHaveBeenCalled(); - // ======= Not call since scroll to the bottom ======= - jest.runAllTimers(); - preventDefault.mockReset(); + act(() => { + // ======= Not call since scroll to the bottom ======= + jest.runAllTimers(); + preventDefault.mockReset(); - // start - const touchEvent2 = new Event('touchstart'); - touchEvent2.touches = [{ pageY: 500 }]; - getElement().dispatchEvent(touchEvent2); + // start + const touchEvent2 = new Event('touchstart'); + touchEvent2.touches = [{ pageY: 500 }]; + getElement().dispatchEvent(touchEvent2); - // move - const moveEvent2 = new Event('touchmove'); - moveEvent2.touches = [{ pageY: 0 }]; - moveEvent2.preventDefault = preventDefault; - getElement().dispatchEvent(moveEvent2); + // move + const moveEvent2 = new Event('touchmove'); + moveEvent2.touches = [{ pageY: 0 }]; + moveEvent2.preventDefault = preventDefault; + getElement().dispatchEvent(moveEvent2); + }); expect(preventDefault).not.toHaveBeenCalled(); }); @@ -137,16 +220,18 @@ describe('List.Touch', () => { const preventDefault = jest.fn(); const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) }); - const touchEvent = new Event('touchstart'); - touchEvent.preventDefault = preventDefault; - wrapper.find('.rc-virtual-list-scrollbar').instance().dispatchEvent(touchEvent); + act(() => { + const touchEvent = new Event('touchstart'); + touchEvent.preventDefault = preventDefault; + wrapper.find('.rc-virtual-list-scrollbar').instance().dispatchEvent(touchEvent); + }); expect(preventDefault).toHaveBeenCalled(); }); it('nest touch', async () => { const { container } = render( - + {({ id }) => id === '0' ? (