tpmjs/packages/ui/src/Textarea/Textarea.tsx
Ajax Davis ded939e643 feat(ui): add comprehensive form component library with playground showcase
Implemented 8 production-ready form components with full accessibility:

Components Added:
- Textarea: Multi-line text input with character counter
- Checkbox: Custom styled with indeterminate state support
- Radio & RadioGroup: Context-based radio button groups
- Switch: Toggle with animated thumb and loading state
- Select: Native select with custom styling and option groups
- Slider: Range input with marks, value display, cross-browser support
- FormField: Wrapper component with label, error, and helper text

Features:
- Full accessibility (ARIA attributes, semantic HTML)
- Controlled/uncontrolled patterns via useControlled hook
- Dark mode support with semantic tokens
- Design tokens and shared variant system
- Comprehensive test coverage (856 tests passing)
- Form-specific design tokens (formTokens)
- Shared form variant base classes (formVariants)

Playground Updates:
- Added comprehensive Forms section showcasing all components
- Interactive examples with state management
- Complete form composition example
- All components fully functional and themed

Test Coverage:
- 10+ describe blocks per component
- All edge cases covered
- Accessibility testing
- Cross-browser compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 01:56:54 +10:00

107 lines
2.6 KiB
TypeScript

import { cn } from '@tpmjs/utils/cn';
import { forwardRef, useMemo } from 'react';
import type { TextareaProps } from './types';
import { textareaVariants } from './variants';
/**
* Textarea component
*
* A multi-line text input component with support for character counting,
* different states, sizes, and resize behaviors.
*
* @example
* ```tsx
* import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
*
* function MyComponent() {
* return (
* <Textarea
* placeholder="Enter your message..."
* rows={5}
* maxLength={500}
* showCount
* state="default"
* size="md"
* />
* );
* }
* ```
*/
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
(
{
className,
state = 'default',
size = 'md',
fullWidth = true,
resize = 'vertical',
showCount = false,
maxLength,
value,
defaultValue,
disabled = false,
...props
},
ref
) => {
// Calculate character count for display
const characterCount = useMemo(() => {
if (!showCount) return null;
const currentValue = value ?? defaultValue ?? '';
const current =
typeof currentValue === 'string' ? currentValue.length : String(currentValue).length;
return {
current,
max: maxLength,
};
}, [showCount, value, defaultValue, maxLength]);
return (
<div className={cn('relative', fullWidth && 'w-full')}>
<textarea
ref={ref}
className={cn(
textareaVariants({
state,
size,
fullWidth: fullWidth ? 'true' : 'false',
resize,
}),
className
)}
disabled={disabled}
maxLength={maxLength}
value={value}
defaultValue={defaultValue}
aria-invalid={state === 'error' ? 'true' : undefined}
{...props}
/>
{/* Character counter */}
{showCount && characterCount && (
<div
className={cn(
'mt-1.5 text-xs',
state === 'error' ? 'text-error' : 'text-foreground-secondary',
'transition-colors duration-200'
)}
aria-live="polite"
aria-atomic="true"
>
{characterCount.current}
{characterCount.max !== undefined && (
<>
<span className="text-foreground-tertiary"> / </span>
{characterCount.max}
</>
)}
</div>
)}
</div>
);
}
);
Textarea.displayName = 'Textarea';