Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
pprunty committed Nov 29, 2023
1 parent aea962c commit 015403f
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 111 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"eslint-plugin-react-hooks": "4.2.0",
"gh-pages": "3.2.3",
"prettier": "2.3.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"rollup": "2.79.1",
"rollup-plugin-peer-deps-external": "2.2.4",
Expand Down
311 changes: 201 additions & 110 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useState, createRef, ChangeEvent, KeyboardEvent, useEffect, useRef, useMemo} from 'react';
import React, {useState, useEffect, useRef} from 'react';
import styled from 'styled-components';
import * as Yup from 'yup';

Expand Down Expand Up @@ -39,47 +39,102 @@ const validateOtp = async (otpValue: string, inputType: string, length: number,
}
};

interface OtpInputProps {
length: number;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onFullFill: () => void;
autoComplete?: string; // Add the autoComplete prop
setFieldError: (field: string, message: string | undefined) => void; // Add this line
setFieldTouched: (field: string, isTouched: boolean, shouldValidate?: boolean) => void; // Add this line
autoFocus?: boolean; // Add autoFocus prop
autoSubmit?: boolean; // Add autoSubmit prop
inputType?: string | 'alphanumeric' | 'numeric' | 'alphabetic'; // Add inputType prop
onBlur: (e: React.FocusEvent<any>) => void;
textColor?: string;
backgroundColor?: string;
highlightColor?: string;
borderColor?: string;
}

const Container = styled.div`
display: flex;
justify-content: center;
max-width: inherit;
//align-items: center; // Optional, for vertical centering
// Additional style for space between third and fourth input
.extra-space {
margin-right: 15px; // Adjust the space as needed
}
`;

// Modify the Input styled component to remove cursor
const Input = styled.input`
width: 35px;
height: 35px;
margin: 0 4px;
const hexToRgba = (hex: any, alpha = 1) => {
let r: any = 0, g: any = 0, b: any = 0;
if (hex.length === 4) {
r = "0x" + hex[1] + hex[1];
g = "0x" + hex[2] + hex[2];
b = "0x" + hex[3] + hex[3];
} else if (hex.length === 7) {
r = "0x" + hex[1] + hex[2];
g = "0x" + hex[3] + hex[4];
b = "0x" + hex[5] + hex[6];
}
return `rgba(${+r}, ${+g}, ${+b}, ${alpha})`;
};

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
textColor?: string;
backgroundColor?: string;
highlightColor?: string;
borderColor?: string;
}

const Input = styled.input<InputProps>`
width: 34px; // Larger width for easier tapping
height: 38px; // Larger height for visibility
margin: 0 6px; // Space between input boxes
text-align: center;
font-size: 20px;
border: 1px solid #ccc;
border-radius: 4px;
caret-color: transparent; // Remove cursor
font-family: Monospaced, monospace;
border: 1.5px solid ${props => (props.borderColor || '#ccc')};
border-radius: 10px; // Rounded corners
//caret-color: blue; // Visible caret color
caret-color: transparent;
color: ${props => props.textColor || '#000000'}; // Default to black if not provided
background: ${props => props.backgroundColor || '#fff'};
&[type='number'] {
-moz-appearance: textfield; // For Firefox
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
&:focus {
background-color: #ffffff; // Example: light blue background
border-color: #ff7418; // Example: blue border
// You can add other styles like box-shadow, etc.
border-color: ${props => props.highlightColor || '#ff8000'}; // Change border color on focus
outline: none; // Remove default outline
box-shadow: 0 0 5px ${props => props.highlightColor ? hexToRgba(props.highlightColor, 0.4) : 'rgba(218, 143, 82, 0.3)'}; // Use highlightColor for shadow
}
`;
export interface OtpInputProps {
length: number;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onFullFill: () => void;
autoComplete?: string; // Add the autoComplete prop
setFieldError: (field: string, message: string | undefined) => void; // Add this line
setFieldTouched: (field: string, isTouched: boolean, shouldValidate?: boolean) => void; // Add this line
autoFocus?: boolean; // Add autoFocus prop
autoSubmit?: boolean; // Add autoSubmit prop
keyHandling?: boolean; // Add keyHandling prop
inputType?: 'alphanumeric' | 'numeric' | 'alphabetic'; // Add inputType prop
onBlur: (e: React.FocusEvent<any>) => void;
&::placeholder {
color: #ced4da;
}
}
// Styles for error feedback
&.error {
border-color: #dc3545;
}
// Responsive design adjustments
@media (max-width: 600px) {
width: 30px;
height: 42px;
}
`;

// todo: add autoSubmit field, colorHighlight field (optional), autoFocus = True, keyHandling
const OtpInput: React.FC<OtpInputProps> = ({
length,
onChange,
Expand All @@ -91,70 +146,75 @@ const OtpInput: React.FC<OtpInputProps> = ({
inputType = 'numeric',
autoFocus = true,
autoSubmit = true,
keyHandling = true
textColor,
backgroundColor,
highlightColor,
borderColor
}) => {
const [localOtp, setLocalOtp] = useState<string[]>(new Array(length).fill(''));
const inputRefs = useRef<(HTMLInputElement | null)[]>(new Array(length).fill(null));
const otpSchema = useMemo(() => createOtpSchema(inputType, length), [inputType, length]);
const [hasUserStartedTyping, setHasUserStartedTyping] = useState<boolean>(false);

// Custom hook for OTP validation
useEffect(() => {
const otpValue = localOtp.join('');
if (hasUserStartedTyping) {
validateOtp(otpValue, inputType, length, setFieldError);
} else {
setFieldError("otp", undefined); // Clear error initially
}
}, [localOtp, inputType, length, setFieldError, hasUserStartedTyping]);
const [localOtp, setLocalOtp] = useState<string[]>(new Array(length).fill(''));
const inputRefs = useRef<(HTMLInputElement | null)[]>(new Array(length).fill(null));
const [hasUserStartedTyping, setHasUserStartedTyping] = useState<boolean>(false);
const [hasAutoSubmitted, setHasAutoSubmitted] = useState<boolean>(false);

useEffect(() => {
if (autoFocus && inputRefs.current[0]) {
inputRefs.current[0].focus();
setFieldTouched("otp", true);
// Custom hook for OTP validation
useEffect(() => {
const otpValue = localOtp.join('');
if (hasUserStartedTyping) {
validateOtp(otpValue, inputType, length, setFieldError);
} else {
setFieldError("otp", undefined); // Clear error initially
}
}, [localOtp, inputType, length, setFieldError, hasUserStartedTyping]);

}
}, [autoFocus]);
useEffect(() => {
if (autoFocus && inputRefs.current[0]) {
inputRefs.current[0].focus();
setFieldTouched("otp", true);
}
}, [autoFocus, setFieldTouched]); // Include setFieldTouched in the dependency array

const handleChange = async (event: React.ChangeEvent<HTMLInputElement>, index: number) => {
if (!hasUserStartedTyping) {
setHasUserStartedTyping(true);
}
setFieldTouched("otp", true);
const newOtp = [...localOtp];
const newValue = event.target.value.slice(-1);
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
// event.target.select();
};

if (isValidInput(newValue, inputType)) {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, index: number) => {
if (!hasUserStartedTyping) {
setHasUserStartedTyping(true);
}
setFieldTouched("otp", true);

const newValue = event.target.value.slice(-1); // Take the last character
const newOtp = [...localOtp];
newOtp[index] = newValue;

// Update state with the new OTP value
setLocalOtp(newOtp);
const newOtpValue = newOtp.join('');

onChange({
target: {
name: "otp",
value: newOtpValue
value: newOtp.join('')
}
} as React.ChangeEvent<HTMLInputElement>);

if (index < length - 1) {
inputRefs.current[index + 1]?.focus();
}

try {
await otpSchema.validate(newOtpValue);
setFieldError("otp", undefined);
if (autoSubmit && newOtpValue.length === length) {
onFullFill();
}
} catch (err) {
if (err instanceof Yup.ValidationError) {
setFieldError("otp", err.message);
// Move focus or handle auto-submit
if (newValue.length === 1 && isValidInput(newValue, inputType)) {
if (index < length - 1) {
// Move focus to the next field if not the last one
inputRefs.current[index + 1]?.focus();
} else {
// Check if all fields are filled out and handle auto-submit
const isOtpComplete = newOtp.every(val => val.length === 1);
if (autoSubmit && isOtpComplete && !hasAutoSubmitted) {
onFullFill();
setHasAutoSubmitted(true);
}
}
}
}
};
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => {
if (keyHandling) {

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => {
if (e.key === "Backspace") {
e.preventDefault();
if (localOtp[index] === '' && index > 0) {
Expand All @@ -174,47 +234,78 @@ const OtpInput: React.FC<OtpInputProps> = ({
setLocalOtp(newOtp);
}
} else if (e.key === "ArrowLeft" && index > 0) {
e.preventDefault()
inputRefs.current[index - 1]?.focus();
} else if (e.key === "ArrowRight" && index < length - 1) {
e.preventDefault()
inputRefs.current[index + 1]?.focus();
}
}
};
};


const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>, index: number) => {
if (event.key === 'Enter') {
event.preventDefault();
const isFullFilled = localOtp.every((val) => val !== '');
if (isFullFilled) {
onFullFill();
} else {
setFieldError("otp", "Please fill in all OTP fields before submitting.");
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>, index: number) => {
if (event.key === 'Enter') {
event.preventDefault();
const isFullFilled = localOtp.every((val) => val !== '');
if (isFullFilled) {
onFullFill();
} else {
setFieldError("otp", "Please fill in all OTP fields before submitting.");
}
} else if (!isValidInput(event.key, inputType)) {
event.preventDefault();
setFieldError("otp", `Only ${inputType} characters are allowed.`);
}
} else if (!isValidInput(event.key, inputType)) {
event.preventDefault();
setFieldError("otp", `Only ${inputType} characters are allowed.`);
}
};
};

return (
<Container>
{localOtp.map((data, index) => (
<Input
name="otp"
key={index}
maxLength={1}
value={data}
ref={el => inputRefs.current[index] = el}
onChange={e => handleChange(e, index)}
onKeyDown={e => keyHandling && handleKeyDown(e, index)}
onKeyPress={e => handleKeyPress(e, index)}
onBlur={onBlur} // Use the onBlur prop
autoComplete={autoComplete}
/>
))}
</Container>
);
};
const getInputType = (inputType: string): string => {
switch (inputType) {
case 'alphanumeric':
case 'alphabetic':
return 'text';
case 'numeric':
return 'number';
default:
return 'text';
}
};

const isEvenLength = length % 2 === 0;
const midpointIndex = length % 2 === 0 ? (length / 2) - 1 : null;


return (
<Container>
{localOtp.map((data, index) => (
<>
<Input
name="otp"
type={getInputType(inputType)} // Set the type dynamically
key={index}
maxLength={1}
value={data}
ref={el => inputRefs.current[index] = el}
onChange={e => handleChange(e, index)}
onKeyDown={e => handleKeyDown(e, index)}
onKeyPress={e => handleKeyPress(e, index)}
onBlur={onBlur} // Use the onBlur prop
autoComplete={autoComplete}
className={isEvenLength && index === midpointIndex ? 'extra-space' : ''}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
textColor={textColor}
onFocus={handleFocus}
backgroundColor={backgroundColor}
highlightColor={highlightColor}
borderColor={borderColor}
/>
{isEvenLength && index === midpointIndex && <div className="spacer"></div>}
</>
))}
</Container>
);
}
;

export default OtpInput;

0 comments on commit 015403f

Please sign in to comment.