代码语言:javascript复制
目录
1. 组件展示
2. 关键技术
3. 关键实现
4. 组件接口
1. 组件展示
组件特性:
- 滑动箭头,只有当待滑动内容无法完整显示时才出现。
- 滑动过程使用动画体现。
- 滑动到左边界时,左滑动箭头给出不可滑动标识。
- 滑动到右边界时,右滑动箭头给出不可滑动标识。
- 浏览器缩放时,也能满足上述条件。
2. 关键技术
- 如何实现竖直居中?
- absolute top:50% transform(-50%, -50%)
- 如何避免用户点击滑动箭头时,意外选中文本?
- css3 -> user-select:none
- 如何实现 slider 元素横向布局?
- css -> display:inline-block whitespace:no-wrap
- 如何实现滑动动画?
- css3 -> transition:transform translate3d
- 如何监听 slider 容器尺寸变更?
- resize-observer-polyfill
- 如何实现防抖?
- loadsh -> debounce
- 如何操作 DOM?
- React -> Refs
- 如何指示用户按钮不可点击?
- css -> cursor: not-allowed;
- 如何度量组件尺寸?
- domElement.offsetWidth
- 如何包装开发自定义HTML结构?
- React -> React.Chidren.map
- 这里注意空元素问题
- React -> React.Chidren.map
- 滑动基本原理
3. 关键实现
3.1. Slider.tsx
代码语言:javascript复制import React from "react"
import classnames from "classnames"
import {LeftOutlined, RightOutlined} from "@ant-design/icons"
import ResizeObserver from 'resize-observer-polyfill';
import debounce from 'lodash/debounce';
import {isTransform3dSupported} from "./util"
export interface SliderProps {
className?: string,
style?: React.CSSProperties,
selectedIndex?: number,
onClick?: (index: number) => void,
}
export interface SliderState {
showBtnPrevNext: boolean,
btnNextDisabled: boolean,
btnPrevDisabled: boolean
}
export default class Slider extends React.Component<SliderProps, SliderState> {
private wrapperRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
private scrollerRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
private containerRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
private offset: number = 0;
private debouncedResize: any;
private resizeObserver: any;
constructor(props) {
super(props);
this.state = {
showBtnPrevNext: false,
btnNextDisabled: false,
btnPrevDisabled: false,
};
}
componentDidMount(): void {
this.debouncedResize = debounce(() => {
this.updateScrollerPosition(this.offset);
}, 200);
this.resizeObserver = new ResizeObserver(this.debouncedResize);
this.resizeObserver.observe(this.wrapperRef.current);
}
componentWillUnmount(): void {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.debouncedResize && this.debouncedResize.cancel) {
this.debouncedResize.cancel();
}
}
private needShowPrevOrNext() {
return !(this.wrapperRef.current.offsetWidth > this.scrollerRef.current.offsetWidth);
}
private updateScrollerPosition(offset) {
const maxScrollerXOffset = 0;
const minScrollerXOffset = this.wrapperRef.current.offsetWidth - this.scrollerRef.current.offsetWidth;
let target = -1;
if (!this.needShowPrevOrNext()) {
target = 0;
} else {
target = Math.max(Math.min(maxScrollerXOffset, offset), minScrollerXOffset);
}
this.offset = target;
const scrollerStyle = this.scrollerRef.current.style;
const transformSupported = isTransform3dSupported(scrollerStyle);
if (transformSupported) {
scrollerStyle.transform = `translate3d(${target}px,0,0)`;
} else {
scrollerStyle.left = `${target}px`;
}
if (this.needShowPrevOrNext()) {
this.setState({
showBtnPrevNext: true,
btnPrevDisabled: this.offset == 0,
btnNextDisabled: !(this.offset > minScrollerXOffset)
});
} else {
this.setState({
showBtnPrevNext: false,
btnPrevDisabled: true,
btnNextDisabled: true,
});
}
}
handleClick = (index: number) => {
const {onClick} = this.props;
if (onClick) {
onClick(index);
}
};
handlePrev = () => {
const containerNode = this.wrapperRef.current;
const {offset} = this;
this.updateScrollerPosition(offset containerNode.offsetWidth);
};
handleNext = () => {
const containerNode = this.wrapperRef.current;
const {offset} = this;
this.updateScrollerPosition(offset - containerNode.offsetWidth);
};
render(): React.ReactElement<any, string | React.JSXElementConstructor<any>> | string | number | {} | React.ReactNodeArray | React.ReactPortal | boolean | null | undefined {
const {
className,
style,
selectedIndex,
children
} = this.props;
const {
showBtnPrevNext,
btnNextDisabled,
btnPrevDisabled
} = this.state;
return (
<div className={classnames("mousex-slider", className)} style={style}>
<span
className={classnames("mousex-slider-btn-prev", {
"mousex-slider-btn-show": showBtnPrevNext,
"mousex-slider-btn-disabled": btnPrevDisabled
})}
onClick={this.handlePrev}>
<LeftOutlined className={"mousex-slider-btn-prev-icon"}/>
</span>
<div className={"mousex-slider-items-wrapper"} ref={this.wrapperRef}>
<div className={classnames("mousex-slider-items-scroller", "animated")} ref={this.scrollerRef}>
<div className={"mousex-slider-items-container"} ref={this.containerRef}>
{
React.Children.map(children, (child, index) => {
if(!child){
return null;
}
return (
<div onClick={() => this.handleClick(index)}
className={index === selectedIndex ? "mousex-slider-item selected" : "mousex-slider-item"}>
{child}
</div>
)
})
}
</div>
</div>
</div>
<span
className={classnames("mousex-slider-btn-next", {
"mousex-slider-btn-show": showBtnPrevNext,
"mousex-slider-btn-disabled": btnNextDisabled
})}
onClick={this.handleNext}>
<RightOutlined className={"mousex-slider-btn-next-icon"}/>
</span>
</div>
);
}
}
3.2. slider.less
代码语言:javascript复制.mousex-slider {
position: relative;
padding: 0 32px;
}
.mousex-slider-btn-prev,
.mousex-slider-btn-next {
position: absolute;
top: 0;
width: 32px;
height: 100%;
color: rgb(213, 219, 230);
cursor: pointer;
user-select: none;
}
.mousex-slider-btn-prev,
.mousex-slider-btn-next {
display: none;
}
.mousex-slider-btn-prev {
left: 0;
}
.mousex-slider-btn-next {
right: 0;
}
.mousex-slider-btn-show {
display: inline;
}
.mousex-slider-btn-disabled {
cursor: not-allowed;
}
.mousex-slider-btn-prev-icon,
.mousex-slider-btn-next-icon {
position: absolute;
top: 50%;
left: 50%;
font-size: 32px;
font-weight: 700;
transform: translate(-50%, -50%);
}
.mousex-slider-items-wrapper {
position: relative;
margin: 0 16px;
white-space: nowrap;
overflow: hidden;
}
.mousex-slider-items-scroller {
display: inline-block;
}
.mousex-slider-items-scroller.animated {
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.mousex-slider-item {
display: inline-block;
width: 166px;
height: 99px;
margin: 3px 35px 3px 3px;
border-radius: 2px;
cursor: pointer;
}
.mousex-slider-item:last-of-type {
margin-right: 0;
}
.mousex-slider-item.selected,
.mousex-slider-item.selected:hover {
box-shadow: 0 0 3px 3px #c0ddff;
}
.mousex-slider-item:hover {
box-shadow: 0 0 2px 2px rgba(219, 229, 240, 1);
}
4. 组件接口
代码语言:javascript复制import React, {useState} from "react"
import Slider from "../components/slider"
import "../components/slider/slider.less"
import "./app.less"
export default function App(){
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div>
<div className={`gallery b${selectedIndex 1}`}></div>
<Slider selectedIndex={selectedIndex} onClick={(index)=> setSelectedIndex(index)}>
<div className={"preview b1"}></div>
<div className={"preview b2"}></div>
<div className={"preview b3"}></div>
<div className={"preview b4"}></div>
<div className={"preview b5"}></div>
<div className={"preview b6"}></div>
<div className={"preview b7"}></div>
<div className={"preview b8"}></div>
</Slider>
</div>
);
}
参考:
react: https://react.docschina.org/ rc-tabs: https://github.com/react-component/tabs