128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
import { useMemo } from 'react'
|
|
import type { ReactNode } from 'react'
|
|
|
|
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
label?: string
|
|
error?: string
|
|
helperText?: string
|
|
leftIcon?: ReactNode
|
|
rightIcon?: ReactNode
|
|
}
|
|
|
|
function generateId(prefix: string, providedId?: string): string {
|
|
if (providedId) {
|
|
return providedId
|
|
}
|
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 5)}`
|
|
}
|
|
|
|
function getErrorClasses(error: string | undefined): string {
|
|
if (!error) {
|
|
return ''
|
|
}
|
|
return 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50'
|
|
}
|
|
|
|
function getTextareaClasses(params: {
|
|
error: string | undefined
|
|
leftIcon: ReactNode | undefined
|
|
rightIcon: ReactNode | undefined
|
|
className: string
|
|
}): string {
|
|
const baseClasses = 'block w-full px-3 py-2 border rounded-lg bg-cyber-dark text-cyber-accent placeholder-cyber-accent/50 focus:outline-none focus:ring-2 focus:ring-neon-cyan focus:border-neon-cyan transition-colors resize-y'
|
|
const errorClasses = getErrorClasses(params.error)
|
|
const paddingLeft = params.leftIcon ? 'pl-10' : ''
|
|
const paddingRight = params.rightIcon ? 'pr-10' : ''
|
|
return `${baseClasses} ${errorClasses} ${paddingLeft} ${paddingRight} ${params.className}`.trim()
|
|
}
|
|
|
|
function getAriaDescribedBy(textareaId: string, error: string | undefined, helperText: string | undefined): string | undefined {
|
|
if (error) {
|
|
return `${textareaId}-error`
|
|
}
|
|
if (helperText) {
|
|
return `${textareaId}-helper`
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function TextareaLabel({ textareaId, label }: { textareaId: string; label: string }): React.ReactElement {
|
|
return (
|
|
<label htmlFor={textareaId} className="block text-sm font-medium text-cyber-accent mb-1">
|
|
{label}
|
|
</label>
|
|
)
|
|
}
|
|
|
|
function TextareaIcons({ leftIcon, rightIcon }: { leftIcon?: ReactNode; rightIcon?: ReactNode }): React.ReactElement | null {
|
|
if (!leftIcon && !rightIcon) {
|
|
return null
|
|
}
|
|
return (
|
|
<>
|
|
{leftIcon && (
|
|
<div className="absolute top-3 left-0 pl-3 flex items-start pointer-events-none text-cyber-accent/50">
|
|
{leftIcon}
|
|
</div>
|
|
)}
|
|
{rightIcon && (
|
|
<div className="absolute top-3 right-0 pr-3 flex items-start text-cyber-accent/50">
|
|
{rightIcon}
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
function TextareaError({ textareaId, error }: { textareaId: string; error: string }): React.ReactElement {
|
|
return (
|
|
<p id={`${textareaId}-error`} className="mt-1 text-sm text-red-400" role="alert">
|
|
{error}
|
|
</p>
|
|
)
|
|
}
|
|
|
|
function TextareaHelper({ textareaId, helperText }: { textareaId: string; helperText: string }): React.ReactElement {
|
|
return (
|
|
<p id={`${textareaId}-helper`} className="mt-1 text-sm text-cyber-accent/70">
|
|
{helperText}
|
|
</p>
|
|
)
|
|
}
|
|
|
|
export function Textarea({
|
|
label,
|
|
error,
|
|
helperText,
|
|
leftIcon,
|
|
rightIcon,
|
|
className = '',
|
|
id,
|
|
...props
|
|
}: TextareaProps): React.ReactElement {
|
|
const textareaId = useMemo(() => generateId('textarea', id), [id])
|
|
const textareaClasses = useMemo(
|
|
() => getTextareaClasses({ error, leftIcon, rightIcon, className }),
|
|
[error, leftIcon, rightIcon, className]
|
|
)
|
|
const ariaDescribedBy = useMemo(() => getAriaDescribedBy(textareaId, error, helperText), [textareaId, error, helperText])
|
|
|
|
return (
|
|
<div className="w-full">
|
|
{label && <TextareaLabel textareaId={textareaId} label={label} />}
|
|
<div className="relative">
|
|
<TextareaIcons leftIcon={leftIcon} rightIcon={rightIcon} />
|
|
<textarea
|
|
id={textareaId}
|
|
className={textareaClasses}
|
|
aria-invalid={error ? 'true' : 'false'}
|
|
aria-describedby={ariaDescribedBy}
|
|
{...props}
|
|
/>
|
|
</div>
|
|
{error && <TextareaError textareaId={textareaId} error={error} />}
|
|
{helperText && !error && <TextareaHelper textareaId={textareaId} helperText={helperText} />}
|
|
</div>
|
|
)
|
|
}
|