Skip to content

Commit 1e88230

Browse files
authored
chore: basic wheel support (#211)
* chore: use wheel of it * chore: simple wheel * chore: unlock wheel * test: wheel test * chore: add cls & hover show
1 parent 05ea3cb commit 1e88230

File tree

7 files changed

+190
-81
lines changed

7 files changed

+190
-81
lines changed

examples/horizontal-scroll.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const Demo = () => {
6464
boxSizing: 'border-box',
6565
}}
6666
onScroll={(e) => {
67-
console.log('Scroll:', e);
67+
// console.log('Scroll:', e);
6868
}}
6969
>
7070
{(item) => <ForwardMyItem {...item} />}

src/List.tsx

Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
22
import { useRef, useState } from 'react';
33
import classNames from 'classnames';
4+
import type { ResizeObserverProps } from 'rc-resize-observer';
5+
import ResizeObserver from 'rc-resize-observer';
46
import Filler from './Filler';
57
import type { InnerProps } from './Filler';
68
import type { ScrollBarDirectionType, ScrollBarRef } from './ScrollBar';
@@ -14,6 +16,8 @@ import useFrameWheel from './hooks/useFrameWheel';
1416
import useMobileTouchMove from './hooks/useMobileTouchMove';
1517
import useOriginScroll from './hooks/useOriginScroll';
1618
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
19+
import { getSpinSize } from './utils/scrollbarUtil';
20+
import { useEvent } from 'rc-util';
1721

1822
const EMPTY_DATA = [];
1923

@@ -97,7 +101,6 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
97101
const mergedData = data || EMPTY_DATA;
98102
const componentRef = useRef<HTMLDivElement>();
99103
const fillerInnerRef = useRef<HTMLDivElement>();
100-
const scrollBarRef = useRef<ScrollBarRef>(); // Hack on scrollbar to enable flash call
101104

102105
// =============================== Item Key ===============================
103106

@@ -232,6 +235,25 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
232235
rangeRef.current.start = start;
233236
rangeRef.current.end = end;
234237

238+
// ================================= Size =================================
239+
const [size, setSize] = React.useState({ width: 0, height });
240+
const onHolderResize: ResizeObserverProps['onResize'] = (sizeInfo) => {
241+
setSize(sizeInfo);
242+
};
243+
244+
// Hack on scrollbar to enable flash call
245+
const verticalScrollBarRef = useRef<ScrollBarRef>();
246+
const horizontalScrollBarRef = useRef<ScrollBarRef>();
247+
248+
const horizontalScrollBarSpinSize = React.useMemo(
249+
() => getSpinSize(size.width, scrollWidth),
250+
[size.width, scrollWidth],
251+
);
252+
const verticalScrollBarSpinSize = React.useMemo(
253+
() => getSpinSize(size.height, scrollHeight),
254+
[size.height, scrollHeight],
255+
);
256+
235257
// =============================== In Range ===============================
236258
const maxScrollHeight = scrollHeight - height;
237259
const maxScrollHeightRef = useRef(maxScrollHeight);
@@ -273,17 +295,33 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
273295
onScroll?.(e);
274296
}
275297

298+
const onWheelDelta = useEvent((offsetXY, fromHorizontal) => {
299+
if (fromHorizontal) {
300+
// Horizontal scroll no need sync virtual position
301+
setOffsetLeft((left) => {
302+
let newLeft = left + offsetXY;
303+
304+
const max = scrollWidth - size.width;
305+
newLeft = Math.max(newLeft, 0);
306+
newLeft = Math.min(newLeft, max);
307+
308+
return newLeft;
309+
});
310+
} else {
311+
syncScrollTop((top) => {
312+
const newTop = top + offsetXY;
313+
return newTop;
314+
});
315+
}
316+
});
317+
276318
// Since this added in global,should use ref to keep update
277319
const [onRawWheel, onFireFoxScroll] = useFrameWheel(
278320
useVirtual,
279321
isScrollAtTop,
280322
isScrollAtBottom,
281-
(offsetY) => {
282-
syncScrollTop((top) => {
283-
const newTop = top + offsetY;
284-
return newTop;
285-
});
286-
},
323+
!!scrollWidth,
324+
onWheelDelta,
287325
);
288326

289327
// Mobile touch move
@@ -317,6 +355,11 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
317355
}, [useVirtual]);
318356

319357
// ================================= Ref ==================================
358+
const delayHideScrollBar = () => {
359+
verticalScrollBarRef.current?.delayHidden();
360+
horizontalScrollBarRef.current?.delayHidden();
361+
};
362+
320363
const scrollTo = useScrollTo<T>(
321364
componentRef,
322365
mergedData,
@@ -325,9 +368,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
325368
getKey,
326369
collectHeight,
327370
syncScrollTop,
328-
() => {
329-
scrollBarRef.current?.delayHidden();
330-
},
371+
delayHideScrollBar,
331372
);
332373

333374
React.useImperativeHandle(ref, () => ({
@@ -379,51 +420,57 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
379420
{...containerProps}
380421
{...restProps}
381422
>
382-
<Component
383-
className={`${prefixCls}-holder`}
384-
style={componentStyle}
385-
ref={componentRef}
386-
onScroll={onFallbackScroll}
387-
>
388-
<Filler
389-
prefixCls={prefixCls}
390-
height={scrollHeight}
391-
offsetX={offsetLeft}
392-
offsetY={offset}
393-
scrollWidth={scrollWidth}
394-
onInnerResize={collectHeight}
395-
ref={fillerInnerRef}
396-
innerProps={innerProps}
397-
rtl={isRTL}
423+
<ResizeObserver onResize={onHolderResize}>
424+
<Component
425+
className={`${prefixCls}-holder`}
426+
style={componentStyle}
427+
ref={componentRef}
428+
onScroll={onFallbackScroll}
429+
onMouseEnter={delayHideScrollBar}
398430
>
399-
{listChildren}
400-
</Filler>
401-
</Component>
431+
<Filler
432+
prefixCls={prefixCls}
433+
height={scrollHeight}
434+
offsetX={offsetLeft}
435+
offsetY={offset}
436+
scrollWidth={scrollWidth}
437+
onInnerResize={collectHeight}
438+
ref={fillerInnerRef}
439+
innerProps={innerProps}
440+
rtl={isRTL}
441+
>
442+
{listChildren}
443+
</Filler>
444+
</Component>
445+
</ResizeObserver>
402446

403447
{useVirtual && scrollHeight > height && (
404448
<ScrollBar
405-
ref={scrollBarRef}
449+
ref={verticalScrollBarRef}
406450
prefixCls={prefixCls}
407451
scrollOffset={offsetTop}
408452
scrollRange={scrollHeight}
409453
rtl={isRTL}
410454
onScroll={onScrollBar}
411455
onStartMove={onScrollbarStartMove}
412456
onStopMove={onScrollbarStopMove}
413-
height={height}
457+
spinSize={verticalScrollBarSpinSize}
458+
containerSize={size.height}
414459
/>
415460
)}
416461

417462
{useVirtual && scrollWidth && (
418463
<ScrollBar
419-
ref={scrollBarRef}
464+
ref={horizontalScrollBarRef}
420465
prefixCls={prefixCls}
421466
scrollOffset={offsetLeft}
422467
scrollRange={scrollWidth}
423468
rtl={isRTL}
424469
onScroll={onScrollBar}
425470
onStartMove={onScrollbarStartMove}
426471
onStopMove={onScrollbarStopMove}
472+
spinSize={horizontalScrollBarSpinSize}
473+
containerSize={size.width}
427474
horizontal
428475
/>
429476
)}

src/ScrollBar.tsx

Lines changed: 23 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import * as React from 'react';
22
import classNames from 'classnames';
3-
import ResizeObserver, { type ResizeObserverProps } from 'rc-resize-observer';
43
import raf from 'rc-util/lib/raf';
54

6-
const MIN_SIZE = 20;
7-
85
export type ScrollBarDirectionType = 'ltr' | 'rtl';
96

107
export interface ScrollBarProps {
@@ -17,8 +14,8 @@ export interface ScrollBarProps {
1714
onStopMove: () => void;
1815
horizontal?: boolean;
1916

20-
// This can be remove when test move to @testing-lib
21-
height?: number;
17+
spinSize: number;
18+
containerSize: number;
2219
}
2320

2421
export interface ScrollBarRef {
@@ -43,7 +40,8 @@ const ScrollBar = React.forwardRef<ScrollBarRef, ScrollBarProps>((props, ref) =>
4340
onStopMove,
4441
onScroll,
4542
horizontal,
46-
height = 0,
43+
spinSize,
44+
containerSize,
4745
} = props;
4846

4947
const [dragging, setDragging] = React.useState(false);
@@ -52,12 +50,6 @@ const ScrollBar = React.forwardRef<ScrollBarRef, ScrollBarProps>((props, ref) =>
5250

5351
const isLTR = !rtl;
5452

55-
// ========================= Size =========================
56-
const [containerSize, setContainerSize] = React.useState<number>(height);
57-
const onResize: ResizeObserverProps['onResize'] = (size) => {
58-
setContainerSize(horizontal ? size.width : size.height);
59-
};
60-
6153
// ========================= Refs =========================
6254
const scrollbarRef = React.useRef<HTMLDivElement>();
6355
const thumbRef = React.useRef<HTMLDivElement>();
@@ -72,17 +64,9 @@ const ScrollBar = React.forwardRef<ScrollBarRef, ScrollBarProps>((props, ref) =>
7264

7365
visibleTimeoutRef.current = setTimeout(() => {
7466
setVisible(false);
75-
}, 2000);
67+
}, 3000);
7668
};
7769

78-
// ========================= Spin =========================
79-
const spinSize = React.useMemo(() => {
80-
let baseSize = (containerSize / scrollRange) * 100;
81-
baseSize = Math.max(baseSize, MIN_SIZE);
82-
baseSize = Math.min(baseSize, containerSize / 2);
83-
return Math.floor(baseSize);
84-
}, [containerSize, scrollRange]);
85-
8670
// ======================== Range =========================
8771
const enableScrollRange = scrollRange - containerSize || 0;
8872
const enableOffsetRange = containerSize - spinSize || 0;
@@ -256,27 +240,26 @@ const ScrollBar = React.forwardRef<ScrollBarRef, ScrollBarProps>((props, ref) =>
256240
}
257241

258242
return (
259-
<ResizeObserver onResize={onResize}>
243+
<div
244+
ref={scrollbarRef}
245+
className={classNames(scrollbarPrefixCls, {
246+
[`${scrollbarPrefixCls}-horizontal`]: horizontal,
247+
[`${scrollbarPrefixCls}-vertical`]: !horizontal,
248+
[`${scrollbarPrefixCls}-visible`]: visible,
249+
})}
250+
style={containerStyle}
251+
onMouseDown={onContainerMouseDown}
252+
onMouseMove={delayHidden}
253+
>
260254
<div
261-
ref={scrollbarRef}
262-
className={classNames(scrollbarPrefixCls, {
263-
[`${scrollbarPrefixCls}-horizontal`]: horizontal,
264-
[`${scrollbarPrefixCls}-vertical`]: !horizontal,
255+
ref={thumbRef}
256+
className={classNames(`${scrollbarPrefixCls}-thumb`, {
257+
[`${scrollbarPrefixCls}-thumb-moving`]: dragging,
265258
})}
266-
style={containerStyle}
267-
onMouseDown={onContainerMouseDown}
268-
onMouseMove={delayHidden}
269-
>
270-
<div
271-
ref={thumbRef}
272-
className={classNames(`${scrollbarPrefixCls}-thumb`, {
273-
[`${scrollbarPrefixCls}-thumb-moving`]: dragging,
274-
})}
275-
style={thumbStyle}
276-
onMouseDown={onThumbMouseDown}
277-
/>
278-
</div>
279-
</ResizeObserver>
259+
style={thumbStyle}
260+
onMouseDown={onThumbMouseDown}
261+
/>
262+
</div>
280263
);
281264
});
282265

src/hooks/useFrameWheel.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ import useOriginScroll from './useOriginScroll';
55

66
interface FireFoxDOMMouseScrollEvent {
77
detail: number;
8-
preventDefault: Function;
8+
preventDefault: VoidFunction;
99
}
1010

1111
export default function useFrameWheel(
1212
inVirtual: boolean,
1313
isScrollAtTop: boolean,
1414
isScrollAtBottom: boolean,
15-
onWheelDelta: (offset: number) => void,
15+
horizontalScroll: boolean,
16+
/***
17+
* Return `true` when you need to prevent default event
18+
*/
19+
onWheelDelta: (offset: number, horizontal?: boolean) => void,
1620
): [(e: WheelEvent) => void, (e: FireFoxDOMMouseScrollEvent) => void] {
1721
const offsetRef = useRef(0);
1822
const nextFrameRef = useRef<number>(null);
@@ -24,9 +28,7 @@ export default function useFrameWheel(
2428
// Scroll status sync
2529
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
2630

27-
function onWheel(event: WheelEvent) {
28-
if (!inVirtual) return;
29-
31+
function onWheelY(event: WheelEvent) {
3032
raf.cancel(nextFrameRef.current);
3133

3234
const { deltaY } = event;
@@ -50,6 +52,44 @@ export default function useFrameWheel(
5052
});
5153
}
5254

55+
function onWheelX(event: WheelEvent) {
56+
const { deltaX } = event;
57+
58+
onWheelDelta(deltaX, true);
59+
60+
if (!isFF) {
61+
event.preventDefault();
62+
}
63+
}
64+
65+
// Check for which direction does wheel do
66+
const wheelDirectionRef = useRef<'x' | 'y' | null>(null);
67+
const wheelDirectionCleanRef = useRef<number>(null);
68+
69+
function onWheel(event: WheelEvent) {
70+
if (!inVirtual) return;
71+
72+
// Wait for 2 frame to clean direction
73+
raf.cancel(wheelDirectionCleanRef.current);
74+
wheelDirectionCleanRef.current = raf(() => {
75+
wheelDirectionRef.current = null;
76+
}, 2);
77+
78+
const { deltaX, deltaY } = event;
79+
const absX = Math.abs(deltaX);
80+
const absY = Math.abs(deltaY);
81+
82+
if (wheelDirectionRef.current === null) {
83+
wheelDirectionRef.current = horizontalScroll && absX > absY ? 'x' : 'y';
84+
}
85+
86+
if (wheelDirectionRef.current === 'x') {
87+
onWheelX(event);
88+
} else {
89+
onWheelY(event);
90+
}
91+
}
92+
5393
// A patch for firefox
5494
function onFireFoxScroll(event: FireFoxDOMMouseScrollEvent) {
5595
if (!inVirtual) return;

0 commit comments

Comments
 (0)