expo-app-ui 1.0.3 → 1.0.4
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/README.md +13 -0
- package/package.json +1 -1
- package/templates/components/ui/custom-modal.tsx +161 -28
package/README.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
A UI component library for Expo React Native. Copy components directly into your project and customize them to your needs.
|
|
4
4
|
|
|
5
|
+
## Component Showcase
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
|
|
9
|
+
<img src="https://expo-apps-ui.vercel.app/examples/buttons-example.png" alt="Button Component" width="150" />
|
|
10
|
+
<img src="https://expo-apps-ui.vercel.app/examples/custom-modal-example.gif" alt="Custom Modal" width="150" />
|
|
11
|
+
<img src="https://expo-apps-ui.vercel.app/examples/otp-input-example.gif" alt="OTP Input" width="150" />
|
|
12
|
+
<img src="https://expo-apps-ui.vercel.app/examples/top-loading-bar-example.gif" alt="Top Loading Bar" width="150" />
|
|
13
|
+
|
|
14
|
+
*Button • Custom Modal • OTP Input • Loading Bar*
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
5
18
|
## 📚 Documentation
|
|
6
19
|
|
|
7
20
|
**👉 [View Full Documentation →](https://expo-apps-ui.vercel.app)**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A UI component library for Expo React Native. Copy components directly into your project and customize them to your needs. Documentation: https://expo-apps-ui.vercel.app",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -6,8 +6,11 @@ import {
|
|
|
6
6
|
StyleSheet,
|
|
7
7
|
TouchableWithoutFeedback,
|
|
8
8
|
useWindowDimensions,
|
|
9
|
-
StyleProp,
|
|
10
|
-
ViewStyle,
|
|
9
|
+
StyleProp,
|
|
10
|
+
ViewStyle,
|
|
11
|
+
Keyboard,
|
|
12
|
+
Platform,
|
|
13
|
+
BackHandler,
|
|
11
14
|
} from "react-native";
|
|
12
15
|
import Animated, {
|
|
13
16
|
useSharedValue,
|
|
@@ -17,14 +20,25 @@ import Animated, {
|
|
|
17
20
|
runOnJS,
|
|
18
21
|
} from "react-native-reanimated";
|
|
19
22
|
|
|
23
|
+
// Default colors - using black and white as defaults
|
|
24
|
+
const defaultColors = {
|
|
25
|
+
white: "#FFFFFF",
|
|
26
|
+
black: "#000000",
|
|
27
|
+
backdrop: "rgba(0, 0, 0, 0.5)",
|
|
28
|
+
};
|
|
29
|
+
|
|
20
30
|
// Define the props interface
|
|
21
31
|
interface CustomModalProps {
|
|
22
32
|
visible: boolean;
|
|
23
33
|
onClose: () => void;
|
|
24
34
|
preventBackgroundTouchEvent?: boolean;
|
|
25
35
|
children: React.ReactNode;
|
|
26
|
-
style?: StyleProp<ViewStyle>; //
|
|
27
|
-
modalStyle?: StyleProp<ViewStyle>; //
|
|
36
|
+
style?: StyleProp<ViewStyle>; // Prop for root container
|
|
37
|
+
modalStyle?: StyleProp<ViewStyle>; // Prop for the content box
|
|
38
|
+
noBackdrop?: boolean; // Hide backdrop
|
|
39
|
+
backgroundColor?: string; // Modal background color
|
|
40
|
+
backdropColor?: string; // Backdrop color
|
|
41
|
+
borderRadius?: number; // Border radius for modal
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
const MODAL_ANIMATION_DURATION = 300;
|
|
@@ -36,27 +50,106 @@ const CustomModal: React.FC<CustomModalProps> = ({
|
|
|
36
50
|
onClose,
|
|
37
51
|
preventBackgroundTouchEvent,
|
|
38
52
|
children,
|
|
39
|
-
style,
|
|
40
|
-
modalStyle,
|
|
53
|
+
style,
|
|
54
|
+
modalStyle,
|
|
55
|
+
noBackdrop = false,
|
|
56
|
+
backgroundColor = defaultColors.white,
|
|
57
|
+
backdropColor = defaultColors.backdrop,
|
|
58
|
+
borderRadius = 15,
|
|
41
59
|
}) => {
|
|
42
60
|
const { height } = useWindowDimensions();
|
|
43
61
|
|
|
44
62
|
const [isModalRendered, setIsModalRendered] = useState(visible);
|
|
45
63
|
const backdropOpacity = useSharedValue(0);
|
|
46
64
|
const modalTranslateY = useSharedValue(height);
|
|
65
|
+
const keyboardOffset = useSharedValue(0);
|
|
66
|
+
|
|
67
|
+
// Check if modal is positioned at bottom (bottom sheet style)
|
|
68
|
+
const flattenedStyle = style ? StyleSheet.flatten(style) : {};
|
|
69
|
+
const isBottomSheet = (flattenedStyle as any)?.justifyContent === "flex-end";
|
|
47
70
|
|
|
48
71
|
const backdropAnimatedStyle = useAnimatedStyle(() => ({
|
|
49
|
-
opacity: backdropOpacity.value,
|
|
72
|
+
opacity: noBackdrop ? 0 : backdropOpacity.value,
|
|
50
73
|
}));
|
|
51
74
|
|
|
52
|
-
const modalAnimatedStyle = useAnimatedStyle(() =>
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
const modalAnimatedStyle = useAnimatedStyle(() => {
|
|
76
|
+
if (isBottomSheet) {
|
|
77
|
+
// For bottom sheets, position at bottom and translate from below
|
|
78
|
+
// Subtract keyboardOffset to move modal up when keyboard appears
|
|
79
|
+
return {
|
|
80
|
+
transform: [
|
|
81
|
+
{
|
|
82
|
+
translateY: modalTranslateY.value - keyboardOffset.value,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// For centered modals, use standard transform
|
|
88
|
+
return {
|
|
89
|
+
transform: [{ translateY: modalTranslateY.value - keyboardOffset.value }],
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Handle Android back button
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!visible) return;
|
|
96
|
+
|
|
97
|
+
const backHandler = BackHandler.addEventListener(
|
|
98
|
+
"hardwareBackPress",
|
|
99
|
+
() => {
|
|
100
|
+
if (!preventBackgroundTouchEvent) {
|
|
101
|
+
onClose();
|
|
102
|
+
return true; // Prevent default back behavior
|
|
103
|
+
}
|
|
104
|
+
return false; // Allow default back behavior if preventBackgroundTouchEvent is true
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return () => backHandler.remove();
|
|
109
|
+
}, [visible, preventBackgroundTouchEvent, onClose]);
|
|
110
|
+
|
|
111
|
+
// Handle keyboard events for bottom sheet modals
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!visible || !isBottomSheet) {
|
|
114
|
+
// Reset keyboard offset when modal is not visible or not a bottom sheet
|
|
115
|
+
keyboardOffset.value = 0;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const showEvent =
|
|
120
|
+
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
|
|
121
|
+
const hideEvent =
|
|
122
|
+
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
|
|
123
|
+
|
|
124
|
+
const keyboardWillShowListener = Keyboard.addListener(showEvent, (e) => {
|
|
125
|
+
const keyboardHeight = e.endCoordinates.height;
|
|
126
|
+
keyboardOffset.value = withTiming(keyboardHeight, {
|
|
127
|
+
duration: Platform.OS === "ios" ? e.duration || 250 : 250,
|
|
128
|
+
easing: Easing.out(Easing.ease),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const keyboardWillHideListener = Keyboard.addListener(hideEvent, () => {
|
|
133
|
+
keyboardOffset.value = withTiming(0, {
|
|
134
|
+
duration: 250,
|
|
135
|
+
easing: Easing.out(Easing.ease),
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
keyboardWillShowListener.remove();
|
|
141
|
+
keyboardWillHideListener.remove();
|
|
142
|
+
};
|
|
143
|
+
}, [visible, isBottomSheet, keyboardOffset]);
|
|
55
144
|
|
|
56
145
|
useEffect(() => {
|
|
57
146
|
if (visible) {
|
|
58
147
|
setIsModalRendered(true);
|
|
59
|
-
|
|
148
|
+
// Ensure modalTranslateY starts from height for bottom sheets
|
|
149
|
+
if (isBottomSheet) {
|
|
150
|
+
modalTranslateY.value = height;
|
|
151
|
+
}
|
|
152
|
+
backdropOpacity.value = withTiming(noBackdrop ? 0 : 0.5, {
|
|
60
153
|
duration: MODAL_ANIMATION_DURATION,
|
|
61
154
|
easing: backdropEasing,
|
|
62
155
|
});
|
|
@@ -69,6 +162,10 @@ const CustomModal: React.FC<CustomModalProps> = ({
|
|
|
69
162
|
duration: MODAL_ANIMATION_DURATION,
|
|
70
163
|
easing: backdropEasing,
|
|
71
164
|
});
|
|
165
|
+
keyboardOffset.value = withTiming(0, {
|
|
166
|
+
duration: MODAL_ANIMATION_DURATION,
|
|
167
|
+
easing: modalEasing,
|
|
168
|
+
});
|
|
72
169
|
modalTranslateY.value = withTiming(
|
|
73
170
|
height,
|
|
74
171
|
{
|
|
@@ -82,23 +179,46 @@ const CustomModal: React.FC<CustomModalProps> = ({
|
|
|
82
179
|
}
|
|
83
180
|
);
|
|
84
181
|
}
|
|
85
|
-
}, [visible, height, backdropOpacity, modalTranslateY]);
|
|
182
|
+
}, [visible, height, isBottomSheet, backdropOpacity, modalTranslateY, keyboardOffset, noBackdrop]);
|
|
86
183
|
|
|
87
184
|
if (!isModalRendered) {
|
|
88
185
|
return null;
|
|
89
186
|
}
|
|
90
187
|
|
|
91
188
|
return (
|
|
92
|
-
// Apply the custom root 'style' prop here
|
|
93
189
|
<Animated.View style={[styles.container, style]}>
|
|
94
|
-
{!
|
|
95
|
-
<TouchableWithoutFeedback
|
|
96
|
-
|
|
190
|
+
{!noBackdrop && (
|
|
191
|
+
<TouchableWithoutFeedback
|
|
192
|
+
onPress={() => {
|
|
193
|
+
// By default, backdrop touch closes the modal
|
|
194
|
+
// Only prevent if explicitly set to true
|
|
195
|
+
if (!preventBackgroundTouchEvent) {
|
|
196
|
+
onClose();
|
|
197
|
+
}
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<Animated.View
|
|
201
|
+
style={[
|
|
202
|
+
styles.backdrop,
|
|
203
|
+
{ backgroundColor: backdropColor },
|
|
204
|
+
backdropAnimatedStyle,
|
|
205
|
+
]}
|
|
206
|
+
/>
|
|
97
207
|
</TouchableWithoutFeedback>
|
|
98
208
|
)}
|
|
99
209
|
|
|
100
|
-
|
|
101
|
-
|
|
210
|
+
<Animated.View
|
|
211
|
+
style={[
|
|
212
|
+
styles.modalView,
|
|
213
|
+
{
|
|
214
|
+
backgroundColor,
|
|
215
|
+
borderRadius: isBottomSheet ? borderRadius : borderRadius,
|
|
216
|
+
...(isBottomSheet && styles.modalViewBottomSheet),
|
|
217
|
+
},
|
|
218
|
+
modalAnimatedStyle,
|
|
219
|
+
modalStyle,
|
|
220
|
+
]}
|
|
221
|
+
>
|
|
102
222
|
{children}
|
|
103
223
|
</Animated.View>
|
|
104
224
|
</Animated.View>
|
|
@@ -112,25 +232,38 @@ const styles = StyleSheet.create({
|
|
|
112
232
|
left: 0,
|
|
113
233
|
right: 0,
|
|
114
234
|
bottom: 0,
|
|
115
|
-
// Changed to 'flex-end' to act like a bottom-sheet
|
|
116
235
|
justifyContent: "center",
|
|
117
236
|
alignItems: "center",
|
|
118
237
|
zIndex: 1000,
|
|
119
238
|
},
|
|
120
239
|
backdrop: {
|
|
121
240
|
...StyleSheet.absoluteFillObject,
|
|
122
|
-
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
123
241
|
},
|
|
124
242
|
modalView: {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
243
|
+
padding: 20,
|
|
244
|
+
width: "90%",
|
|
245
|
+
maxWidth: 500,
|
|
246
|
+
maxHeight: "80%",
|
|
247
|
+
shadowColor: "#000",
|
|
248
|
+
shadowOffset: {
|
|
249
|
+
width: 0,
|
|
250
|
+
height: 2,
|
|
251
|
+
},
|
|
252
|
+
shadowOpacity: 0.25,
|
|
253
|
+
shadowRadius: 3.84,
|
|
254
|
+
elevation: 5,
|
|
255
|
+
},
|
|
256
|
+
modalViewBottomSheet: {
|
|
257
|
+
position: "absolute",
|
|
258
|
+
bottom: 0,
|
|
259
|
+
left: 0,
|
|
260
|
+
right: 0,
|
|
130
261
|
width: "100%",
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
262
|
+
maxWidth: "100%",
|
|
263
|
+
borderTopLeftRadius: 20,
|
|
264
|
+
borderTopRightRadius: 20,
|
|
265
|
+
borderBottomLeftRadius: 0,
|
|
266
|
+
borderBottomRightRadius: 0,
|
|
134
267
|
},
|
|
135
268
|
});
|
|
136
269
|
|