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 |
is there any way that we can do focus on the next keyboard button click? If multiple text fields we are using and on next button click it will focus on next textfield. How we can achive with custom above textfield?
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
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
is there any way that we can do focus on the next keyboard button click? If multiple text fields we are using and on next button click it will focus on next textfield. How we can achive with custom above textfield?