rn-floating-input 0.1.0

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.
@@ -0,0 +1,229 @@
1
+ import React, { useEffect, useImperativeHandle, useRef, useState } from "react";
2
+ import { Pressable, TextInput as RNTextInput, View } from "react-native";
3
+ import Animated, {
4
+ interpolate,
5
+ LinearTransition,
6
+ useAnimatedStyle,
7
+ useSharedValue,
8
+ withSequence,
9
+ withTiming,
10
+ } from "react-native-reanimated";
11
+
12
+ import { baseStyles, DEFAULT_ANIMATION, DEFAULT_THEME } from "./defaults";
13
+ import type { FloatingInputProps, FloatingInputRef } from "./types";
14
+
15
+ export const FloatingInput = React.forwardRef<
16
+ FloatingInputRef,
17
+ FloatingInputProps
18
+ >(
19
+ (
20
+ {
21
+ value,
22
+ onChangeText,
23
+ onBlur,
24
+ onFocus,
25
+ onPress,
26
+ label,
27
+ placeholder,
28
+ error,
29
+ touched,
30
+ maxLength,
31
+ keyboardType = "default",
32
+ autoFocus = false,
33
+ editable = true,
34
+ autoCapitalize,
35
+ secureTextEntry = false,
36
+ right,
37
+ renderInput,
38
+ theme: themeProp,
39
+ styles: stylesProp,
40
+ style,
41
+ animationConfig: animationProp,
42
+ textInputProps,
43
+ },
44
+ ref
45
+ ) => {
46
+ const inputRef = useRef<RNTextInput>(null);
47
+ const [isFocused, setIsFocused] = useState(false);
48
+ const hasError = !!touched && !!error;
49
+
50
+ const theme = { ...DEFAULT_THEME, ...themeProp };
51
+ const anim = { ...DEFAULT_ANIMATION, ...animationProp };
52
+
53
+ const shouldBeActive = isFocused || !!value;
54
+ const focusAnim = useSharedValue(value ? 1 : 0);
55
+ const shakeAnim = useSharedValue(0);
56
+
57
+ useImperativeHandle(ref, () => ({
58
+ focus: () => inputRef.current?.focus(),
59
+ blur: () => inputRef.current?.blur(),
60
+ clear: () => inputRef.current?.clear(),
61
+ isFocused: () => inputRef.current?.isFocused() ?? false,
62
+ }));
63
+
64
+ useEffect(() => {
65
+ focusAnim.value = withTiming(shouldBeActive ? 1 : 0, {
66
+ duration: anim.labelDuration,
67
+ });
68
+ }, [shouldBeActive]);
69
+
70
+ useEffect(() => {
71
+ if (!hasError) return;
72
+ const mag = anim.shakeMagnitude;
73
+ const dur = anim.shakeDuration;
74
+ shakeAnim.value = withSequence(
75
+ withTiming(-mag, { duration: dur }),
76
+ withTiming(mag, { duration: dur }),
77
+ withTiming(-mag, { duration: dur }),
78
+ withTiming(0, { duration: dur })
79
+ );
80
+ }, [hasError]);
81
+
82
+ const labelAnimatedStyle = useAnimatedStyle(() => ({
83
+ transform: [
84
+ { translateX: shakeAnim.value },
85
+ {
86
+ translateY: interpolate(
87
+ focusAnim.value,
88
+ [0, 1],
89
+ [0, -anim.labelTranslateY]
90
+ ),
91
+ },
92
+ ],
93
+ fontSize: interpolate(
94
+ focusAnim.value,
95
+ [0, 1],
96
+ [theme.fontSize, theme.labelActiveFontSize]
97
+ ),
98
+ }));
99
+
100
+ function handleFocus() {
101
+ setIsFocused(true);
102
+ onFocus?.();
103
+ }
104
+
105
+ function handleBlur() {
106
+ setIsFocused(false);
107
+ onBlur?.();
108
+ }
109
+
110
+ function renderTextInput() {
111
+ const inputProps = {
112
+ ref: inputRef,
113
+ editable,
114
+ autoFocus,
115
+ maxLength,
116
+ value,
117
+ placeholder: shouldBeActive ? placeholder : undefined,
118
+ placeholderTextColor: theme.placeholderColor,
119
+ onChangeText,
120
+ onBlur: handleBlur,
121
+ onFocus: handleFocus,
122
+ keyboardType,
123
+ autoCapitalize,
124
+ secureTextEntry,
125
+ selectionColor: theme.selectionColor,
126
+ cursorColor: theme.selectionColor,
127
+ selectionHandleColor: theme.selectionColor,
128
+ underlineColorAndroid: "transparent" as const,
129
+ style: [
130
+ baseStyles.input,
131
+ {
132
+ color: theme.inputColor,
133
+ fontFamily: theme.fontFamily,
134
+ fontSize: theme.fontSize,
135
+ },
136
+ right ? baseStyles.inputWithRight : undefined,
137
+ stylesProp?.input,
138
+ ],
139
+ ...textInputProps,
140
+ };
141
+
142
+ const inputElement = renderInput ? (
143
+ renderInput(inputProps)
144
+ ) : (
145
+ <RNTextInput {...inputProps} />
146
+ );
147
+
148
+ return (
149
+ <View
150
+ style={[
151
+ baseStyles.inputContainer,
152
+ {
153
+ backgroundColor: theme.backgroundColor,
154
+ borderRadius: theme.borderRadius,
155
+ minHeight: theme.minHeight,
156
+ },
157
+ style,
158
+ stylesProp?.inputContainer,
159
+ ]}
160
+ >
161
+ <View
162
+ style={[
163
+ baseStyles.labelAndInputArea,
164
+ stylesProp?.labelAndInputArea,
165
+ ]}
166
+ >
167
+ <Animated.Text
168
+ style={[
169
+ baseStyles.label,
170
+ {
171
+ color: hasError ? theme.errorColor : theme.labelColor,
172
+ fontFamily: theme.fontFamily,
173
+ },
174
+ labelAnimatedStyle,
175
+ stylesProp?.label,
176
+ ]}
177
+ >
178
+ {label}
179
+ </Animated.Text>
180
+ {inputElement}
181
+ </View>
182
+ {right && (
183
+ <View style={[baseStyles.rightContainer, stylesProp?.right]}>
184
+ {right}
185
+ </View>
186
+ )}
187
+ </View>
188
+ );
189
+ }
190
+
191
+ function renderContent() {
192
+ if (onPress) {
193
+ return (
194
+ <Pressable onPress={onPress}>
195
+ <View pointerEvents="none">{renderTextInput()}</View>
196
+ </Pressable>
197
+ );
198
+ }
199
+
200
+ return renderTextInput();
201
+ }
202
+
203
+ return (
204
+ <Animated.View
205
+ layout={LinearTransition.springify()}
206
+ style={[baseStyles.container, stylesProp?.container]}
207
+ >
208
+ {renderContent()}
209
+ {hasError && (
210
+ <Animated.Text
211
+ style={[
212
+ baseStyles.errorText,
213
+ {
214
+ fontSize: theme.labelActiveFontSize,
215
+ color: theme.errorColor,
216
+ fontFamily: theme.fontFamily,
217
+ },
218
+ stylesProp?.error,
219
+ ]}
220
+ >
221
+ {error}
222
+ </Animated.Text>
223
+ )}
224
+ </Animated.View>
225
+ );
226
+ }
227
+ );
228
+
229
+ FloatingInput.displayName = "FloatingInput";
@@ -0,0 +1,58 @@
1
+ import { StyleSheet } from 'react-native'
2
+
3
+ import type { FloatingInputAnimationConfig, FloatingInputTheme } from './types'
4
+
5
+ export const DEFAULT_THEME: Required<FloatingInputTheme> = {
6
+ backgroundColor: '#EDEFF2',
7
+ labelColor: '#878A99',
8
+ inputColor: '#36373D',
9
+ errorColor: '#E3152E',
10
+ selectionColor: '#31BE30',
11
+ placeholderColor: '#878A99',
12
+ borderRadius: 14,
13
+ minHeight: 56,
14
+ fontSize: 16,
15
+ labelActiveFontSize: 12,
16
+ fontFamily: 'System',
17
+ }
18
+
19
+ export const DEFAULT_ANIMATION: Required<FloatingInputAnimationConfig> = {
20
+ labelDuration: 200,
21
+ shakeMagnitude: 2,
22
+ shakeDuration: 50,
23
+ labelTranslateY: 10,
24
+ }
25
+
26
+ export const baseStyles = StyleSheet.create({
27
+ container: {},
28
+ inputContainer: {
29
+ flexDirection: 'row',
30
+ alignItems: 'center',
31
+ },
32
+ labelAndInputArea: {
33
+ flex: 1,
34
+ justifyContent: 'center',
35
+ },
36
+ label: {
37
+ position: 'absolute',
38
+ left: 16,
39
+ },
40
+ input: {
41
+ paddingTop: 24,
42
+ paddingBottom: 8,
43
+ paddingLeft: 16,
44
+ paddingRight: 16,
45
+ },
46
+ inputWithRight: {
47
+ paddingRight: 48,
48
+ },
49
+ rightContainer: {
50
+ position: 'absolute',
51
+ right: 12,
52
+ alignSelf: 'center',
53
+ },
54
+ errorText: {
55
+ marginLeft: 8,
56
+ marginTop: 6,
57
+ },
58
+ })
package/src/index.tsx ADDED
@@ -0,0 +1,9 @@
1
+ export { FloatingInput } from './FloatingInput'
2
+ export { DEFAULT_ANIMATION, DEFAULT_THEME } from './defaults'
3
+ export type {
4
+ FloatingInputAnimationConfig,
5
+ FloatingInputProps,
6
+ FloatingInputRef,
7
+ FloatingInputStyles,
8
+ FloatingInputTheme,
9
+ } from './types'
package/src/types.ts ADDED
@@ -0,0 +1,79 @@
1
+ import type {
2
+ KeyboardTypeOptions,
3
+ StyleProp,
4
+ TextInputProps,
5
+ TextStyle,
6
+ ViewStyle,
7
+ } from 'react-native'
8
+
9
+ export interface FloatingInputTheme {
10
+ backgroundColor?: string
11
+ labelColor?: string
12
+ inputColor?: string
13
+ errorColor?: string
14
+ selectionColor?: string
15
+ placeholderColor?: string
16
+ borderRadius?: number
17
+ minHeight?: number
18
+ fontSize?: number
19
+ labelActiveFontSize?: number
20
+ fontFamily?: string
21
+ }
22
+
23
+ export interface FloatingInputStyles {
24
+ container?: StyleProp<ViewStyle>
25
+ inputContainer?: StyleProp<ViewStyle>
26
+ labelAndInputArea?: StyleProp<ViewStyle>
27
+ label?: StyleProp<TextStyle>
28
+ input?: StyleProp<TextStyle>
29
+ error?: StyleProp<TextStyle>
30
+ right?: StyleProp<ViewStyle>
31
+ }
32
+
33
+ export interface FloatingInputAnimationConfig {
34
+ labelDuration?: number
35
+ shakeMagnitude?: number
36
+ shakeDuration?: number
37
+ labelTranslateY?: number
38
+ }
39
+
40
+ export interface FloatingInputRef {
41
+ focus: () => void
42
+ blur: () => void
43
+ clear: () => void
44
+ isFocused: () => boolean
45
+ }
46
+
47
+ export interface FloatingInputProps {
48
+ // Value & behavior
49
+ value?: string
50
+ onChangeText?: (text: string) => void
51
+ onBlur?: () => void
52
+ onFocus?: () => void
53
+ onPress?: () => void
54
+ label?: string
55
+ placeholder?: string
56
+ error?: string
57
+ touched?: boolean
58
+
59
+ // TextInput pass-through
60
+ maxLength?: number
61
+ keyboardType?: KeyboardTypeOptions
62
+ autoFocus?: boolean
63
+ editable?: boolean
64
+ autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters'
65
+ secureTextEntry?: boolean
66
+
67
+ // Layout
68
+ right?: React.ReactNode
69
+ renderInput?: (props: TextInputProps) => React.ReactNode
70
+
71
+ // Customization
72
+ theme?: FloatingInputTheme
73
+ styles?: FloatingInputStyles
74
+ style?: StyleProp<ViewStyle>
75
+ animationConfig?: FloatingInputAnimationConfig
76
+
77
+ // Escape hatch
78
+ textInputProps?: Omit<TextInputProps, 'value' | 'onChangeText' | 'onBlur' | 'onFocus'>
79
+ }