@haroldtran/react-native-modals 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +184 -0
- package/package.json +83 -0
- package/src/BottomModal.tsx +13 -0
- package/src/Modal.tsx +61 -0
- package/src/ModalPortal.tsx +137 -0
- package/src/animations/Animation.tsx +37 -0
- package/src/animations/FadeAnimation.tsx +41 -0
- package/src/animations/ScaleAnimation.tsx +37 -0
- package/src/animations/SlideAnimation.tsx +88 -0
- package/src/components/Backdrop.tsx +53 -0
- package/src/components/BaseModal.tsx +294 -0
- package/src/components/BottomModal.tsx +36 -0
- package/src/components/DraggableView.tsx +248 -0
- package/src/components/ModalButton.tsx +67 -0
- package/src/components/ModalContent.tsx +31 -0
- package/src/components/ModalContext.tsx +8 -0
- package/src/components/ModalFooter.tsx +48 -0
- package/src/components/ModalTitle.tsx +47 -0
- package/src/components/SlideAnimation.tsx +91 -0
- package/src/constants/Constants.ts +5 -0
- package/src/index.tsx +32 -0
- package/src/type.ts +90 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 Jack Lam <phattran1201@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# @haroldtran/react-native-modals
|
|
2
|
+
|
|
3
|
+
> Cross-platform React Native modal components and utilities for building flexible dialogs, bottom sheets, and animated modals on iOS and Android.
|
|
4
|
+
|
|
5
|
+
**Maintained and enhanced by [Harold - @phattran1201](https://github.com/phattran1201) 👨💻**
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Declarative `Modal` component with customizable title, content, footer and animations
|
|
10
|
+
- Bottom sheet-style `BottomModal`
|
|
11
|
+
- Imperative `ModalPortal` API for showing/updating/dismissing modals from anywhere in your app
|
|
12
|
+
- Built-in animations: `FadeAnimation`, `ScaleAnimation`, `SlideAnimation` and the base `Animation` class for custom animations
|
|
13
|
+
- Backdrop control and swipe-to-dismiss support
|
|
14
|
+
- TypeScript types included
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Install the published package (scoped):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install --save @haroldtran/react-native-modals
|
|
24
|
+
# or
|
|
25
|
+
yarn add @haroldtran/react-native-modals
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Peer dependencies: react, react-native
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quick Setup
|
|
33
|
+
|
|
34
|
+
The library exposes an imperative portal that must be mounted near the root of your app. Add `ModalPortal` to your app root so the portal can render modals:
|
|
35
|
+
|
|
36
|
+
```jsx
|
|
37
|
+
import React from 'react';
|
|
38
|
+
import { ModalPortal } from '@haroldtran/react-native-modals';
|
|
39
|
+
|
|
40
|
+
export default function Root({ children }) {
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
{children}
|
|
44
|
+
<ModalPortal />
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If you use Redux or other providers, mount `ModalPortal` alongside them.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Basic Usage
|
|
55
|
+
|
|
56
|
+
```jsx
|
|
57
|
+
import React, { useState } from 'react';
|
|
58
|
+
import { View, Button, Text } from 'react-native';
|
|
59
|
+
import { Modal, ModalContent } from '@haroldtran/react-native-modals';
|
|
60
|
+
|
|
61
|
+
export default function Example() {
|
|
62
|
+
const [visible, setVisible] = useState(false);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<View>
|
|
66
|
+
<Button title="Show Modal" onPress={() => setVisible(true)} />
|
|
67
|
+
|
|
68
|
+
<Modal
|
|
69
|
+
visible={visible}
|
|
70
|
+
onTouchOutside={() => setVisible(false)}
|
|
71
|
+
>
|
|
72
|
+
<ModalContent>
|
|
73
|
+
<Text>Hello from the modal</Text>
|
|
74
|
+
</ModalContent>
|
|
75
|
+
</Modal>
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Imperative API (ModalPortal)
|
|
84
|
+
|
|
85
|
+
Use the `ModalPortal` to show modals programmatically from anywhere in your app. The portal returns an id which you can use to update or dismiss that modal.
|
|
86
|
+
|
|
87
|
+
```jsx
|
|
88
|
+
import { ModalPortal } from '@haroldtran/react-native-modals';
|
|
89
|
+
|
|
90
|
+
// Show a modal and keep the returned id
|
|
91
|
+
const id = ModalPortal.show(
|
|
92
|
+
<View>
|
|
93
|
+
<Text>Imperative modal</Text>
|
|
94
|
+
</View>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Update the modal content later
|
|
98
|
+
ModalPortal.update(id, {
|
|
99
|
+
children: (
|
|
100
|
+
<View>
|
|
101
|
+
<Text>Updated</Text>
|
|
102
|
+
</View>
|
|
103
|
+
),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Dismiss a specific modal
|
|
107
|
+
ModalPortal.dismiss(id);
|
|
108
|
+
|
|
109
|
+
// Dismiss all open modals
|
|
110
|
+
ModalPortal.dismissAll();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Animations
|
|
116
|
+
|
|
117
|
+
The library includes a base `Animation` class and several concrete implementations:
|
|
118
|
+
|
|
119
|
+
- `FadeAnimation` — fade in/out
|
|
120
|
+
- `ScaleAnimation` — scale from/to a value
|
|
121
|
+
- `SlideAnimation` — slide from `top`, `bottom`, `left` or `right`
|
|
122
|
+
|
|
123
|
+
Example: passing a `SlideAnimation` to a `Modal`
|
|
124
|
+
|
|
125
|
+
```jsx
|
|
126
|
+
import { Modal, SlideAnimation } from '@haroldtran/react-native-modals';
|
|
127
|
+
|
|
128
|
+
<Modal
|
|
129
|
+
visible={visible}
|
|
130
|
+
modalAnimation={new SlideAnimation({ slideFrom: 'bottom' })}
|
|
131
|
+
>
|
|
132
|
+
<ModalContent />
|
|
133
|
+
</Modal>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Create a custom animation by extending `Animation` and overriding `in`, `out` and `getAnimations()`.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Components & Types (exports)
|
|
141
|
+
|
|
142
|
+
The package exports the following components and TypeScript types:
|
|
143
|
+
|
|
144
|
+
- Modal (default export)
|
|
145
|
+
- BottomModal
|
|
146
|
+
- ModalPortal
|
|
147
|
+
- Backdrop
|
|
148
|
+
- ModalButton
|
|
149
|
+
- ModalContent
|
|
150
|
+
- ModalTitle
|
|
151
|
+
- ModalFooter
|
|
152
|
+
- Animation, FadeAnimation, ScaleAnimation, SlideAnimation
|
|
153
|
+
|
|
154
|
+
Types:
|
|
155
|
+
|
|
156
|
+
- DragEvent, SwipeDirection, ModalProps, ModalFooterProps, ModalButtonProps, ModalTitleProps, ModalContentProps, BackdropProps
|
|
157
|
+
|
|
158
|
+
For more details see the `src` folder and the types in `src/type.ts`.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Tips & Notes
|
|
163
|
+
|
|
164
|
+
- The `ModalPortal` must be mounted for the imperative APIs to work.
|
|
165
|
+
- `Modal` supports swipe-to-dismiss and provides callbacks for swipe move, release and completed swipe events.
|
|
166
|
+
- The modal backdrop, overlay color and opacity are configurable via props.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Contributors
|
|
171
|
+
|
|
172
|
+
<table>
|
|
173
|
+
<tbody>
|
|
174
|
+
<tr>
|
|
175
|
+
<td align="center">
|
|
176
|
+
<a href="https://github.com/phattran1201">
|
|
177
|
+
<img src="https://avatars.githubusercontent.com/u/36856455" width="100;" alt="phattran1201"/>
|
|
178
|
+
<br />
|
|
179
|
+
<sub><b>Harold Tran</b></sub>
|
|
180
|
+
</a>
|
|
181
|
+
</td>
|
|
182
|
+
</tr>
|
|
183
|
+
</tbody>
|
|
184
|
+
</table>
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haroldtran/react-native-modals",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "React Native Modals Library for IOS & Android.",
|
|
5
|
+
"main": "/src/index.tsx",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"prebuild": "yarn && tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/index.tsx",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/phattran1201/react-native-modals.git"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"modal",
|
|
26
|
+
"dialog",
|
|
27
|
+
"react-native",
|
|
28
|
+
"react-native-modals",
|
|
29
|
+
"modals",
|
|
30
|
+
"react-component",
|
|
31
|
+
"ios",
|
|
32
|
+
"android",
|
|
33
|
+
"haroldtran",
|
|
34
|
+
"phattran1201"
|
|
35
|
+
],
|
|
36
|
+
"author": "Harold Tran <phattran1201@gmail.com> (https://github.com/phattran1201)",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"homepage": "https://github.com/phattran1201/react-native-modals",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"babel-plugin-flow-react-proptypes": "^9.1.1",
|
|
41
|
+
"prop-types": "^15.6.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": "*",
|
|
45
|
+
"react-native": "*"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/react": "^19.2.0",
|
|
49
|
+
"@types/react-native": "^0.73.0",
|
|
50
|
+
"babel-cli": "^6.26.0",
|
|
51
|
+
"babel-core": "^6.26.3",
|
|
52
|
+
"babel-eslint": "^8.0.2",
|
|
53
|
+
"babel-jest": "^23.0.1",
|
|
54
|
+
"babel-preset-react-native": "^4.0.0",
|
|
55
|
+
"enzyme": "^3.10.0",
|
|
56
|
+
"enzyme-adapter-react-16": "^1.14.0",
|
|
57
|
+
"eslint": "^4.11.0",
|
|
58
|
+
"eslint-config-airbnb": "^16.1.0",
|
|
59
|
+
"eslint-plugin-import": "^2.8.0",
|
|
60
|
+
"eslint-plugin-jest": "^21.17.0",
|
|
61
|
+
"eslint-plugin-jsx-a11y": "^6.0.2",
|
|
62
|
+
"eslint-plugin-react": "^7.5.1",
|
|
63
|
+
"flow-bin": "^0.59.0",
|
|
64
|
+
"jest": "^22.0.0",
|
|
65
|
+
"jest-enzyme": "^7.1.0",
|
|
66
|
+
"react": "^16.0.0",
|
|
67
|
+
"react-dom": "^16.0.0",
|
|
68
|
+
"react-native": ">=0.79.0",
|
|
69
|
+
"react-native-mock-render": "^0.0.26",
|
|
70
|
+
"rimraf": "^2.6.2"
|
|
71
|
+
},
|
|
72
|
+
"jest": {
|
|
73
|
+
"setupTestFrameworkScriptFile": "./node_modules/jest-enzyme/lib/index.js",
|
|
74
|
+
"setupFiles": [
|
|
75
|
+
"./__tests__/setup-tests.js"
|
|
76
|
+
],
|
|
77
|
+
"testRegex": "__tests__/.+\\.test.js$",
|
|
78
|
+
"modulePathIgnorePatterns": [
|
|
79
|
+
"modals-example/node_modules"
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
"packageManager": "yarn@4.9.2"
|
|
83
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import ModalPortal from './ModalPortal';
|
|
4
|
+
import Modal from './Modal';
|
|
5
|
+
|
|
6
|
+
class BottomModal extends Modal {
|
|
7
|
+
show() {
|
|
8
|
+
const { children, ...options } = this.props;
|
|
9
|
+
this.id = ModalPortal.show(children, { ...options, type: 'bottomModal' });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default BottomModal;
|
package/src/Modal.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ModalPortal from "./ModalPortal";
|
|
3
|
+
import type { ModalProps } from "./type";
|
|
4
|
+
|
|
5
|
+
export default class Modal extends React.Component<ModalProps> {
|
|
6
|
+
id: string | null = null;
|
|
7
|
+
|
|
8
|
+
componentDidMount() {
|
|
9
|
+
if (!ModalPortal.ref) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`Can not use ${this.constructor.name} component until ModalPortal is mounted`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
if (this.props.visible) {
|
|
15
|
+
this.show();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
componentDidUpdate(prevProps: ModalProps) {
|
|
20
|
+
if (prevProps.visible !== this.props.visible) {
|
|
21
|
+
if (this.props.visible) {
|
|
22
|
+
this.show();
|
|
23
|
+
} else {
|
|
24
|
+
this.dismiss();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// always re-render
|
|
28
|
+
if (this.id) {
|
|
29
|
+
this.update();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
componentWillUnmount() {
|
|
34
|
+
if (this.id) {
|
|
35
|
+
this.dismiss();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
show() {
|
|
40
|
+
const { children, ...options } = this.props;
|
|
41
|
+
this.id = ModalPortal.show(children, options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
dismiss() {
|
|
45
|
+
if (this.id) {
|
|
46
|
+
ModalPortal.dismiss(this.id);
|
|
47
|
+
this.id = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
update() {
|
|
52
|
+
const { visible: _, ...props } = this.props;
|
|
53
|
+
if (this.id) {
|
|
54
|
+
ModalPortal.update(this.id, props);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
render() {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DeviceEventEmitter } from "react-native";
|
|
3
|
+
import BaseModal from "./components/BaseModal";
|
|
4
|
+
import BottomModal from "./components/BottomModal";
|
|
5
|
+
import type { ModalProps } from "./type";
|
|
6
|
+
|
|
7
|
+
type StackItem = ModalProps & {
|
|
8
|
+
key: string;
|
|
9
|
+
type?: "modal" | "bottomModal";
|
|
10
|
+
onDismiss?: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let modal: ModalPortal | null = null;
|
|
14
|
+
|
|
15
|
+
class ModalPortal extends React.Component<{}, { stack: StackItem[] }> {
|
|
16
|
+
id: number;
|
|
17
|
+
constructor(props: {}) {
|
|
18
|
+
super(props);
|
|
19
|
+
this.state = { stack: [] };
|
|
20
|
+
this.id = 0;
|
|
21
|
+
modal = this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static get ref() {
|
|
25
|
+
return modal;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static get size() {
|
|
29
|
+
return modal?.state.stack.length ?? 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static show(
|
|
33
|
+
children: React.ReactNode,
|
|
34
|
+
props: ModalProps & { type?: "modal" | "bottomModal" } = {},
|
|
35
|
+
) {
|
|
36
|
+
return modal!.show({ children, ...props });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static update(key: string, props: ModalProps) {
|
|
40
|
+
modal?.update(key, props);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static dismiss(key?: string) {
|
|
44
|
+
modal?.dismiss(key);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static dismissAll() {
|
|
48
|
+
modal?.dismissAll();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get current() {
|
|
52
|
+
if (this.state.stack.length) {
|
|
53
|
+
return this.state.stack[this.state.stack.length - 1].key;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
generateKey = () => `modal-${this.id++}`;
|
|
59
|
+
|
|
60
|
+
getIndex = (key: string) => this.state.stack.findIndex((i) => i.key === key);
|
|
61
|
+
|
|
62
|
+
getProps = (
|
|
63
|
+
props: ModalProps & { key?: string; type?: "modal" | "bottomModal" },
|
|
64
|
+
) => {
|
|
65
|
+
const key = (props && (props as any).key) || this.generateKey();
|
|
66
|
+
return { visible: true, ...props, key } as StackItem;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
show = (props: ModalProps & { type?: "modal" | "bottomModal" }) => {
|
|
70
|
+
const mergedProps = this.getProps(props);
|
|
71
|
+
this.setState(({ stack }) => ({ stack: stack.concat(mergedProps) }));
|
|
72
|
+
return mergedProps.key;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
update = (key: string, props: ModalProps) => {
|
|
76
|
+
const mergedProps = this.getProps({ ...props, key });
|
|
77
|
+
this.setState(({ stack }) => {
|
|
78
|
+
const index = this.getIndex(key);
|
|
79
|
+
if (index >= 0) {
|
|
80
|
+
stack[index] = { ...stack[index], ...mergedProps };
|
|
81
|
+
}
|
|
82
|
+
return { stack };
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
dismiss = (key: string | null = this.current) => {
|
|
87
|
+
if (!key) return;
|
|
88
|
+
const idx = this.getIndex(key);
|
|
89
|
+
if (idx < 0) return;
|
|
90
|
+
const props = { ...this.state.stack[idx], visible: false };
|
|
91
|
+
DeviceEventEmitter.emit("ModalDismiss", key);
|
|
92
|
+
this.update(key, props);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
dismissAll = () => {
|
|
96
|
+
this.state.stack.forEach(({ key }) => this.dismiss(key));
|
|
97
|
+
DeviceEventEmitter.emit("ModalDismiss", "all");
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
dismissHandler = (key: string) => {
|
|
101
|
+
// dismiss hander: which will remove data from stack and call onDismissed callback
|
|
102
|
+
const idx = this.getIndex(key);
|
|
103
|
+
if (idx < 0) return;
|
|
104
|
+
const { onDismiss = () => {} } = this.state.stack[idx];
|
|
105
|
+
this.setState(
|
|
106
|
+
({ stack }) => ({ stack: stack.filter((i) => i.key !== key) }),
|
|
107
|
+
onDismiss,
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
renderModal = (item: StackItem) => {
|
|
112
|
+
const { type = "modal", key, ...props } = item;
|
|
113
|
+
if (type === "modal") {
|
|
114
|
+
return (
|
|
115
|
+
<BaseModal
|
|
116
|
+
{...props}
|
|
117
|
+
key={key}
|
|
118
|
+
onDismiss={() => this.dismissHandler(key)}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
} else if (type === "bottomModal") {
|
|
122
|
+
return (
|
|
123
|
+
<BottomModal
|
|
124
|
+
{...props}
|
|
125
|
+
key={key}
|
|
126
|
+
onDismiss={() => this.dismissHandler(key)}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
render() {
|
|
133
|
+
return this.state.stack.map(this.renderModal);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default ModalPortal;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
/* eslint class-methods-use-this: ["error", { "exceptMethods": ["in", "out", "getAnimations"] }] */
|
|
4
|
+
/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "onFinished" }] */
|
|
5
|
+
|
|
6
|
+
import { Animated } from "react-native";
|
|
7
|
+
|
|
8
|
+
export type AnimationConfig = {
|
|
9
|
+
initialValue?: number;
|
|
10
|
+
useNativeDriver?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Base Animation class
|
|
14
|
+
export default class Animation {
|
|
15
|
+
useNativeDriver: boolean;
|
|
16
|
+
animate: Animated.Value;
|
|
17
|
+
|
|
18
|
+
constructor({
|
|
19
|
+
initialValue = 0,
|
|
20
|
+
useNativeDriver = true,
|
|
21
|
+
}: AnimationConfig = {}) {
|
|
22
|
+
this.animate = new Animated.Value(initialValue);
|
|
23
|
+
this.useNativeDriver = useNativeDriver;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
in(onFinished?: Function): void {
|
|
27
|
+
throw Error("not implemented yet");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
out(onFinished?: Function): void {
|
|
31
|
+
throw Error("not implemented yet");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getAnimations(): Object {
|
|
35
|
+
throw Error("not implemented yet");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import { Animated } from "react-native";
|
|
4
|
+
import Animation, { type AnimationConfig } from "./Animation";
|
|
5
|
+
|
|
6
|
+
type FadeAnimationConfig = AnimationConfig & {
|
|
7
|
+
animationDuration?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default class FadeAnimation extends Animation {
|
|
11
|
+
animationDuration: number;
|
|
12
|
+
|
|
13
|
+
constructor({
|
|
14
|
+
initialValue = 0,
|
|
15
|
+
useNativeDriver = false,
|
|
16
|
+
animationDuration = 200,
|
|
17
|
+
}: FadeAnimationConfig = {}) {
|
|
18
|
+
super({ initialValue, useNativeDriver });
|
|
19
|
+
this.animationDuration = animationDuration;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
in(onFinished: Function = () => {}): void {
|
|
23
|
+
Animated.timing(this.animate, {
|
|
24
|
+
toValue: 1,
|
|
25
|
+
duration: this.animationDuration,
|
|
26
|
+
useNativeDriver: this.useNativeDriver,
|
|
27
|
+
}).start((result: { finished: boolean }) => onFinished(result));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
out(onFinished: Function = () => {}): void {
|
|
31
|
+
Animated.timing(this.animate, {
|
|
32
|
+
toValue: 0,
|
|
33
|
+
duration: this.animationDuration,
|
|
34
|
+
useNativeDriver: this.useNativeDriver,
|
|
35
|
+
}).start((result: { finished: boolean }) => onFinished(result));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getAnimations(): Object {
|
|
39
|
+
return { opacity: this.animate };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import { Animated } from "react-native";
|
|
4
|
+
import Animation from "./Animation";
|
|
5
|
+
|
|
6
|
+
export default class ScaleAnimation extends Animation {
|
|
7
|
+
in(onFinished: (result?: { finished: boolean }) => void = () => {}): void {
|
|
8
|
+
Animated.spring(this.animate, {
|
|
9
|
+
toValue: 1,
|
|
10
|
+
velocity: 0,
|
|
11
|
+
tension: 65,
|
|
12
|
+
friction: 7,
|
|
13
|
+
useNativeDriver: this.useNativeDriver,
|
|
14
|
+
}).start((result: { finished: boolean }) => onFinished(result));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
out(onFinished: (result?: { finished: boolean }) => void = () => {}): void {
|
|
18
|
+
Animated.timing(this.animate, {
|
|
19
|
+
toValue: 0,
|
|
20
|
+
duration: 200,
|
|
21
|
+
useNativeDriver: this.useNativeDriver,
|
|
22
|
+
}).start((result: { finished: boolean }) => onFinished(result));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getAnimations(): Object {
|
|
26
|
+
return {
|
|
27
|
+
transform: [
|
|
28
|
+
{
|
|
29
|
+
scale: this.animate.interpolate({
|
|
30
|
+
inputRange: [0, 1],
|
|
31
|
+
outputRange: [0, 1],
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Animated, Dimensions } from "react-native";
|
|
2
|
+
import Animation from "./Animation";
|
|
3
|
+
|
|
4
|
+
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
|
|
5
|
+
|
|
6
|
+
export type SlideFrom = "top" | "bottom" | "left" | "right";
|
|
7
|
+
|
|
8
|
+
export default class SlideAnimation extends Animation {
|
|
9
|
+
slideFrom: SlideFrom;
|
|
10
|
+
|
|
11
|
+
static SLIDE_FROM_TOP: SlideFrom = "top";
|
|
12
|
+
static SLIDE_FROM_BOTTOM: SlideFrom = "bottom";
|
|
13
|
+
static SLIDE_FROM_LEFT: SlideFrom = "left";
|
|
14
|
+
static SLIDE_FROM_RIGHT: SlideFrom = "right";
|
|
15
|
+
|
|
16
|
+
constructor({
|
|
17
|
+
initialValue = 0,
|
|
18
|
+
useNativeDriver = true,
|
|
19
|
+
slideFrom = SlideAnimation.SLIDE_FROM_BOTTOM,
|
|
20
|
+
} = {}) {
|
|
21
|
+
super({ initialValue, useNativeDriver });
|
|
22
|
+
this.slideFrom = slideFrom as SlideFrom;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
in(onFinished: Animated.EndCallback = () => {}, options: any = {}): void {
|
|
26
|
+
Animated.spring(this.animate, {
|
|
27
|
+
toValue: 1,
|
|
28
|
+
velocity: 0,
|
|
29
|
+
tension: 65,
|
|
30
|
+
friction: 11,
|
|
31
|
+
useNativeDriver: this.useNativeDriver,
|
|
32
|
+
...options,
|
|
33
|
+
}).start(onFinished);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
out(onFinished: Animated.EndCallback = () => {}, options: any = {}): void {
|
|
37
|
+
Animated.spring(this.animate, {
|
|
38
|
+
toValue: 0,
|
|
39
|
+
velocity: 0,
|
|
40
|
+
tension: 65,
|
|
41
|
+
friction: 11,
|
|
42
|
+
useNativeDriver: this.useNativeDriver,
|
|
43
|
+
...options,
|
|
44
|
+
}).start(onFinished);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getAnimations(): any {
|
|
48
|
+
const transform: any[] = [];
|
|
49
|
+
|
|
50
|
+
if (this.slideFrom === SlideAnimation.SLIDE_FROM_TOP) {
|
|
51
|
+
transform.push({
|
|
52
|
+
translateY: this.animate.interpolate({
|
|
53
|
+
inputRange: [0, 1],
|
|
54
|
+
outputRange: [-SCREEN_HEIGHT, 0],
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
} else if (this.slideFrom === SlideAnimation.SLIDE_FROM_BOTTOM) {
|
|
58
|
+
transform.push({
|
|
59
|
+
translateY: this.animate.interpolate({
|
|
60
|
+
inputRange: [0, 1],
|
|
61
|
+
outputRange: [SCREEN_HEIGHT, 0],
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
} else if (this.slideFrom === SlideAnimation.SLIDE_FROM_LEFT) {
|
|
65
|
+
transform.push({
|
|
66
|
+
translateX: this.animate.interpolate({
|
|
67
|
+
inputRange: [0, 1],
|
|
68
|
+
outputRange: [-SCREEN_WIDTH, 0],
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
} else if (this.slideFrom === SlideAnimation.SLIDE_FROM_RIGHT) {
|
|
72
|
+
transform.push({
|
|
73
|
+
translateX: this.animate.interpolate({
|
|
74
|
+
inputRange: [0, 1],
|
|
75
|
+
outputRange: [SCREEN_WIDTH, 0],
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`\n slideFrom: ${this.slideFrom} not supported. 'slideFrom' must be 'top' | 'bottom' | 'left' | 'right'\n `,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
transform,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|