245 lines
7.1 KiB
JavaScript
245 lines
7.1 KiB
JavaScript
import { clsx } from 'clsx';
|
|
import raf from "@rc-component/util/es/raf";
|
|
import * as React from 'react';
|
|
import { getPageXY } from "./hooks/useScrollDrag";
|
|
const ScrollBar = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
const {
|
|
prefixCls,
|
|
rtl,
|
|
scrollOffset,
|
|
scrollRange,
|
|
onStartMove,
|
|
onStopMove,
|
|
onScroll,
|
|
horizontal,
|
|
spinSize,
|
|
containerSize,
|
|
style,
|
|
thumbStyle: propsThumbStyle,
|
|
showScrollBar
|
|
} = props;
|
|
const [dragging, setDragging] = React.useState(false);
|
|
const [pageXY, setPageXY] = React.useState(null);
|
|
const [startTop, setStartTop] = React.useState(null);
|
|
const isLTR = !rtl;
|
|
|
|
// ========================= Refs =========================
|
|
const scrollbarRef = React.useRef();
|
|
const thumbRef = React.useRef();
|
|
|
|
// ======================= Visible ========================
|
|
const [visible, setVisible] = React.useState(showScrollBar);
|
|
const visibleTimeoutRef = React.useRef();
|
|
const delayHidden = () => {
|
|
if (showScrollBar === true || showScrollBar === false) return;
|
|
clearTimeout(visibleTimeoutRef.current);
|
|
setVisible(true);
|
|
visibleTimeoutRef.current = setTimeout(() => {
|
|
setVisible(false);
|
|
}, 3000);
|
|
};
|
|
|
|
// ======================== Range =========================
|
|
const enableScrollRange = scrollRange - containerSize || 0;
|
|
const enableOffsetRange = containerSize - spinSize || 0;
|
|
|
|
// ========================= Top ==========================
|
|
const top = React.useMemo(() => {
|
|
if (scrollOffset === 0 || enableScrollRange === 0) {
|
|
return 0;
|
|
}
|
|
const ptg = scrollOffset / enableScrollRange;
|
|
return ptg * enableOffsetRange;
|
|
}, [scrollOffset, enableScrollRange, enableOffsetRange]);
|
|
|
|
// ====================== Container =======================
|
|
const onContainerMouseDown = e => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
};
|
|
|
|
// ======================== Thumb =========================
|
|
const stateRef = React.useRef({
|
|
top,
|
|
dragging,
|
|
pageY: pageXY,
|
|
startTop
|
|
});
|
|
stateRef.current = {
|
|
top,
|
|
dragging,
|
|
pageY: pageXY,
|
|
startTop
|
|
};
|
|
const onThumbMouseDown = e => {
|
|
setDragging(true);
|
|
setPageXY(getPageXY(e, horizontal));
|
|
setStartTop(stateRef.current.top);
|
|
onStartMove();
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
};
|
|
|
|
// ======================== Effect ========================
|
|
|
|
// React make event as passive, but we need to preventDefault
|
|
// Add event on dom directly instead.
|
|
// ref: https://github.com/facebook/react/issues/9809
|
|
React.useEffect(() => {
|
|
const onScrollbarTouchStart = e => {
|
|
e.preventDefault();
|
|
};
|
|
const scrollbarEle = scrollbarRef.current;
|
|
const thumbEle = thumbRef.current;
|
|
scrollbarEle.addEventListener('touchstart', onScrollbarTouchStart, {
|
|
passive: false
|
|
});
|
|
thumbEle.addEventListener('touchstart', onThumbMouseDown, {
|
|
passive: false
|
|
});
|
|
return () => {
|
|
scrollbarEle.removeEventListener('touchstart', onScrollbarTouchStart);
|
|
thumbEle.removeEventListener('touchstart', onThumbMouseDown);
|
|
};
|
|
}, []);
|
|
|
|
// Pass to effect
|
|
const enableScrollRangeRef = React.useRef();
|
|
enableScrollRangeRef.current = enableScrollRange;
|
|
const enableOffsetRangeRef = React.useRef();
|
|
enableOffsetRangeRef.current = enableOffsetRange;
|
|
React.useEffect(() => {
|
|
if (dragging) {
|
|
let moveRafId;
|
|
const onMouseMove = e => {
|
|
const {
|
|
dragging: stateDragging,
|
|
pageY: statePageY,
|
|
startTop: stateStartTop
|
|
} = stateRef.current;
|
|
raf.cancel(moveRafId);
|
|
const rect = scrollbarRef.current.getBoundingClientRect();
|
|
const scale = containerSize / (horizontal ? rect.width : rect.height);
|
|
if (stateDragging) {
|
|
const offset = (getPageXY(e, horizontal) - statePageY) * scale;
|
|
let newTop = stateStartTop;
|
|
if (!isLTR && horizontal) {
|
|
newTop -= offset;
|
|
} else {
|
|
newTop += offset;
|
|
}
|
|
const tmpEnableScrollRange = enableScrollRangeRef.current;
|
|
const tmpEnableOffsetRange = enableOffsetRangeRef.current;
|
|
const ptg = tmpEnableOffsetRange ? newTop / tmpEnableOffsetRange : 0;
|
|
let newScrollTop = Math.ceil(ptg * tmpEnableScrollRange);
|
|
newScrollTop = Math.max(newScrollTop, 0);
|
|
newScrollTop = Math.min(newScrollTop, tmpEnableScrollRange);
|
|
moveRafId = raf(() => {
|
|
onScroll(newScrollTop, horizontal);
|
|
});
|
|
}
|
|
};
|
|
const onMouseUp = () => {
|
|
setDragging(false);
|
|
onStopMove();
|
|
};
|
|
window.addEventListener('mousemove', onMouseMove, {
|
|
passive: true
|
|
});
|
|
window.addEventListener('touchmove', onMouseMove, {
|
|
passive: true
|
|
});
|
|
window.addEventListener('mouseup', onMouseUp, {
|
|
passive: true
|
|
});
|
|
window.addEventListener('touchend', onMouseUp, {
|
|
passive: true
|
|
});
|
|
return () => {
|
|
window.removeEventListener('mousemove', onMouseMove);
|
|
window.removeEventListener('touchmove', onMouseMove);
|
|
window.removeEventListener('mouseup', onMouseUp);
|
|
window.removeEventListener('touchend', onMouseUp);
|
|
raf.cancel(moveRafId);
|
|
};
|
|
}
|
|
}, [dragging]);
|
|
React.useEffect(() => {
|
|
delayHidden();
|
|
return () => {
|
|
clearTimeout(visibleTimeoutRef.current);
|
|
};
|
|
}, [scrollOffset]);
|
|
|
|
// ====================== Imperative ======================
|
|
React.useImperativeHandle(ref, () => ({
|
|
delayHidden
|
|
}));
|
|
|
|
// ======================== Render ========================
|
|
const scrollbarPrefixCls = `${prefixCls}-scrollbar`;
|
|
const containerStyle = {
|
|
position: 'absolute',
|
|
visibility: visible ? null : 'hidden'
|
|
};
|
|
const thumbStyle = {
|
|
position: 'absolute',
|
|
borderRadius: 99,
|
|
background: 'var(--rc-virtual-list-scrollbar-bg, rgba(0, 0, 0, 0.5))',
|
|
cursor: 'pointer',
|
|
userSelect: 'none'
|
|
};
|
|
if (horizontal) {
|
|
Object.assign(containerStyle, {
|
|
height: 8,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0
|
|
});
|
|
Object.assign(thumbStyle, {
|
|
height: '100%',
|
|
width: spinSize,
|
|
[isLTR ? 'left' : 'right']: top
|
|
});
|
|
} else {
|
|
Object.assign(containerStyle, {
|
|
width: 8,
|
|
top: 0,
|
|
bottom: 0,
|
|
[isLTR ? 'right' : 'left']: 0
|
|
});
|
|
Object.assign(thumbStyle, {
|
|
width: '100%',
|
|
height: spinSize,
|
|
top
|
|
});
|
|
}
|
|
return /*#__PURE__*/React.createElement("div", {
|
|
ref: scrollbarRef,
|
|
className: clsx(scrollbarPrefixCls, {
|
|
[`${scrollbarPrefixCls}-horizontal`]: horizontal,
|
|
[`${scrollbarPrefixCls}-vertical`]: !horizontal,
|
|
[`${scrollbarPrefixCls}-visible`]: visible
|
|
}),
|
|
style: {
|
|
...containerStyle,
|
|
...style
|
|
},
|
|
onMouseDown: onContainerMouseDown,
|
|
onMouseMove: delayHidden
|
|
}, /*#__PURE__*/React.createElement("div", {
|
|
ref: thumbRef,
|
|
className: clsx(`${scrollbarPrefixCls}-thumb`, {
|
|
[`${scrollbarPrefixCls}-thumb-moving`]: dragging
|
|
}),
|
|
style: {
|
|
...thumbStyle,
|
|
...propsThumbStyle
|
|
},
|
|
onMouseDown: onThumbMouseDown
|
|
}));
|
|
});
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
ScrollBar.displayName = 'ScrollBar';
|
|
}
|
|
export default ScrollBar; |