diff --git a/src/swiper/Swiper.tsx b/src/swiper/Swiper.tsx index da400476..c3b38563 100644 --- a/src/swiper/Swiper.tsx +++ b/src/swiper/Swiper.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useRef, Ref, useImperativeHandle } from 'react'; import classNames from 'classnames'; import { Icon } from 'tdesign-icons-react'; import { TdSwiperProps, SwiperChangeSource } from './type'; @@ -12,354 +12,358 @@ export interface SwiperProps extends TdSwiperProps, StyledProps { children?: React.ReactNode; } -const Swiper = forwardRefWithStatics((props: SwiperProps) => { - const { - // animation = 'slide', // 轮播切换动画效果类型(暂时没用) - autoplay = true, // 是否自动播放 - current, // 当前轮播在哪一项(下标) - defaultCurrent = 0, // 当前轮播在哪一项(下标),非受控属性 - direction = 'horizontal', // 轮播滑动方向,包括横向滑动和纵向滑动两个方向 - duration = 300, // 滑动动画时长 - height = 180, - interval = 5000, // 轮播间隔时间 - onChange = noop, // 轮播切换时触发 - loop = true, - navigation = null, // 导航器全部配置 - className, - style, - children, - } = props; +const Swiper = forwardRefWithStatics( + (props: SwiperProps, ref: Ref) => { + const { + // animation = 'slide', // 轮播切换动画效果类型(暂时没用) + autoplay = true, // 是否自动播放 + current, // 当前轮播在哪一项(下标) + defaultCurrent = 0, // 当前轮播在哪一项(下标),非受控属性 + direction = 'horizontal', // 轮播滑动方向,包括横向滑动和纵向滑动两个方向 + duration = 300, // 滑动动画时长 + height = 180, + interval = 5000, // 轮播间隔时间 + onChange = noop, // 轮播切换时触发 + loop = true, + navigation = null, // 导航器全部配置 + className, + style, + children, + } = props; - const { classPrefix } = useConfig(); - const swiperBaseClassName = `${classPrefix}-swiper`; - const switchClassName = classNames(swiperBaseClassName, className); + const { classPrefix } = useConfig(); + const swiperBaseClassName = `${classPrefix}-swiper`; + const switchClassName = classNames(swiperBaseClassName, className); - const [currentIndex, setCurrentIndex] = useState(defaultCurrent + 1); - const [moveStartSite, setMoveStartSite] = useState(null); // 滑动state - const [touchMoveDistance, setTouchMoveDistance] = useState(0); - const [swiperOuterWidth, setSwiperOuterWidth] = useState(0); // swiper外层宽度 - const [containerWidth, setContainerWidth] = useState(0); - const [animation, setAnimation] = useState(true); - const swiperTimer = useRef(null); // 计时器指针 - const isHovering = useRef(false); - const swiper = useRef(null); - const wrapperRef = useRef(null); + const [currentIndex, setCurrentIndex] = useState(defaultCurrent + 1); + const [moveStartSite, setMoveStartSite] = useState(null); // 滑动state + const [touchMoveDistance, setTouchMoveDistance] = useState(0); + const [swiperOuterWidth, setSwiperOuterWidth] = useState(0); // swiper外层宽度 + const [containerWidth, setContainerWidth] = useState(0); + const [animation, setAnimation] = useState(true); + const swiperTimer = useRef(null); // 计时器指针 + const isHovering = useRef(false); + const swiper = useRef(null); + const wrapperRef = useRef(null); - useEffect(() => { - if (swiper) { - setContainerWidth(swiper.current.clientWidth); - if (direction === 'vertical') { - setSwiperOuterWidth(swiper.current.clientHeight); - } else { - setSwiperOuterWidth(swiper.current.clientWidth); + useEffect(() => { + if (swiper) { + setContainerWidth(swiper.current.clientWidth); + if (direction === 'vertical') { + setSwiperOuterWidth(swiper.current.clientHeight); + } else { + setSwiperOuterWidth(swiper.current.clientWidth); + } } - } - }, [direction]); + }, [direction]); - // 用于控制 wrap 位移 - const wrapDisplacement = useMemo(() => `-${currentIndex * 100 - (touchMoveDistance / swiperOuterWidth) * 100}%`, [ - currentIndex, - touchMoveDistance, - swiperOuterWidth, - ]); + // 用于控制 wrap 位移 + const wrapDisplacement = useMemo( + () => `-${currentIndex * 100 - (touchMoveDistance / swiperOuterWidth) * 100}%`, + [currentIndex, touchMoveDistance, swiperOuterWidth], + ); - /** ************************************************************ - * 获取children,并创建子节点 - */ - // 过滤SwiperItem的子元素 - const childrenList = useMemo( - () => - React.Children.toArray(children).filter( - (child: JSX.Element) => child.type.displayName === SwiperItem.displayName, - ), - [children], - ); + /** ************************************************************ + * 获取children,并创建子节点 + */ + // 过滤SwiperItem的子元素 + const childrenList = useMemo( + () => + React.Children.toArray(children).filter( + (child: JSX.Element) => child.type.displayName === SwiperItem.displayName, + ), + [children], + ); - const childrenLength = childrenList.length; + const childrenLength = childrenList.length; - // // 创建渲染用的节点列表 - const swiperItemList = childrenList.map((child: JSX.Element, index: number) => - React.cloneElement(child, { - value: index, - style: { height: `${height}px`, width: `${containerWidth}px` }, - className: `${classPrefix}-swiper__item`, - ...child.props, - }), - ); + // // 创建渲染用的节点列表 + const swiperItemList = childrenList.map((child: JSX.Element, index: number) => + React.cloneElement(child, { + value: index, + style: { height: `${height}px`, width: `${containerWidth}px` }, + className: `${classPrefix}-swiper__item`, + ...child.props, + }), + ); - // 子节点不为空时,复制第一个子节点到列表最后,复制最后一个节点到列表最前(为了滑动创建的占位元素) - if (childrenLength > 0) { - const firstEle = swiperItemList[0]; - const lastEle = swiperItemList[childrenLength - 1]; - swiperItemList.push(React.cloneElement(firstEle, { ...firstEle.props, key: `${firstEle.key}-cloned` })); - swiperItemList.unshift(React.cloneElement(lastEle, { ...lastEle.props, key: `${lastEle.key}-cloned` })); - } + // 子节点不为空时,复制第一个子节点到列表最后,复制最后一个节点到列表最前(为了滑动创建的占位元素) + if (childrenLength > 0) { + const firstEle = swiperItemList[0]; + const lastEle = swiperItemList[childrenLength - 1]; + swiperItemList.push(React.cloneElement(firstEle, { ...firstEle.props, key: `${firstEle.key}-cloned` })); + swiperItemList.unshift(React.cloneElement(lastEle, { ...lastEle.props, key: `${lastEle.key}-cloned` })); + } - const swiperItemLength = swiperItemList.length; + const swiperItemLength = swiperItemList.length; - /** ************************************************************ - * 处理默认跳转 及 动画逻辑 - */ - // 统一跳转处理函数 - const swiperTo = useCallback( - (index: number, context: { source: SwiperChangeSource }) => { - // 若禁止循环播放 - if (!loop) { + /** ************************************************************ + * 处理默认跳转 及 动画逻辑 + */ + // 统一跳转处理函数 + const swiperTo = useCallback( + (index: number, context: { source: SwiperChangeSource }) => { + // 若禁止循环播放 + if (!loop) { + if (index === childrenLength + 1) { + setAnimation(true); + setCurrentIndex(1); + onChange(0, context); + return; + } + } + // 事件通知 if (index === childrenLength + 1) { - setAnimation(true); - setCurrentIndex(1); onChange(0, context); - return; + } else if (index === 0) { + onChange(childrenLength - 1, context); + } else { + onChange((index % (childrenLength + 1)) - 1, context); } - } - // 事件通知 - if (index === childrenLength + 1) { - onChange(0, context); - } else if (index === 0) { - onChange(childrenLength - 1, context); - } else { - onChange((index % (childrenLength + 1)) - 1, context); - } - - // 设置内部 index - setAnimation(true); - setCurrentIndex(index); - }, - [childrenLength, onChange, loop], - ); - // 定时器 - const setTimer = useCallback(() => { - if (autoplay && interval > 0) { - swiperTimer.current = setTimeout( - () => { - swiperTo(currentIndex + 1, { source: 'autoplay' }); - }, - currentIndex === 1 ? interval - (duration + 50) : interval, // 当 index 为 1 的时候,表明刚从克隆的最后一项跳转过来,已经经历了duration + 50 的间隔时间,减去即可 - ); - } - }, [autoplay, currentIndex, duration, interval, swiperTo]); + // 设置内部 index + setAnimation(true); + setCurrentIndex(index); + }, + [childrenLength, onChange, loop], + ); - const clearTimer = useCallback(() => { - if (swiperTimer.current) { - clearTimeout(swiperTimer.current); - swiperTimer.current = null; - } - }, []); - - // 监听 current 参数变化 - useEffect(() => { - if (current && (current < 0 || current >= childrenLength)) return; - if (current !== undefined) { - swiperTo(current + 1, { source: '' }); - } - }, [current, childrenLength, swiperTo]); + // 定时器 + const setTimer = useCallback(() => { + if (autoplay && interval > 0) { + swiperTimer.current = setTimeout( + () => { + swiperTo(currentIndex + 1, { source: 'autoplay' }); + }, + currentIndex === 1 ? interval - (duration + 50) : interval, // 当 index 为 1 的时候,表明刚从克隆的最后一项跳转过来,已经经历了duration + 50 的间隔时间,减去即可 + ); + } + }, [autoplay, currentIndex, duration, interval, swiperTo]); - // 在非鼠标 hover 状态时,添加切换下一个组件的定时器 - useEffect(() => { - // 设置自动播放的定时器 - if (!isHovering.current) { - clearTimer(); - setTimer(); - } - }, [clearTimer, setTimer]); + const clearTimer = useCallback(() => { + if (swiperTimer.current) { + clearTimeout(swiperTimer.current); + swiperTimer.current = null; + } + }, []); - // 动画完成后取消 css 属性 - useEffect(() => { - setTimeout(() => { - setAnimation(false); - // 如果当前在最后一个(占位)元素,则立刻跳转至第2个(第一个真实)元素 - if (currentIndex + 1 >= swiperItemLength) { - setCurrentIndex(1); + // 监听 current 参数变化 + useEffect(() => { + if (current && (current < 0 || current >= childrenLength)) return; + if (current !== undefined) { + swiperTo(current + 1, { source: '' }); } - // 如果当前在第一个(占位)元素,则立刻跳到最后一个(倒数第二个真实)元素 - if (currentIndex === 0) { - setCurrentIndex(swiperItemLength - 2); + }, [current, childrenLength, swiperTo]); + + // 在非鼠标 hover 状态时,添加切换下一个组件的定时器 + useEffect(() => { + // 设置自动播放的定时器 + if (!isHovering.current) { + clearTimer(); + setTimer(); } - }, duration + 50); // 多 50ms 的间隔时间防止动画未执行完就跳转了 - }, [currentIndex, swiperItemLength, duration, direction]); + }, [clearTimer, setTimer]); - /** ****************************************************************** - * 触摸事件处理方法 - */ - // 触摸滑动事件 - 开始 - const handleTouchStart = (e: React.TouchEvent) => { - e.stopPropagation(); - isHovering.current = true; - clearTimer(); - if (direction === 'vertical') { - setMoveStartSite(e.touches[0].clientY); - } else { - setMoveStartSite(e.touches[0].clientX); - } - }; + // 动画完成后取消 css 属性 + useEffect(() => { + setTimeout(() => { + setAnimation(false); + // 如果当前在最后一个(占位)元素,则立刻跳转至第2个(第一个真实)元素 + if (currentIndex + 1 >= swiperItemLength) { + setCurrentIndex(1); + } + // 如果当前在第一个(占位)元素,则立刻跳到最后一个(倒数第二个真实)元素 + if (currentIndex === 0) { + setCurrentIndex(swiperItemLength - 2); + } + }, duration + 50); // 多 50ms 的间隔时间防止动画未执行完就跳转了 + }, [currentIndex, swiperItemLength, duration, direction]); - // 触摸滑动事件 - 滑动中 - const handleTouchMove = useCallback( - (e: React.TouchEvent) => { + /** ****************************************************************** + * 触摸事件处理方法 + */ + // 触摸滑动事件 - 开始 + const handleTouchStart = (e: React.TouchEvent) => { e.stopPropagation(); + isHovering.current = true; + clearTimer(); + if (direction === 'vertical') { + setMoveStartSite(e.touches[0].clientY); + } else { + setMoveStartSite(e.touches[0].clientX); + } + }; - if (moveStartSite) { - if (direction === 'vertical') { - const nowDistence = e.touches[0].clientY - moveStartSite; - if ( - !loop && - ((currentIndex === 1 && nowDistence > 0) || (currentIndex === childrenLength && nowDistence < 0)) - ) { - return; - } - setTouchMoveDistance(nowDistence); - } else { - const nowDistence = e.touches[0].clientX - moveStartSite; - if ( - !loop && - ((currentIndex === 1 && nowDistence > 0) || (currentIndex === childrenLength && nowDistence < 0)) - ) { - return; + // 触摸滑动事件 - 滑动中 + const handleTouchMove = useCallback( + (e: React.TouchEvent) => { + e.stopPropagation(); + + if (moveStartSite) { + if (direction === 'vertical') { + const nowDistence = e.touches[0].clientY - moveStartSite; + if ( + !loop && + ((currentIndex === 1 && nowDistence > 0) || (currentIndex === childrenLength && nowDistence < 0)) + ) { + return; + } + setTouchMoveDistance(nowDistence); + } else { + const nowDistence = e.touches[0].clientX - moveStartSite; + if ( + !loop && + ((currentIndex === 1 && nowDistence > 0) || (currentIndex === childrenLength && nowDistence < 0)) + ) { + return; + } + setTouchMoveDistance(nowDistence); } - setTouchMoveDistance(nowDistence); } - } - }, - [setTouchMoveDistance, moveStartSite, direction, currentIndex, loop, childrenLength], - ); + }, + [setTouchMoveDistance, moveStartSite, direction, currentIndex, loop, childrenLength], + ); - // 触摸滑动事件 - 结束 - const handleTouchEnd = (e: React.TouchEvent) => { - e.stopPropagation(); - if (touchMoveDistance / swiperOuterWidth <= -0.3) { - // swiperTo(currentIndex + 1, { source: 'touch' }); - // 这里不用 swiperTo 是防止滑动距离不够返回当前页动画会失效 - setCurrentIndex(currentIndex + 1); - onChange(currentIndex === childrenLength ? 0 : currentIndex, { source: 'touch' }); - } + // 触摸滑动事件 - 结束 + const handleTouchEnd = (e: React.TouchEvent) => { + e.stopPropagation(); + if (touchMoveDistance / swiperOuterWidth <= -0.3) { + // swiperTo(currentIndex + 1, { source: 'touch' }); + // 这里不用 swiperTo 是防止滑动距离不够返回当前页动画会失效 + setCurrentIndex(currentIndex + 1); + onChange(currentIndex === childrenLength ? 0 : currentIndex, { source: 'touch' }); + } - if (touchMoveDistance / swiperOuterWidth >= 0.3) { - // swiperTo(currentIndex - 1, { source: 'touch' }); - setCurrentIndex(currentIndex - 1); - onChange(currentIndex - 1 === 0 ? 2 : currentIndex - 2, { source: 'touch' }); - } - setAnimation(true); - setTouchMoveDistance(0); - // 将 TouchMoveDistance 重置为0后,要关闭动画,否则在下一次自动触发动画时滑动操作会有bug - setTimeout(() => { - setAnimation(false); - }, duration + 50); - isHovering.current = false; - setTimer(); - }; + if (touchMoveDistance / swiperOuterWidth >= 0.3) { + // swiperTo(currentIndex - 1, { source: 'touch' }); + setCurrentIndex(currentIndex - 1); + onChange(currentIndex - 1 === 0 ? 2 : currentIndex - 2, { source: 'touch' }); + } + setAnimation(true); + setTouchMoveDistance(0); + // 将 TouchMoveDistance 重置为0后,要关闭动画,否则在下一次自动触发动画时滑动操作会有bug + setTimeout(() => { + setAnimation(false); + }, duration + 50); + isHovering.current = false; + setTimer(); + }; - /** - * 点击两侧的滑动控制按钮事件 - */ - const clickSlideBtn = useCallback( - (flag: 'left' | 'right') => { - if (flag === 'left') { - if (!loop && currentIndex === 1) return; - if (currentIndex === 0) { - swiperTo(childrenLength - 1, { source: 'touch' }); - } else { - swiperTo(currentIndex - 1, { source: 'touch' }); - } - } else if (flag === 'right') { - if (!loop && currentIndex === childrenLength) return; - if (currentIndex === childrenLength + 1) { - swiperTo(2, { source: 'touch' }); - } else { - swiperTo(currentIndex + 1, { source: 'touch' }); + /** + * 点击两侧的滑动控制按钮事件 + */ + const clickSlideBtn = useCallback( + (flag: 'left' | 'right') => { + if (flag === 'left') { + if (!loop && currentIndex === 1) return; + if (currentIndex === 0) { + swiperTo(childrenLength - 1, { source: 'touch' }); + } else { + swiperTo(currentIndex - 1, { source: 'touch' }); + } + } else if (flag === 'right') { + if (!loop && currentIndex === childrenLength) return; + if (currentIndex === childrenLength + 1) { + swiperTo(2, { source: 'touch' }); + } else { + swiperTo(currentIndex + 1, { source: 'touch' }); + } } + }, + [currentIndex, swiperTo, childrenLength, loop], + ); + + // navigation.type === 'fraction' 时当前滚动值 + const fractionCurrent = useMemo(() => { + if (currentIndex < 1) { + return childrenLength; } - }, - [currentIndex, swiperTo, childrenLength, loop], - ); + if (currentIndex > childrenLength) { + return 1; + } + return currentIndex; + }, [childrenLength, currentIndex]); - // navigation.type === 'fraction' 时当前滚动值 - const fractionCurrent = useMemo(() => { - if (currentIndex < 1) { - return childrenLength; - } - if (currentIndex > childrenLength) { - return 1; + // 构造 css 对象 + // 加入了 translateZ 属性是为了使移动的 div 单独列为一个 layer 以提高滑动性能,参考:https://segmentfault.com/a/1190000010364647 + let wrapperStyle = {}; + if (direction === 'vertical') { + wrapperStyle = { + height: `${swiperItemLength * 100}%`, + top: wrapDisplacement, + transition: animation ? `top ${duration / 1000}s` : '', + flexDirection: 'column', + }; + } else { + wrapperStyle = { + width: `${swiperItemLength * 100}%`, + left: wrapDisplacement, + transition: animation ? `left ${duration / 1000}s` : '', + flexDirection: 'row', + }; } - return currentIndex; - }, [childrenLength, currentIndex]); - // 构造 css 对象 - // 加入了 translateZ 属性是为了使移动的 div 单独列为一个 layer 以提高滑动性能,参考:https://segmentfault.com/a/1190000010364647 - let wrapperStyle = {}; - if (direction === 'vertical') { - wrapperStyle = { - height: `${swiperItemLength * 100}%`, - top: wrapDisplacement, - transition: animation ? `top ${duration / 1000}s` : '', - flexDirection: 'column', - }; - } else { - wrapperStyle = { - width: `${swiperItemLength * 100}%`, - left: wrapDisplacement, - transition: animation ? `left ${duration / 1000}s` : '', - flexDirection: 'row', - }; - } + useImperativeHandle(ref, () => swiper.current); - return ( -
- {/* 渲染子节点 */} -
- {swiperItemList} -
- {navigation && childrenLength < navigation?.minShowNum ? null : ( - <> - {/* 渲染底部导航 */} - {navigation && 'type' in navigation && ( - - {(['dots', 'dots-bar'].includes(navigation.type) && - childrenList.map((_: JSX.Element, i: number) => ( - swiperTo(i + 1, { source: 'touch' })} - /> - ))) || - null} - {(navigation.type === 'fraction' && {`${fractionCurrent}/${childrenLength}`}) || null} - - )} - {/* 渲染左右两边的按钮 */} - {(direction === 'horizontal' && navigation && navigation.showSlideBtn && ( - - clickSlideBtn('left')}> - + return ( +
+ {/* 渲染子节点 */} +
+ {swiperItemList} +
+ {navigation && childrenLength < navigation?.minShowNum ? null : ( + <> + {/* 渲染底部导航 */} + {navigation && 'type' in navigation && ( + + {(['dots', 'dots-bar'].includes(navigation.type) && + childrenList.map((_: JSX.Element, i: number) => ( + swiperTo(i + 1, { source: 'touch' })} + /> + ))) || + null} + {(navigation.type === 'fraction' && {`${fractionCurrent}/${childrenLength}`}) || null} - clickSlideBtn('right')}> - + )} + {/* 渲染左右两边的按钮 */} + {(direction === 'horizontal' && navigation && navigation.showSlideBtn && ( + + clickSlideBtn('left')}> + + + clickSlideBtn('right')}> + + - - )) || - null} - - )} -
- ); -}, { - SwiperItem -}); + )) || + null} + + )} +
+ ); + }, + { + SwiperItem, + }, +); Swiper.displayName = 'Swiper';