diff --git a/docs/demo/nest.md b/docs/demo/nest.md new file mode 100644 index 00000000..9060adbc --- /dev/null +++ b/docs/demo/nest.md @@ -0,0 +1,8 @@ +--- +title: Nest +nav: + title: Demo + path: /demo +--- + + diff --git a/examples/nest.tsx b/examples/nest.tsx new file mode 100644 index 00000000..bb2f2121 --- /dev/null +++ b/examples/nest.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import List from '../src/List'; +import './basic.less'; + +interface Item { + id: number; +} + +const data: Item[] = []; +for (let i = 0; i < 100; i += 1) { + data.push({ + id: i, + }); +} + +const MyItem: React.ForwardRefRenderFunction = ({ id }, ref) => ( +
+ + {(item, index, props) => ( +
+ {id}-{index} +
+ )} +
+
+); + +const ForwardMyItem = React.forwardRef(MyItem); + +const onScroll: React.UIEventHandler = (e) => { + // console.log('scroll:', e.currentTarget.scrollTop); +}; + +const Demo = () => { + return ( + + + {(item, _, props) => } + + + ); +}; + +export default Demo; + +/* eslint-enable */ diff --git a/src/Context.tsx b/src/Context.tsx new file mode 100644 index 00000000..aa93610c --- /dev/null +++ b/src/Context.tsx @@ -0,0 +1,3 @@ +import * as React from 'react'; + +export const WheelLockContext = React.createContext<(lock: boolean) => void>(() => {}); diff --git a/src/List.tsx b/src/List.tsx index ad8ff805..d9fb2786 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -379,8 +379,6 @@ export function RawList(props: ListProps, ref: React.Ref) { const onWheelDelta: Parameters[6] = useEvent((offsetXY, fromHorizontal) => { if (fromHorizontal) { - // Horizontal scroll no need sync virtual position - flushSync(() => { setOffsetLeft((left) => { const nextOffsetLeft = left + (isRTL ? -offsetXY : offsetXY); @@ -393,6 +391,7 @@ export function RawList(props: ListProps, ref: React.Ref) { } else { syncScrollTop((top) => { const newTop = top + offsetXY; + return newTop; }); } @@ -410,17 +409,31 @@ export function RawList(props: ListProps, ref: React.Ref) { ); // Mobile touch move - useMobileTouchMove(useVirtual, componentRef, (isHorizontal, delta, smoothOffset) => { + useMobileTouchMove(useVirtual, componentRef, (isHorizontal, delta, smoothOffset, e) => { + const event = e as TouchEvent & { + _virtualHandled?: boolean; + }; + if (originScroll(isHorizontal, delta, smoothOffset)) { return false; } - onRawWheel({ - preventDefault() {}, - deltaX: isHorizontal ? delta : 0, - deltaY: isHorizontal ? 0 : delta, - } as WheelEvent); - return true; + // Fix nest List trigger TouchMove event + if (!event || !event._virtualHandled) { + if (event) { + event._virtualHandled = true; + } + + onRawWheel({ + preventDefault() {}, + deltaX: isHorizontal ? delta : 0, + deltaY: isHorizontal ? 0 : delta, + } as WheelEvent); + + return true; + } + + return false; }); useLayoutEffect(() => { @@ -548,6 +561,10 @@ export function RawList(props: ListProps, ref: React.Ref) { containerProps.dir = 'rtl'; } + if (process.env.NODE_ENV !== 'production') { + containerProps['data-dev-offset-top'] = offsetTop; + } + return (
void, + onWheelDelta: (offset: number, horizontal: boolean) => void, ): [(e: WheelEvent) => void, (e: FireFoxDOMMouseScrollEvent) => void] { const offsetRef = useRef(0); const nextFrameRef = useRef(null); @@ -35,15 +35,25 @@ export default function useFrameWheel( isScrollAtRight, ); - function onWheelY(event: WheelEvent, deltaY: number) { + function onWheelY(e: WheelEvent, deltaY: number) { raf.cancel(nextFrameRef.current); - offsetRef.current += deltaY; - wheelValueRef.current = deltaY; - // Do nothing when scroll at the edge, Skip check when is in scroll if (originScroll(false, deltaY)) return; + // Skip if nest List has handled this event + const event = e as WheelEvent & { + _virtualHandled?: boolean; + }; + if (!event._virtualHandled) { + event._virtualHandled = true; + } else { + return; + } + + offsetRef.current += deltaY; + wheelValueRef.current = deltaY; + // Proxy of scroll events if (!isFF) { event.preventDefault(); @@ -53,7 +63,7 @@ export default function useFrameWheel( // Patch a multiple for Firefox to fix wheel number too small // ref: https://github.com/ant-design/ant-design/issues/26372#issuecomment-679460266 const patchMultiple = isMouseScrollRef.current ? 10 : 1; - onWheelDelta(offsetRef.current * patchMultiple); + onWheelDelta(offsetRef.current * patchMultiple, false); offsetRef.current = 0; }); } diff --git a/src/hooks/useMobileTouchMove.ts b/src/hooks/useMobileTouchMove.ts index 17740e9e..74945628 100644 --- a/src/hooks/useMobileTouchMove.ts +++ b/src/hooks/useMobileTouchMove.ts @@ -7,7 +7,12 @@ const SMOOTH_PTG = 14 / 15; export default function useMobileTouchMove( inVirtual: boolean, listRef: React.RefObject, - callback: (isHorizontal: boolean, offset: number, smoothOffset?: boolean) => boolean, + callback: ( + isHorizontal: boolean, + offset: number, + smoothOffset: boolean, + e?: TouchEvent, + ) => boolean, ) { const touchedRef = useRef(false); const touchXRef = useRef(0); @@ -34,22 +39,27 @@ export default function useMobileTouchMove( touchYRef.current = currentY; } - if (callback(isHorizontal, isHorizontal ? offsetX : offsetY)) { + const scrollHandled = callback(isHorizontal, isHorizontal ? offsetX : offsetY, false, e); + if (scrollHandled) { e.preventDefault(); } + // Smooth interval clearInterval(intervalRef.current); - intervalRef.current = setInterval(() => { - if (isHorizontal) { - offsetX *= SMOOTH_PTG; - } else { - offsetY *= SMOOTH_PTG; - } - const offset = Math.floor(isHorizontal ? offsetX : offsetY); - if (!callback(isHorizontal, offset, true) || Math.abs(offset) <= 0.1) { - clearInterval(intervalRef.current); - } - }, 16); + + if (scrollHandled) { + intervalRef.current = setInterval(() => { + if (isHorizontal) { + offsetX *= SMOOTH_PTG; + } else { + offsetY *= SMOOTH_PTG; + } + const offset = Math.floor(isHorizontal ? offsetX : offsetY); + if (!callback(isHorizontal, offset, true) || Math.abs(offset) <= 0.1) { + clearInterval(intervalRef.current); + } + }, 16); + } } }; diff --git a/tests/scroll.test.js b/tests/scroll.test.js index 314c1db2..8eee0a43 100644 --- a/tests/scroll.test.js +++ b/tests/scroll.test.js @@ -1,10 +1,9 @@ import '@testing-library/jest-dom'; -import { createEvent, fireEvent, render } from '@testing-library/react'; +import { act, createEvent, fireEvent, render } from '@testing-library/react'; import { mount } from 'enzyme'; import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil'; import { resetWarned } from 'rc-util/lib/warning'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import List from '../src'; import { spyElementPrototypes } from './utils/domHook'; @@ -51,11 +50,13 @@ describe('List.Scroll', () => { }); function genList(props, func = mount) { - let node = ( - - {({ id }) =>
  • {id}
  • } -
    - ); + const mergedProps = { + component: 'ul', + itemKey: 'id', + children: ({ id }) =>
  • {id}
  • , + ...props, + }; + let node = ; if (props.ref) { node =
    {node}
    ; @@ -494,4 +495,43 @@ describe('List.Scroll', () => { expect(container.querySelector('.rc-virtual-list-scrollbar-thumb')).toBeVisible(); }); + + it('nest scroll', async () => { + const { container } = genList( + { + itemHeight: 20, + height: 100, + data: genData(100), + children: ({ id }) => + id === '0' ? ( +
  • + + {({ id }) =>
  • {id}
  • } +
    + + ) : ( +
  • + ), + }, + render, + ); + + fireEvent.wheel(container.querySelector('ul ul li'), { + deltaY: 10, + }); + + await act(async () => { + jest.advanceTimersByTime(1000000); + await Promise.resolve(); + }); + + expect(container.querySelectorAll('[data-dev-offset-top]')[0]).toHaveAttribute( + 'data-dev-offset-top', + '0', + ); + expect(container.querySelectorAll('[data-dev-offset-top]')[1]).toHaveAttribute( + 'data-dev-offset-top', + '10', + ); + }); }); diff --git a/tests/touch.test.js b/tests/touch.test.js index 12691ab7..415bc87c 100644 --- a/tests/touch.test.js +++ b/tests/touch.test.js @@ -1,7 +1,8 @@ -import React from 'react'; +import { act, fireEvent, render } from '@testing-library/react'; import { mount } from 'enzyme'; -import { spyElementPrototypes } from './utils/domHook'; +import React from 'react'; import List from '../src'; +import { spyElementPrototypes } from './utils/domHook'; function genData(count) { return new Array(count).fill(null).map((_, index) => ({ id: String(index) })); @@ -123,11 +124,55 @@ describe('List.Touch', () => { const touchEvent = new Event('touchstart'); touchEvent.preventDefault = preventDefault; - wrapper - .find('.rc-virtual-list-scrollbar') - .instance() - .dispatchEvent(touchEvent); + wrapper.find('.rc-virtual-list-scrollbar').instance().dispatchEvent(touchEvent); expect(preventDefault).toHaveBeenCalled(); }); + + it('nest touch', async () => { + const { container } = render( + + {({ id }) => + id === '0' ? ( +
  • + + {({ id }) =>
  • {id}
  • } + + + ) : ( +
  • + ) + } + , + ); + + const targetLi = container.querySelector('ul ul li'); + + fireEvent.touchStart(targetLi, { + touches: [{ pageY: 0 }], + }); + + fireEvent.touchMove(targetLi, { + touches: [{ pageY: -1 }], + }); + + await act(async () => { + jest.advanceTimersByTime(1000000); + await Promise.resolve(); + }); + + expect(container.querySelectorAll('[data-dev-offset-top]')[0]).toHaveAttribute( + 'data-dev-offset-top', + '0', + ); + + // inner not to be 0 + expect(container.querySelectorAll('[data-dev-offset-top]')[1]).toHaveAttribute( + 'data-dev-offset-top', + ); + expect(container.querySelectorAll('[data-dev-offset-top]')[1]).not.toHaveAttribute( + 'data-dev-offset-top', + '0', + ); + }); });