From 23f56b1a730b7f39bce800b6cf438bca131c4fce Mon Sep 17 00:00:00 2001 From: Jerome Lin Date: Tue, 28 Apr 2020 19:10:52 +0800 Subject: [PATCH 1/3] feat: add BackToTop --- .../__snapshots__/index.test.jsx.snap | 37 +++++ .../back-to-top/__tests__/index.test.jsx | 11 ++ components/back-to-top/demo.md | 78 +++++++++ components/back-to-top/index.tsx | 154 ++++++++++++++++++ components/back-to-top/style/index.tsx | 2 + components/index.tsx | 1 + site/site.config.js | 6 + 7 files changed, 289 insertions(+) create mode 100644 components/back-to-top/__tests__/__snapshots__/index.test.jsx.snap create mode 100644 components/back-to-top/__tests__/index.test.jsx create mode 100644 components/back-to-top/demo.md create mode 100644 components/back-to-top/index.tsx create mode 100644 components/back-to-top/style/index.tsx diff --git a/components/back-to-top/__tests__/__snapshots__/index.test.jsx.snap b/components/back-to-top/__tests__/__snapshots__/index.test.jsx.snap new file mode 100644 index 000000000..78a6caae3 --- /dev/null +++ b/components/back-to-top/__tests__/__snapshots__/index.test.jsx.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Badge renders correctly 1`] = ` + + + + + } + > +
+ Up +
+
+
+`; diff --git a/components/back-to-top/__tests__/index.test.jsx b/components/back-to-top/__tests__/index.test.jsx new file mode 100644 index 000000000..10b56c177 --- /dev/null +++ b/components/back-to-top/__tests__/index.test.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import BackToTop from '../index'; + +describe('Badge', () => { + it('renders correctly', () => { + const wrapper = mount(Up); + expect(toJson(wrapper)).toMatchSnapshot(); + }); +}); diff --git a/components/back-to-top/demo.md b/components/back-to-top/demo.md new file mode 100644 index 000000000..a19140e47 --- /dev/null +++ b/components/back-to-top/demo.md @@ -0,0 +1,78 @@ + +# BackToTop 返回顶部 + + + +## 基本用法 +```jsx +import { useRef, useState } from 'react'; +import { Cell, BackToTop, Message, Button, Icon } from 'zarm'; + +const Demo = () => { + const containerRef = useRef(); + const [useWindowScroll, setUserWindowScroll] = useState(true); + + const list = []; + for (let i = 0; i < 100; i++) + list.push(第 {i + 1} 行); + + const styles = { + button: { + position: 'fixed', + bottom: 30, + right: 30, + width: 60, + height: 60, + lineHeight: '60px', + textAlign: 'center', + backgroundColor: '#fff', + color: '#999', + fontSize: 20, + borderRadius: 30, + opacity: 0.9, + boxShadow: '0 2px 10px 0 rgba(0, 0, 0, 0.3)', + cursor: 'pointer', + } + }; + + let scrollContainer; + + if (!useWindowScroll) { + styles.container = { + overflow: 'auto', + maxHeight: 400, + }; + styles.button.position = 'absolute'; + scrollContainer = () => containerRef.current; + } + + return ( + <> + }> + 当前使用的是 `{useWindowScroll ? 'window' : 'div' }` 作为滚动容器。 + + +
{list}
+ console.log('click back to top')} + > + Up + + + ) +}; + +ReactDOM.render(, mountNode); +``` + + + +## API + +| 属性 | 类型 | 默认值 | 说明 | +| :--- | :--- | :--- | :--- | +| speed | number | 100 | 每10毫秒滑动的距离 | +| visibleDistance | number | 400 | 离滚动条顶部的可视距离 | +| scrollContainer | () => HTMLElement | () => document.body | 设置滚动容器 | +| onClick | (event?: MouseEvent<HTMLDivElement>) => void | - | 点击后触发的回调函数 | diff --git a/components/back-to-top/index.tsx b/components/back-to-top/index.tsx new file mode 100644 index 000000000..7ed9b84c7 --- /dev/null +++ b/components/back-to-top/index.tsx @@ -0,0 +1,154 @@ + +import React, { PureComponent, MouseEvent, CSSProperties } from 'react'; +import { createPortal } from 'react-dom'; +import classnames from 'classnames'; +import Events from '../utils/events'; +import Throttle from '../utils/throttle'; + +type getContainerFunc = () => HTMLElement; + +export interface BackToTopProps { + prefixCls?: string; + className?: string; + speed?: number; + visibleDistance?: number; + scrollContainer?: HTMLElement | getContainerFunc; + onClick: (event?: MouseEvent) => void; +} + +export interface BackToTopStates { + visible: boolean; +} + +export default class BackToTop extends PureComponent { + static displayName = 'BackToTop'; + + static defaultProps = { + prefixCls: 'za-back-to-top', + speed: 100, + visibleDistance: 400, + scrollContainer: window, + }; + + readonly state: BackToTopStates = { + visible: false, + }; + + private container: HTMLDivElement; + + private timer: number; + + componentDidMount() { + this.bindEvent(); + } + + componentDidUpdate(prevProps: BackToTopProps) { + const { scrollContainer } = this.props; + if (prevProps.scrollContainer !== scrollContainer) { + this.bindEvent(); + } + } + + componentWillUnmount() { + this.unBindEvent(); + } + + onScroll = () => { + this.setState({ + visible: this.getScrollTop > this.props.visibleDistance!, + }); + }; + + get getParentElement(): Element { + if (typeof this.getScrollContainer === 'object' && this.getScrollContainer instanceof Window) { + return document.body; + } + return this.getScrollContainer; + } + + get getScrollContainer(): HTMLElement | Window { + const { scrollContainer } = this.props; + if (scrollContainer) { + if (typeof scrollContainer === 'function') { + return scrollContainer(); + } + if (typeof scrollContainer === 'object' && scrollContainer instanceof HTMLElement) { + return scrollContainer; + } + } + return window; + } + + get getScrollTop(): number { + return this.getScrollContainer !== window + ? (this.getScrollContainer as HTMLElement).scrollTop + : document.documentElement.scrollTop + document.body.scrollTop; + } + + get getContainer(): HTMLElement { + const { prefixCls, className } = this.props; + if (!this.container) { + const container = document.createElement('div'); + container.className = classnames(`${prefixCls}-container`, className); + this.container = container; + } + return this.container; + } + + scrollToTop = (e: MouseEvent) => { + const { speed, onClick } = this.props; + + if (typeof onClick === 'function') { + onClick(e); + } + + // 速度设置为0或者无穷大时,直接到顶 + if (speed === 0 || speed === Infinity) { + this.getScrollContainer.scrollTo(0, 0); + return; + } + + this.timer = setInterval(() => { + let st = this.getScrollTop; + st -= speed!; + if (st > 0) { + this.getScrollContainer.scrollTo(0, st); + } else { + this.getScrollContainer.scrollTo(0, 0); + clearInterval(this.timer); + } + }, 10); + }; + + bindEvent() { + if (this.getScrollContainer) { + this.getParentElement instanceof HTMLElement && this.getParentElement.appendChild(this.container); + Events.on(this.getScrollContainer, 'scroll', Throttle(this.onScroll, 250)); + } + } + + unBindEvent() { + clearInterval(this.timer); + if (this.getScrollContainer) { + this.getParentElement instanceof HTMLElement && this.getParentElement.removeChild(this.container); + Events.off(this.getScrollContainer, 'scroll', Throttle(this.onScroll, 250)); + } + } + + render() { + const { prefixCls, children } = this.props; + const { visible } = this.state; + const style: CSSProperties = {}; + + if (!visible) { + style.display = 'none'; + } + + return createPortal( +
+ {children} +
, + this.getContainer, + ); + } +} diff --git a/components/back-to-top/style/index.tsx b/components/back-to-top/style/index.tsx new file mode 100644 index 000000000..7e8273286 --- /dev/null +++ b/components/back-to-top/style/index.tsx @@ -0,0 +1,2 @@ + +import '../../style'; diff --git a/components/index.tsx b/components/index.tsx index 36d5b162d..211b0f838 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -1,6 +1,7 @@ export { default as Collapse } from './collapse'; export { default as ActionSheet } from './action-sheet'; // export { default as Alert } from './alert'; +export { default as BackToTop } from './back-to-top'; export { default as Badge } from './badge'; export { default as Button } from './button'; export { default as Calendar } from './calendar'; diff --git a/site/site.config.js b/site/site.config.js index f014aaf25..b9c25506b 100644 --- a/site/site.config.js +++ b/site/site.config.js @@ -259,6 +259,12 @@ module.exports = { module: () => import('@/components/drag/demo.md'), style: false, }, + { + key: 'back-to-top', + name: '返回顶部', + module: () => import('@/components/back-to-top/demo.md'), + style: false, + }, ], }, design: [ From 2270541bfbf9ad71fbac86e87e8f045b2e80d7e5 Mon Sep 17 00:00:00 2001 From: Jerome Lin Date: Tue, 28 Apr 2020 19:24:34 +0800 Subject: [PATCH 2/3] docs: update api --- components/back-to-top/demo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/back-to-top/demo.md b/components/back-to-top/demo.md index a19140e47..f9815054c 100644 --- a/components/back-to-top/demo.md +++ b/components/back-to-top/demo.md @@ -74,5 +74,5 @@ ReactDOM.render(, mountNode); | :--- | :--- | :--- | :--- | | speed | number | 100 | 每10毫秒滑动的距离 | | visibleDistance | number | 400 | 离滚动条顶部的可视距离 | -| scrollContainer | () => HTMLElement | () => document.body | 设置滚动容器 | +| scrollContainer | HTMLElement \| () => HTMLElement | window | 设置滚动容器 | | onClick | (event?: MouseEvent<HTMLDivElement>) => void | - | 点击后触发的回调函数 | From 94761898b037220938681af9f3de682e0a2a7133 Mon Sep 17 00:00:00 2001 From: Jerome Lin Date: Wed, 29 Apr 2020 14:59:28 +0800 Subject: [PATCH 3/3] fix: unmount will not unbind event --- components/back-to-top/index.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/components/back-to-top/index.tsx b/components/back-to-top/index.tsx index 7ed9b84c7..81b2b21e4 100644 --- a/components/back-to-top/index.tsx +++ b/components/back-to-top/index.tsx @@ -54,9 +54,11 @@ export default class BackToTop extends PureComponent { - this.setState({ - visible: this.getScrollTop > this.props.visibleDistance!, - }); + Throttle(() => { + this.setState({ + visible: this.getScrollTop > this.props.visibleDistance!, + }); + }, 250); }; get getParentElement(): Element { @@ -123,7 +125,7 @@ export default class BackToTop extends PureComponent