@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 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
+ }