Skip to content

Commit

Permalink
feat: add swiper
Browse files Browse the repository at this point in the history
  • Loading branch information
taoyage committed Jun 26, 2022
1 parent 27114ed commit 36410b1
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 6 deletions.
4 changes: 2 additions & 2 deletions packages/pull-to-refresh/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IPullStatus } from './types';
import { TPullStatus, TPullKey } from './types';

export const PULL_STATUS: IPullStatus = {
export const PULL_STATUS: Record<TPullKey, TPullStatus> = {
PULLING: 'pulling',
CAN_RELEASE: 'canRelease',
REFRESHING: 'refreshing',
Expand Down
5 changes: 1 addition & 4 deletions packages/pull-to-refresh/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
export type TPullStatus = 'pulling' | 'canRelease' | 'refreshing' | 'complete';

export interface IPullStatus {
[key: string]: TPullStatus;
}
export type TPullKey = 'PULLING' | 'CAN_RELEASE' | 'REFRESHING' | 'COMPLETE';
17 changes: 17 additions & 0 deletions packages/swiper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import InternalSwiper from './swiper';
import SwiperItem from './swiper-item';

export type { SwiperProps } from './swiper';
export type { SwiperItemProps } from './swiper-item';

type InternalSwiperType = typeof InternalSwiper;

export interface SwiperInterface extends InternalSwiperType {
Item: typeof SwiperItem;
}

const Swiper = InternalSwiper as SwiperInterface;

Swiper.Item = SwiperItem;

export default Swiper;
6 changes: 6 additions & 0 deletions packages/swiper/styles/swiper-item.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.ygm-swiper-item {
display: block;
width: 100%;
height: 100%;
white-space: normal;
}
23 changes: 23 additions & 0 deletions packages/swiper/styles/swiper-page-indicator.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.ygm-swiper-page-indicator {
display: flex;
width: auto;

&-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background-color: $ygm-color-weak;
margin-right: 5px;

&:last-child {
margin-right: 0;
}

&-active {
width: 13px;
height: 5px;
border-radius: 2px;
background-color: $ygm-color-primary;
}
}
}
39 changes: 39 additions & 0 deletions packages/swiper/styles/swiper.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.ygm-swiper {
width: 100%;
height: auto;
position: relative;
touch-action: pan-y;

&-track {
width: 100%;
height: 100%;
position: relative;
flex-wrap: nowrap;
display: flex;
overflow: hidden;

&-inner {
width: 100%;
height: 100%;
position: relative;
flex-wrap: nowrap;
display: flex;
overflow: hidden;
}
}

&-slide {
width: 100%;
position: relative;
display: block;
flex-shrink: 0;
white-space: unset;
}

&-indicator {
position: absolute;
bottom: 6px;
left: 50%;
transform: translateX(-50%);
}
}
20 changes: 20 additions & 0 deletions packages/swiper/swiper-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';

import './styles/swiper-item.scss';

export interface SwiperItemProps {
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
children?: React.ReactNode;
}

const SwiperItem: React.FC<SwiperItemProps> = React.memo((props) => {
return (
<div className="ygm-swiper-item" onClick={props.onClick}>
{props.children}
</div>
);
});

SwiperItem.displayName = 'SwiperItem';

export default SwiperItem;
33 changes: 33 additions & 0 deletions packages/swiper/swiper-page-indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import cx from 'classnames';

import './styles/swiper-page-indicator.scss';

export interface SwiperPageIndicatorProps {
current: number;
total: number;
indicatorClassName?: string;
}

const classPrefix = 'ygm-swiper-page-indicator';

const SwiperPageIndicator: React.FC<SwiperPageIndicatorProps> = React.memo((props) => {
const dots: React.ReactElement[] = React.useMemo(() => {
return Array(props.total)
.fill(0)
.map((_, index) => (
<div
key={index}
className={cx(`${classPrefix}-dot`, {
[`${classPrefix}-dot-active`]: props.current === index,
})}
/>
));
}, [props]);

return <div className={classPrefix}>{dots}</div>;
});

SwiperPageIndicator.displayName = 'SwiperPageIndicator';

export default SwiperPageIndicator;
172 changes: 172 additions & 0 deletions packages/swiper/swiper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React from 'react';
import SwiperPageIndicator from '@/swiper/swiper-page-indicator';

import { modulus } from './utils';

import './styles/swiper.scss';

export interface SwiperProps {
autoplay?: boolean;
defaultIndex?: number;
autoplayInterval?: number;
children: React.ReactElement | React.ReactElement[];
showIndicator?: boolean;
indicatorClassName?: string;
}

const Swiper: React.FC<SwiperProps> = React.memo((props) => {
const [currentIndex, setCurrentIndex] = React.useState<number>(props.defaultIndex || 0);
const [dragging, setDragging] = React.useState<boolean>(false);

const trackRef = React.useRef<HTMLDivElement>(null);
const startRef = React.useRef<number>(0);
const slideRatioRef = React.useRef<number>(0);
const intervalRef = React.useRef<number>(0);
const autoPlaying = React.useRef<boolean>(false);

const count = React.useMemo(() => React.Children.count(props.children), [props.children]);

const getTransition = React.useCallback(
(position: number) => {
if (dragging) {
return '';
} else if (autoPlaying.current) {
if (position === -100 || position === 0) {
return 'transform 0.3s ease-out';
} else {
return '';
}
}
return 'transform 0.3s ease-out';
},
[dragging]
);

const getFinalPosition = React.useCallback(
(index: number) => {
let finalPosition = -currentIndex * 100 + index * 100;
const totalWidth = count * 100;
const flagWidth = totalWidth / 2;

finalPosition = modulus(finalPosition + flagWidth, totalWidth) - flagWidth;
return finalPosition;
},
[count, currentIndex]
);

const renderSwiperItem = React.useCallback(() => {
return (
<div className="ygm-swiper-track-inner">
{React.Children.map(props.children, (child, index) => {
const position = getFinalPosition(index);
return (
<div
className="ygm-swiper-slide"
style={{
transform: `translate3d(${position}%, 0px, 0px)`,
left: `-${index * 100}%`,
transition: getTransition(position),
}}
>
{child}
</div>
);
})}
</div>
);
}, [props.children, getFinalPosition, getTransition]);

const swipeTo = React.useCallback(
(index: number) => {
const targetIndex = modulus(index, count);
setCurrentIndex(targetIndex);
},
[count]
);

const swipeNext = React.useCallback(() => {
swipeTo(currentIndex + 1);
}, [swipeTo, currentIndex]);

const getSlideRatio = React.useCallback((diff: number) => {
const element = trackRef.current;
if (!element) return 0;
return diff / element.offsetWidth;
}, []);

const onTouchMove = React.useCallback(
(e: TouchEvent) => {
const currentX = e.changedTouches[0].clientX;
const diff = startRef.current - currentX;
slideRatioRef.current = getSlideRatio(diff);

setCurrentIndex(currentIndex + slideRatioRef.current);
},
[currentIndex, getSlideRatio]
);

const onTouchEnd = React.useCallback(() => {
const element = trackRef.current;
if (!element) return;
const index = Math.round(slideRatioRef.current);
slideRatioRef.current = 0;
swipeTo(currentIndex + index);
setDragging(false);
element.removeEventListener('touchmove', onTouchMove);
element.removeEventListener('touchend', onTouchEnd);
}, [currentIndex, onTouchMove, swipeTo]);

const onTouchStart = React.useCallback(
(e: React.TouchEvent<HTMLDivElement>) => {
const element = trackRef.current;
if (!element) return;

startRef.current = e.changedTouches[0].clientX;
setDragging(true);
clearInterval(intervalRef.current);
autoPlaying.current = false;
element.addEventListener('touchmove', onTouchMove);
element.addEventListener('touchend', onTouchEnd);
},
[onTouchEnd, onTouchMove]
);

React.useEffect(() => {
if (!props.autoplay || dragging) return;
intervalRef.current = window.setInterval(() => {
autoPlaying.current = true;
swipeNext();
}, props.autoplayInterval);
return () => {
clearInterval(intervalRef.current);
};
}, [dragging, props.autoplay, props.autoplayInterval, swipeNext]);

return (
<div className="ygm-swiper">
<div className="ygm-swiper-track" ref={trackRef} onTouchStart={onTouchStart}>
{renderSwiperItem()}
</div>
{props.showIndicator && (
<div className="ygm-swiper-indicator">
<SwiperPageIndicator
total={count}
current={slideRatioRef.current > 0 ? Math.floor(currentIndex) : Math.ceil(currentIndex)}
indicatorClassName={props.indicatorClassName}
/>
</div>
)}
</div>
);
});

Swiper.defaultProps = {
autoplay: false,
defaultIndex: 0,
autoplayInterval: 3000,
showIndicator: true,
};

Swiper.displayName = 'Swiper';

export default Swiper;
4 changes: 4 additions & 0 deletions packages/swiper/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const modulus = (value: number, division: number) => {
const remainder = value % division;
return remainder < 0 ? remainder + division : remainder;
};
11 changes: 11 additions & 0 deletions stories/swiper/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.swiper-demo {
&-content {
height: 120px;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
font-size: 48px;
user-select: none;
}
}
37 changes: 37 additions & 0 deletions stories/swiper/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { Meta } from '@storybook/react';

import Swiper from '@/swiper';

import './index.scss';

const SwiperStory: Meta = {
title: '信息展示/Swiper 轮播图',
component: Swiper,
subcomponents: { 'Swiper.Item': Swiper.Item },
};

const colors = ['#ace0ff', '#bcffbd', '#e4fabd'];

export const Basic = () => {
return (
<div className="swiper-demo">
<div>
<h3>基本用法</h3>
<Swiper autoplay={false}>
{colors.map((color, index) => (
<Swiper.Item key={index}>
<div className="swiper-demo-content" style={{ background: color }}>
{index + 1}
</div>
</Swiper.Item>
))}
</Swiper>
</div>
</div>
);
};

Basic.storyName = '基本用法';

export default SwiperStory;

0 comments on commit 36410b1

Please sign in to comment.