@haroldtran/react-native-modals 0.0.6 → 0.0.10

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/package.json CHANGED
@@ -1,26 +1,7 @@
1
1
  {
2
2
  "name": "@haroldtran/react-native-modals",
3
- "version": "0.0.6",
3
+ "version": "0.0.10",
4
4
  "description": "React Native Modals Library for IOS & Android.",
5
- "main": "/src/index.tsx",
6
- "scripts": {
7
- "test": "jest",
8
- "prebuild": "bun install && 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
5
  "keywords": [
25
6
  "modal",
26
7
  "dialog",
@@ -33,56 +14,51 @@
33
14
  "haroldtran",
34
15
  "phattran1201"
35
16
  ],
36
- "author": "Harold Tran <phattran1201@gmail.com> (https://github.com/phattran1201)",
37
- "license": "MIT",
38
17
  "homepage": "https://github.com/phattran1201/react-native-modals",
39
- "dependencies": {
40
- "@sbaiahmed1/react-native-blur": "^4.4.1",
41
- "babel-plugin-flow-react-proptypes": "^9.1.1",
42
- "prop-types": "^15.6.0"
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/phattran1201/react-native-modals.git"
43
21
  },
44
- "peerDependencies": {
45
- "react": "*",
46
- "react-native": "*"
22
+ "license": "MIT",
23
+ "author": "Harold Tran <phattran1201@gmail.com> (https://github.com/phattran1201)",
24
+ "exports": {
25
+ ".": "./src/index.tsx",
26
+ "./package.json": "./package.json"
47
27
  },
28
+ "main": "src/index.tsx",
29
+ "types": "src/index.tsx",
30
+ "files": [
31
+ "src"
32
+ ],
33
+ "scripts": {
34
+ "format": "prettier --write \"src/**/*.{ts,tsx}\"",
35
+ "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
36
+ "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
37
+ "release": "npm login && npm publish && git push --follow-tags && gh release create v$(node -p \"require('./package.json').version\") --generate-notes",
38
+ "test": "echo \"Error: no test specified\" && exit 1",
39
+ "tsc": "tsc --noEmit"
40
+ },
41
+ "dependencies": {},
48
42
  "devDependencies": {
49
- "@types/react": "^19.2.0",
43
+ "@babel/core": "^7.29.0",
44
+ "@babel/preset-env": "^7.29.2",
45
+ "@babel/preset-typescript": "^7.28.5",
46
+ "@react-native/babel-preset": "^0.84.1",
47
+ "@react-native/eslint-config": "^0.84.1",
48
+ "@types/react": "^19.2.14",
50
49
  "@types/react-native": "^0.73.0",
51
- "babel-cli": "^6.26.0",
52
- "babel-core": "^6.26.3",
53
- "babel-eslint": "^8.0.2",
54
- "babel-jest": "^23.0.1",
55
- "babel-preset-react-native": "^4.0.0",
56
- "enzyme": "^3.10.0",
57
- "enzyme-adapter-react-16": "^1.14.0",
58
- "eslint": "^4.11.0",
59
- "eslint-config-airbnb": "^16.1.0",
60
- "eslint-plugin-import": "^2.8.0",
61
- "eslint-plugin-jest": "^21.17.0",
62
- "eslint-plugin-jsx-a11y": "^6.0.2",
63
- "eslint-plugin-react": "^7.5.1",
64
- "flow-bin": "^0.59.0",
65
- "jest": "^22.0.0",
66
- "jest-enzyme": "^7.1.0",
67
- "react": "^16.0.0",
68
- "react-dom": "^16.0.0",
69
- "react-native": ">=0.79.0",
70
- "react-native-mock-render": "^0.0.26",
71
- "rimraf": "^2.6.2"
50
+ "eslint": "^10.1.0",
51
+ "prettier": "^3.8.1",
52
+ "react": "^18.0.0",
53
+ "react-native": ">=0.74.0",
54
+ "typescript": "^6.0.2"
72
55
  },
73
- "jest": {
74
- "setupTestFrameworkScriptFile": "./node_modules/jest-enzyme/lib/index.js",
75
- "setupFiles": [
76
- "./__tests__/setup-tests.js"
77
- ],
78
- "testRegex": "__tests__/.+\\.test.js$",
79
- "modulePathIgnorePatterns": [
80
- "modals-example/node_modules"
81
- ]
56
+ "peerDependencies": {
57
+ "react": "*",
58
+ "react-native": "*"
82
59
  },
83
60
  "packageManager": "yarn@4.9.2",
84
- "trustedDependencies": [
85
- "core-js",
86
- "fsevents"
87
- ]
88
- }
61
+ "publishConfig": {
62
+ "access": "public"
63
+ }
64
+ }
@@ -1,7 +1,5 @@
1
- // @flow
2
-
3
- import ModalPortal from './ModalPortal';
4
1
  import Modal from './Modal';
2
+ import ModalPortal from './ModalPortal';
5
3
 
6
4
  class BottomModal extends Modal {
7
5
  show() {
package/src/Modal.tsx CHANGED
@@ -1,15 +1,13 @@
1
- import React from "react";
2
- import ModalPortal from "./ModalPortal";
3
- import type { ModalProps } from "./type";
1
+ import React from 'react';
2
+ import ModalPortal from './ModalPortal';
3
+ import type { ModalProps } from './type';
4
4
 
5
5
  export default class Modal extends React.Component<ModalProps> {
6
6
  id: string | null = null;
7
7
 
8
8
  componentDidMount() {
9
9
  if (!ModalPortal.ref) {
10
- throw new Error(
11
- `Can not use ${this.constructor.name} component until ModalPortal is mounted`,
12
- );
10
+ throw new Error(`Can not use ${this.constructor.name} component until ModalPortal is mounted`);
13
11
  }
14
12
  if (this.props.visible) {
15
13
  this.show();
@@ -1,20 +1,28 @@
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 { EmitModalPortal, ModalProps, StackItem } from "./type";
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 { EmitModalPortal, ModalProps, StackItem } from './type';
6
6
 
7
7
  let modal: ModalPortal | null = null;
8
+ let modalCounter = 0;
9
+
10
+ export default class ModalPortal extends React.Component<{}, { stack: StackItem[] }> {
11
+ private dismissHandlers = new Map<string, () => void>();
8
12
 
9
- class ModalPortal extends React.Component<{}, { stack: StackItem[] }> {
10
- id: number;
11
13
  constructor(props: {}) {
12
14
  super(props);
13
15
  this.state = { stack: [] };
14
- this.id = 0;
15
16
  modal = this;
16
17
  }
17
18
 
19
+ componentWillUnmount() {
20
+ if (modal === this) {
21
+ modal = null;
22
+ }
23
+ this.dismissHandlers.clear();
24
+ }
25
+
18
26
  static get ref() {
19
27
  return modal;
20
28
  }
@@ -23,10 +31,7 @@ class ModalPortal extends React.Component<{}, { stack: StackItem[] }> {
23
31
  return modal?.state.stack.length ?? 0;
24
32
  }
25
33
 
26
- static show(
27
- children: React.ReactNode,
28
- props: ModalProps & { key?: string; type?: "modal" | "bottomModal" } = {},
29
- ) {
34
+ static show(children: React.ReactNode, props: ModalProps & { key?: string } = {}) {
30
35
  return modal!.show({ children, ...props, key: props.key });
31
36
  }
32
37
 
@@ -43,104 +48,132 @@ class ModalPortal extends React.Component<{}, { stack: StackItem[] }> {
43
48
  }
44
49
 
45
50
  get current() {
46
- if (this.state.stack.length) {
47
- return this.state.stack[this.state.stack.length - 1].key;
51
+ const { stack } = this.state;
52
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
53
+ if (stack[i].visible) {
54
+ return stack[i].key;
55
+ }
48
56
  }
49
57
  return null;
50
58
  }
51
59
 
52
- generateKey = () => `modal-${this.id++}`;
53
-
54
- getIndex = (key: string) => this.state.stack.findIndex((i) => i.key === key);
60
+ generateKey = () => {
61
+ modalCounter++;
62
+ return `modal-${modalCounter}`;
63
+ };
55
64
 
56
- getProps = (
57
- props: ModalProps & { key?: string; type?: "modal" | "bottomModal" },
58
- ) => {
65
+ getProps = (props: ModalProps & { key?: string }) => {
59
66
  const key = props?.key || this.generateKey();
60
67
  return { visible: true, ...props, key } as StackItem;
61
68
  };
62
69
 
63
- show = (
64
- props: ModalProps & { key?: string; type?: "modal" | "bottomModal" },
65
- ) => {
70
+ show = (props: ModalProps & { key?: string }) => {
66
71
  const mergedProps = this.getProps(props);
67
- this.setState(({ stack }) => ({ stack: stack.concat(mergedProps) }));
68
- DeviceEventEmitter.emit("ModalPortal", {
69
- type: "show",
70
- key: mergedProps.key,
71
- stack: this.state.stack,
72
- } as EmitModalPortal);
72
+
73
+ this.setState(
74
+ ({ stack }) => {
75
+ const index = stack.findIndex((i) => i.key === mergedProps.key);
76
+ let newStack;
77
+ if (index > -1) {
78
+ const item = { ...stack[index], ...mergedProps };
79
+ newStack = stack.filter((i) => i.key !== mergedProps.key).concat(item);
80
+ } else {
81
+ newStack = stack.concat(mergedProps);
82
+ }
83
+ return { stack: newStack };
84
+ },
85
+ () => {
86
+ DeviceEventEmitter.emit('ModalPortal', {
87
+ type: 'show',
88
+ key: mergedProps.key,
89
+ stack: this.state.stack,
90
+ } as EmitModalPortal);
91
+ },
92
+ );
73
93
  return mergedProps.key;
74
94
  };
75
95
 
76
96
  update = (key: string, props: ModalProps) => {
77
97
  const mergedProps = this.getProps({ ...props, key });
78
- this.setState(({ stack }) => {
79
- const index = this.getIndex(key);
80
- if (index >= 0) {
81
- stack[index] = { ...stack[index], ...mergedProps };
82
- DeviceEventEmitter.emit("ModalPortal", {
83
- type: mergedProps.visible ? "update" : "dismiss",
98
+ this.setState(
99
+ ({ stack }) => {
100
+ const index = stack.findIndex((i) => i.key === key);
101
+ if (index === -1) {
102
+ return null;
103
+ }
104
+ const isShowing = !stack[index].visible && mergedProps.visible;
105
+ const newStack = [...stack];
106
+
107
+ if (isShowing) {
108
+ const item = { ...newStack[index], ...mergedProps };
109
+ newStack.splice(index, 1);
110
+ newStack.push(item);
111
+ } else {
112
+ newStack[index] = { ...newStack[index], ...mergedProps };
113
+ }
114
+
115
+ return { stack: newStack };
116
+ },
117
+ () => {
118
+ DeviceEventEmitter.emit('ModalPortal', {
119
+ type: mergedProps.visible ? 'update' : 'dismiss',
84
120
  key,
85
- stack,
121
+ stack: this.state.stack,
86
122
  } as EmitModalPortal);
87
- }
88
- return { stack };
89
- });
123
+ },
124
+ );
90
125
  };
91
126
 
92
127
  dismiss = (key: string | null = this.current) => {
93
128
  if (!key) return;
94
- const idx = this.getIndex(key);
95
- if (idx < 0) return;
96
- const props = { ...this.state.stack[idx], visible: false };
97
- this.update(key, props);
129
+ this.update(key, { visible: false });
98
130
  };
99
131
 
100
132
  dismissAll = () => {
101
- this.state.stack.forEach(({ key }) => this.dismiss(key));
133
+ this.setState(
134
+ ({ stack }) => {
135
+ if (stack.every((item) => !item.visible)) return null;
136
+ const newStack = stack.map((item) => ({ ...item, visible: false }));
137
+ return { stack: newStack };
138
+ },
139
+ () => {
140
+ DeviceEventEmitter.emit('ModalPortal', {
141
+ type: 'dismissAll',
142
+ stack: this.state.stack,
143
+ } as EmitModalPortal);
144
+ },
145
+ );
102
146
  };
103
147
 
104
148
  dismissHandler = (key: string) => {
105
- // dismiss hander: which will remove data from stack and call onDismissed callback
106
- const idx = this.getIndex(key);
107
- if (idx < 0) return;
108
- const { onDismiss = () => {} } = this.state.stack[idx];
149
+ const item = this.state.stack.find((i) => i.key === key);
150
+ if (!item) return;
151
+
109
152
  this.setState(
110
153
  ({ stack }) => ({ stack: stack.filter((i) => i.key !== key) }),
111
- onDismiss,
154
+ () => {
155
+ item.onDismiss?.();
156
+ this.dismissHandlers.delete(key);
157
+ },
112
158
  );
113
159
  };
114
160
 
115
- renderModal = (item: StackItem) => {
116
- const { type = "modal", key, ...props } = item;
117
- if (type === "modal") {
118
- return (
119
- <BaseModal
120
- {...props}
121
- key={key}
122
- onDismiss={() => this.dismissHandler(key)}
123
- />
124
- );
125
- } else if (type === "bottomModal") {
126
- return (
127
- <BottomModal
128
- {...props}
129
- key={key}
130
- onDismiss={() => this.dismissHandler(key)}
131
- />
132
- );
161
+ getStableDismissHandler = (key: string) => {
162
+ let handler = this.dismissHandlers.get(key);
163
+ if (!handler) {
164
+ handler = () => this.dismissHandler(key);
165
+ this.dismissHandlers.set(key, handler);
133
166
  }
167
+ return handler;
168
+ };
169
+
170
+ renderModal = (item: StackItem) => {
171
+ const { type = 'modal', key, ...props } = item;
172
+ const Component = type === 'bottomModal' ? BottomModal : BaseModal;
173
+ return <Component {...props} key={key} onDismiss={this.getStableDismissHandler(key)} />;
134
174
  };
135
175
 
136
176
  render() {
137
- return this.state?.stack
138
- ?.filter(
139
- (item, index, self) =>
140
- index === self?.findIndex((t) => t?.key === item?.key),
141
- )
142
- ?.map(this.renderModal);
177
+ return this.state.stack.map(this.renderModal);
143
178
  }
144
179
  }
145
-
146
- export default ModalPortal;
@@ -1,37 +1,35 @@
1
- // @flow
2
-
3
1
  /* eslint class-methods-use-this: ["error", { "exceptMethods": ["in", "out", "getAnimations"] }] */
4
2
  /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "onFinished" }] */
5
3
 
6
- import { Animated } from "react-native";
4
+ import { Animated } from 'react-native';
7
5
 
8
6
  export type AnimationConfig = {
9
7
  initialValue?: number;
10
8
  useNativeDriver?: boolean;
9
+ animationDuration?: number;
11
10
  };
12
11
 
13
12
  // Base Animation class
14
13
  export default class Animation {
15
14
  useNativeDriver: boolean;
16
15
  animate: Animated.Value;
16
+ animationDuration: number;
17
17
 
18
- constructor({
19
- initialValue = 0,
20
- useNativeDriver = true,
21
- }: AnimationConfig = {}) {
18
+ constructor({ initialValue = 0, useNativeDriver = true, animationDuration = 200 }: AnimationConfig = {}) {
22
19
  this.animate = new Animated.Value(initialValue);
23
20
  this.useNativeDriver = useNativeDriver;
21
+ this.animationDuration = animationDuration;
24
22
  }
25
23
 
26
- in(onFinished?: Function): void {
27
- throw Error("not implemented yet");
24
+ in(onFinished?: Function, duration?: number): void {
25
+ throw Error('not implemented yet');
28
26
  }
29
27
 
30
- out(onFinished?: Function): void {
31
- throw Error("not implemented yet");
28
+ out(onFinished?: Function, duration?: number): void {
29
+ throw Error('not implemented yet');
32
30
  }
33
31
 
34
32
  getAnimations(): Object {
35
- throw Error("not implemented yet");
33
+ throw Error('not implemented yet');
36
34
  }
37
35
  }
@@ -1,36 +1,26 @@
1
- // @flow
2
-
3
- import { Animated } from "react-native";
4
- import Animation, { type AnimationConfig } from "./Animation";
1
+ import { Animated } from 'react-native';
2
+ import Animation, { type AnimationConfig } from './Animation';
5
3
 
6
4
  type FadeAnimationConfig = AnimationConfig & {
7
5
  animationDuration?: number;
8
6
  };
9
-
10
7
  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;
8
+ constructor({ initialValue = 0, animationDuration = 200, useNativeDriver = true }: AnimationConfig = {}) {
9
+ super({ initialValue, useNativeDriver, animationDuration });
20
10
  }
21
11
 
22
- in(onFinished: Function = () => {}): void {
12
+ in(onFinished: (result?: { finished: boolean }) => void = () => {}, duration?: number): void {
23
13
  Animated.timing(this.animate, {
24
14
  toValue: 1,
25
- duration: this.animationDuration,
15
+ duration: duration || this.animationDuration,
26
16
  useNativeDriver: this.useNativeDriver,
27
17
  }).start((result: { finished: boolean }) => onFinished(result));
28
18
  }
29
19
 
30
- out(onFinished: Function = () => {}): void {
20
+ out(onFinished: (result?: { finished: boolean }) => void = () => {}, duration?: number): void {
31
21
  Animated.timing(this.animate, {
32
22
  toValue: 0,
33
- duration: this.animationDuration,
23
+ duration: duration || this.animationDuration,
34
24
  useNativeDriver: this.useNativeDriver,
35
25
  }).start((result: { finished: boolean }) => onFinished(result));
36
26
  }
@@ -1,10 +1,17 @@
1
- // @flow
2
-
3
- import { Animated } from "react-native";
4
- import Animation from "./Animation";
1
+ import { Animated } from 'react-native';
2
+ import Animation from './Animation';
5
3
 
6
4
  export default class ScaleAnimation extends Animation {
7
- in(onFinished: (result?: { finished: boolean }) => void = () => {}): void {
5
+ in(onFinished: (result?: { finished: boolean }) => void = () => {}, duration?: number): void {
6
+ const finalDuration = duration || this.animationDuration;
7
+ if (finalDuration) {
8
+ Animated.timing(this.animate, {
9
+ toValue: 1,
10
+ duration: finalDuration,
11
+ useNativeDriver: this.useNativeDriver,
12
+ }).start((result: { finished: boolean }) => onFinished(result));
13
+ return;
14
+ }
8
15
  Animated.spring(this.animate, {
9
16
  toValue: 1,
10
17
  velocity: 0,
@@ -14,10 +21,10 @@ export default class ScaleAnimation extends Animation {
14
21
  }).start((result: { finished: boolean }) => onFinished(result));
15
22
  }
16
23
 
17
- out(onFinished: (result?: { finished: boolean }) => void = () => {}): void {
24
+ out(onFinished: (result?: { finished: boolean }) => void = () => {}, duration?: number): void {
18
25
  Animated.timing(this.animate, {
19
26
  toValue: 0,
20
- duration: 200,
27
+ duration: duration || this.animationDuration,
21
28
  useNativeDriver: this.useNativeDriver,
22
29
  }).start((result: { finished: boolean }) => onFinished(result));
23
30
  }
@@ -1,46 +1,68 @@
1
- import { Animated, Dimensions } from "react-native";
2
- import Animation from "./Animation";
1
+ import { Animated, Dimensions } from 'react-native';
2
+ import Animation from './Animation';
3
3
 
4
- const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
4
+ let { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
5
5
 
6
- export type SlideFrom = "top" | "bottom" | "left" | "right";
6
+ Dimensions.addEventListener('change', ({ window }) => {
7
+ SCREEN_WIDTH = window.width;
8
+ SCREEN_HEIGHT = window.height;
9
+ });
10
+
11
+ export type SlideFrom = 'top' | 'bottom' | 'left' | 'right';
7
12
 
8
13
  export default class SlideAnimation extends Animation {
9
14
  slideFrom: SlideFrom;
10
15
 
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";
16
+ static SLIDE_FROM_TOP: SlideFrom = 'top';
17
+ static SLIDE_FROM_BOTTOM: SlideFrom = 'bottom';
18
+ static SLIDE_FROM_LEFT: SlideFrom = 'left';
19
+ static SLIDE_FROM_RIGHT: SlideFrom = 'right';
15
20
 
16
21
  constructor({
17
22
  initialValue = 0,
18
23
  useNativeDriver = true,
24
+ animationDuration = 200,
19
25
  slideFrom = SlideAnimation.SLIDE_FROM_BOTTOM,
20
26
  } = {}) {
21
- super({ initialValue, useNativeDriver });
27
+ super({ initialValue, useNativeDriver, animationDuration });
22
28
  this.slideFrom = slideFrom as SlideFrom;
23
29
  }
24
30
 
25
- in(onFinished: Animated.EndCallback = () => {}, options: any = {}): void {
31
+ in(onFinished: Animated.EndCallback = () => {}, duration?: number): void {
32
+ const finalDuration = duration || this.animationDuration;
33
+ if (finalDuration) {
34
+ Animated.timing(this.animate, {
35
+ toValue: 1,
36
+ duration: finalDuration,
37
+ useNativeDriver: this.useNativeDriver,
38
+ }).start(onFinished);
39
+ return;
40
+ }
26
41
  Animated.spring(this.animate, {
27
42
  toValue: 1,
28
43
  velocity: 0,
29
44
  tension: 65,
30
45
  friction: 11,
31
46
  useNativeDriver: this.useNativeDriver,
32
- ...options,
33
47
  }).start(onFinished);
34
48
  }
35
49
 
36
- out(onFinished: Animated.EndCallback = () => {}, options: any = {}): void {
50
+ out(onFinished: Animated.EndCallback = () => {}, duration?: number): void {
51
+ const finalDuration = duration || this.animationDuration;
52
+ if (finalDuration) {
53
+ Animated.timing(this.animate, {
54
+ toValue: 0,
55
+ duration: finalDuration,
56
+ useNativeDriver: this.useNativeDriver,
57
+ }).start(onFinished);
58
+ return;
59
+ }
37
60
  Animated.spring(this.animate, {
38
61
  toValue: 0,
39
62
  velocity: 0,
40
63
  tension: 65,
41
64
  friction: 11,
42
65
  useNativeDriver: this.useNativeDriver,
43
- ...options,
44
66
  }).start(onFinished);
45
67
  }
46
68