Skip to content

Commit

Permalink
feat(swiper): 跟据新的设计重构swiper
Browse files Browse the repository at this point in the history
  • Loading branch information
duenyang committed Mar 9, 2022
1 parent 621930b commit 3d55eeb
Show file tree
Hide file tree
Showing 7 changed files with 434 additions and 130 deletions.
264 changes: 210 additions & 54 deletions src/swiper/Swiper.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import React, { useState, useEffect, useCallback, useMemo, useRef, isValidElement } from 'react';
import classnames from 'classnames';
import { IconFont } from 'tdesign-icons-react';
import useConfig from '../_util/useConfig';
import noop from '../_util/noop';
import { TdSwiperProps, SwiperChangeSource } from './type';
import { TdSwiperProps, SwiperChangeSource, SwiperNavigation } from './type';
import { StyledProps } from '../common';

import SwiperItem from './SwiperItem';
Expand All @@ -11,26 +12,51 @@ export interface SwiperProps extends TdSwiperProps, StyledProps {
children?: React.ReactNode;
}

const defaultNavigation = {
placement: 'inside',
showSlideBtn: 'always',
size: 'medium',
type: 'bars',
};

const Swiper = (props: SwiperProps) => {
const {
// animation = 'slide', // 轮播切换动画效果类型(暂时没用)
// theme
animation = 'slide', // 轮播切换动画效果类型
autoplay = true, // 是否自动播放
current, // 当前轮播在哪一项(下标)
current = 0, // 当前轮播在哪一项(下标)
defaultCurrent = 0, // 当前轮播在哪一项(下标),非受控属性
direction = 'horizontal', // 轮播滑动方向,包括横向滑动和纵向滑动两个方向
duration = 300, // 滑动动画时长
interval = 5000, // 轮播间隔时间
trigger = 'hover',
height,
loop = true,
stopOnHover = true,
onChange = noop, // 轮播切换时触发
className,
children,
navigation,
type = 'default',
} = props;
const { classPrefix } = useConfig();

let navigationConfig = defaultNavigation;
let navigationNode = null;
if (isValidElement(navigation)) {
navigationNode = navigation;
} else {
navigationConfig = { ...defaultNavigation, ...(navigation as SwiperNavigation) };
}

const [currentIndex, setCurrentIndex] = useState(defaultCurrent);
const [animation, setAnimation] = useState(true);
const [needAnimation, setNeedAnimation] = useState(true);
const [arrowShow, setArrowShow] = useState(navigationConfig.showSlideBtn === 'always');
const swiperTimer = useRef(null); // 计时器指针
const isHovering = useRef(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const swiperWrap = useRef(null);

const getWrapAttribute = (attr) => swiperWrap.current?.parentNode?.[attr];

// 进行子组件筛选,创建子节点列表
const childrenList = useMemo(
Expand All @@ -44,12 +70,23 @@ const Swiper = (props: SwiperProps) => {

// 创建渲染用的节点列表
const swiperItemList = childrenList.map((child: JSX.Element, index: number) =>
React.cloneElement(child, { value: index, ...child.props }),
React.cloneElement(child, {
key: index,
index,
currentIndex,
needAnimation,
childrenLength,
getWrapAttribute,
...props,
...child.props,
}),
);
// 子节点不为空时,复制第一个子节点到列表最后
if (childrenLength > 0) {
if (childrenLength > 0 && type === 'default') {
const firstEle = swiperItemList[0];
swiperItemList.push(React.cloneElement(firstEle, { ...firstEle.props, key: `${firstEle.key}-cloned` }));
swiperItemList.push(
React.cloneElement(firstEle, { ...firstEle.props, key: childrenLength, index: childrenLength }),
);
}
const swiperItemLength = swiperItemList.length;

Expand All @@ -59,7 +96,7 @@ const Swiper = (props: SwiperProps) => {
// 事件通知
onChange(index % childrenLength, context);
// 设置内部 index
setAnimation(true);
setNeedAnimation(true);
setCurrentIndex(index);
},
[childrenLength, onChange],
Expand All @@ -76,88 +113,207 @@ const Swiper = (props: SwiperProps) => {
);
}
}, [autoplay, currentIndex, duration, interval, swiperTo]);

const clearTimer = useCallback(() => {
if (swiperTimer.current) {
clearTimeout(swiperTimer.current);
swiperTimer.current = null;
}
}, []);

const isEnd = useCallback(() => {
if (type === 'card') {
return !loop && currentIndex + 1 >= swiperItemLength;
}
return !loop && currentIndex + 2 >= swiperItemLength;
}, [loop, currentIndex, swiperItemLength, type]);

// 监听 current 参数变化
useEffect(() => {
if (current !== undefined) {
swiperTo(current % childrenLength, { source: '' });
swiperTo(current % childrenLength, { source: 'autoplay' });
}
}, [current, childrenLength, swiperTo]);

// 在非鼠标 hover 状态时,添加切换下一个组件的定时器
useEffect(() => {
// 设置自动播放的定时器
if (!isHovering.current) {
clearTimer();
setTimer();
}
}, [clearTimer, setTimer]);

// 动画完成后取消 css 属性
useEffect(() => {
setTimeout(() => {
setAnimation(false);
if (currentIndex + 1 >= swiperItemLength) {
setNeedAnimation(false);
if (isEnd()) {
clearTimer();
}
if (currentIndex + 1 >= swiperItemLength && type !== 'card') {
setCurrentIndex(0);
}
}, duration + 50); // 多 50ms 的间隔时间参考了 react-slick 的动画间隔取值
}, [currentIndex, swiperItemLength, duration, direction]);
}, [currentIndex, swiperItemLength, duration, direction, animation, type, clearTimer, isEnd]);

useEffect(() => {
if (!isHovering.current || !stopOnHover) {
clearTimer();
setTimer();
}
}, [setTimer, clearTimer, stopOnHover]);

// 鼠标移入移出事件
const onMouseEnter = () => {
isHovering.current = true;
clearTimer();
if (stopOnHover) {
clearTimer();
}
if (navigationConfig.showSlideBtn === 'hover') {
setArrowShow(true);
}
};
const onMouseLeave = () => {
isHovering.current = false;
setTimer();
if (!isEnd()) {
setTimer();
}
if (navigationConfig.showSlideBtn === 'hover') {
setArrowShow(false);
}
};

const navMouseAction = (action: 'enter' | 'leave' | 'click', index: number) => {
if (action === 'enter' && trigger === 'hover') {
swiperTo(index, { source: 'hover' });
}
if (action === 'click' && trigger === 'click') {
swiperTo(index, { source: 'click' });
}
};

const arrowClick = (direction: 'left' | 'right') => {
if (direction === 'right') {
if (type === 'card') {
return swiperTo(currentIndex + 1 >= swiperItemLength ? 0 : currentIndex + 1, { source: 'click' });
}
return swiperTo(currentIndex + 1, { source: 'click' });
}
if (direction === 'left') {
if (currentIndex - 1 < 0) {
return swiperTo(childrenLength - 1, { source: 'click' });
}
return swiperTo(currentIndex - 1, { source: 'click' });
}
};

const createArrow = (type: 'default' | 'fraction') => {
if (!arrowShow) {
return '';
}
if (navigationConfig.type === 'fraction' && type === 'default') {
return '';
}
const fractionIndex = currentIndex + 1 > childrenLength ? 1 : currentIndex + 1;
return (
<div
className={classnames(`${classPrefix}-swiper__arrow`, {
[`${classPrefix}-swiper__arrow-default`]: type === 'default',
})}
>
<div className={`${classPrefix}-swiper__arrow__left`} onClick={() => arrowClick('left')}>
<IconFont name="chevron-left" />
</div>
{type === 'fraction' ? (
<div className="t-swiper__navigation__text-fraction">
{fractionIndex}/{childrenLength}
</div>
) : (
''
)}
<div className={`${classPrefix}-swiper__arrow__right`} onClick={() => arrowClick('right')}>
<IconFont name="chevron-right" />
</div>
</div>
);
};

const createNavigation = () => {
if (navigationConfig.type === 'fraction') {
return (
<div className={classnames(`${classPrefix}-swiper__navigation`, `${classPrefix}-swiper__navigation-fraction`)}>
{createArrow('fraction')}
</div>
);
}
return navigationNode ? (
<>{navigationNode}</>
) : (
<ul
className={classnames(`${classPrefix}-swiper__navigation`, {
[`${classPrefix}-swiper__navigation-bars`]: navigationConfig.type === 'bars',
})}
>
{childrenList.map((_: JSX.Element, i: number) => (
<li
key={i}
className={classnames(`${classPrefix}-swiper__navigation__item`, {
[`${classPrefix}-is-active`]: i === currentIndex % childrenLength,
})}
onClick={() => navMouseAction('click', i)}
onMouseEnter={() => navMouseAction('enter', i)}
onMouseLeave={() => navMouseAction('leave', i)}
/>
))}
</ul>
);
};

// 构造 css 对象
// 加入了 translateZ 属性是为了使移动的 div 单独列为一个 layer 以提高滑动性能,参考:https://segmentfault.com/a/1190000010364647
let wrapperStyle = {};
if (direction === 'vertical') {
wrapperStyle = {
height: `${swiperItemLength * 100}%`,
top: `-${currentIndex * 100}%`,
transition: animation ? `top ${duration / 1000}s` : '',
};
} else {
wrapperStyle = {
width: `${swiperItemLength * 100}%`,
left: `-${currentIndex * 100}%`,
transition: animation ? `left ${duration / 1000}s` : '',
};
}
const getWrapperStyle = () => {
const offsetHeight = height ? `${height}px` : `${getWrapAttribute('offsetHeight')}px`;
if (type === 'card' || animation === 'fade') {
return {
height: offsetHeight,
};
}
if (animation === 'slide') {
if (direction === 'vertical') {
return {
height: offsetHeight,
transform: `translate3d(0, -${currentIndex * 100}%, 0px)`,
transition: needAnimation ? `transform ${duration / 1000}s` : '',
};
}
return {
transform: `translate3d(-${currentIndex * 100}%, 0px, 0px)`,
transition: needAnimation ? `transform ${duration / 1000}s` : '',
};
}
};

return (
<div
className={classnames(`${classPrefix}-swiper`, className)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
ref={swiperWrap}
>
{/* 渲染子节点 */}
<div className={`${classPrefix}-swiper__content`}>
<div ref={wrapperRef} className={`${classPrefix}-swiper__swiper-wrap--${direction}`} style={wrapperStyle}>
{swiperItemList}
<div
className={classnames(`${classPrefix}-swiper__wrap`, {
[`${classPrefix}-swiper-inside`]: navigationConfig.placement === 'inside',
[`${classPrefix}-swiper-outside`]: navigationConfig.placement === 'outside',
[`${classPrefix}-swiper-vertical`]: direction === 'vertical',
[`${classPrefix}-swiper-large`]: navigationConfig.size === 'large',
[`${classPrefix}-swiper-small`]: navigationConfig.size === 'small',
})}
>
<div
className={classnames(`${classPrefix}-swiper__content`, {
[`${classPrefix}-swiper-fade`]: animation === 'fade',
[`${classPrefix}-swiper-card`]: type === 'card',
})}
style={{ height: '' }}
>
<div className={`${classPrefix}-swiper__container`} style={getWrapperStyle()}>
{swiperItemList}
</div>
</div>
{createNavigation()}
{createArrow('default')}
</div>
{/* 渲染右侧切换小点 */}
<ul className={`${classPrefix}-swiper__trigger-wrap`}>
{childrenList.map((_: JSX.Element, i: number) => (
<li
key={i}
className={i === currentIndex % childrenLength ? `${classPrefix}-swiper__trigger--active` : ''}
onClick={() => swiperTo(i, { source: 'touch' })}
/>
))}
</ul>
</div>
);
};
Expand Down
Loading

0 comments on commit 3d55eeb

Please sign in to comment.