Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: basic wheel support #211

Merged
merged 5 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/horizontal-scroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const Demo = () => {
boxSizing: 'border-box',
}}
onScroll={(e) => {
console.log('Scroll:', e);
// console.log('Scroll:', e);
}}
>
{(item) => <ForwardMyItem {...item} />}
Expand Down
111 changes: 79 additions & 32 deletions src/List.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as React from 'react';
import { useRef, useState } from 'react';
import classNames from 'classnames';
import type { ResizeObserverProps } from 'rc-resize-observer';
import ResizeObserver from 'rc-resize-observer';
import Filler from './Filler';
import type { InnerProps } from './Filler';
import type { ScrollBarDirectionType, ScrollBarRef } from './ScrollBar';
Expand All @@ -14,6 +16,8 @@ import useFrameWheel from './hooks/useFrameWheel';
import useMobileTouchMove from './hooks/useMobileTouchMove';
import useOriginScroll from './hooks/useOriginScroll';
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
import { getSpinSize } from './utils/scrollbarUtil';
import { useEvent } from 'rc-util';
Copy link
Member

@afc163 afc163 Aug 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"rc-util": "^5.15.0"

这个库依赖的 rc-util 的版本需要升级到 5.36.0 以上:react-component/util@25fd32b ,否则会引发 ant-design/ant-design#44271 报错。


const EMPTY_DATA = [];

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

// =============================== Item Key ===============================

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

// ================================= Size =================================
const [size, setSize] = React.useState({ width: 0, height });
const onHolderResize: ResizeObserverProps['onResize'] = (sizeInfo) => {
setSize(sizeInfo);
};

// Hack on scrollbar to enable flash call
const verticalScrollBarRef = useRef<ScrollBarRef>();
const horizontalScrollBarRef = useRef<ScrollBarRef>();

const horizontalScrollBarSpinSize = React.useMemo(
() => getSpinSize(size.width, scrollWidth),
[size.width, scrollWidth],
);
const verticalScrollBarSpinSize = React.useMemo(
() => getSpinSize(size.height, scrollHeight),
[size.height, scrollHeight],
);

// =============================== In Range ===============================
const maxScrollHeight = scrollHeight - height;
const maxScrollHeightRef = useRef(maxScrollHeight);
Expand Down Expand Up @@ -273,17 +295,33 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
onScroll?.(e);
}

const onWheelDelta = useEvent((offsetXY, fromHorizontal) => {
if (fromHorizontal) {
// Horizontal scroll no need sync virtual position
setOffsetLeft((left) => {
let newLeft = left + offsetXY;

const max = scrollWidth - size.width;
newLeft = Math.max(newLeft, 0);
newLeft = Math.min(newLeft, max);

return newLeft;
});
} else {
syncScrollTop((top) => {
const newTop = top + offsetXY;
return newTop;
});
}
});

// Since this added in global,should use ref to keep update
const [onRawWheel, onFireFoxScroll] = useFrameWheel(
useVirtual,
isScrollAtTop,
isScrollAtBottom,
(offsetY) => {
syncScrollTop((top) => {
const newTop = top + offsetY;
return newTop;
});
},
!!scrollWidth,
onWheelDelta,
);

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

// ================================= Ref ==================================
const delayHideScrollBar = () => {
verticalScrollBarRef.current?.delayHidden();
horizontalScrollBarRef.current?.delayHidden();
};

const scrollTo = useScrollTo<T>(
componentRef,
mergedData,
Expand All @@ -325,9 +368,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
getKey,
collectHeight,
syncScrollTop,
() => {
scrollBarRef.current?.delayHidden();
},
delayHideScrollBar,
);

React.useImperativeHandle(ref, () => ({
Expand Down Expand Up @@ -379,51 +420,57 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
{...containerProps}
{...restProps}
>
<Component
className={`${prefixCls}-holder`}
style={componentStyle}
ref={componentRef}
onScroll={onFallbackScroll}
>
<Filler
prefixCls={prefixCls}
height={scrollHeight}
offsetX={offsetLeft}
offsetY={offset}
scrollWidth={scrollWidth}
onInnerResize={collectHeight}
ref={fillerInnerRef}
innerProps={innerProps}
rtl={isRTL}
<ResizeObserver onResize={onHolderResize}>
<Component
className={`${prefixCls}-holder`}
style={componentStyle}
ref={componentRef}
onScroll={onFallbackScroll}
onMouseEnter={delayHideScrollBar}
>
{listChildren}
</Filler>
</Component>
<Filler
prefixCls={prefixCls}
height={scrollHeight}
offsetX={offsetLeft}
offsetY={offset}
scrollWidth={scrollWidth}
onInnerResize={collectHeight}
ref={fillerInnerRef}
innerProps={innerProps}
rtl={isRTL}
>
{listChildren}
</Filler>
</Component>
</ResizeObserver>

{useVirtual && scrollHeight > height && (
<ScrollBar
ref={scrollBarRef}
ref={verticalScrollBarRef}
prefixCls={prefixCls}
scrollOffset={offsetTop}
scrollRange={scrollHeight}
rtl={isRTL}
onScroll={onScrollBar}
onStartMove={onScrollbarStartMove}
onStopMove={onScrollbarStopMove}
height={height}
spinSize={verticalScrollBarSpinSize}
containerSize={size.height}
/>
)}

{useVirtual && scrollWidth && (
<ScrollBar
ref={scrollBarRef}
ref={horizontalScrollBarRef}
prefixCls={prefixCls}
scrollOffset={offsetLeft}
scrollRange={scrollWidth}
rtl={isRTL}
onScroll={onScrollBar}
onStartMove={onScrollbarStartMove}
onStopMove={onScrollbarStopMove}
spinSize={horizontalScrollBarSpinSize}
containerSize={size.width}
horizontal
/>
)}
Expand Down
63 changes: 23 additions & 40 deletions src/ScrollBar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import * as React from 'react';
import classNames from 'classnames';
import ResizeObserver, { type ResizeObserverProps } from 'rc-resize-observer';
import raf from 'rc-util/lib/raf';

const MIN_SIZE = 20;

export type ScrollBarDirectionType = 'ltr' | 'rtl';

export interface ScrollBarProps {
Expand All @@ -17,8 +14,8 @@ export interface ScrollBarProps {
onStopMove: () => void;
horizontal?: boolean;

// This can be remove when test move to @testing-lib
height?: number;
spinSize: number;
containerSize: number;
}

export interface ScrollBarRef {
Expand All @@ -43,7 +40,8 @@ const ScrollBar = React.forwardRef<ScrollBarRef, ScrollBarProps>((props, ref) =>
onStopMove,
onScroll,
horizontal,
height = 0,
spinSize,
containerSize,
} = props;

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

const isLTR = !rtl;

// ========================= Size =========================
const [containerSize, setContainerSize] = React.useState<number>(height);
const onResize: ResizeObserverProps['onResize'] = (size) => {
setContainerSize(horizontal ? size.width : size.height);
};

// ========================= Refs =========================
const scrollbarRef = React.useRef<HTMLDivElement>();
const thumbRef = React.useRef<HTMLDivElement>();
Expand All @@ -72,17 +64,9 @@ const ScrollBar = React.forwardRef<ScrollBarRef, ScrollBarProps>((props, ref) =>

visibleTimeoutRef.current = setTimeout(() => {
setVisible(false);
}, 2000);
}, 3000);
};

// ========================= Spin =========================
const spinSize = React.useMemo(() => {
let baseSize = (containerSize / scrollRange) * 100;
baseSize = Math.max(baseSize, MIN_SIZE);
baseSize = Math.min(baseSize, containerSize / 2);
return Math.floor(baseSize);
}, [containerSize, scrollRange]);

// ======================== Range =========================
const enableScrollRange = scrollRange - containerSize || 0;
const enableOffsetRange = containerSize - spinSize || 0;
Expand Down Expand Up @@ -256,27 +240,26 @@ const ScrollBar = React.forwardRef<ScrollBarRef, ScrollBarProps>((props, ref) =>
}

return (
<ResizeObserver onResize={onResize}>
<div
ref={scrollbarRef}
className={classNames(scrollbarPrefixCls, {
[`${scrollbarPrefixCls}-horizontal`]: horizontal,
[`${scrollbarPrefixCls}-vertical`]: !horizontal,
[`${scrollbarPrefixCls}-visible`]: visible,
})}
style={containerStyle}
onMouseDown={onContainerMouseDown}
onMouseMove={delayHidden}
>
<div
ref={scrollbarRef}
className={classNames(scrollbarPrefixCls, {
[`${scrollbarPrefixCls}-horizontal`]: horizontal,
[`${scrollbarPrefixCls}-vertical`]: !horizontal,
ref={thumbRef}
className={classNames(`${scrollbarPrefixCls}-thumb`, {
[`${scrollbarPrefixCls}-thumb-moving`]: dragging,
})}
style={containerStyle}
onMouseDown={onContainerMouseDown}
onMouseMove={delayHidden}
>
<div
ref={thumbRef}
className={classNames(`${scrollbarPrefixCls}-thumb`, {
[`${scrollbarPrefixCls}-thumb-moving`]: dragging,
})}
style={thumbStyle}
onMouseDown={onThumbMouseDown}
/>
</div>
</ResizeObserver>
style={thumbStyle}
onMouseDown={onThumbMouseDown}
/>
</div>
);
});

Expand Down
50 changes: 45 additions & 5 deletions src/hooks/useFrameWheel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import useOriginScroll from './useOriginScroll';

interface FireFoxDOMMouseScrollEvent {
detail: number;
preventDefault: Function;
preventDefault: VoidFunction;
}

export default function useFrameWheel(
inVirtual: boolean,
isScrollAtTop: boolean,
isScrollAtBottom: boolean,
onWheelDelta: (offset: number) => void,
horizontalScroll: boolean,
/***
* Return `true` when you need to prevent default event
*/
onWheelDelta: (offset: number, horizontal?: boolean) => void,
): [(e: WheelEvent) => void, (e: FireFoxDOMMouseScrollEvent) => void] {
const offsetRef = useRef(0);
const nextFrameRef = useRef<number>(null);
Expand All @@ -24,9 +28,7 @@ export default function useFrameWheel(
// Scroll status sync
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);

function onWheel(event: WheelEvent) {
if (!inVirtual) return;

function onWheelY(event: WheelEvent) {
raf.cancel(nextFrameRef.current);

const { deltaY } = event;
Expand All @@ -50,6 +52,44 @@ export default function useFrameWheel(
});
}

function onWheelX(event: WheelEvent) {
const { deltaX } = event;

onWheelDelta(deltaX, true);

if (!isFF) {
event.preventDefault();
}
}

// Check for which direction does wheel do
const wheelDirectionRef = useRef<'x' | 'y' | null>(null);
const wheelDirectionCleanRef = useRef<number>(null);

function onWheel(event: WheelEvent) {
if (!inVirtual) return;

// Wait for 2 frame to clean direction
raf.cancel(wheelDirectionCleanRef.current);
wheelDirectionCleanRef.current = raf(() => {
wheelDirectionRef.current = null;
}, 2);

const { deltaX, deltaY } = event;
const absX = Math.abs(deltaX);
const absY = Math.abs(deltaY);

if (wheelDirectionRef.current === null) {
wheelDirectionRef.current = horizontalScroll && absX > absY ? 'x' : 'y';
}

if (wheelDirectionRef.current === 'x') {
onWheelX(event);
} else {
onWheelY(event);
}
}

// A patch for firefox
function onFireFoxScroll(event: FireFoxDOMMouseScrollEvent) {
if (!inVirtual) return;
Expand Down
Loading
Loading