Skip to content

Commit dbbffb3

Browse files
authored
feat: support touch horizontal scroll (#271)
* feat: support touch horizontal scroll * update
1 parent 47ff7bc commit dbbffb3

File tree

5 files changed

+97
-24
lines changed

5 files changed

+97
-24
lines changed

src/List.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,15 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
309309

310310
const isScrollAtTop = offsetTop <= 0;
311311
const isScrollAtBottom = offsetTop >= maxScrollHeight;
312+
const isScrollAtLeft = offsetLeft <= 0;
313+
const isScrollAtRight = offsetLeft >= scrollWidth;
312314

313-
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
315+
const originScroll = useOriginScroll(
316+
isScrollAtTop,
317+
isScrollAtBottom,
318+
isScrollAtLeft,
319+
isScrollAtRight,
320+
);
314321

315322
// ================================ Scroll ================================
316323
const getVirtualScrollInfo = () => ({
@@ -370,7 +377,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
370377
return tmpOffsetLeft;
371378
};
372379

373-
const onWheelDelta: Parameters<typeof useFrameWheel>[4] = useEvent((offsetXY, fromHorizontal) => {
380+
const onWheelDelta: Parameters<typeof useFrameWheel>[6] = useEvent((offsetXY, fromHorizontal) => {
374381
if (fromHorizontal) {
375382
// Horizontal scroll no need sync virtual position
376383

@@ -396,17 +403,23 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
396403
useVirtual,
397404
isScrollAtTop,
398405
isScrollAtBottom,
406+
isScrollAtLeft,
407+
isScrollAtRight,
399408
!!scrollWidth,
400409
onWheelDelta,
401410
);
402411

403412
// Mobile touch move
404-
useMobileTouchMove(useVirtual, componentRef, (deltaY, smoothOffset) => {
405-
if (originScroll(deltaY, smoothOffset)) {
413+
useMobileTouchMove(useVirtual, componentRef, (isHorizontal, delta, smoothOffset) => {
414+
if (originScroll(isHorizontal, delta, smoothOffset)) {
406415
return false;
407416
}
408417

409-
onRawWheel({ preventDefault() {}, deltaY } as WheelEvent);
418+
onRawWheel({
419+
preventDefault() {},
420+
deltaX: isHorizontal ? delta : 0,
421+
deltaY: isHorizontal ? 0 : delta,
422+
} as WheelEvent);
410423
return true;
411424
});
412425

src/hooks/useFrameWheel.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useRef } from 'react';
21
import raf from 'rc-util/lib/raf';
2+
import { useRef } from 'react';
33
import isFF from '../utils/isFirefox';
44
import useOriginScroll from './useOriginScroll';
55

@@ -12,6 +12,8 @@ export default function useFrameWheel(
1212
inVirtual: boolean,
1313
isScrollAtTop: boolean,
1414
isScrollAtBottom: boolean,
15+
isScrollAtLeft: boolean,
16+
isScrollAtRight: boolean,
1517
horizontalScroll: boolean,
1618
/***
1719
* Return `true` when you need to prevent default event
@@ -26,7 +28,12 @@ export default function useFrameWheel(
2628
const isMouseScrollRef = useRef<boolean>(false);
2729

2830
// Scroll status sync
29-
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
31+
const originScroll = useOriginScroll(
32+
isScrollAtTop,
33+
isScrollAtBottom,
34+
isScrollAtLeft,
35+
isScrollAtRight,
36+
);
3037

3138
function onWheelY(event: WheelEvent, deltaY: number) {
3239
raf.cancel(nextFrameRef.current);
@@ -35,7 +42,7 @@ export default function useFrameWheel(
3542
wheelValueRef.current = deltaY;
3643

3744
// Do nothing when scroll at the edge, Skip check when is in scroll
38-
if (originScroll(deltaY)) return;
45+
if (originScroll(false, deltaY)) return;
3946

4047
// Proxy of scroll events
4148
if (!isFF) {

src/hooks/useMobileTouchMove.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import * as React from 'react';
2-
import { useRef } from 'react';
31
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
2+
import type * as React from 'react';
3+
import { useRef } from 'react';
44

55
const SMOOTH_PTG = 14 / 15;
66

77
export default function useMobileTouchMove(
88
inVirtual: boolean,
99
listRef: React.RefObject<HTMLDivElement>,
10-
callback: (offsetY: number, smoothOffset?: boolean) => boolean,
10+
callback: (isHorizontal: boolean, offset: number, smoothOffset?: boolean) => boolean,
1111
) {
1212
const touchedRef = useRef(false);
13+
const touchXRef = useRef(0);
1314
const touchYRef = useRef(0);
1415

1516
const elementRef = useRef<HTMLElement>(null);
@@ -22,20 +23,30 @@ export default function useMobileTouchMove(
2223

2324
const onTouchMove = (e: TouchEvent) => {
2425
if (touchedRef.current) {
26+
const currentX = Math.ceil(e.touches[0].pageX);
2527
const currentY = Math.ceil(e.touches[0].pageY);
28+
let offsetX = touchXRef.current - currentX;
2629
let offsetY = touchYRef.current - currentY;
27-
touchYRef.current = currentY;
30+
const isHorizontal = Math.abs(offsetX) > Math.abs(offsetY);
31+
if (isHorizontal) {
32+
touchXRef.current = currentX;
33+
} else {
34+
touchYRef.current = currentY;
35+
}
2836

29-
if (callback(offsetY)) {
37+
if (callback(isHorizontal, isHorizontal ? offsetX : offsetY)) {
3038
e.preventDefault();
3139
}
32-
3340
// Smooth interval
3441
clearInterval(intervalRef.current);
3542
intervalRef.current = setInterval(() => {
36-
offsetY *= SMOOTH_PTG;
37-
38-
if (!callback(offsetY, true) || Math.abs(offsetY) <= 0.1) {
43+
if (isHorizontal) {
44+
offsetX *= SMOOTH_PTG;
45+
} else {
46+
offsetY *= SMOOTH_PTG;
47+
}
48+
const offset = isHorizontal ? offsetX : offsetY;
49+
if (!callback(isHorizontal, offset, true) || Math.abs(offset) <= 0.1) {
3950
clearInterval(intervalRef.current);
4051
}
4152
}, 16);
@@ -53,6 +64,7 @@ export default function useMobileTouchMove(
5364

5465
if (e.touches.length === 1 && !touchedRef.current) {
5566
touchedRef.current = true;
67+
touchXRef.current = Math.ceil(e.touches[0].pageX);
5668
touchYRef.current = Math.ceil(e.touches[0].pageY);
5769

5870
elementRef.current = e.target as HTMLElement;

src/hooks/useOriginScroll.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { useRef } from 'react';
22

3-
export default (isScrollAtTop: boolean, isScrollAtBottom: boolean) => {
3+
export default (
4+
isScrollAtTop: boolean,
5+
isScrollAtBottom: boolean,
6+
isScrollAtLeft: boolean,
7+
isScrollAtRight: boolean,
8+
) => {
49
// Do lock for a wheel when scrolling
510
const lockRef = useRef(false);
611
const lockTimeoutRef = useRef(null);
@@ -18,16 +23,23 @@ export default (isScrollAtTop: boolean, isScrollAtBottom: boolean) => {
1823
const scrollPingRef = useRef({
1924
top: isScrollAtTop,
2025
bottom: isScrollAtBottom,
26+
left: isScrollAtLeft,
27+
right: isScrollAtRight,
2128
});
2229
scrollPingRef.current.top = isScrollAtTop;
2330
scrollPingRef.current.bottom = isScrollAtBottom;
31+
scrollPingRef.current.left = isScrollAtLeft;
32+
scrollPingRef.current.right = isScrollAtRight;
2433

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);
34+
return (isHorizontal: boolean, delta: number, smoothOffset = false) => {
35+
const originScroll = isHorizontal
36+
? // Pass origin wheel when on the left
37+
(delta < 0 && scrollPingRef.current.left) ||
38+
// Pass origin wheel when on the right
39+
(delta > 0 && scrollPingRef.current.right) // Pass origin wheel when on the top
40+
: (delta < 0 && scrollPingRef.current.top) ||
41+
// Pass origin wheel when on the bottom
42+
(delta > 0 && scrollPingRef.current.bottom);
3143

3244
if (smoothOffset && originScroll) {
3345
// No need lock anymore when it's smooth offset from touchMove interval

tests/scrollWidth.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,4 +273,33 @@ describe('List.scrollWidth', () => {
273273
marginLeft: '-800px',
274274
});
275275
});
276+
277+
it('touch horizontal', async () => {
278+
const { container } = await genList({
279+
itemHeight: ITEM_HEIGHT,
280+
height: 100,
281+
data: genData(100),
282+
scrollWidth: 1000,
283+
});
284+
285+
fireEvent.touchStart(container.querySelector('.rc-virtual-list-holder')!, {
286+
touches: [{ pageX: 100, pageY: 0 }],
287+
});
288+
289+
fireEvent.touchMove(container.querySelector('.rc-virtual-list-holder')!, {
290+
touches: [{ pageX: 0, pageY: 0 }],
291+
});
292+
293+
fireEvent.touchEnd(container.querySelector('.rc-virtual-list-holder')!, {
294+
touches: [{ pageX: 0, pageY: 0 }],
295+
});
296+
297+
act(() => {
298+
jest.runAllTimers();
299+
});
300+
301+
expect(container.querySelector('.rc-virtual-list-holder-inner')).toHaveStyle({
302+
marginLeft: '-900px',
303+
});
304+
});
276305
});

0 commit comments

Comments
 (0)