Files

96 lines
2.3 KiB
TypeScript

import { useEffect, useRef } from 'react';
import {
Pressable,
StyleSheet,
Text,
TextInput,
View,
type TextInput as TextInputType,
} from 'react-native';
import { colors } from '../theme/colors';
interface Props {
value: string;
onChange: (next: string) => void;
length?: number;
autoFocus?: boolean;
disabled?: boolean;
ariaLabel: string;
onSubmit?: () => void;
}
// Six-slot numeric PIN entry. The actual input is an invisible TextInput
// that captures the numeric keyboard; visible slots render bullets when
// filled. Tapping anywhere on the row re-focuses the input.
export function PinInput({
value,
onChange,
length = 6,
autoFocus,
disabled,
ariaLabel,
onSubmit,
}: Props) {
const ref = useRef<TextInputType | null>(null);
useEffect(() => {
if (autoFocus) ref.current?.focus();
}, [autoFocus]);
return (
<Pressable onPress={() => ref.current?.focus()} style={styles.row}>
<TextInput
ref={ref}
testID="pin-input"
accessibilityLabel={ariaLabel}
keyboardType="numeric"
textContentType="oneTimeCode"
autoComplete="one-time-code"
maxLength={length}
editable={!disabled}
value={value}
onChangeText={(t) => onChange(t.replace(/\D/g, '').slice(0, length))}
onSubmitEditing={() => {
if (value.length === length) onSubmit?.();
}}
style={styles.hidden}
/>
<View style={styles.slots}>
{Array.from({ length }).map((_, i) => {
const filled = i < value.length;
return (
<View key={i} style={[styles.slot, filled && styles.slotFilled]}>
{filled && <Text style={styles.bullet}></Text>}
</View>
);
})}
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
row: { alignItems: 'center' },
hidden: {
position: 'absolute',
width: 1,
height: 1,
opacity: 0,
},
slots: { flexDirection: 'row', gap: 8 },
slot: {
width: 40,
height: 48,
borderRadius: 10,
borderWidth: 1,
borderColor: colors.border,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.surface,
},
slotFilled: {
borderColor: colors.accent,
backgroundColor: colors.bg,
},
bullet: { color: colors.text, fontSize: 22 },
});