List components #31
107
frontend/src/components/TextInput/TextInput.tsx
Normal file
107
frontend/src/components/TextInput/TextInput.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import type { InputHTMLAttributes, ReactNode, CSSProperties } from 'react';
|
||||
// isValidElement: checks if something is a React element (e.g., <svg>, <MyComponent />)
|
||||
// cloneElement: creates a copy of a React element with modified/additional props
|
||||
import { cloneElement, isValidElement } from 'react';
|
||||
|
||||
export type TextInputSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
// Omit<... 'size'> removes the native 'size' attribute from input elements so we can use our own
|
||||
export interface TextInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||
size?: TextInputSize;
|
||||
icon?: ReactNode;
|
||||
error?: boolean;
|
||||
fullWidth?: boolean;
|
||||
customWidth?: string;
|
||||
label?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const heightClasses: Record<TextInputSize, string> = {
|
||||
sm: 'h-(--control-height-sm)',
|
||||
md: 'h-(--control-height-md)',
|
||||
lg: 'h-(--control-height-lg)',
|
||||
};
|
||||
|
||||
const widthClasses: Record<TextInputSize, string> = {
|
||||
sm: 'w-(--text-input-default-width-md)',
|
||||
md: 'w-(--text-input-default-width-md)',
|
||||
lg: 'w-(--text-input-default-width-lg)',
|
||||
};
|
||||
|
||||
const radiusClasses: Record<TextInputSize, string> = {
|
||||
sm: 'rounded-(--border-radius-sm)',
|
||||
md: 'rounded-(--border-radius-sm)',
|
||||
lg: 'rounded-(--border-radius-md)',
|
||||
};
|
||||
|
||||
const textClasses: Record<TextInputSize, string> = {
|
||||
sm: 'body-normal-md',
|
||||
md: 'body-normal-md',
|
||||
lg: 'body-normal-lg',
|
||||
};
|
||||
|
||||
const iconContainerStyles: Record<TextInputSize, CSSProperties> = {
|
||||
sm: { width: 'var(--control-height-sm)', height: 'var(--control-height-sm)' },
|
||||
md: { width: 'var(--control-height-md)', height: 'var(--control-height-md)' },
|
||||
lg: { width: 'var(--control-height-lg)', height: 'var(--control-height-lg)' },
|
||||
};
|
||||
|
||||
const iconStyles: Record<TextInputSize, CSSProperties> = {
|
||||
sm: { width: 'var(--font-size-body-md)', height: 'var(--font-size-body-md)' },
|
||||
md: { width: 'var(--font-size-body-md)', height: 'var(--font-size-body-md)' },
|
||||
lg: { width: 'var(--font-size-body-lg)', height: 'var(--font-size-body-lg)' },
|
||||
};
|
||||
|
||||
const baseClasses = 'bg-base-canvas border-(length:--border-width-sm) border-base-ink-medium placeholder:text-base-ink-medium';
|
||||
|
||||
// focus-within: applies styles when any child element (the input) has focus
|
||||
const defaultStateClasses =
|
||||
'hover:border-base-ink-placeholder focus-within:border-primary focus-within:outline focus-within:outline-sky-100 focus-within:outline-(length:--border-width-lg)';
|
||||
|
||||
const errorStateClasses = 'border-fire-100 outline outline-fire-100 outline-(length:--border-width-sm)';
|
||||
|
||||
export default function TextInput({
|
||||
size = 'md',
|
||||
icon,
|
||||
error = false,
|
||||
fullWidth = false,
|
||||
customWidth,
|
||||
label,
|
||||
message,
|
||||
className = '',
|
||||
...props
|
||||
}: TextInputProps) {
|
||||
const widthClass = fullWidth ? 'w-full' : widthClasses[size];
|
||||
const widthStyle = customWidth ? { width: customWidth } : undefined;
|
||||
const stateClasses = error ? errorStateClasses : defaultStateClasses;
|
||||
const inputPadding = icon ? 'pr-(--padding-md)' : 'px-(--padding-md)';
|
||||
|
||||
const inputField = (
|
||||
<div
|
||||
className={`flex items-center ${baseClasses} ${heightClasses[size]} ${widthClass} ${radiusClasses[size]} ${stateClasses} ${className}`}
|
||||
style={widthStyle}
|
||||
>
|
||||
{icon && (
|
||||
<div className="flex items-center justify-center shrink-0 text-base-ink-placeholder" style={iconContainerStyles[size]}>
|
||||
{isValidElement<{ style?: CSSProperties }>(icon) ? cloneElement(icon, { style: iconStyles[size] }) : icon}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
className={`flex-1 h-full bg-transparent border-none outline-none placeholder:text-base-ink-medium ${inputPadding} ${textClasses[size]}`}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!label && !message) {
|
||||
return inputField;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-(--spacing-sm)">
|
||||
{label && <label className="body-bold-md text-base-ink-strong">{label}</label>}
|
||||
{inputField}
|
||||
{message && <span className="body-light-sm text-base-ink-strong">{message}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user