Input
Inputs are fairly simple and minimal but when it came to scaling this for production/scalability, it became apparent that there was a huge gap between the components I made previously versus what went into this one. There were so many states and factors to consider such as a developer jumping in and adding in any native html props without prop bombing my component. Another factor that I thought was important that I neglected in the past was accessibility through keyboard navigation. I have to go back and implement those features in the previous components and check if any other components need refs. Lastly, I thought it was really cool to dig into state management and understanding how to manually sort out styling priority depending on state versus utilizing twmerge and clsx like I have in the past without really knowing why it worked in the first place.
Code
// Input.tsx
import { cn } from "src/lib/utils"
import { useId } from "react"
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error: string | boolean;
ref: React.Ref<HTMLInputElement>;
}
const variantInputStyles = {
error: 'border-red-500 focus:ring-2 focus-ring-red-300',
normal: 'border-gray-200 focus:ring-2 focus-ring-blue-300',
disabled: 'bg-gray-50 border-gray-100 cursor-not-allowed text-gray-100'
}
const Input = ({label, error, ref, ...props}: InputProps) => {
const generatedId = useId();
const inputId = props.id || generatedId;
function getInputStyles() {
const styles = [];
if (props.disabled) {
styles.push(variantInputStyles.disabled);
}
else if (error) {
styles.push(variantInputStyles.error);
}
else {
styles.push(variantInputStyles.normal);
}
return styles.join(' ');
}
return (
<div className = "flex flex-col gap-1">
<label
className = "font-medium text-gray-700"
htmlFor={inputId}
>
{label}
</label>
<input
{...props}
ref={ref}
id={inputId}
aria-invalid = {!!error}
aria-describedby= {error ? `${inputId}-error` : undefined}
className = {cn("w-full px-3 py-2 rounded-md border",
getInputStyles())}
/>
{error && typeof error === 'string' && (
<p
className = "text-sm text-red-600 mt-1"
id = {`${inputId}-error`}
>
{error}
</p>
)}
</div>
)
}
export default Input