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
accessibilityLabelfrom 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>
);
}