javascript react 手写前端浏览器滚动条,完美实现原生滚动条效果。
其次是滑块滑动的距离也是一个比值,计算的是滑块在可滑动轨道的一个滑动比值,滑动滑块的时候同时也需要滑动内容区,内容区的滑动距离也是一个比值。这就是一个最终的实现效果,滚动条支持点击滑动,左边内容区也能同步滑动,鼠标滚动同时也支持,手写的滚动条同步滚动。主要的难点就是计算滑块的一个高度,根据不同的内容大小同比例算出滑块在容器中的一个比例。类比就是 滑块的一个大小/滑块可以滑动的一个大小=容器的一个大
·
react手写滚动条效果
封装的一个完整的select组件实现手写滚动条效果不适用原生的scroll,使用div仿原生滚动条效果。
主要的难点就是计算滑块的一个高度,根据不同的内容大小同比例算出滑块在容器中的一个比例。
其次是滑块滑动的距离也是一个比值,计算的是滑块在可滑动轨道的一个滑动比值,滑动滑块的时候同时也需要滑动内容区,内容区的滑动距离也是一个比值。需要注意滑动滑块的那个距离是不能用作直接设置内容区的scrollTop一定也是一个比值。
类比就是 滑块的一个大小/滑块可以滑动的一个大小=容器的一个大小/容器可滑动的一个大小
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import './transition.css';
export type Option = {
label: string;
value: string | number | symbol;
disabled: boolean;
};
export type OptionType = Omit<Option, 'disabled'> &
Partial<Pick<Option, 'disabled'>>;
export type Iprops = {
style: Record<string, any>;
onChange: (value: OptionType) => void;
classnameSelect: string;
size?: string;
placeholder: string;
disabled: boolean;
option: OptionType[];
emptyImg: string;
};
const Select: React.FC<Partial<Iprops>> = (props: any) => {
const {
style,
onChange,
classnameSelect,
size,
placeholder,
disabled,
option,
emptyImg,
} = props;
const [focused, setFocused] = useState<boolean>(false);
const [chooseStyle, setChooseStyle] = useState<Record<string, any>>({
display: 'none',
});
const contentRef = useRef<HTMLDivElement>(null);
const selectRef = useRef<HTMLDivElement>(null);
const chooseRef = useRef<HTMLDivElement>(null);
const scrollbar = useRef<HTMLDivElement>(null); //获取可视区
const scrollInner = useRef<HTMLDivElement>(null);
const scrollContent = useRef<HTMLDivElement>(null);
const scrollContentNode = useRef<HTMLDivElement>(null);
const [scrollStyle, setScrollStyle] = useState({});
const [scrollY, setScrollY] = useState(0);
const [isMouseDown, setIsMouseDown] = useState(false); // 状态用于记录鼠标是否按下
const [scrollPosition, setScrollPosition] = useState(0); // 状态用于记录滚动位置
const [contentDistance, setContentDistance] = useState(0); // 状态用于记录滚动位置
const classnameContent = classNames('select-content', {
['ant-select-focused']: focused,
[classnameSelect]: classnameSelect,
[`ant-select-size-${size}`]: size,
[`ant-select-disabled`]: disabled,
});
const classnamee = classNames('choose-content', []);
const [value, setValue] = useState<string | number>();
useEffect(() => {
function handleClickOutside(event: Record<string, any>) {
if (
selectRef.current &&
!selectRef.current?.contains(event.target) &&
!chooseRef.current?.contains(event.target)
) {
setFocused(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [selectRef]);
const onResize = (e) => {
const { width } = selectRef.current?.getBoundingClientRect();
setChooseStyle({
...chooseStyle,
width: width + 'px',
height: 'auto',
marginTop: 5 + 'px',
});
};
useEffect(() => {
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
};
}, [onResize]);
useEffect(() => {
scrollContentNode.current?.classList.add('scrollHover');
if (focused) {
const divClient = scrollbar.current!.clientHeight;
//滑块的大小的
const distance =
(divClient * divClient) /
(contentRef.current!.clientHeight - divClient);
setScrollStyle({
height: `${Math.floor(
(scrollbar?.current!.clientHeight /
(contentRef?.current!.clientHeight -
scrollbar.current!.clientHeight +
8)) *
100,
)}%`,
transform: `translateY(${
(contentDistance / (contentRef.current!.clientHeight - divClient)) *
((divClient - distance) / distance) *
100
}%)`,
});
const flag =
contentRef.current!.clientHeight >
Number(scrollInner.current!.style.maxHeight?.split('px')[0]);
console.log(flag);
if (!flag) {
scrollContentNode.current?.classList.remove('scrollHover');
}
scrollContentNode.current!.style.opacity = flag ? '1' : '0'; //断言
}
}, [focused]);
const mousemoveFun = useCallback(
(e) => {
if (isMouseDown) {
// 根据滑动距离更新滚动位置
setScrollY(scrollY + e.clientY - scrollPosition);
const divClient = scrollbar.current!.clientHeight;
//滑块的大小的
const distance =
(divClient * divClient) /
(contentRef.current!.clientHeight - divClient);
const contentDistanceScroll =
((scrollY + e.clientY - scrollPosition) *
(contentRef?.current!.clientHeight - divClient)) /
(divClient - distance);
setContentDistance(contentDistanceScroll);
scrollInner.current!.scrollTop = contentDistanceScroll;
}
},
[isMouseDown],
);
//计算滑块滑动的距离
useEffect(() => {
if (scrollbar.current && contentRef.current) {
const divClient = scrollbar.current!.clientHeight;
//滑块的高度 页面滑动距离/(滑动容器距离-视口距离)=滑块滑动距离/(视口距离-滑块高度)
const distance =
(divClient * divClient) /
(contentRef.current!.clientHeight - divClient);
setScrollStyle({
...scrollStyle,
transform: `translateY(${
(contentDistance / (contentRef.current!.clientHeight - divClient)) *
((divClient - distance) / distance) *
100
}%)`,
});
if (!isMouseDown) {
document.removeEventListener('mousemove', mousemoveFun);
} else {
document.addEventListener('mousemove', mousemoveFun);
}
}
return () => {
if (document) {
document.removeEventListener('mousemove', mousemoveFun);
}
};
}, [isMouseDown, contentDistance]);
return (
<>
<div
ref={selectRef}
onClick={(e: any) => {
if (disabled) {
return;
}
setFocused(true);
const { width, height } = selectRef.current?.getBoundingClientRect();
setChooseStyle({
width: width + 'px',
height: 'auto',
marginTop: 5 + 'px',
padding: '4.0px 0',
});
}}
style={style}
className={classnameContent}
>
<div className="ant-select-selector">
<span className="ant-select-selection-search">
<input className="ant-select-selection-search-input" />
</span>
{value ? (
<span className="ant-select-selection-item">{value}</span>
) : (
<span className="ant-select-selection-placeholder">
{placeholder}
</span>
)}
</div>
<span className="ant-select-arrow">
<span>
{focused ? (
<UpOutlined
style={{ color: 'rgb(0, 0, 0, 0.2)', fontSize: '12.0px' }}
/>
) : (
<DownOutlined
style={{ color: 'rgb(0, 0, 0, 0.2)', fontSize: '12.0px' }}
/>
)}
</span>
</span>
<CSSTransition
in={focused}
timeout={200}
classNames={'my-node'}
//可选属性,当动画出场后,在页面上移除包裹的节点
//https://www.cnblogs.com/nimon-hugo/p/12781481.html
onEnter={(el: Record<string, any>) => {
//可选属性,动画进场后的回调,el指被包裹的dom
el.style.display = 'block';
}}
onExited={(el: Record<string, any>) => {
//出场后的回调,可以在吃操作setSate操作
el.style.display = 'none';
}}
>
<div
onClick={(e) => {
setFocused(true);
}}
ref={chooseRef}
style={chooseStyle}
className={classnamee}
>
<div ref={scrollbar} className="ant-scrollbar">
<div
onScroll={(e) => {
setContentDistance(e.currentTarget!.scrollTop);
}}
style={{
maxHeight: '255.0px',
overflow: 'scroll',
overflowAnchor: 'none',
marginRight: '-18.0px',
marginBottom: '-18.0px',
}}
className="rc-virtual-list-holder-inner"
ref={scrollInner}
>
<div ref={contentRef}>
{option.length !== 0 ? (
option?.map((item: OptionType) => (
<div
key={item.value}
title={item.label}
onClick={(e) => {
setValue(item.value);
setFocused(false);
onChange?.(item);
e.stopPropagation();
}}
className={classNames('ant-select-item', [
{
'ant-select-option-selected': item.value === value,
},
])}
>
<div className="ant-select-item-option-content">
{item.label}
</div>
</div>
))
) : (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<img src={emptyImg} />
</div>
)}
</div>
</div>
<div ref={scrollContentNode} className="ant-scroll">
<div
style={scrollStyle}
onMouseDown={(e) => {
setScrollPosition(e.clientY);
setIsMouseDown(true);
scrollContentNode.current!.style.opacity = '1'; //断言
document.addEventListener('mouseup', (ev) => {
setIsMouseDown(false);
scrollContentNode.current!.style.opacity = '0';
scrollContentNode.current?.classList.add('scrollHover');
document.removeEventListener('mousemove', mousemoveFun);
});
}}
className="ant-scroll-content"
ref={scrollContent}
></div>
</div>
</div>
</div>
</CSSTransition>
</div>
</>
);
};
export default Select;
Select.defaultProps = {
placeholder: '请选择',
option: [],
emptyImg: 'https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg',
};
.my-node-enter {
opacity: 0;
}
.my-node-enter-active,
.my-node-enter-done {
animation: antSlideUpIn 0.2s forwards ease-in-out;
}
.my-node-exit {
opacity: 1;
}
.my-node-exit-active,
.my-node-exit-done {
animation: antSlideUpOut 0.2s forwards ease-in-out;
}
@keyframes antSlideUpIn {
0% {
transform: scaleY(0.8);
transform-origin: 0% 0%;
opacity: 0;
}
to {
transform: scaleY(1);
transform-origin: 0% 0%;
opacity: 1;
}
}
@keyframes antSlideUpOut {
0% {
transform: scaleY(1);
transform-origin: 0% 0%;
opacity: 1;
}
to {
transform: scaleY(0.8);
transform-origin: 0% 0%;
opacity: 0;
}
}
这就是一个最终的实现效果,滚动条支持点击滑动,左边内容区也能同步滑动,鼠标滚动同时也支持,手写的滚动条同步滚动。
更多推荐
已为社区贡献2条内容
所有评论(0)