Skip to content

Commit de67525

Browse files
authored
fix: nest scroll should block (#288)
* docs: add demo * chore: prevent scroll * chore: comment * chore: tmp of it * fix: not scroll * chore: update * chore: rollback * test: cov
1 parent 02dfd34 commit de67525

File tree

8 files changed

+243
-41
lines changed

8 files changed

+243
-41
lines changed

docs/demo/nest.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: Nest
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
<code src="../../examples/nest.tsx"></code>

examples/nest.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as React from 'react';
2+
import List from '../src/List';
3+
import './basic.less';
4+
5+
interface Item {
6+
id: number;
7+
}
8+
9+
const data: Item[] = [];
10+
for (let i = 0; i < 100; i += 1) {
11+
data.push({
12+
id: i,
13+
});
14+
}
15+
16+
const MyItem: React.ForwardRefRenderFunction<any, Item> = ({ id }, ref) => (
17+
<div style={{ padding: 20, background: 'yellow' }} ref={ref}>
18+
<List
19+
data={data}
20+
height={200}
21+
itemHeight={20}
22+
itemKey="id"
23+
style={{
24+
border: '1px solid blue',
25+
boxSizing: 'border-box',
26+
background: 'white',
27+
}}
28+
// debug={`inner_${id}`}
29+
>
30+
{(item, index, props) => (
31+
<div {...(item as any)} {...props} style={{ height: 20, border: '1px solid cyan' }}>
32+
{id}-{index}
33+
</div>
34+
)}
35+
</List>
36+
</div>
37+
);
38+
39+
const ForwardMyItem = React.forwardRef(MyItem);
40+
41+
const onScroll: React.UIEventHandler<HTMLElement> = (e) => {
42+
// console.log('scroll:', e.currentTarget.scrollTop);
43+
};
44+
45+
const Demo = () => {
46+
return (
47+
<React.StrictMode>
48+
<List
49+
id="list"
50+
data={data}
51+
height={800}
52+
itemHeight={20}
53+
itemKey="id"
54+
style={{
55+
border: '1px solid red',
56+
boxSizing: 'border-box',
57+
}}
58+
onScroll={onScroll}
59+
// debug="outer"
60+
>
61+
{(item, _, props) => <ForwardMyItem {...item} {...props} />}
62+
</List>
63+
</React.StrictMode>
64+
);
65+
};
66+
67+
export default Demo;
68+
69+
/* eslint-enable */

src/Context.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import * as React from 'react';
2+
3+
export const WheelLockContext = React.createContext<(lock: boolean) => void>(() => {});

src/List.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,6 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
379379

380380
const onWheelDelta: Parameters<typeof useFrameWheel>[6] = useEvent((offsetXY, fromHorizontal) => {
381381
if (fromHorizontal) {
382-
// Horizontal scroll no need sync virtual position
383-
384382
flushSync(() => {
385383
setOffsetLeft((left) => {
386384
const nextOffsetLeft = left + (isRTL ? -offsetXY : offsetXY);
@@ -393,6 +391,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
393391
} else {
394392
syncScrollTop((top) => {
395393
const newTop = top + offsetXY;
394+
396395
return newTop;
397396
});
398397
}
@@ -410,17 +409,31 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
410409
);
411410

412411
// Mobile touch move
413-
useMobileTouchMove(useVirtual, componentRef, (isHorizontal, delta, smoothOffset) => {
412+
useMobileTouchMove(useVirtual, componentRef, (isHorizontal, delta, smoothOffset, e) => {
413+
const event = e as TouchEvent & {
414+
_virtualHandled?: boolean;
415+
};
416+
414417
if (originScroll(isHorizontal, delta, smoothOffset)) {
415418
return false;
416419
}
417420

418-
onRawWheel({
419-
preventDefault() {},
420-
deltaX: isHorizontal ? delta : 0,
421-
deltaY: isHorizontal ? 0 : delta,
422-
} as WheelEvent);
423-
return true;
421+
// Fix nest List trigger TouchMove event
422+
if (!event || !event._virtualHandled) {
423+
if (event) {
424+
event._virtualHandled = true;
425+
}
426+
427+
onRawWheel({
428+
preventDefault() {},
429+
deltaX: isHorizontal ? delta : 0,
430+
deltaY: isHorizontal ? 0 : delta,
431+
} as WheelEvent);
432+
433+
return true;
434+
}
435+
436+
return false;
424437
});
425438

426439
useLayoutEffect(() => {
@@ -548,6 +561,10 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
548561
containerProps.dir = 'rtl';
549562
}
550563

564+
if (process.env.NODE_ENV !== 'production') {
565+
containerProps['data-dev-offset-top'] = offsetTop;
566+
}
567+
551568
return (
552569
<div
553570
ref={containerRef}

src/hooks/useFrameWheel.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function useFrameWheel(
1818
/***
1919
* Return `true` when you need to prevent default event
2020
*/
21-
onWheelDelta: (offset: number, horizontal?: boolean) => void,
21+
onWheelDelta: (offset: number, horizontal: boolean) => void,
2222
): [(e: WheelEvent) => void, (e: FireFoxDOMMouseScrollEvent) => void] {
2323
const offsetRef = useRef(0);
2424
const nextFrameRef = useRef<number>(null);
@@ -35,15 +35,25 @@ export default function useFrameWheel(
3535
isScrollAtRight,
3636
);
3737

38-
function onWheelY(event: WheelEvent, deltaY: number) {
38+
function onWheelY(e: WheelEvent, deltaY: number) {
3939
raf.cancel(nextFrameRef.current);
4040

41-
offsetRef.current += deltaY;
42-
wheelValueRef.current = deltaY;
43-
4441
// Do nothing when scroll at the edge, Skip check when is in scroll
4542
if (originScroll(false, deltaY)) return;
4643

44+
// Skip if nest List has handled this event
45+
const event = e as WheelEvent & {
46+
_virtualHandled?: boolean;
47+
};
48+
if (!event._virtualHandled) {
49+
event._virtualHandled = true;
50+
} else {
51+
return;
52+
}
53+
54+
offsetRef.current += deltaY;
55+
wheelValueRef.current = deltaY;
56+
4757
// Proxy of scroll events
4858
if (!isFF) {
4959
event.preventDefault();
@@ -53,7 +63,7 @@ export default function useFrameWheel(
5363
// Patch a multiple for Firefox to fix wheel number too small
5464
// ref: https://github.com/ant-design/ant-design/issues/26372#issuecomment-679460266
5565
const patchMultiple = isMouseScrollRef.current ? 10 : 1;
56-
onWheelDelta(offsetRef.current * patchMultiple);
66+
onWheelDelta(offsetRef.current * patchMultiple, false);
5767
offsetRef.current = 0;
5868
});
5969
}

src/hooks/useMobileTouchMove.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ const SMOOTH_PTG = 14 / 15;
77
export default function useMobileTouchMove(
88
inVirtual: boolean,
99
listRef: React.RefObject<HTMLDivElement>,
10-
callback: (isHorizontal: boolean, offset: number, smoothOffset?: boolean) => boolean,
10+
callback: (
11+
isHorizontal: boolean,
12+
offset: number,
13+
smoothOffset: boolean,
14+
e?: TouchEvent,
15+
) => boolean,
1116
) {
1217
const touchedRef = useRef(false);
1318
const touchXRef = useRef(0);
@@ -34,22 +39,27 @@ export default function useMobileTouchMove(
3439
touchYRef.current = currentY;
3540
}
3641

37-
if (callback(isHorizontal, isHorizontal ? offsetX : offsetY)) {
42+
const scrollHandled = callback(isHorizontal, isHorizontal ? offsetX : offsetY, false, e);
43+
if (scrollHandled) {
3844
e.preventDefault();
3945
}
46+
4047
// Smooth interval
4148
clearInterval(intervalRef.current);
42-
intervalRef.current = setInterval(() => {
43-
if (isHorizontal) {
44-
offsetX *= SMOOTH_PTG;
45-
} else {
46-
offsetY *= SMOOTH_PTG;
47-
}
48-
const offset = Math.floor(isHorizontal ? offsetX : offsetY);
49-
if (!callback(isHorizontal, offset, true) || Math.abs(offset) <= 0.1) {
50-
clearInterval(intervalRef.current);
51-
}
52-
}, 16);
49+
50+
if (scrollHandled) {
51+
intervalRef.current = setInterval(() => {
52+
if (isHorizontal) {
53+
offsetX *= SMOOTH_PTG;
54+
} else {
55+
offsetY *= SMOOTH_PTG;
56+
}
57+
const offset = Math.floor(isHorizontal ? offsetX : offsetY);
58+
if (!callback(isHorizontal, offset, true) || Math.abs(offset) <= 0.1) {
59+
clearInterval(intervalRef.current);
60+
}
61+
}, 16);
62+
}
5363
}
5464
};
5565

tests/scroll.test.js

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import '@testing-library/jest-dom';
2-
import { createEvent, fireEvent, render } from '@testing-library/react';
2+
import { act, createEvent, fireEvent, render } from '@testing-library/react';
33
import { mount } from 'enzyme';
44
import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil';
55
import { resetWarned } from 'rc-util/lib/warning';
66
import React from 'react';
7-
import { act } from 'react-dom/test-utils';
87
import List from '../src';
98
import { spyElementPrototypes } from './utils/domHook';
109

@@ -51,11 +50,13 @@ describe('List.Scroll', () => {
5150
});
5251

5352
function genList(props, func = mount) {
54-
let node = (
55-
<List component="ul" itemKey="id" {...props}>
56-
{({ id }) => <li>{id}</li>}
57-
</List>
58-
);
53+
const mergedProps = {
54+
component: 'ul',
55+
itemKey: 'id',
56+
children: ({ id }) => <li>{id}</li>,
57+
...props,
58+
};
59+
let node = <List {...mergedProps} />;
5960

6061
if (props.ref) {
6162
node = <div>{node}</div>;
@@ -494,4 +495,43 @@ describe('List.Scroll', () => {
494495

495496
expect(container.querySelector('.rc-virtual-list-scrollbar-thumb')).toBeVisible();
496497
});
498+
499+
it('nest scroll', async () => {
500+
const { container } = genList(
501+
{
502+
itemHeight: 20,
503+
height: 100,
504+
data: genData(100),
505+
children: ({ id }) =>
506+
id === '0' ? (
507+
<li>
508+
<List component="ul" itemKey="id" itemHeight={20} height={100} data={genData(100)}>
509+
{({ id }) => <li>{id}</li>}
510+
</List>
511+
</li>
512+
) : (
513+
<li />
514+
),
515+
},
516+
render,
517+
);
518+
519+
fireEvent.wheel(container.querySelector('ul ul li'), {
520+
deltaY: 10,
521+
});
522+
523+
await act(async () => {
524+
jest.advanceTimersByTime(1000000);
525+
await Promise.resolve();
526+
});
527+
528+
expect(container.querySelectorAll('[data-dev-offset-top]')[0]).toHaveAttribute(
529+
'data-dev-offset-top',
530+
'0',
531+
);
532+
expect(container.querySelectorAll('[data-dev-offset-top]')[1]).toHaveAttribute(
533+
'data-dev-offset-top',
534+
'10',
535+
);
536+
});
497537
});

tests/touch.test.js

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React from 'react';
1+
import { act, fireEvent, render } from '@testing-library/react';
22
import { mount } from 'enzyme';
3-
import { spyElementPrototypes } from './utils/domHook';
3+
import React from 'react';
44
import List from '../src';
5+
import { spyElementPrototypes } from './utils/domHook';
56

67
function genData(count) {
78
return new Array(count).fill(null).map((_, index) => ({ id: String(index) }));
@@ -123,11 +124,55 @@ describe('List.Touch', () => {
123124

124125
const touchEvent = new Event('touchstart');
125126
touchEvent.preventDefault = preventDefault;
126-
wrapper
127-
.find('.rc-virtual-list-scrollbar')
128-
.instance()
129-
.dispatchEvent(touchEvent);
127+
wrapper.find('.rc-virtual-list-scrollbar').instance().dispatchEvent(touchEvent);
130128

131129
expect(preventDefault).toHaveBeenCalled();
132130
});
131+
132+
it('nest touch', async () => {
133+
const { container } = render(
134+
<List component="ul" itemHeight={20} height={100} data={genData(100)}>
135+
{({ id }) =>
136+
id === '0' ? (
137+
<li>
138+
<List component="ul" itemKey="id" itemHeight={20} height={100} data={genData(100)}>
139+
{({ id }) => <li>{id}</li>}
140+
</List>
141+
</li>
142+
) : (
143+
<li />
144+
)
145+
}
146+
</List>,
147+
);
148+
149+
const targetLi = container.querySelector('ul ul li');
150+
151+
fireEvent.touchStart(targetLi, {
152+
touches: [{ pageY: 0 }],
153+
});
154+
155+
fireEvent.touchMove(targetLi, {
156+
touches: [{ pageY: -1 }],
157+
});
158+
159+
await act(async () => {
160+
jest.advanceTimersByTime(1000000);
161+
await Promise.resolve();
162+
});
163+
164+
expect(container.querySelectorAll('[data-dev-offset-top]')[0]).toHaveAttribute(
165+
'data-dev-offset-top',
166+
'0',
167+
);
168+
169+
// inner not to be 0
170+
expect(container.querySelectorAll('[data-dev-offset-top]')[1]).toHaveAttribute(
171+
'data-dev-offset-top',
172+
);
173+
expect(container.querySelectorAll('[data-dev-offset-top]')[1]).not.toHaveAttribute(
174+
'data-dev-offset-top',
175+
'0',
176+
);
177+
});
133178
});

0 commit comments

Comments
 (0)