diff --git a/site/mobile/mobile.config.js b/site/mobile/mobile.config.js index 812b5c22..16e2ffbd 100644 --- a/site/mobile/mobile.config.js +++ b/site/mobile/mobile.config.js @@ -132,5 +132,10 @@ export default { name: 'Collapse', component: () => import('tdesign-mobile-react/collapse/_example/index.jsx'), }, + { + title: 'PullDownRefresh 下拉刷新', + name: 'pull-down-refresh', + component: () => import('tdesign-mobile-react/pull-down-refresh/_example/index.jsx'), + }, ], }; diff --git a/site/web/site.config.js b/site/web/site.config.js index 9b2b7563..9b3ffced 100644 --- a/site/web/site.config.js +++ b/site/web/site.config.js @@ -336,12 +336,12 @@ export default { // path: '/mobile-react/components/progress', // component: () => import('tdesign-mobile-react/progress/progress.md'), // }, - // { - // title: 'PullDownRefresh 下拉刷新', - // name: 'pull-down-refresh', - // path: '/mobile-react/components/pull-down-refresh', - // component: () => import('tdesign-mobile-react/pull-down-refresh/pull-down-refresh.md'), - // }, + { + title: 'PullDownRefresh 下拉刷新', + name: 'pull-down-refresh', + path: '/mobile-react/components/pull-down-refresh', + component: () => import('tdesign-mobile-react/pull-down-refresh/pull-down-refresh.md'), + }, ], }, ], diff --git a/src/_util/delay.ts b/src/_util/delay.ts new file mode 100644 index 00000000..1c764b64 --- /dev/null +++ b/src/_util/delay.ts @@ -0,0 +1,5 @@ +export default function delay(time: number) { + return new Promise((resolve) => { + setTimeout(resolve, time); + }); +} diff --git a/src/_util/getScrollParent.ts b/src/_util/getScrollParent.ts new file mode 100644 index 00000000..b4ee89a5 --- /dev/null +++ b/src/_util/getScrollParent.ts @@ -0,0 +1,20 @@ +type ScrollElement = HTMLElement | Window; + +const overflowScrollReg = /scroll|auto|overlay/i; + +export default function getScrollParent( + el: Element | null | undefined, + root: ScrollElement | null | undefined = window, +): Window | Element | null | undefined { + let node = el; + + while (node && node !== root && node.nodeType === 1) { + const { overflowY } = window.getComputedStyle(node); + if (overflowScrollReg.test(overflowY)) { + return node; + } + node = node.parentNode as Element; + } + + return root; +} diff --git a/src/index.ts b/src/index.ts index ae06f1b4..ec7d6fe5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,4 +25,4 @@ export * from './swipe-cell'; export * from './tag'; export * from './toast'; export * from './collapse'; - +export * from './pull-down-refresh'; diff --git a/src/pull-down-refresh/PullDownRefresh.tsx b/src/pull-down-refresh/PullDownRefresh.tsx new file mode 100644 index 00000000..40fd260c --- /dev/null +++ b/src/pull-down-refresh/PullDownRefresh.tsx @@ -0,0 +1,164 @@ +import React, { useRef, useState, ReactNode } from 'react'; +import identity from 'lodash/identity'; +import uniqueId from 'lodash/uniqueId'; +import { useDrag } from '@use-gesture/react'; +import { useSpring, animated } from '@react-spring/web'; +import { Loading } from 'tdesign-mobile-react'; +import useConfig from '../_util/useConfig'; +import withNativeProps, { NativeProps } from '../_util/withNativeProps'; +import getScrollParent from '../_util/getScrollParent'; +import delay from '../_util/delay'; +import { TdPullDownRefreshProps } from './type'; + +export enum PullStatusEnum { + normal, + loading, + loosing, + pulling, + success, +} + +function getStatusText(status: PullStatusEnum, loadingTexts: string[]) { + switch (status) { + case PullStatusEnum.pulling: + return loadingTexts[0]; + case PullStatusEnum.loosing: + return loadingTexts[1]; + case PullStatusEnum.loading: + return loadingTexts[2]; + case PullStatusEnum.success: + return loadingTexts[3]; + default: + return ''; + } +} + +export interface PullDownRefreshProps extends TdPullDownRefreshProps, NativeProps { + disabled?: boolean; + threshold?: number; + onRefresh?: () => Promise; +} + +const defaultProps = { + loadingBarHeight: 50, + loadingTexts: ['下拉刷新', '松手刷新', '正在刷新', '刷新完成'], + maxBarHeight: 80, + threshold: 50, + refreshTimeout: 3000, + disabled: false, + onRefresh: () => delay(2000), + onTimeout: identity, +}; + +const PullDownRefresh: React.FC = (props) => { + const { + children, + disabled, + loadingTexts, + loadingProps, + loadingBarHeight, + maxBarHeight, + threshold, + refreshTimeout, + onRefresh, + onTimeout, + } = props; + const [status, originalSetStatus] = useState(PullStatusEnum.normal); + const rootRef = useRef(null); + const scrollParentRef = useRef(null); + const { classPrefix } = useConfig(); + const name = `${classPrefix}-pull-down-refresh`; + const setStatus = (nextStatus: PullStatusEnum) => { + if (nextStatus !== status) originalSetStatus(nextStatus); + }; + + const [{ y }, api] = useSpring( + () => ({ + y: 0, + config: { tension: 300, friction: 30, clamp: true }, + }), + [], + ); + + const doRefresh = async () => { + setStatus(PullStatusEnum.loading); + api.start({ y: loadingBarHeight }); + try { + const timeoutId = uniqueId(`${name}-timeout_`); + let timeoutTid: any; + const res = await Promise.race([ + onRefresh(), + new Promise((resolve) => { + timeoutTid = setTimeout(() => { + resolve(timeoutId); + onTimeout(); + }, refreshTimeout); + }), + ]); + clearTimeout(timeoutTid); + if (res !== timeoutId) { + setStatus(PullStatusEnum.success); + } + } finally { + api.start({ + to: async (next) => { + await next({ y: 0 }); + setStatus(PullStatusEnum.normal); + }, + }); + } + }; + + useDrag( + (state) => { + const [, offsetY] = state.offset; + if (state.first) { + scrollParentRef.current = getScrollParent(rootRef.current); + setStatus(PullStatusEnum.pulling); + } + if (!scrollParentRef.current) return; + if (state.last) { + if (status === PullStatusEnum.loosing) { + doRefresh(); + } else { + setStatus(PullStatusEnum.normal); + api.start({ y: 0 }); + } + } else { + setStatus(offsetY >= threshold ? PullStatusEnum.loosing : PullStatusEnum.pulling); + api.start({ y: offsetY, immediate: true }); + } + }, + { + target: rootRef, + from: [0, y.get()], + bounds: { top: 0, bottom: maxBarHeight }, + pointer: { touch: true }, + axis: 'y', + enabled: !disabled && status !== PullStatusEnum.loading, + }, + ); + + const statusText = getStatusText(status, loadingTexts); + let statusNode: ReactNode = statusText; + if (status === PullStatusEnum.loading) { + statusNode = ; + } + + return withNativeProps( + props, +
+ +
+ {statusNode} +
+ {children} +
+
, + ); +}; + +PullDownRefresh.defaultProps = defaultProps; +PullDownRefresh.displayName = 'PullDownRefresh'; + +export default PullDownRefresh; diff --git a/src/pull-down-refresh/_example/base.jsx b/src/pull-down-refresh/_example/base.jsx new file mode 100644 index 00000000..2183d88f --- /dev/null +++ b/src/pull-down-refresh/_example/base.jsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react'; +import { PullDownRefresh } from 'tdesign-mobile-react'; + +export default function Demo() { + const [count, setCount] = useState(0); + return ( +
+ + new Promise((resolve) => { + setCount(count + 1); + setTimeout(() => { + resolve(); + }, 1000); + }) + } + > +
已下拉{count}次
+
+
+ ); +} diff --git a/src/pull-down-refresh/_example/index.jsx b/src/pull-down-refresh/_example/index.jsx new file mode 100644 index 00000000..9308cda3 --- /dev/null +++ b/src/pull-down-refresh/_example/index.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Tabs, TabPanel } from 'tdesign-mobile-react/tabs'; +import BaseDemo from './base'; +import TimieoutDemo from './timeout'; +import LoadingTextsDemo from './loading-texts'; +import './style/index.less'; + +export default function Demo() { + return ( + + + + + + + + + + + + ); +} diff --git a/src/pull-down-refresh/_example/loading-texts.jsx b/src/pull-down-refresh/_example/loading-texts.jsx new file mode 100644 index 00000000..914f42fa --- /dev/null +++ b/src/pull-down-refresh/_example/loading-texts.jsx @@ -0,0 +1,25 @@ +import React, { useState } from 'react'; +import { PullDownRefresh } from 'tdesign-mobile-react'; + +export default function Demo() { + const [count, setCount] = useState(0); + return ( +
+ + new Promise((resolve) => { + setCount(count + 1); + setTimeout(() => { + resolve(); + }, 1000); + }) + } + > +
已下拉{count}次
+
+
+ ); +} diff --git a/src/pull-down-refresh/_example/style/index.less b/src/pull-down-refresh/_example/style/index.less new file mode 100644 index 00000000..0b9a01ee --- /dev/null +++ b/src/pull-down-refresh/_example/style/index.less @@ -0,0 +1,5 @@ +.pull-down-refresh-content { + height: calc(100vh - 55px); + background: #fff; + padding: 20px 16px; +} \ No newline at end of file diff --git a/src/pull-down-refresh/_example/timeout.jsx b/src/pull-down-refresh/_example/timeout.jsx new file mode 100644 index 00000000..132e48c9 --- /dev/null +++ b/src/pull-down-refresh/_example/timeout.jsx @@ -0,0 +1,26 @@ +import React, { useState } from 'react'; +import { PullDownRefresh, Toast } from 'tdesign-mobile-react'; + +export default function Demo() { + const [count, setCount] = useState(0); + return ( +
+ { + Toast({ message: '已超时' }); + }} + onRefresh={() => + new Promise((resolve) => { + setCount(count + 1); + setTimeout(() => { + resolve(); + }, 2000); + }) + } + > +
已下拉{count}次
+
+
+ ); +} diff --git a/src/pull-down-refresh/index.ts b/src/pull-down-refresh/index.ts new file mode 100644 index 00000000..0fa663c1 --- /dev/null +++ b/src/pull-down-refresh/index.ts @@ -0,0 +1,8 @@ +import _PullDownRefresh from './PullDownRefresh'; +import './style'; + +export type { PullDownRefreshProps } from './PullDownRefresh'; +export * from './type'; + +export const PullDownRefresh = _PullDownRefresh; +export default PullDownRefresh; diff --git a/src/pull-down-refresh/pull-down-refresh.md b/src/pull-down-refresh/pull-down-refresh.md new file mode 100644 index 00000000..af9b86d7 --- /dev/null +++ b/src/pull-down-refresh/pull-down-refresh.md @@ -0,0 +1,17 @@ +:: BASE_DOC :: + +## API + +### PullDownRefresh Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +loadingBarHeight | Number | 50 | 加载中下拉高度 | N +loadingProps | Object | - | 加载loading样式。TS 类型:`TdLoadingProps`,[Loading API Documents](./loading?tab=api)。[详细类型定义](https://github.com/TDesignOteam/tdesign-mobile-react/tree/develop/src/pull-down-refresh/type.ts) | N +loadingTexts | Array | [] | 提示语,组件内部默认值为 ['下拉刷新', '松手刷新', '正在刷新', '刷新完成']。TS 类型:`string[]` | N +maxBarHeight | Number | 80 | 最大下拉高度 | N +refreshTimeout | Number | 3000 | 刷新超时时间 | N +onRefresh | Function | | TS 类型:`() => void`
结束下拉时触发 | N +onTimeout | Function | | TS 类型:`() => void`
刷新超时触发 | N diff --git a/src/pull-down-refresh/style/css.js b/src/pull-down-refresh/style/css.js new file mode 100644 index 00000000..6a9a4b13 --- /dev/null +++ b/src/pull-down-refresh/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/pull-down-refresh/style/index.js b/src/pull-down-refresh/style/index.js new file mode 100644 index 00000000..4cb56ee6 --- /dev/null +++ b/src/pull-down-refresh/style/index.js @@ -0,0 +1 @@ +import '@common/style/mobile/components/pull-down-refresh/_index.less'; diff --git a/src/pull-down-refresh/type.ts b/src/pull-down-refresh/type.ts new file mode 100644 index 00000000..e2e84093 --- /dev/null +++ b/src/pull-down-refresh/type.ts @@ -0,0 +1,42 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdLoadingProps } from '../loading'; + +export interface TdPullDownRefreshProps { + /** + * 加载中下拉高度 + * @default 50 + */ + loadingBarHeight?: number; + /** + * 加载loading样式 + */ + loadingProps?: TdLoadingProps; + /** + * 提示语,组件内部默认值为 ['下拉刷新', '松手刷新', '正在刷新', '刷新完成'] + * @default [] + */ + loadingTexts?: string[]; + /** + * 最大下拉高度 + * @default 80 + */ + maxBarHeight?: number; + /** + * 刷新超时时间 + * @default 3000 + */ + refreshTimeout?: number; + /** + * 结束下拉时触发 + */ + onRefresh?: () => void; + /** + * 刷新超时触发 + */ + onTimeout?: () => void; +}