UTA DevHub

Input Component Example

Complete implementation of the Input component with all variants, states, and features

Input Component Example

A comprehensive example of our foundation Input component implementation.

Complete Implementation

/**
 * @title Input Component
 * @description Text input component with variants, validation, and accessibility
 * @source https://github.com/yourorg/react-native-template/blob/main/src/ui/foundation/Input/Input.tsx
 * @docs /docs/ui-development/architecture/foundation-guide#input-components
 */
 
// ui/foundation/Input/Input.tsx
import React, { useState, forwardRef } from 'react';
import { TextInput, View, Text } from 'react-native';
import { theme } from '@/core/shared/styles/theme';
import { typography } from '@/core/shared/styles/typography';
import type { InputProps } from './types';
 
export const Input = forwardRef<TextInput, InputProps>(({
  label,
  error,
  helper,
  size = 'medium',
  variant = 'outlined',
  disabled = false,
  required = false,
  icon,
  iconRight,
  style,
  ...props
}, ref) => {
  const [isFocused, setIsFocused] = useState(false);
 
  const getSizeStyles = () => {
    switch (size) {
      case 'small':
        return {
          height: 36,
          paddingHorizontal: theme.spacing.sm,
          fontSize: typography.sizes.small,
        };
      case 'medium':
        return {
          height: 44,
          paddingHorizontal: theme.spacing.md,
          fontSize: typography.sizes.medium,
        };
      case 'large':
        return {
          height: 52,
          paddingHorizontal: theme.spacing.lg,
          fontSize: typography.sizes.large,
        };
    }
  };
 
  const getVariantStyles = () => {
    const baseStyles = {
      borderRadius: theme.radii.md,
      backgroundColor: disabled ? theme.colors.gray[50] : theme.colors.white,
    };
 
    if (error) {
      return {
        ...baseStyles,
        borderWidth: 1,
        borderColor: theme.colors.error[500],
      };
    }
 
    switch (variant) {
      case 'outlined':
        return {
          ...baseStyles,
          borderWidth: 1,
          borderColor: isFocused 
            ? theme.colors.primary[500] 
            : theme.colors.border.default,
        };
      case 'filled':
        return {
          ...baseStyles,
          backgroundColor: disabled 
            ? theme.colors.gray[100] 
            : theme.colors.gray[50],
          borderWidth: 0,
        };
    }
  };
 
  const inputStyles = {
    ...getSizeStyles(),
    ...getVariantStyles(),
    color: disabled ? theme.colors.text.disabled : theme.colors.text.primary,
  };
 
  const containerStyles = {
    marginBottom: (error || helper) ? theme.spacing.xs : 0,
  };
 
  return (
    <View style={[containerStyles, style]}>
      {label && (
        <Text style={{
          fontSize: typography.sizes.small,
          fontWeight: typography.weights.medium,
          color: theme.colors.text.secondary,
          marginBottom: theme.spacing.xs,
        }}>
          {label}
          {required && (
            <Text style={{ color: theme.colors.error[500] }}> *</Text>
          )}
        </Text>
      )}
      
      <View style={{ position: 'relative' }}>
        {icon && (
          <View style={{
            position: 'absolute',
            left: theme.spacing.sm,
            top: '50%',
            transform: [{ translateY: -10 }],
            zIndex: 1,
          }}>
            {icon}
          </View>
        )}
        
        <TextInput
          ref={ref}
          style={[
            inputStyles,
            icon && { paddingLeft: theme.spacing.xl },
            iconRight && { paddingRight: theme.spacing.xl },
          ]}
          placeholderTextColor={theme.colors.text.placeholder}
          editable={!disabled}
          onFocus={() => setIsFocused(true)}
          onBlur={() => setIsFocused(false)}
          accessibilityLabel={label}
          accessibilityState={{ disabled }}
          {...props}
        />
        
        {iconRight && (
          <View style={{
            position: 'absolute',
            right: theme.spacing.sm,
            top: '50%',
            transform: [{ translateY: -10 }],
          }}>
            {iconRight}
          </View>
        )}
      </View>
      
      {(error || helper) && (
        <Text style={{
          fontSize: typography.sizes.small,
          color: error ? theme.colors.error[500] : theme.colors.text.secondary,
          marginTop: theme.spacing.xs,
        }}>
          {error || helper}
        </Text>
      )}
    </View>
  );
});
 
Input.displayName = 'Input';

TypeScript Interface

// ui/foundation/Input/types.ts
import type { TextInputProps, ViewStyle } from 'react-native';
 
export interface InputProps extends Omit<TextInputProps, 'style'> {
  /**
   * Label displayed above the input
   */
  label?: string;
  
  /**
   * Error message displayed below the input
   */
  error?: string;
  
  /**
   * Helper text displayed below the input
   */
  helper?: string;
  
  /**
   * Visual style variant
   * @default 'outlined'
   */
  variant?: 'outlined' | 'filled';
  
  /**
   * Size of the input affecting height and text size
   * @default 'medium'
   */
  size?: 'small' | 'medium' | 'large';
  
  /**
   * Whether the input is disabled
   * @default false
   */
  disabled?: boolean;
  
  /**
   * Whether the input is required
   * @default false
   */
  required?: boolean;
  
  /**
   * Icon element to display at the start of the input
   */
  icon?: React.ReactNode;
  
  /**
   * Icon element to display at the end of the input
   */
  iconRight?: React.ReactNode;
  
  /**
   * Custom styles applied to the container
   */
  style?: ViewStyle;
}

Usage Examples

Basic Usage

<Input
  label="Email"
  placeholder="Enter your email"
  value={email}
  onChangeText={setEmail}
/>

With Validation

<Input
  label="Password"
  placeholder="Enter password"
  value={password}
  onChangeText={setPassword}
  error={passwordError}
  helper="Must be at least 8 characters"
  secureTextEntry
  required
/>

With Icons

<Input
  label="Search"
  placeholder="Search products..."
  value={search}
  onChangeText={setSearch}
  icon={<SearchIcon size={20} color={theme.colors.gray[400]} />}
  iconRight={
    search ? (
      <TouchableOpacity onPress={() => setSearch('')}>
        <CloseIcon size={20} color={theme.colors.gray[400]} />
      </TouchableOpacity>
    ) : null
  }
/>

Different Variants and Sizes

// Filled variant
<Input
  variant="filled"
  label="Company Name"
  value={company}
  onChangeText={setCompany}
/>
 
// Large size
<Input
  size="large"
  label="Title"
  value={title}
  onChangeText={setTitle}
/>
 
// Disabled state
<Input
  label="Read Only"
  value="This cannot be edited"
  disabled
/>

Testing

// ui/foundation/Input/Input.test.tsx
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { Input } from './Input';
 
describe('Input', () => {
  it('renders with label', () => {
    const { getByText } = render(
      <Input label="Email" />
    );
    expect(getByText('Email')).toBeTruthy();
  });
 
  it('shows required asterisk', () => {
    const { getByText } = render(
      <Input label="Email" required />
    );
    expect(getByText('*')).toBeTruthy();
  });
 
  it('displays error message', () => {
    const { getByText } = render(
      <Input label="Email" error="Invalid email" />
    );
    expect(getByText('Invalid email')).toBeTruthy();
  });
 
  it('displays helper text', () => {
    const { getByText } = render(
      <Input label="Email" helper="Enter your work email" />
    );
    expect(getByText('Enter your work email')).toBeTruthy();
  });
 
  it('calls onChangeText when typing', () => {
    const onChangeText = jest.fn();
    const { getByPlaceholderText } = render(
      <Input placeholder="Type here" onChangeText={onChangeText} />
    );
    
    fireEvent.changeText(getByPlaceholderText('Type here'), 'Hello');
    expect(onChangeText).toHaveBeenCalledWith('Hello');
  });
 
  it('is not editable when disabled', () => {
    const { getByPlaceholderText } = render(
      <Input placeholder="Type here" disabled />
    );
    
    const input = getByPlaceholderText('Type here');
    expect(input.props.editable).toBe(false);
  });
 
  it('handles focus and blur', () => {
    const onFocus = jest.fn();
    const onBlur = jest.fn();
    const { getByPlaceholderText } = render(
      <Input 
        placeholder="Type here" 
        onFocus={onFocus}
        onBlur={onBlur}
      />
    );
    
    const input = getByPlaceholderText('Type here');
    fireEvent(input, 'focus');
    expect(onFocus).toHaveBeenCalled();
    
    fireEvent(input, 'blur');
    expect(onBlur).toHaveBeenCalled();
  });
});

Accessibility

The Input component includes comprehensive accessibility support:

  • Labels: Automatically sets accessibilityLabel from the label prop
  • States: Communicates disabled state through accessibilityState
  • Error Announcements: Error messages are announced to screen readers
  • Required Fields: Required indicator is included in the label
  • Helper Text: Additional context is provided through helper text

Integration Example

// features/auth/components/LoginForm.tsx
import React, { useState } from 'react';
import { View } from 'react-native';
import { Input } from '@/ui/foundation/Input';
import { Button } from '@/ui/foundation/Button';
import { useAuth } from '@/core/domains/auth/hooks';
import { validateEmail } from '@/core/shared/utils/validation';
 
export function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState<Record<string, string>>({});
  const { login, isLoading } = useAuth();
 
  const handleSubmit = async () => {
    const newErrors: Record<string, string> = {};
    
    if (!validateEmail(email)) {
      newErrors.email = 'Please enter a valid email';
    }
    
    if (password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }
    
    setErrors(newErrors);
    
    if (Object.keys(newErrors).length === 0) {
      await login({ email, password });
    }
  };
 
  return (
    <View style={{ padding: theme.spacing.lg }}>
      <Input
        label="Email"
        placeholder="Enter your email"
        value={email}
        onChangeText={setEmail}
        error={errors.email}
        keyboardType="email-address"
        autoCapitalize="none"
        required
      />
      
      <Input
        label="Password"
        placeholder="Enter your password"
        value={password}
        onChangeText={setPassword}
        error={errors.password}
        secureTextEntry
        required
        style={{ marginTop: theme.spacing.md }}
      />
      
      <Button
        onPress={handleSubmit}
        loading={isLoading}
        style={{ marginTop: theme.spacing.lg }}
      >
        Sign In
      </Button>
    </View>
  );
}

On this page