feat: add Textarea primitive
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
21
src/design-system/primitives/Textarea/Textarea.module.css
Normal file
21
src/design-system/primitives/Textarea/Textarea.module.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.textarea {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-raised);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.textarea::placeholder { color: var(--text-faint); }
|
||||
.textarea:focus { border-color: var(--amber); box-shadow: 0 0 0 3px var(--amber-bg); }
|
||||
|
||||
.resizeVertical { resize: vertical; }
|
||||
.resizeHorizontal { resize: horizontal; }
|
||||
.resizeNone { resize: none; }
|
||||
.resizeBoth { resize: both; }
|
||||
79
src/design-system/primitives/Textarea/Textarea.test.tsx
Normal file
79
src/design-system/primitives/Textarea/Textarea.test.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { createRef } from 'react'
|
||||
import { Textarea } from './Textarea'
|
||||
|
||||
describe('Textarea', () => {
|
||||
it('renders a textarea element', () => {
|
||||
render(<Textarea placeholder="Enter text" />)
|
||||
expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders with default rows of 3', () => {
|
||||
render(<Textarea />)
|
||||
const el = screen.getByRole('textbox')
|
||||
expect(el).toHaveAttribute('rows', '3')
|
||||
})
|
||||
|
||||
it('accepts a custom rows value', () => {
|
||||
render(<Textarea rows={5} />)
|
||||
expect(screen.getByRole('textbox')).toHaveAttribute('rows', '5')
|
||||
})
|
||||
|
||||
it('accepts user input', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<Textarea />)
|
||||
const el = screen.getByRole('textbox')
|
||||
await user.type(el, 'hello world')
|
||||
expect(el).toHaveValue('hello world')
|
||||
})
|
||||
|
||||
it('calls onChange when value changes', async () => {
|
||||
const onChange = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(<Textarea onChange={onChange} />)
|
||||
await user.type(screen.getByRole('textbox'), 'a')
|
||||
expect(onChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('applies resize class for vertical (default)', () => {
|
||||
render(<Textarea />)
|
||||
const el = screen.getByRole('textbox')
|
||||
expect(el.className).toContain('resizeVertical')
|
||||
})
|
||||
|
||||
it('applies resize class for none', () => {
|
||||
render(<Textarea resize="none" />)
|
||||
const el = screen.getByRole('textbox')
|
||||
expect(el.className).toContain('resizeNone')
|
||||
})
|
||||
|
||||
it('applies resize class for horizontal', () => {
|
||||
render(<Textarea resize="horizontal" />)
|
||||
const el = screen.getByRole('textbox')
|
||||
expect(el.className).toContain('resizeHorizontal')
|
||||
})
|
||||
|
||||
it('applies resize class for both', () => {
|
||||
render(<Textarea resize="both" />)
|
||||
const el = screen.getByRole('textbox')
|
||||
expect(el.className).toContain('resizeBoth')
|
||||
})
|
||||
|
||||
it('forwards ref to the textarea element', () => {
|
||||
const ref = createRef<HTMLTextAreaElement>()
|
||||
render(<Textarea ref={ref} />)
|
||||
expect(ref.current).toBeInstanceOf(HTMLTextAreaElement)
|
||||
})
|
||||
|
||||
it('passes additional props to the textarea', () => {
|
||||
render(<Textarea disabled data-testid="ta" />)
|
||||
expect(screen.getByTestId('ta')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('applies custom className', () => {
|
||||
render(<Textarea className="custom" />)
|
||||
expect(screen.getByRole('textbox').className).toContain('custom')
|
||||
})
|
||||
})
|
||||
31
src/design-system/primitives/Textarea/Textarea.tsx
Normal file
31
src/design-system/primitives/Textarea/Textarea.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import styles from './Textarea.module.css'
|
||||
import { forwardRef, type TextareaHTMLAttributes } from 'react'
|
||||
|
||||
type ResizeProp = 'vertical' | 'horizontal' | 'none' | 'both'
|
||||
|
||||
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
resize?: ResizeProp
|
||||
}
|
||||
|
||||
const resizeClassMap: Record<ResizeProp, string> = {
|
||||
vertical: styles.resizeVertical,
|
||||
horizontal: styles.resizeHorizontal,
|
||||
none: styles.resizeNone,
|
||||
both: styles.resizeBoth,
|
||||
}
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ resize = 'vertical', className, rows = 3, ...rest }, ref) => {
|
||||
const resizeClass = resizeClassMap[resize]
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
rows={rows}
|
||||
className={`${styles.textarea} ${resizeClass} ${className ?? ''}`}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Textarea.displayName = 'Textarea'
|
||||
Reference in New Issue
Block a user