专栏首页WebJ2EE【React】【案例】:简易轮播组件

【React】【案例】:简易轮播组件

目录
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
      • 这里注意空元素问题
  • 滑动基本原理

3. 关键实现

3.1. Slider.tsx

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

.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. 组件接口

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


本文分享自微信公众号 - WebJ2EE(WebJ2EE),作者:WEBJ2EE

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • JS:Web Storage API(localStorage、sessionStorage)

    Web Storage API 提供了存储机制,通过该机制,浏览器可以安全地存储键值对,比使用 cookie 更加直观。Web Storage 包含如下两种机制...

    WEBJ2EE
  • 【前端】:浏览器渲染模式

    在很久以前的网络上,页面通常有两种版本:为网景(Netscape)的 Navigator准备的版本以及为微软(Microsoft)的 Internet Expl...

    WEBJ2EE
  • WEB性能调优:gzip 与 chunked

    gzip 是 GNU zip 的缩写,是一种流行的文件压缩算法;gzip 常用于压缩CSS、JS、HTML 等纯文本内容,可以节省大量网络带宽流量;

    WEBJ2EE
  • 编程小白 | 每日一练(57)

    这道理放在编程上也一并受用。在编程方面有着天赋异禀的人毕竟是少数,我们大多数人想要从编程小白进阶到高手,需要经历的是日积月累的学习,那么如何学习呢?当然是每天都...

    闫小林
  • 重磅!!|“NLP系列教程01”之自然语言处理概要

    作者出该系列教程的目的是让大家能够掌握深度学习算法在自然语言处理中应用,同时也希望能够加深自己对自然语言处理的理解。

    ShuYini
  • 17.Swift学习之类

    YungFan
  • 薅百度GPU羊毛!PaddlePaddle大升级,比Google更懂中文,打响AI开发者争夺战

    深度学习已经推动人工智能进入工业大生产阶段,而深度学习框架则是智能时代的操作系统。

    AI科技大本营
  • 美国按需购物平台Choosy获540万美元种子融资

    数据猿
  • 【专知AI日报0930】一文了解最新AI业界动态

    【导读】《专知AI日报》,每天精选AI业界发生的最新最具有影响力的动态事件,为你简文速读了解。 1. 【Yoshua Bengio宣布即将终止Theano的开发...

    WZEARW
  • Java后端学习流程

    首先,我个人比较推崇的学习方法是:先学java前端,也就是HTML,css,js,因为学习java以后肯定是往java ee方向发展的,学习完前端,在学习后端很...

    Java团长

扫码关注云+社区

领取腾讯云代金券