Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add BackToTop #449

Merged
merged 3 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions components/back-to-top/__tests__/__snapshots__/index.test.jsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Badge renders correctly 1`] = `
<BackToTop
prefixCls="za-back-to-top"
scrollContainer={[Window]}
speed={100}
visibleDistance={400}
>
<Portal
containerInfo={
<div
class="za-back-to-top-container"
>
<div
class="za-back-to-top"
style="display: none;"
>
Up
</div>
</div>
}
>
<div
className="za-back-to-top"
onClick={[Function]}
style={
Object {
"display": "none",
}
}
>
Up
</div>
</Portal>
</BackToTop>
`;
11 changes: 11 additions & 0 deletions components/back-to-top/__tests__/index.test.jsx
Original file line number Diff line number Diff line change
@@ -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(<BackToTop>Up</BackToTop>);
expect(toJson(wrapper)).toMatchSnapshot();
});
});
78 changes: 78 additions & 0 deletions components/back-to-top/demo.md
Original file line number Diff line number Diff line change
@@ -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(<Cell key={+i}>第 {i + 1} 行</Cell>);

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 (
<>
<Message theme="warning" icon={<Icon type="warning-round" />}>
当前使用的是 `{useWindowScroll ? 'window' : 'div' }` 作为滚动容器。
<Button theme="primary" size="xs" onClick={() => setUserWindowScroll(!useWindowScroll)}>点击切换</Button>
</Message>
<div ref={containerRef} style={styles.container}>{list}</div>
<BackToTop
scrollContainer={scrollContainer}
onClick={() => console.log('click back to top')}
>
<span style={styles.button}>Up</span>
</BackToTop>
</>
)
};

ReactDOM.render(<Demo />, mountNode);
```



## API

| 属性 | 类型 | 默认值 | 说明 |
| :--- | :--- | :--- | :--- |
| speed | number | 100 | 每10毫秒滑动的距离 |
| visibleDistance | number | 400 | 离滚动条顶部的可视距离 |
| scrollContainer | HTMLElement \| () => HTMLElement | window | 设置滚动容器 |
| onClick | (event?: MouseEvent&lt;HTMLDivElement&gt;) => void | - | 点击后触发的回调函数 |
156 changes: 156 additions & 0 deletions components/back-to-top/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@

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<HTMLDivElement>) => void;
}

export interface BackToTopStates {
visible: boolean;
}

export default class BackToTop extends PureComponent<BackToTopProps, BackToTopStates> {
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 = () => {
Throttle(() => {
this.setState({
visible: this.getScrollTop > this.props.visibleDistance!,
});
}, 250);
};

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<HTMLDivElement>) => {
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', this.onScroll);
}
}

unBindEvent() {
clearInterval(this.timer);
if (this.getScrollContainer) {
this.getParentElement instanceof HTMLElement && this.getParentElement.removeChild(this.container);
Events.off(this.getScrollContainer, 'scroll', this.onScroll);
}
}

render() {
const { prefixCls, children } = this.props;
const { visible } = this.state;
const style: CSSProperties = {};

if (!visible) {
style.display = 'none';
}

return createPortal(
<div className={prefixCls} style={style} onClick={this.scrollToTop}>
{children}
</div>,
this.getContainer,
);
}
}
2 changes: 2 additions & 0 deletions components/back-to-top/style/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

import '../../style';
1 change: 1 addition & 0 deletions components/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
6 changes: 6 additions & 0 deletions site/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down