Skip to content

Instantly share code, notes, and snippets.

@halilb
Last active November 10, 2023 07:31
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save halilb/9ac8e43e95ffbda42d52c34d420e78a4 to your computer and use it in GitHub Desktop.
Save halilb/9ac8e43e95ffbda42d52c34d420e78a4 to your computer and use it in GitHub Desktop.
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,
},
})
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
@iaminarush
Copy link

The label's position onFocus position is different if the label text have different width, demo here

@deckyfx
Copy link

deckyfx commented Dec 20, 2022

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