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.