Last active
June 4, 2024 02:25
-
-
Save halilb/9ac8e43e95ffbda42d52c34d420e78a4 to your computer and use it in GitHub Desktop.
A material TextField with React Native https://bilir.me/blog/creating-an-animated-textfield-with-react-native
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState } from 'react' | |
import { StyleSheet, Text, ScrollView, Button } from 'react-native' | |
import TextField from './components/TextField' | |
export default function App() { | |
const [value, setValue] = useState('') | |
const [error, setError] = useState<string | null>(null) | |
return ( | |
<ScrollView contentContainerStyle={styles.content}> | |
<Text style={styles.title}>Payment details</Text> | |
<TextField | |
style={styles.textField} | |
value={value} | |
label="Cardholder name" | |
errorText={error} | |
onChangeText={(text) => setValue(text)} | |
/> | |
<Button | |
title="Set error" | |
onPress={() => setError('This field is required.')} | |
/> | |
</ScrollView> | |
) | |
} | |
const styles = StyleSheet.create({ | |
content: { | |
paddingTop: 96, | |
paddingHorizontal: 36, | |
}, | |
title: { | |
fontFamily: 'Avenir-Heavy', | |
color: 'black', | |
fontSize: 32, | |
fontWeight: 'bold', | |
marginBottom: 32, | |
}, | |
textField: { | |
marginBottom: 32, | |
}, | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef, useState } from 'react' | |
import { | |
Text, | |
TextInput, | |
StyleSheet, | |
View, | |
Animated, | |
Easing, | |
TouchableWithoutFeedback, | |
} from 'react-native' | |
type Props = React.ComponentProps<typeof TextInput> & { | |
label: string | |
errorText?: string | null | |
} | |
const TextField: React.FC<Props> = (props) => { | |
const { | |
label, | |
errorText, | |
value, | |
style, | |
onBlur, | |
onFocus, | |
...restOfProps | |
} = props | |
const [isFocused, setIsFocused] = useState(false) | |
const inputRef = useRef<TextInput>(null) | |
const focusAnim = useRef(new Animated.Value(0)).current | |
useEffect(() => { | |
Animated.timing(focusAnim, { | |
toValue: isFocused || !!value ? 1 : 0, | |
duration: 150, | |
easing: Easing.bezier(0.4, 0, 0.2, 1), | |
useNativeDriver: true, | |
}).start() | |
}, [focusAnim, isFocused, value]) | |
let color = isFocused ? '#080F9C' : '#B9C4CA' | |
if (errorText) { | |
color = '#B00020' | |
} | |
return ( | |
<View style={style}> | |
<TextInput | |
style={[ | |
styles.input, | |
{ | |
borderColor: color, | |
}, | |
]} | |
ref={inputRef} | |
{...restOfProps} | |
value={value} | |
onBlur={(event) => { | |
setIsFocused(false) | |
onBlur?.(event) | |
}} | |
onFocus={(event) => { | |
setIsFocused(true) | |
onFocus?.(event) | |
}} | |
/> | |
<TouchableWithoutFeedback onPress={() => inputRef.current?.focus()}> | |
<Animated.View | |
style={[ | |
styles.labelContainer, | |
{ | |
transform: [ | |
{ | |
scale: focusAnim.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [1, 0.75], | |
}), | |
}, | |
{ | |
translateY: focusAnim.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [24, -12], | |
}), | |
}, | |
{ | |
translateX: focusAnim.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [16, 0], | |
}), | |
}, | |
], | |
}, | |
]} | |
> | |
<Text | |
style={[ | |
styles.label, | |
{ | |
color, | |
}, | |
]} | |
> | |
{label} | |
{errorText ? '*' : ''} | |
</Text> | |
</Animated.View> | |
</TouchableWithoutFeedback> | |
{!!errorText && <Text style={styles.error}>{errorText}</Text>} | |
</View> | |
) | |
} | |
const styles = StyleSheet.create({ | |
input: { | |
padding: 24, | |
borderWidth: 1, | |
borderRadius: 4, | |
fontFamily: 'Avenir-Medium', | |
fontSize: 16, | |
}, | |
labelContainer: { | |
position: 'absolute', | |
paddingHorizontal: 8, | |
backgroundColor: 'white', | |
}, | |
label: { | |
fontFamily: 'Avenir-Heavy', | |
fontSize: 16, | |
}, | |
error: { | |
marginTop: 4, | |
marginLeft: 12, | |
fontSize: 12, | |
color: '#B00020', | |
fontFamily: 'Avenir-Medium', | |
}, | |
}) | |
export default TextField |
okay thanks i will check that
The label's position onFocus position is different if the label text have different width, demo here
It's been two years, but a hack to solve @iaminarush issue is to modify the translationX into
translateX: focusAnim.interpolate({
inputRange: [0, 1],
outputRange: [16, 16 - props.label.length],
})
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I believe this needs to be handled at the parent of the text input. You may check this component which handles focusing to the next input: https://github.com/halilb/rn-credit-card/blob/master/src/components/CreditCardForm.tsx#L71