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.