Skip to content

Commit 1f3216d

Browse files
authored
fix: Not block scroll when list is not scrollable (#55)
* chore: Move keepInRange in List * add canScroll hooks * stop smooth when touch done * test case * clean up
1 parent 1a96ead commit 1f3216d

File tree

7 files changed

+161
-64
lines changed

7 files changed

+161
-64
lines changed

examples/switch.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ function getData(count: number) {
3636

3737
const Demo = () => {
3838
const [height, setHeight] = React.useState(100);
39-
const [data, setData] = React.useState(getData(100));
39+
const [data, setData] = React.useState(getData(20));
4040

4141
return (
4242
<React.StrictMode>
43-
<div>
43+
<div style={{ height: '150vh' }}>
4444
<h2>Switch</h2>
4545
<span
4646
onChange={(e: any) => {

src/List.tsx

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import ScrollBar from './ScrollBar';
66
import { RenderFunc, SharedConfig, GetKey } from './interface';
77
import useChildren from './hooks/useChildren';
88
import useHeights from './hooks/useHeights';
9-
import useInRange from './hooks/useInRange';
109
import useScrollTo from './hooks/useScrollTo';
1110
import useDiffItem from './hooks/useDiffItem';
1211
import useFrameWheel from './hooks/useFrameWheel';
1312
import useMobileTouchMove from './hooks/useMobileTouchMove';
13+
import useOriginScroll from './hooks/useOriginScroll';
1414

1515
const EMPTY_DATA = [];
1616

@@ -186,7 +186,22 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
186186
rangeRef.current.end = end;
187187

188188
// =============================== In Range ===============================
189-
const keepInRange = useInRange(scrollHeight, height);
189+
const maxScrollHeight = scrollHeight - height;
190+
const maxScrollHeightRef = useRef(maxScrollHeight);
191+
maxScrollHeightRef.current = maxScrollHeight;
192+
193+
function keepInRange(newScrollTop: number) {
194+
let newTop = Math.max(newScrollTop, 0);
195+
if (!Number.isNaN(maxScrollHeightRef.current)) {
196+
newTop = Math.min(newTop, maxScrollHeightRef.current);
197+
}
198+
return newTop;
199+
}
200+
201+
const isScrollAtTop = scrollTop <= 0;
202+
const isScrollAtBottom = scrollTop >= maxScrollHeight;
203+
204+
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
190205

191206
// ================================ Scroll ================================
192207
function onScrollBar(newScrollTop: number) {
@@ -209,16 +224,27 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
209224
}
210225

211226
// Since this added in global,should use ref to keep update
212-
const [onRawWheel, onFireFoxScroll] = useFrameWheel(inVirtual, offsetY => {
213-
syncScrollTop(top => {
214-
const newTop = keepInRange(top + offsetY);
215-
return newTop;
216-
});
217-
});
227+
const [onRawWheel, onFireFoxScroll] = useFrameWheel(
228+
inVirtual,
229+
isScrollAtTop,
230+
isScrollAtBottom,
231+
offsetY => {
232+
syncScrollTop(top => {
233+
const newTop = keepInRange(top + offsetY);
234+
235+
return newTop;
236+
});
237+
},
238+
);
218239

219240
// Mobile touch move
220-
useMobileTouchMove(inVirtual, componentRef, deltaY => {
241+
useMobileTouchMove(inVirtual, componentRef, (deltaY, smoothOffset) => {
242+
if (originScroll(deltaY, smoothOffset)) {
243+
return false;
244+
}
245+
221246
onRawWheel({ preventDefault() {}, deltaY } as WheelEvent);
247+
return true;
222248
});
223249

224250
React.useEffect(() => {

src/hooks/useFrameWheel.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useRef } from 'react';
22
import raf from 'rc-util/lib/raf';
33
import isFF from '../utils/isFirefox';
4+
import useOriginScroll from './useOriginScroll';
45

56
interface FireFoxDOMMouseScrollEvent {
67
detail: number;
@@ -9,6 +10,8 @@ interface FireFoxDOMMouseScrollEvent {
910

1011
export default function useFrameWheel(
1112
inVirtual: boolean,
13+
isScrollAtTop: boolean,
14+
isScrollAtBottom: boolean,
1215
onWheelDelta: (offset: number) => void,
1316
): [(e: WheelEvent) => void, (e: FireFoxDOMMouseScrollEvent) => void] {
1417
const offsetRef = useRef(0);
@@ -18,19 +21,26 @@ export default function useFrameWheel(
1821
const wheelValueRef = useRef<number>(null);
1922
const isMouseScrollRef = useRef<boolean>(false);
2023

24+
// Scroll status sync
25+
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
26+
2127
function onWheel(event: WheelEvent) {
2228
if (!inVirtual) return;
2329

30+
raf.cancel(nextFrameRef.current);
31+
32+
const { deltaY } = event;
33+
offsetRef.current += deltaY;
34+
wheelValueRef.current = deltaY;
35+
36+
// Do nothing when scroll at the edge, Skip check when is in scroll
37+
if (originScroll(deltaY)) return;
38+
2439
// Proxy of scroll events
2540
if (!isFF) {
2641
event.preventDefault();
2742
}
2843

29-
raf.cancel(nextFrameRef.current);
30-
31-
offsetRef.current += event.deltaY;
32-
wheelValueRef.current = event.deltaY;
33-
3444
nextFrameRef.current = raf(() => {
3545
// Patch a multiple for Firefox to fix wheel number too small
3646
// ref: https://github.com/ant-design/ant-design/issues/26372#issuecomment-679460266

src/hooks/useInRange.tsx

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/hooks/useMobileTouchMove.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const SMOOTH_PTG = 14 / 15;
66
export default function useMobileTouchMove(
77
inVirtual: boolean,
88
listRef: React.RefObject<HTMLDivElement>,
9-
callback: (offsetY: number) => void,
9+
callback: (offsetY: number, smoothOffset?: boolean) => boolean,
1010
) {
1111
const touchedRef = useRef(false);
1212
const touchYRef = useRef(0);
@@ -20,21 +20,20 @@ export default function useMobileTouchMove(
2020

2121
const onTouchMove = (e: TouchEvent) => {
2222
if (touchedRef.current) {
23-
e.preventDefault();
24-
2523
const currentY = Math.ceil(e.touches[0].pageY);
2624
let offsetY = touchYRef.current - currentY;
2725
touchYRef.current = currentY;
2826

29-
callback(offsetY);
27+
if (callback(offsetY)) {
28+
e.preventDefault();
29+
}
3030

3131
// Smooth interval
3232
clearInterval(intervalRef.current);
3333
intervalRef.current = setInterval(() => {
3434
offsetY *= SMOOTH_PTG;
35-
callback(offsetY);
3635

37-
if (Math.abs(offsetY) <= 0.1) {
36+
if (!callback(offsetY, true) || Math.abs(offsetY) <= 0.1) {
3837
clearInterval(intervalRef.current);
3938
}
4039
}, 16);

src/hooks/useOriginScroll.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useRef } from 'react';
2+
3+
export default (isScrollAtTop: boolean, isScrollAtBottom: boolean) => {
4+
// Do lock for a wheel when scrolling
5+
const lockRef = useRef(false);
6+
const lockTimeoutRef = useRef(null);
7+
function lockScroll() {
8+
clearTimeout(lockTimeoutRef.current);
9+
10+
lockRef.current = true;
11+
12+
lockTimeoutRef.current = setTimeout(() => {
13+
lockRef.current = false;
14+
}, 50);
15+
}
16+
17+
// Pass to ref since global add is in closure
18+
const scrollPingRef = useRef({
19+
top: isScrollAtTop,
20+
bottom: isScrollAtBottom,
21+
});
22+
scrollPingRef.current.top = isScrollAtTop;
23+
scrollPingRef.current.bottom = isScrollAtBottom;
24+
25+
return (deltaY: number, smoothOffset = false) => {
26+
const originScroll =
27+
// Pass origin wheel when on the top
28+
(deltaY < 0 && scrollPingRef.current.top) ||
29+
// Pass origin wheel when on the bottom
30+
(deltaY > 0 && scrollPingRef.current.bottom);
31+
32+
if (smoothOffset && originScroll) {
33+
// No need lock anymore when it's smooth offset from touchMove interval
34+
clearTimeout(lockTimeoutRef.current);
35+
lockRef.current = false;
36+
} else if (!originScroll || lockRef.current) {
37+
lockScroll();
38+
}
39+
40+
return !lockRef.current && originScroll;
41+
};
42+
};

tests/touch.test.js

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,33 +47,74 @@ describe('List.Touch', () => {
4747
return mount(node);
4848
}
4949

50-
it('touch content', () => {
51-
const listRef = React.createRef();
52-
const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100), ref: listRef });
50+
describe('touch content', () => {
51+
it('touch scroll should work', () => {
52+
const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) });
5353

54-
function getElement() {
55-
return wrapper.find('.rc-virtual-list-holder').instance();
56-
}
54+
function getElement() {
55+
return wrapper.find('.rc-virtual-list-holder').instance();
56+
}
5757

58-
// start
59-
const touchEvent = new Event('touchstart');
60-
touchEvent.touches = [{ pageY: 100 }];
61-
getElement().dispatchEvent(touchEvent);
58+
// start
59+
const touchEvent = new Event('touchstart');
60+
touchEvent.touches = [{ pageY: 100 }];
61+
getElement().dispatchEvent(touchEvent);
62+
63+
// move
64+
const moveEvent = new Event('touchmove');
65+
moveEvent.touches = [{ pageY: 90 }];
66+
getElement().dispatchEvent(moveEvent);
67+
68+
// end
69+
const endEvent = new Event('touchend');
70+
getElement().dispatchEvent(endEvent);
71+
72+
// smooth
73+
jest.runAllTimers();
74+
expect(wrapper.find('ul').instance().scrollTop > 10).toBeTruthy();
75+
76+
wrapper.unmount();
77+
});
6278

63-
// move
64-
const moveEvent = new Event('touchmove');
65-
moveEvent.touches = [{ pageY: 90 }];
66-
getElement().dispatchEvent(moveEvent);
79+
it('not call when not scroll-able', () => {
80+
const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) });
6781

68-
// end
69-
const endEvent = new Event('touchend');
70-
getElement().dispatchEvent(endEvent);
82+
function getElement() {
83+
return wrapper.find('.rc-virtual-list-holder').instance();
84+
}
7185

72-
// smooth
73-
jest.runAllTimers();
74-
expect(wrapper.find('ul').instance().scrollTop > 10).toBeTruthy();
86+
// start
87+
const touchEvent = new Event('touchstart');
88+
touchEvent.touches = [{ pageY: 500 }];
89+
getElement().dispatchEvent(touchEvent);
7590

76-
wrapper.unmount();
91+
// move
92+
const preventDefault = jest.fn();
93+
const moveEvent = new Event('touchmove');
94+
moveEvent.touches = [{ pageY: 0 }];
95+
moveEvent.preventDefault = preventDefault;
96+
getElement().dispatchEvent(moveEvent);
97+
98+
// Call preventDefault
99+
expect(preventDefault).toHaveBeenCalled();
100+
101+
// ======= Not call since scroll to the bottom =======
102+
jest.runAllTimers();
103+
preventDefault.mockReset();
104+
105+
// start
106+
const touchEvent2 = new Event('touchstart');
107+
touchEvent2.touches = [{ pageY: 500 }];
108+
getElement().dispatchEvent(touchEvent2);
109+
110+
// move
111+
const moveEvent2 = new Event('touchmove');
112+
moveEvent2.touches = [{ pageY: 0 }];
113+
moveEvent2.preventDefault = preventDefault;
114+
getElement().dispatchEvent(moveEvent2);
115+
116+
expect(preventDefault).not.toHaveBeenCalled();
117+
});
77118
});
78119

79120
it('should container preventDefault', () => {

0 commit comments

Comments
 (0)