0

I have a custom InputField component which uses the React Native Text Input. For some fields, the input will require an X button when the field is focused so user can clear the field. This button is absolutely positioned inside the text input which is why I believe this is not working (as per previous Stack Overflow issues, but the solutions haven't worked for me).

In the same component, I have a check for if the input is the password field which has another icon which works perfectly fine when the keyboard opens up and the field is focused, but despite trying to exactly replicate this for other fields that require the X icon, I can't seem to get the same functionality to get a button to work and be clickable when absolutely positioned inside a text input.

Would point out that I believe the differences between the Text Input with the X icon and the password input is the password inputs is in a modal. And because the password fields are in a modal (modal made with BottomSheetComponent), I am using a custom BottomSheetInputField for this instead of the custom Input Field (same code with minor ref differences) - both attached below

Password input = click on the field, opens modal with 3 password text inputs Inputs with X icon = click on the field, no modal, opens keyboard and changes icon from Pencil to X. Clicking X should clear the field (not working.)

InputField Component:

import React, { useState } from 'react'; import { View, Text, StyleSheet, TextInput, TouchableOpacity, StyleProp, TextStyle, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { colors, font, fontSize } from '@/src/constants/AppDesign'; import { XIcon } from '@/src/components/icons'; interface Props { label?: string; value: string; onChangeText: (text: string) => void; placeholder?: string; keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad'; autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters'; autoComplete?: 'email' | 'password' | 'off' | undefined; secureTextEntry?: boolean; isPassword?: boolean; withSuffix?: boolean; suffixIcon?: React.ReactNode; containerStyle?: StyleProp<TextStyle>; onFocus?: () => void; onBlur?: () => void; onClear?: () => void; } export function InputField({ label, value, onChangeText, placeholder, keyboardType = 'default', autoCapitalize = 'none', autoComplete, secureTextEntry, isPassword = false, withSuffix = false, suffixIcon, containerStyle, onFocus, onBlur, onClear, }: Props) { const [showPassword, setShowPassword] = useState(false); const renderSuffixIcon = () => { if (containerStyle && onClear) { return ( <TouchableOpacity onPress={onClear} style={styles.suffixIcon}> <View style={styles.clearIconContainer}> <XIcon color={colors.textWhite} width={12} height={12} /> </View> </TouchableOpacity> ); } return suffixIcon && <View style={styles.suffixIcon}>{suffixIcon}</View>; }; return ( <View style={styles.formGroup}> {label && <Text style={styles.label}>{label}</Text>} {isPassword ? ( <View style={styles.passwordContainer}> <TextInput style={[styles.passwordInput, containerStyle]} value={value} onChangeText={onChangeText} secureTextEntry={!showPassword} placeholder={placeholder} autoCapitalize={autoCapitalize} autoComplete={autoComplete} onFocus={onFocus} onBlur={onBlur} /> <TouchableOpacity style={styles.eyeIcon} onPress={() => setShowPassword(!showPassword)}> <Ionicons name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={24} color={colors.textLighter} /> </TouchableOpacity> </View> ) : withSuffix ? ( <View style={styles.suffixContainer}> <TextInput style={[styles.input, containerStyle]} value={value} onChangeText={onChangeText} placeholder={placeholder} keyboardType={keyboardType} autoCapitalize={autoCapitalize} autoComplete={autoComplete} secureTextEntry={secureTextEntry} onFocus={onFocus} onBlur={onBlur} /> {renderSuffixIcon()} </View> ) : ( <TextInput style={[styles.input, containerStyle]} value={value} onChangeText={onChangeText} placeholder={placeholder} keyboardType={keyboardType} autoCapitalize={autoCapitalize} autoComplete={autoComplete} secureTextEntry={secureTextEntry} onFocus={onFocus} onBlur={onBlur} /> )} </View> ); } const styles = StyleSheet.create({ formGroup: { marginBottom: 14, width: '100%', }, label: { fontSize: fontSize.xs, fontFamily: font.roboto, marginBottom: 8, color: colors.textLight, }, input: { height: 50, borderRadius: 8, paddingHorizontal: 16, backgroundColor: colors.backgroundLight, width: '100%', fontFamily: font.roboto, }, passwordContainer: { position: 'relative', width: '100%', }, passwordInput: { height: 50, borderRadius: 8, paddingHorizontal: 16, backgroundColor: colors.backgroundLight, width: '100%', fontFamily: font.roboto, }, suffixContainer: { position: 'relative', width: '100%', }, suffixIcon: { position: 'absolute', right: 16, top: 15, }, eyeIcon: { position: 'absolute', right: 12, top: 13, }, clearIconContainer: { height: 18, width: 18, backgroundColor: colors.primary, borderRadius: 10, justifyContent: 'center', alignItems: 'center', }, }); 

BottomSheetInputField component:

import React, { forwardRef, useCallback, useState } from 'react'; import { View, Text, StyleSheet, TextInput, TouchableOpacity, StyleProp, TextStyle, NativeSyntheticEvent, TextInputFocusEventData, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { colors, font, fontSize } from '@/src/constants/AppDesign'; import { XIcon } from '@/src/components/icons'; import { useBottomSheetInternal } from '@gorhom/bottom-sheet'; interface Props { label?: string; value: string; onChangeText: (text: string) => void; placeholder?: string; keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad'; autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters'; autoComplete?: 'email' | 'password' | 'off' | undefined; secureTextEntry?: boolean; isPassword?: boolean; withSuffix?: boolean; suffixIcon?: React.ReactNode; containerStyle?: StyleProp<TextStyle>; onFocus?: (event: NativeSyntheticEvent<TextInputFocusEventData>) => void; onBlur?: (event: NativeSyntheticEvent<TextInputFocusEventData>) => void; onClear?: () => void; } export const BottomSheetInputField = forwardRef<TextInput, Props>( ( { onFocus, onBlur, label, value, onChangeText, placeholder, keyboardType, autoCapitalize, autoComplete, secureTextEntry, isPassword, withSuffix, suffixIcon, containerStyle, onClear, }, ref, ) => { const [showPassword, setShowPassword] = useState(false); const { shouldHandleKeyboardEvents } = useBottomSheetInternal(); const handleOnFocus = useCallback( (args: NativeSyntheticEvent<TextInputFocusEventData>) => { shouldHandleKeyboardEvents.value = true; if (onFocus) { onFocus(args); } }, [onFocus, shouldHandleKeyboardEvents], ); const handleOnBlur = useCallback( (args: NativeSyntheticEvent<TextInputFocusEventData>) => { shouldHandleKeyboardEvents.value = false; if (onBlur) { onBlur(args); } }, [onBlur, shouldHandleKeyboardEvents], ); const renderSuffixIcon = () => { if (containerStyle && onClear) { return ( <TouchableOpacity onPress={onClear} style={styles.suffixIcon}> <View style={styles.clearIconContainer}> <XIcon color={colors.textWhite} width={12} height={12} /> </View> </TouchableOpacity> ); } return suffixIcon && <View style={styles.suffixIcon}>{suffixIcon}</View>; }; return ( <View style={styles.formGroup}> {label && <Text style={styles.label}>{label}</Text>} {isPassword ? ( <View style={styles.passwordContainer}> <TextInput ref={ref} style={[styles.passwordInput, containerStyle]} value={value} onChangeText={onChangeText} secureTextEntry={!showPassword} placeholder={placeholder} autoCapitalize={autoCapitalize} autoComplete={autoComplete} onFocus={handleOnFocus} onBlur={handleOnBlur} /> <TouchableOpacity style={styles.eyeIcon} onPress={() => setShowPassword(!showPassword)}> <Ionicons name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={24} color={colors.textLighter} /> </TouchableOpacity> </View> ) : withSuffix ? ( <View style={styles.suffixContainer}> <TextInput ref={ref} style={[styles.input, containerStyle]} value={value} onChangeText={onChangeText} placeholder={placeholder} keyboardType={keyboardType} autoCapitalize={autoCapitalize} autoComplete={autoComplete} secureTextEntry={secureTextEntry} onFocus={handleOnFocus} onBlur={handleOnBlur} /> {renderSuffixIcon()} </View> ) : ( <TextInput ref={ref} style={[styles.input, containerStyle]} value={value} onChangeText={onChangeText} placeholder={placeholder} keyboardType={keyboardType} autoCapitalize={autoCapitalize} autoComplete={autoComplete} secureTextEntry={secureTextEntry} onFocus={handleOnFocus} onBlur={handleOnBlur} /> )} </View> ); }, ); BottomSheetInputField.displayName = 'BottomSheetInputField'; const styles = StyleSheet.create({ formGroup: { marginBottom: 14, width: '100%', }, label: { fontSize: fontSize.xs, fontFamily: font.roboto, marginBottom: 8, color: colors.textLight, }, input: { height: 50, borderRadius: 8, paddingHorizontal: 16, backgroundColor: colors.backgroundLight, width: '100%', fontFamily: font.roboto, }, passwordContainer: { position: 'relative', width: '100%', }, passwordInput: { height: 50, borderRadius: 8, paddingHorizontal: 16, backgroundColor: colors.backgroundLight, width: '100%', fontFamily: font.roboto, }, suffixContainer: { position: 'relative', width: '100%', }, suffixIcon: { position: 'absolute', right: 16, top: 15, }, eyeIcon: { position: 'absolute', right: 12, top: 13, }, clearIconContainer: { height: 18, width: 18, backgroundColor: colors.primary, borderRadius: 10, justifyContent: 'center', alignItems: 'center', }, }); 

firstName and lastName fields (which have the X icon) in the parent file:

 <View style={styles.nameInputContainer}> <View style={styles.nameInputItem}> <InputField label={t('profile.editProfileScreen.nameLabel')} value={values.firstName} onChangeText={handleChange('firstName')} withSuffix suffixIcon={ selectedField !== 'firstName' ? <PencilIcon /> : undefined } containerStyle={ selectedField === 'firstName' ? styles.selectedField : undefined } onFocus={() => setSelectedField('firstName')} onBlur={() => setSelectedField(null)} onClear={() => setFieldValue('firstName', '')} /> </View> <View style={styles.nameInputItem}> <InputField label={t('profile.editProfileScreen.surnameLabel')} value={values.lastName} onChangeText={handleChange('lastName')} withSuffix suffixIcon={ selectedField !== 'lastName' ? <PencilIcon /> : undefined } containerStyle={ selectedField === 'lastName' ? styles.selectedField : undefined } onFocus={() => setSelectedField('lastName')} onBlur={() => setSelectedField(null)} onClear={() => setFieldValue('lastName', '')} /> </View> </View> 

Password field (which is a custom Select Field but triggers the modal when clicked):

 <View style={[styles.customInputContainer, { marginTop: 12 }]}> <View style={styles.customInputTextContainer}> <SelectField label={t('profile.editProfileScreen.passwordLabel')} value={values.password} isPassword={true} onPress={() => { setFieldValue('tempCurrentPassword', ''); setFieldValue('tempNewPassword', ''); setFieldValue('tempConfirmPassword', ''); setSelectedField('password'); setModalTitle(t('profile.editProfileScreen.changePassword')); setCustomModalContentType('password'); setActiveModalType('custom'); setIsModalVisible(true); }} containerStyle={ selectedField === 'password' ? styles.selectedField : undefined } /> </View> </View> 

password fields inside the modal:

} else if (customModalContentType === 'password') { return ( <View style={styles.customModalContainer}> <View style={styles.customModalFieldContainer}> <Text style={styles.customModalLabel}> {t('profile.editProfileScreen.currentPasswordLabel')} </Text> <BottomSheetInputField isPassword={true} value={values.tempCurrentPassword} onChangeText={text => setFieldValue('tempCurrentPassword', text)} /> <TouchableOpacity style={styles.forgotPasswordContainer} onPress={() => {}}> <Text style={styles.forgotPassword}> {t('login.forgotPassword')} </Text> </TouchableOpacity> </View> <View style={styles.customModalFieldContainer}> <Text style={styles.customModalLabel}> {t('profile.editProfileScreen.newPasswordLabel')} </Text> <BottomSheetInputField isPassword={true} value={values.tempNewPassword} onChangeText={text => setFieldValue('tempNewPassword', text)} /> </View> <View style={styles.customModalFieldContainer}> <Text style={styles.customModalLabel}> {t('profile.editProfileScreen.repeatNewPasswordLabel')} </Text> <BottomSheetInputField isPassword={true} value={values.tempConfirmPassword} onChangeText={text => setFieldValue('tempConfirmPassword', text)} /> </View> </View> ); } 

Things I've tried:

  • Removing absolute positioning completely
  • Removing the conditional rendering to test
  • Using the same styles for the X Icon as I did for the eye Icon

None of this worked, would appreciate some support, thanks.

1
  • Hi and welcome to SO. So what exactly is the problem? Is the button not clickable/disabled? Or is it clickable but it doesn't do anything? Is the code for the click event not running at all? Have you set up a breakpoint? Commented Jun 2 at 17:08

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.