10 |
16 |
17 | {/* todos: 现在如果 getScrollContainer 变化,需要设置新 key 来触发 componentWillUnmount */}
18 |
document.body }
23 | : {})}
24 | className="forTest"
25 | direction="down"
26 | refreshing={refreshing}
27 | onRefresh={() => {
28 | setRefreshing(true);
29 | setTimeout(() => {
30 | setRefreshing(false);
31 | }, 1000);
32 | }}
33 | indicator={{ deactivate: '下拉' }}
34 | damping={150}
35 | >
36 | {[1, 2, 3, 4, 5, 6, 7].map(i => (
37 | alert(1)}
41 | >
42 | item {i}
43 |
44 | ))}
45 |
46 |
47 |
#qrcode, .highlight{ display: none }'
51 | : '',
52 | }}
53 | />
54 |
55 | );
56 | };
57 |
58 | export default Example1;
59 |
--------------------------------------------------------------------------------
/packages/dialog/examples/Example.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import Dialog from '../src';
4 |
5 | const Example: React.FunctionComponent = props => {
6 | const [visible, setVisible] = React.useState(false);
7 | const [visible2, setVisible2] = React.useState(false);
8 | const [center, setCenter] = React.useState(false);
9 |
10 | const onClick = () => {
11 | setVisible(true);
12 | };
13 |
14 | const onClose = () => {
15 | setVisible(false);
16 | };
17 |
18 | const onCenter = e => {
19 | setCenter(e.target.checked);
20 | };
21 |
22 | const showDialog2 = () => {
23 | setVisible2(true);
24 | };
25 |
26 | const getDialog1 = () => {
27 | const wrapClassName = center ? 'center' : '';
28 |
29 | return (
30 |
44 | );
45 | };
46 |
47 | const getDialog2 = () => {
48 | return (
49 |
64 | );
65 | };
66 |
67 | return (
68 |
74 |
83 |
84 |
87 |
88 |
92 |
93 | {getDialog1()}
94 | {getDialog2()}
95 |
96 | );
97 | };
98 |
99 | export default Example;
100 |
--------------------------------------------------------------------------------
/packages/pull-to-refresh/README.md:
--------------------------------------------------------------------------------
1 | # @antd-mobile/pull-to-refresh
2 |
3 | ---
4 |
5 | React Mobile PullToRefresh Component.
6 |
7 | [![NPM version][npm-image]][npm-url]
8 |
9 | [npm-image]: http://img.shields.io/npm/v/@antd-mobile/pull-to-refresh.svg?style=flat-square
10 | [npm-url]: http://npmjs.org/package/@antd-mobile/pull-to-refresh
11 | [download-image]: https://img.shields.io/npm/dm/@antd-mobile/pull-to-refresh.svg?style=flat-square
12 | [download-url]: https://npmjs.org/package/@antd-mobile/pull-to-refresh
13 |
14 | ## Screenshots
15 |
16 |

17 |
18 | ## API
19 |
20 | ### props
21 |
22 | | name | description | type | default |
23 | | ----------------- | ------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------- |
24 | | direction | pull direction, can be `up` or `down` | String | `down` |
25 | | distanceToRefresh | distance to pull to refresh | number | 50 |
26 | | refreshing | Whether the view should be indicating an active refresh | bool | false |
27 | | onRefresh | Called when the view starts refreshing. | () => void | - |
28 | | indicator | indicator config | Object | `{ activate: 'release', deactivate: 'pull', release: 'loading', finish: 'finish' }` |
29 | | className | additional css class of root dom node | String | - |
30 | | prefixCls | prefix class | String | 'rmc-pull-to-refresh' |
31 | | damping | pull damping, suggest less than 200 | number | 100 |
32 |
--------------------------------------------------------------------------------
/packages/feedback/tests/feedback.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, mount } from 'enzyme';
3 | import { html } from 'cheerio';
4 | import TouchFeedback from '../src';
5 |
6 | describe('basic', () => {
7 | it('base.', () => {
8 | const wrapper = render(
9 |
14 |
15 | click to active
16 |
17 | ,
18 | );
19 | expect(html(wrapper)).toMatchSnapshot();
20 | });
21 |
22 | it('works ok', async () => {
23 | const instance = mount(
24 |
25 |
26 | click to active
27 |
28 | ,
29 | );
30 |
31 | const d = instance.find(TouchFeedback);
32 | const n = d.find('div').getDOMNode();
33 |
34 | d.simulate('touchstart');
35 | expect(d.state()).toHaveProperty('active', true);
36 | expect(n.className).toBe('normal active');
37 | expect(n.getAttribute('style')).toBe('color: red;');
38 |
39 | d.simulate('touchend');
40 | expect(d.state()).toHaveProperty('active', false);
41 | expect(n.className).toBe('normal');
42 | expect(n.getAttribute('style')).toBe('color: rgb(0, 0, 0);');
43 | });
44 |
45 | it('activeStyle false', async () => {
46 | const instance = mount(
47 |
48 |
49 | click to active
50 |
51 | ,
52 | );
53 |
54 | const d = instance.find(TouchFeedback);
55 | const n = d.find('div').getDOMNode();
56 |
57 | d.simulate('touchstart');
58 | expect(n.className).toBe('normal');
59 | });
60 |
61 | it('disabled', async () => {
62 | const instance = mount(
63 |
68 |
69 | click to active
70 |
71 | ,
72 | );
73 |
74 | const d = instance.find(TouchFeedback);
75 | const n = d.find('div').getDOMNode();
76 |
77 | d.simulate('touchstart');
78 | expect(n.className).toBe('normal');
79 | expect(n.getAttribute('style')).toBe('color: rgb(0, 0, 0);');
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/packages/feedback/src/TouchFeedback.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import classNames from 'classnames';
3 | import { ITouchProps, ITouchState } from './PropTypes';
4 |
5 | export default class TouchFeedback extends React.Component<
6 | ITouchProps,
7 | ITouchState
8 | > {
9 | static defaultProps = {
10 | disabled: false,
11 | };
12 |
13 | state = {
14 | active: false,
15 | };
16 |
17 | componentDidUpdate() {
18 | if (this.props.disabled && this.state.active) {
19 | this.setState({
20 | active: false,
21 | });
22 | }
23 | }
24 |
25 | triggerEvent(type, isActive, ev) {
26 | const eventType = `on${type}`;
27 | const { children } = this.props;
28 |
29 | if (children.props[eventType]) {
30 | children.props[eventType](ev);
31 | }
32 | if (isActive !== this.state.active) {
33 | this.setState({
34 | active: isActive,
35 | });
36 | }
37 | }
38 |
39 | onTouchStart = e => {
40 | this.triggerEvent('TouchStart', true, e);
41 | };
42 |
43 | onTouchMove = e => {
44 | this.triggerEvent('TouchMove', false, e);
45 | };
46 |
47 | onTouchEnd = e => {
48 | this.triggerEvent('TouchEnd', false, e);
49 | };
50 |
51 | onTouchCancel = e => {
52 | this.triggerEvent('TouchCancel', false, e);
53 | };
54 |
55 | onMouseDown = e => {
56 | // pc simulate mobile
57 | this.triggerEvent('MouseDown', true, e);
58 | };
59 |
60 | onMouseUp = e => {
61 | this.triggerEvent('MouseUp', false, e);
62 | };
63 |
64 | onMouseLeave = e => {
65 | this.triggerEvent('MouseLeave', false, e);
66 | };
67 |
68 | render() {
69 | const { children, disabled, activeClassName, activeStyle } = this.props;
70 |
71 | const events = disabled
72 | ? undefined
73 | : {
74 | onTouchStart: this.onTouchStart,
75 | onTouchMove: this.onTouchMove,
76 | onTouchEnd: this.onTouchEnd,
77 | onTouchCancel: this.onTouchCancel,
78 | onMouseDown: this.onMouseDown,
79 | onMouseUp: this.onMouseUp,
80 | onMouseLeave: this.onMouseLeave,
81 | };
82 |
83 | const child = React.Children.only(children);
84 |
85 | if (!disabled && this.state.active) {
86 | let { style, className } = child.props;
87 |
88 | if (activeStyle !== false) {
89 | if (activeStyle) {
90 | style = { ...style, ...activeStyle };
91 | }
92 | className = classNames(className, activeClassName);
93 | }
94 |
95 | return React.cloneElement(child, {
96 | className,
97 | style,
98 | ...events,
99 | });
100 | }
101 |
102 | return React.cloneElement(child, events);
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/packages/dialog/README.md:
--------------------------------------------------------------------------------
1 | # @antd-mobile/dialog
2 |
3 | ---
4 |
5 | react dialog component for mobile
6 |
7 | [![NPM version][npm-image]][npm-url]
8 |
9 | [npm-image]: http://img.shields.io/npm/v/@antd-mobile/dialog.svg?style=flat-square
10 | [npm-url]: http://npmjs.org/package/@antd-mobile/dialog
11 | [download-image]: https://img.shields.io/npm/dm/@antd-mobile/dialog.svg?style=flat-square
12 | [download-url]: https://npmjs.org/package/@antd-mobile/dialog
13 |
14 | ## Screenshot
15 |
16 |

17 |
18 | ## API
19 |
20 | ### props
21 |
22 | | name | description | type | default |
23 | | ------------------ | --------------------------------------------------- | ------------- | ------------- |
24 | | prefixCls | The dialog dom node's prefixCls | String | `rmc-dialog` |
25 | | className | additional className for dialog | String | |
26 | | wrapClassName | additional className for dialog wrap | String | |
27 | | style | Root style for dialog element.Such as width, height | Object | {} |
28 | | zIndex | z-index | Number | |
29 | | bodyStyle | body style for dialog body element.Such as height | Object | {} |
30 | | maskStyle | style for mask element. | Object | {} |
31 | | visible | current dialog's visible status | Boolean | false |
32 | | animation | part of dialog animation css class name | String | |
33 | | maskAnimation | part of dialog's mask animation css class name | String | |
34 | | transitionName | dialog animation css class name | String | |
35 | | maskTransitionName | mask animation css class name | String | |
36 | | title | Title of the dialog | String | React.Element | |
37 | | footer | footer of the dialog | React.Element | |
38 | | closable | whether show close button | Boolean | true |
39 | | mask | whether show mask | Boolean | true |
40 | | maskClosable | whether click mask to close | Boolean | true |
41 | | onClose | called when click close button or mask | function | |
42 |
--------------------------------------------------------------------------------
/packages/dialog/src/style/dialog.less:
--------------------------------------------------------------------------------
1 | .@{prefixCls} {
2 | position: relative;
3 | width: auto;
4 | margin: 10px;
5 |
6 | &-wrap {
7 | position: fixed;
8 | z-index: 1050;
9 | top: 0;
10 | right: 0;
11 | bottom: 0;
12 | left: 0;
13 | overflow: auto;
14 | outline: 0;
15 | -webkit-overflow-scrolling: touch;
16 | }
17 |
18 | &-title {
19 | margin: 0;
20 | font-size: 14px;
21 | font-weight: bold;
22 | line-height: 21px;
23 | }
24 |
25 | &-content {
26 | position: relative;
27 | border: none;
28 | background-clip: padding-box;
29 | background-color: #fff;
30 | border-radius: 6px 6px;
31 | }
32 |
33 | &-close {
34 | position: absolute;
35 | top: 12px;
36 | right: 20px;
37 | border: 0;
38 | background: transparent;
39 | color: #000;
40 | cursor: pointer;
41 | filter: alpha(opacity=20);
42 | font-size: 21px;
43 | font-weight: 700;
44 | line-height: 1;
45 | opacity: 0.2;
46 | text-decoration: none;
47 | text-shadow: 0 1px 0 #fff;
48 |
49 | &-x::after {
50 | content: '×';
51 | }
52 |
53 | &:hover {
54 | filter: alpha(opacity=100);
55 | opacity: 1;
56 | text-decoration: none;
57 | }
58 | }
59 |
60 | &-header {
61 | padding: 13px 20px 14px 20px;
62 | border-bottom: 1px solid #e9e9e9;
63 | background: #fff;
64 | border-radius: 5px 5px 0 0;
65 | color: #666;
66 | }
67 |
68 | &-body {
69 | padding: 20px;
70 | }
71 |
72 | &-footer {
73 | padding: 10px 20px 10px 10px;
74 | border-top: 1px solid #e9e9e9;
75 | border-radius: 0 0 5px 5px;
76 | text-align: right;
77 | }
78 |
79 | .effect() {
80 | animation-duration: 0.3s;
81 | animation-fill-mode: both;
82 | }
83 |
84 | &-zoom-enter,
85 | &-zoom-appear {
86 | animation-play-state: paused;
87 | animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
88 | opacity: 0;
89 | .effect();
90 | }
91 |
92 | &-zoom-leave {
93 | .effect();
94 |
95 | animation-play-state: paused;
96 | animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
97 | }
98 |
99 | &-zoom-enter&-zoom-enter-active,
100 | &-zoom-appear&-zoom-appear-active {
101 | animation-name: rcDialogZoomIn;
102 | animation-play-state: running;
103 | }
104 |
105 | &-zoom-leave&-zoom-leave-active {
106 | animation-name: rcDialogZoomOut;
107 | animation-play-state: running;
108 | }
109 |
110 | @keyframes rcDialogZoomIn {
111 | 0% {
112 | opacity: 0;
113 | transform: scale(0, 0);
114 | }
115 |
116 | 100% {
117 | opacity: 1;
118 | transform: scale(1, 1);
119 | }
120 | }
121 |
122 | @keyframes rcDialogZoomOut {
123 | 0% {
124 | transform: scale(1, 1);
125 | }
126 |
127 | 100% {
128 | opacity: 0;
129 | transform: scale(0, 0);
130 | }
131 | }
132 | }
133 |
134 | @media (min-width: 768px) {
135 | .@{prefixCls} {
136 | width: 600px;
137 | margin: 30px auto;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/packages/dialog/src/DialogWrap.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import Dialog from './Dialog';
4 | import IDialogPropTypes from './IDialogPropTypes';
5 |
6 | function noop() {}
7 |
8 | const IS_REACT_16 = !!ReactDOM.createPortal;
9 |
10 | const CAN_USE_DOM = !!(
11 | typeof window !== 'undefined' &&
12 | window.document &&
13 | window.document.createElement
14 | );
15 |
16 | export default class DialogWrap extends React.Component
{
17 | static defaultProps = {
18 | visible: false,
19 | prefixCls: 'rmc-dialog',
20 | onClose: noop,
21 | };
22 |
23 | _component: any;
24 | container: any;
25 |
26 | componentDidMount() {
27 | if (this.props.visible) {
28 | this.componentDidUpdate();
29 | }
30 | }
31 |
32 | shouldComponentUpdate({ visible }) {
33 | return !!(this.props.visible || visible);
34 | }
35 |
36 | componentWillUnmount() {
37 | if (this.props.visible) {
38 | if (!IS_REACT_16) {
39 | this.renderDialog(false);
40 | } else {
41 | // TODO for react@16 createPortal animation
42 | this.removeContainer();
43 | }
44 | } else {
45 | this.removeContainer();
46 | }
47 | }
48 |
49 | componentDidUpdate() {
50 | if (!IS_REACT_16) {
51 | this.renderDialog(this.props.visible);
52 | }
53 | }
54 |
55 | saveRef = node => {
56 | if (IS_REACT_16) {
57 | this._component = node;
58 | }
59 | };
60 |
61 | getComponent = visible => {
62 | const props = { ...this.props };
63 | ['visible', 'onAnimateLeave'].forEach(key => {
64 | if (props.hasOwnProperty(key)) {
65 | delete props[key];
66 | }
67 | });
68 | return (
69 |
75 | );
76 | };
77 |
78 | removeContainer = () => {
79 | if (this.container) {
80 | if (!IS_REACT_16) {
81 | ReactDOM.unmountComponentAtNode(this.container);
82 | }
83 | (this.container as any).parentNode.removeChild(this.container);
84 | this.container = null;
85 | }
86 | };
87 |
88 | getContainer = () => {
89 | if (!this.container) {
90 | const container = document.createElement('div');
91 | const containerId = `${
92 | this.props.prefixCls
93 | }-container-${new Date().getTime()}`;
94 | container.setAttribute('id', containerId);
95 | document.body.appendChild(container);
96 | this.container = container;
97 | }
98 | return this.container;
99 | };
100 |
101 | renderDialog(visible) {
102 | ReactDOM.unstable_renderSubtreeIntoContainer(
103 | this,
104 | this.getComponent(visible),
105 | this.getContainer(),
106 | );
107 | }
108 |
109 | render() {
110 | if (!CAN_USE_DOM) {
111 | return null;
112 | }
113 |
114 | const { visible } = this.props;
115 | if (IS_REACT_16 && (visible || this._component)) {
116 | return (ReactDOM as any).createPortal(
117 | this.getComponent(visible),
118 | this.getContainer(),
119 | );
120 | }
121 | return null as any;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | working_directory: &working_directory ~/workspace
3 |
4 | base: &base
5 | working_directory: *working_directory
6 | docker:
7 | - image: circleci/node:12
8 | steps:
9 | - checkout
10 | - restore_cache:
11 | key: rmc-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}-{{ checksum "package.json" }}
12 | - run:
13 | name: Install dependencies
14 | command: npm i
15 | - run:
16 | name: Lerna Bootstrap
17 | command: npm run bootstrap
18 | - run:
19 | name: Build
20 | command: npm run ci
21 | - run:
22 | name: Report coverage
23 | command: npm run codecov
24 | - save_cache:
25 | key: rmc-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}-{{ checksum "package.json" }}
26 | paths:
27 | - 'node_modules'
28 |
29 | jobs:
30 | build_node_10:
31 | <<: *base
32 | docker:
33 | - image: circleci/node:10
34 |
35 | build_node_12:
36 | <<: *base
37 | docker:
38 | - image: circleci/node:12
39 |
40 | publish_alpha:
41 | <<: *base
42 | steps:
43 | - checkout
44 | - restore_cache:
45 | key: rmc-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}-{{ checksum "package.json" }}
46 | - run:
47 | name: Install dependencies
48 | command: npm i
49 | - run:
50 | name: Lerna Bootstrap
51 | command: npm run bootstrap
52 | - run:
53 | name: Publish alpha
54 | command: |
55 | set -o errexit
56 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
57 | BRANCH=alpha node ./scripts/publish.js
58 | - run:
59 | name: Report coverage
60 | command: npm run codecov
61 | - run:
62 | name: Doc
63 | command: npm run doc:deploy
64 | - save_cache:
65 | key: rmc-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}-{{ checksum "package.json" }}
66 | paths:
67 | - 'node_modules'
68 |
69 | publish:
70 | <<: *base
71 | steps:
72 | - checkout
73 | - restore_cache:
74 | key: rmc-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}-{{ checksum "package.json" }}
75 | - run:
76 | name: Install dependencies
77 | command: npm i
78 | - run:
79 | name: Lerna Bootstrap
80 | command: npm run bootstrap
81 | - run:
82 | name: Publish
83 | command: |
84 | set -o errexit
85 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
86 | node ./scripts/publish.js
87 | - run:
88 | name: Report coverage
89 | command: npm run codecov
90 | - run:
91 | name: Doc
92 | command: npm run doc:deploy
93 | - save_cache:
94 | key: rmc-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}-{{ checksum "package.json" }}
95 | paths:
96 | - 'node_modules'
97 |
98 | workflows:
99 | version: 2
100 | build:
101 | jobs:
102 | - build_node_10
103 | - build_node_12
104 |
105 | release-alpha:
106 | jobs:
107 | - publish_alpha:
108 | filters:
109 | branches:
110 | only:
111 | - alpha
112 |
113 | release:
114 | jobs:
115 | - publish:
116 | filters:
117 | branches:
118 | only:
119 | - master
120 |
--------------------------------------------------------------------------------
/packages/dialog/src/Dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Animate from 'rc-animate';
3 | import LazyRenderBox from './LazyRenderBox';
4 | import IDialogPropTypes from './IDialogPropTypes';
5 |
6 | function noop() {}
7 |
8 | export default class Dialog extends React.Component {
9 | static defaultProps = {
10 | afterClose: noop,
11 | className: '',
12 | mask: true,
13 | visible: false,
14 | closable: true,
15 | maskClosable: true,
16 | prefixCls: 'rmc-dialog',
17 | onClose: noop,
18 | };
19 |
20 | dialogRef: any;
21 | bodyRef: any;
22 | headerRef: any;
23 | footerRef: any;
24 | wrapRef: any;
25 |
26 | componentWillUnmount() {
27 | // fix: react@16 no dismissing animation
28 | document.body.style.overflow = '';
29 | if (this.wrapRef) {
30 | this.wrapRef.style.display = 'none';
31 | }
32 | }
33 |
34 | getZIndexStyle() {
35 | const style: any = {};
36 | const props = this.props;
37 | if (props.zIndex !== undefined) {
38 | style.zIndex = props.zIndex;
39 | }
40 | return style;
41 | }
42 |
43 | getWrapStyle() {
44 | const wrapStyle = this.props.wrapStyle || {};
45 | return { ...this.getZIndexStyle(), ...wrapStyle };
46 | }
47 |
48 | getMaskStyle() {
49 | const maskStyle = this.props.maskStyle || {};
50 | return { ...this.getZIndexStyle(), ...maskStyle };
51 | }
52 |
53 | getMaskTransitionName() {
54 | const props = this.props;
55 | let transitionName = props.maskTransitionName;
56 | const animation = props.maskAnimation;
57 | if (!transitionName && animation) {
58 | transitionName = `${props.prefixCls}-${animation}`;
59 | }
60 | return transitionName;
61 | }
62 |
63 | getTransitionName() {
64 | const props = this.props;
65 | let transitionName = props.transitionName;
66 | const animation = props.animation;
67 | if (!transitionName && animation) {
68 | transitionName = `${props.prefixCls}-${animation}`;
69 | }
70 | return transitionName;
71 | }
72 |
73 | getMaskElement() {
74 | const props = this.props;
75 | let maskElement;
76 | if (props.mask) {
77 | const maskTransition = this.getMaskTransitionName();
78 | maskElement = (
79 |
87 | );
88 | if (maskTransition) {
89 | maskElement = (
90 |
97 | {maskElement}
98 |
99 | );
100 | }
101 | }
102 | return maskElement;
103 | }
104 |
105 | getDialogElement = () => {
106 | const props = this.props;
107 | const closable = props.closable;
108 | const prefixCls = props.prefixCls;
109 |
110 | let footer;
111 | if (props.footer) {
112 | footer = (
113 | (this.footerRef = el)}
116 | >
117 | {props.footer}
118 |
119 | );
120 | }
121 |
122 | let header;
123 | if (props.title) {
124 | header = (
125 | (this.headerRef = el)}
128 | >
129 |
{props.title}
130 |
131 | );
132 | }
133 |
134 | let closer;
135 | if (closable) {
136 | closer = (
137 |
144 | );
145 | }
146 |
147 | const transitionName = this.getTransitionName();
148 | const dialogElement = (
149 | (this.dialogRef = el)}
153 | style={props.style || {}}
154 | className={`${prefixCls} ${props.className || ''}`}
155 | visible={props.visible}
156 | >
157 |
158 | {closer}
159 | {header}
160 |
(this.bodyRef = el)}
164 | >
165 | {props.children}
166 |
167 | {footer}
168 |
169 |
170 | );
171 | return (
172 |
181 | {dialogElement}
182 |
183 | );
184 | };
185 |
186 | onAnimateAppear = () => {
187 | document.body.style.overflow = 'hidden';
188 | };
189 |
190 | onAnimateLeave = () => {
191 | document.body.style.overflow = '';
192 | if (this.wrapRef) {
193 | this.wrapRef.style.display = 'none';
194 | }
195 | if (this.props.onAnimateLeave) {
196 | this.props.onAnimateLeave();
197 | }
198 | if (this.props.afterClose) {
199 | this.props.afterClose();
200 | }
201 | };
202 |
203 | close = e => {
204 | if (this.props.onClose) {
205 | this.props.onClose(e);
206 | }
207 | };
208 |
209 | onMaskClick = e => {
210 | if (e.target === e.currentTarget) {
211 | this.close(e);
212 | }
213 | };
214 |
215 | render() {
216 | const { props } = this;
217 | const { prefixCls, maskClosable } = props;
218 | const style = this.getWrapStyle();
219 | if (props.visible) {
220 | style.display = null;
221 | }
222 | return (
223 |
224 | {this.getMaskElement()}
225 |
(this.wrapRef = el)}
228 | onClick={maskClosable ? this.onMaskClick : undefined}
229 | role="dialog"
230 | aria-labelledby={props.title}
231 | style={style}
232 | {...props.wrapProps}
233 | >
234 | {this.getDialogElement()}
235 |
236 |
237 | );
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/packages/pull-to-refresh/src/PullToRefresh.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import classNames from 'classnames';
3 | import { IPropsType } from './PropsType';
4 | import StaticRenderer from './StaticRenderer';
5 |
6 | function setTransform(nodeStyle: any, value: any) {
7 | nodeStyle.transform = value;
8 | nodeStyle.webkitTransform = value;
9 | nodeStyle.MozTransform = value;
10 | }
11 |
12 | const isWebView =
13 | typeof navigator !== 'undefined' &&
14 | /(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/i.test(navigator.userAgent);
15 | const DOWN = 'down';
16 | const UP = 'up';
17 | const INDICATOR = {
18 | activate: 'release',
19 | deactivate: 'pull',
20 | release: 'loading',
21 | finish: 'finish',
22 | };
23 |
24 | let supportsPassive = false;
25 | try {
26 | const opts = Object.defineProperty({}, 'passive', {
27 | get() {
28 | supportsPassive = true;
29 | },
30 | });
31 | window.addEventListener('test', null as any, opts);
32 | } catch (e) {
33 | // empty
34 | }
35 | const willPreventDefault = supportsPassive ? { passive: false } : false;
36 | // const willNotPreventDefault = supportsPassive ? { passive: true } : false;
37 |
38 | type ICurrSt = 'activate' | 'deactivate' | 'release' | 'finish';
39 |
40 | interface IState {
41 | currSt: ICurrSt;
42 | dragOnEdge: boolean;
43 | }
44 |
45 | export default class PullToRefresh extends React.Component {
46 | static defaultProps = {
47 | prefixCls: 'rmc-pull-to-refresh',
48 | getScrollContainer: () => undefined,
49 | direction: DOWN,
50 | distanceToRefresh: 25,
51 | damping: 100,
52 | indicator: INDICATOR,
53 | };
54 |
55 | // https://github.com/yiminghe/zscroller/blob/2d97973287135745818a0537712235a39a6a62a1/src/Scroller.js#L355
56 | // currSt: `activate` / `deactivate` / `release` / `finish`
57 | state = {
58 | currSt: 'deactivate' as ICurrSt,
59 | dragOnEdge: false,
60 | };
61 |
62 | containerRef: any;
63 | contentRef: any;
64 | _to: any;
65 | _ScreenY: any;
66 | _startScreenY: any;
67 | _lastScreenY: any;
68 | _timer: any;
69 |
70 | _isMounted = false;
71 |
72 | shouldUpdateChildren = false;
73 |
74 | shouldComponentUpdate(nextProps: any) {
75 | this.shouldUpdateChildren = this.props.children !== nextProps.children;
76 | return true;
77 | }
78 |
79 | componentDidUpdate(prevProps: any) {
80 | if (
81 | prevProps === this.props ||
82 | prevProps.refreshing === this.props.refreshing
83 | ) {
84 | return;
85 | }
86 | // triggerPullToRefresh 需要尽可能减少 setState 次数
87 | this.triggerPullToRefresh();
88 | }
89 |
90 | componentDidMount() {
91 | // `getScrollContainer` most likely return React.Node at the next tick. Need setTimeout
92 | setTimeout(() => {
93 | this.init(this.props.getScrollContainer() || this.containerRef);
94 | this.triggerPullToRefresh();
95 | this._isMounted = true;
96 | });
97 | }
98 |
99 | componentWillUnmount() {
100 | // Should have no setTimeout here!
101 | this.destroy(this.props.getScrollContainer() || this.containerRef);
102 | }
103 |
104 | triggerPullToRefresh = () => {
105 | // 在初始化时、用代码 自动 触发 pullToRefresh
106 | // 注意:当 direction 为 up 时,当 visible length < content length 时、则看不到效果
107 | // 添加this._isMounted的判断,否则组建一实例化,currSt就会是finish
108 | if (!this.state.dragOnEdge && this._isMounted) {
109 | if (this.props.refreshing) {
110 | if (this.props.direction === UP) {
111 | this._lastScreenY = -this.props.distanceToRefresh - 1;
112 | }
113 | if (this.props.direction === DOWN) {
114 | this._lastScreenY = this.props.distanceToRefresh + 1;
115 | }
116 | // change dom need after setState
117 | this.setState({ currSt: 'release' }, () =>
118 | this.setContentStyle(this._lastScreenY),
119 | );
120 | } else {
121 | this.setState({ currSt: 'finish' }, () => this.reset());
122 | }
123 | }
124 | };
125 |
126 | init = (ele: any) => {
127 | if (!ele) {
128 | // like return in destroy fn ???!!
129 | return;
130 | }
131 | this._to = {
132 | touchstart: this.onTouchStart.bind(this, ele),
133 | touchmove: this.onTouchMove.bind(this, ele),
134 | touchend: this.onTouchEnd.bind(this, ele),
135 | touchcancel: this.onTouchEnd.bind(this, ele),
136 | };
137 | Object.keys(this._to).forEach(key => {
138 | ele.addEventListener(key, this._to[key], willPreventDefault);
139 | });
140 | };
141 |
142 | destroy = (ele: any) => {
143 | if (!this._to || !ele) {
144 | // componentWillUnmount fire before componentDidMount, like forceUpdate ???!!
145 | return;
146 | }
147 | Object.keys(this._to).forEach(key => {
148 | ele.removeEventListener(key, this._to[key]);
149 | });
150 | };
151 |
152 | onTouchStart = (_ele: any, e: any) => {
153 | this._ScreenY = this._startScreenY = e.touches[0].screenY;
154 | // 一开始 refreshing 为 true 时 this._lastScreenY 有值
155 | this._lastScreenY = this._lastScreenY || 0;
156 | };
157 |
158 | isEdge = (ele: any, direction: string) => {
159 | const container = this.props.getScrollContainer();
160 | if (container && container === document.body) {
161 | // In chrome61 `document.body.scrollTop` is invalid
162 | const scrollNode = document.scrollingElement
163 | ? document.scrollingElement
164 | : document.body;
165 | if (direction === UP) {
166 | return (
167 | scrollNode.scrollHeight - scrollNode.scrollTop <= window.innerHeight
168 | );
169 | }
170 | if (direction === DOWN) {
171 | return scrollNode.scrollTop <= 0;
172 | }
173 | }
174 | if (direction === UP) {
175 | return ele.scrollHeight - ele.scrollTop === ele.clientHeight;
176 | }
177 | if (direction === DOWN) {
178 | return ele.scrollTop <= 0;
179 | }
180 | // 补全 branch, test 才过的了,但是实际上代码永远不会走到这里,这里为了保证代码的一致性,返回 undefined
181 | return undefined;
182 | };
183 |
184 | damping = (dy: number): number => {
185 | if (Math.abs(this._lastScreenY) > this.props.damping) {
186 | return 0;
187 | }
188 |
189 | const ratio =
190 | Math.abs(this._ScreenY - this._startScreenY) / window.screen.height;
191 | dy *= (1 - ratio) * 0.6;
192 |
193 | return dy;
194 | };
195 |
196 | onTouchMove = (ele: any, e: any) => {
197 | // 使用 pageY 对比有问题
198 | const _screenY = e.touches[0].screenY;
199 | const { direction } = this.props;
200 |
201 | // 拖动方向不符合的不处理
202 | if (
203 | (direction === UP && this._startScreenY < _screenY) ||
204 | (direction === DOWN && this._startScreenY > _screenY)
205 | ) {
206 | return;
207 | }
208 |
209 | if (this.isEdge(ele, direction)) {
210 | if (!this.state.dragOnEdge) {
211 | // 当用户开始往上滑的时候isEdge还是false的话,会导致this._ScreenY不是想要的,只有当isEdge为true时,再上滑,才有意义
212 | // 下面这行代码解决了上面这个问题
213 | this._ScreenY = this._startScreenY = e.touches[0].screenY;
214 |
215 | this.setState({ dragOnEdge: true });
216 | }
217 | e.preventDefault();
218 | // add stopPropagation with fastclick will trigger content onClick event. why?
219 | // ref https://github.com/ant-design/ant-design-mobile/issues/2141
220 | // e.stopPropagation();
221 |
222 | const _diff = Math.round(_screenY - this._ScreenY);
223 | this._ScreenY = _screenY;
224 | this._lastScreenY += this.damping(_diff);
225 |
226 | this.setContentStyle(this._lastScreenY);
227 |
228 | if (Math.abs(this._lastScreenY) < this.props.distanceToRefresh) {
229 | if (this.state.currSt !== 'deactivate') {
230 | // console.log('back to the distance');
231 | this.setState({ currSt: 'deactivate' });
232 | }
233 | } else {
234 | if (this.state.currSt === 'deactivate') {
235 | // console.log('reach to the distance');
236 | this.setState({ currSt: 'activate' });
237 | }
238 | }
239 |
240 | // https://github.com/ant-design/ant-design-mobile/issues/573#issuecomment-339560829
241 | // iOS UIWebView issue, It seems no problem in WKWebView
242 | if (isWebView && e.changedTouches[0].clientY < 0) {
243 | this.onTouchEnd();
244 | }
245 | }
246 | };
247 |
248 | onTouchEnd = () => {
249 | if (this.state.dragOnEdge) {
250 | this.setState({ dragOnEdge: false });
251 | }
252 | if (this.state.currSt === 'activate') {
253 | this.setState({ currSt: 'release' });
254 | this._timer = setTimeout(() => {
255 | if (!this.props.refreshing) {
256 | this.setState({ currSt: 'finish' }, () => this.reset());
257 | }
258 | this._timer = undefined;
259 | }, 1000);
260 | this.props.onRefresh();
261 | } else {
262 | this.reset();
263 | }
264 | };
265 |
266 | reset = () => {
267 | this._lastScreenY = 0;
268 | this.setContentStyle(0);
269 | };
270 |
271 | setContentStyle = (ty: number) => {
272 | // todos: Why sometimes do not have `this.contentRef` ?
273 | if (this.contentRef) {
274 | setTransform(this.contentRef.style, `translate3d(0px,${ty}px,0)`);
275 | }
276 | };
277 |
278 | render() {
279 | const props = { ...this.props };
280 |
281 | delete props.damping;
282 |
283 | const {
284 | className,
285 | prefixCls,
286 | children,
287 | getScrollContainer,
288 | direction,
289 | onRefresh,
290 | refreshing,
291 | indicator,
292 | distanceToRefresh,
293 | ...restProps
294 | } = props;
295 |
296 | const renderChildren = (
297 | children}
300 | />
301 | );
302 |
303 | const renderRefresh = (cls: string) => {
304 | const cla = classNames(
305 | cls,
306 | !this.state.dragOnEdge && `${prefixCls}-transition`,
307 | );
308 | return (
309 |
310 |
(this.contentRef = el)}>
311 | {direction === UP ? renderChildren : null}
312 |
313 | {indicator[this.state.currSt] || INDICATOR[this.state.currSt]}
314 |
315 | {direction === DOWN ? renderChildren : null}
316 |
317 |
318 | );
319 | };
320 |
321 | if (getScrollContainer()) {
322 | return renderRefresh(`${prefixCls}-content ${prefixCls}-${direction}`);
323 | }
324 | return (
325 | (this.containerRef = el)}
327 | className={classNames(
328 | className,
329 | prefixCls,
330 | `${prefixCls}-${direction}`,
331 | )}
332 | {...restProps}
333 | >
334 | {renderRefresh(`${prefixCls}-content`)}
335 |
336 | );
337 | }
338 | }
339 |
--------------------------------------------------------------------------------