前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【React】【案例】:简易轮播组件

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

作者头像
WEBJ2EE
发布2020-04-21 16:26:43
1.2K0
发布2020-04-21 16:26:43
举报
文章被收录于专栏:WebJ2EE
代码语言: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
      • 这里注意空元素问题
  • 滑动基本原理

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


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebJ2EE 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档