@discourser/design-system 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/dist/styles.css +5126 -0
- package/guidelines/Guidelines.md +92 -41
- package/guidelines/components/accordion.md +732 -0
- package/guidelines/components/avatar.md +1015 -0
- package/guidelines/components/badge.md +728 -0
- package/guidelines/components/button.md +75 -40
- package/guidelines/components/card.md +84 -25
- package/guidelines/components/checkbox.md +671 -0
- package/guidelines/components/dialog.md +619 -31
- package/guidelines/components/drawer.md +1616 -0
- package/guidelines/components/heading.md +576 -0
- package/guidelines/components/icon-button.md +92 -37
- package/guidelines/components/input-addon.md +685 -0
- package/guidelines/components/input-group.md +830 -0
- package/guidelines/components/input.md +92 -37
- package/guidelines/components/popover.md +1271 -0
- package/guidelines/components/progress.md +836 -0
- package/guidelines/components/radio-group.md +852 -0
- package/guidelines/components/select.md +1662 -0
- package/guidelines/components/skeleton.md +802 -0
- package/guidelines/components/slider.md +911 -0
- package/guidelines/components/spinner.md +783 -0
- package/guidelines/components/switch.md +105 -38
- package/guidelines/components/tabs.md +1488 -0
- package/guidelines/components/textarea.md +495 -0
- package/guidelines/components/toast.md +784 -0
- package/guidelines/components/tooltip.md +912 -0
- package/guidelines/design-tokens/colors.md +309 -72
- package/guidelines/design-tokens/elevation.md +615 -45
- package/guidelines/design-tokens/spacing.md +654 -74
- package/guidelines/design-tokens/typography.md +432 -50
- package/guidelines/overview-components.md +60 -8
- package/guidelines/overview-imports.md +314 -0
- package/guidelines/overview-patterns.md +3852 -0
- package/package.json +4 -2
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# Textarea
|
|
2
|
+
|
|
3
|
+
**Purpose:** Multi-line text input field for longer user input following Material Design 3 patterns.
|
|
4
|
+
|
|
5
|
+
## When to Use This Component
|
|
6
|
+
|
|
7
|
+
Use Textarea when you need **multi-line text entry** where users may need to write paragraphs, enter line breaks, or provide longer content.
|
|
8
|
+
|
|
9
|
+
**Decision Tree:**
|
|
10
|
+
|
|
11
|
+
| Scenario | Use This | Why |
|
|
12
|
+
| ------------------------------------------------------- | ---------------------------- | ---------------------------------------- |
|
|
13
|
+
| Multi-line text (comments, descriptions, messages, bio) | Textarea ✅ | Allows line breaks, expandable content |
|
|
14
|
+
| Single-line text (name, email, username) | Input | More compact, better UX for short text |
|
|
15
|
+
| Select from predefined options | Select | Prevents typos, faster than typing |
|
|
16
|
+
| Rich text editing (bold, italic, links) | Rich Text Editor | Textarea is plain text only |
|
|
17
|
+
| Code input | Textarea with monospace font | Or use specialized code editor component |
|
|
18
|
+
|
|
19
|
+
**Component Comparison:**
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// ✅ Use Textarea for multi-line content
|
|
23
|
+
<Textarea
|
|
24
|
+
label="Comment"
|
|
25
|
+
placeholder="Share your thoughts..."
|
|
26
|
+
rows={4}
|
|
27
|
+
/>
|
|
28
|
+
|
|
29
|
+
<Textarea
|
|
30
|
+
label="Bio"
|
|
31
|
+
placeholder="Tell us about yourself"
|
|
32
|
+
rows={6}
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
// ❌ Don't use Textarea for single-line text - use Input
|
|
36
|
+
<Textarea
|
|
37
|
+
label="Username"
|
|
38
|
+
rows={1}
|
|
39
|
+
/> // Wrong - single-line input should use Input
|
|
40
|
+
|
|
41
|
+
<Input
|
|
42
|
+
label="Username"
|
|
43
|
+
/> // Correct
|
|
44
|
+
|
|
45
|
+
// ❌ Don't use Textarea when options are predefined - use Select
|
|
46
|
+
<Textarea
|
|
47
|
+
label="Country"
|
|
48
|
+
placeholder="Enter your country"
|
|
49
|
+
/> // Wrong - prone to typos and inconsistent data
|
|
50
|
+
|
|
51
|
+
<Select.Root collection={countries}>
|
|
52
|
+
<Select.Label>Country</Select.Label>
|
|
53
|
+
<Select.Control>
|
|
54
|
+
<Select.Trigger>
|
|
55
|
+
<Select.ValueText placeholder="Select your country" />
|
|
56
|
+
</Select.Trigger>
|
|
57
|
+
</Select.Control>
|
|
58
|
+
<Select.Content>
|
|
59
|
+
{/* country options */}
|
|
60
|
+
</Select.Content>
|
|
61
|
+
</Select.Root> // Correct
|
|
62
|
+
|
|
63
|
+
// ✅ Use Textarea for longer content that needs line breaks
|
|
64
|
+
<form onSubmit={handleSubmit}>
|
|
65
|
+
<Input label="Title" />
|
|
66
|
+
<Textarea
|
|
67
|
+
label="Description"
|
|
68
|
+
rows={5}
|
|
69
|
+
placeholder="Provide a detailed description..."
|
|
70
|
+
/>
|
|
71
|
+
<Button type="submit">Submit</Button>
|
|
72
|
+
</form>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Import
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { Textarea } from '@discourser/design-system';
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Variants
|
|
82
|
+
|
|
83
|
+
The Textarea component supports 4 visual variants:
|
|
84
|
+
|
|
85
|
+
| Variant | Visual Style | Usage | When to Use |
|
|
86
|
+
| --------- | ---------------------------------- | -------------------- | ------------------------------- |
|
|
87
|
+
| `surface` | Subtle background with border | Default text areas | Forms, comments, descriptions |
|
|
88
|
+
| `outline` | Transparent background with border | Prominent text areas | Key inputs that need emphasis |
|
|
89
|
+
| `subtle` | Light background, minimal border | Low-emphasis inputs | Secondary content, notes |
|
|
90
|
+
| `flushed` | Bottom border only, no background | Inline text areas | Minimal designs, embedded forms |
|
|
91
|
+
|
|
92
|
+
### Visual Characteristics
|
|
93
|
+
|
|
94
|
+
- **surface**: Light surface background, thin border, focus ring inside
|
|
95
|
+
- **outline**: No background, thin outline border, focus ring inside
|
|
96
|
+
- **subtle**: Subtle gray background, transparent border until focus
|
|
97
|
+
- **flushed**: No background, bottom border only, minimal appearance
|
|
98
|
+
|
|
99
|
+
## Sizes
|
|
100
|
+
|
|
101
|
+
| Size | Text Style | Padding | Usage |
|
|
102
|
+
| ---- | ---------- | ----------------------------- | --------------------------- |
|
|
103
|
+
| `xs` | sm | 8px vertical, 8px horizontal | Compact UI, inline comments |
|
|
104
|
+
| `sm` | sm | 8px vertical, 10px horizontal | Dense forms, small dialogs |
|
|
105
|
+
| `md` | md | 8px vertical, 12px horizontal | Default, most use cases |
|
|
106
|
+
| `lg` | md | 8px vertical, 14px horizontal | Emphasized inputs, mobile |
|
|
107
|
+
| `xl` | lg | 8px vertical, 16px horizontal | Large content areas, essays |
|
|
108
|
+
|
|
109
|
+
**Recommendation:** Use `md` for most cases. Use `lg` or `xl` for longer content like descriptions or essays.
|
|
110
|
+
|
|
111
|
+
## Props
|
|
112
|
+
|
|
113
|
+
| Prop | Type | Default | Description |
|
|
114
|
+
| -------------- | ------------------------------------------------- | ----------- | --------------------------------- |
|
|
115
|
+
| `variant` | `'surface' \| 'outline' \| 'subtle' \| 'flushed'` | `'surface'` | Visual style variant |
|
|
116
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Textarea size |
|
|
117
|
+
| `rows` | `number` | `3` | Number of visible text lines |
|
|
118
|
+
| `value` | `string` | - | Controlled value |
|
|
119
|
+
| `defaultValue` | `string` | - | Uncontrolled default value |
|
|
120
|
+
| `placeholder` | `string` | - | Placeholder text (use with label) |
|
|
121
|
+
| `disabled` | `boolean` | `false` | Disable interaction |
|
|
122
|
+
| `readOnly` | `boolean` | `false` | Make read-only |
|
|
123
|
+
| `required` | `boolean` | `false` | Mark as required field |
|
|
124
|
+
| `onChange` | `(event: ChangeEvent) => void` | - | Change handler |
|
|
125
|
+
| `onBlur` | `(event: FocusEvent) => void` | - | Blur handler |
|
|
126
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
127
|
+
|
|
128
|
+
**Note:** Textarea extends `TextareaHTMLAttributes<HTMLTextAreaElement>`, so all standard HTML textarea attributes are supported.
|
|
129
|
+
|
|
130
|
+
## Examples
|
|
131
|
+
|
|
132
|
+
### Basic Usage
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// Default textarea
|
|
136
|
+
<Textarea placeholder="Enter your message..." />
|
|
137
|
+
|
|
138
|
+
// Outlined variant
|
|
139
|
+
<Textarea variant="outline" placeholder="Description" />
|
|
140
|
+
|
|
141
|
+
// Subtle variant (low emphasis)
|
|
142
|
+
<Textarea variant="subtle" placeholder="Additional notes" />
|
|
143
|
+
|
|
144
|
+
// Flushed variant (minimal)
|
|
145
|
+
<Textarea variant="flushed" placeholder="Comment" />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### With Field Label
|
|
149
|
+
|
|
150
|
+
Textarea should always be used with a label for accessibility:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { Field } from '@discourser/design-system';
|
|
154
|
+
|
|
155
|
+
<Field.Root>
|
|
156
|
+
<Field.Label>Message</Field.Label>
|
|
157
|
+
<Textarea placeholder="Enter your message..." />
|
|
158
|
+
<Field.HelperText>Maximum 500 characters</Field.HelperText>
|
|
159
|
+
</Field.Root>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Controlled Textarea
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const [message, setMessage] = useState('');
|
|
166
|
+
|
|
167
|
+
<Field.Root>
|
|
168
|
+
<Field.Label>Feedback</Field.Label>
|
|
169
|
+
<Textarea
|
|
170
|
+
value={message}
|
|
171
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
172
|
+
rows={5}
|
|
173
|
+
/>
|
|
174
|
+
<Field.HelperText>{message.length} / 500</Field.HelperText>
|
|
175
|
+
</Field.Root>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Different Sizes
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// Extra small (compact)
|
|
182
|
+
<Textarea size="xs" rows={2} placeholder="Quick note" />
|
|
183
|
+
|
|
184
|
+
// Small
|
|
185
|
+
<Textarea size="sm" rows={3} placeholder="Short comment" />
|
|
186
|
+
|
|
187
|
+
// Medium (default)
|
|
188
|
+
<Textarea size="md" rows={4} placeholder="Standard message" />
|
|
189
|
+
|
|
190
|
+
// Large
|
|
191
|
+
<Textarea size="lg" rows={5} placeholder="Detailed description" />
|
|
192
|
+
|
|
193
|
+
// Extra large
|
|
194
|
+
<Textarea size="xl" rows={6} placeholder="Long-form content" />
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Custom Row Height
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// 3 rows (default)
|
|
201
|
+
<Textarea placeholder="Short message" />
|
|
202
|
+
|
|
203
|
+
// 10 rows for longer content
|
|
204
|
+
<Textarea rows={10} placeholder="Essay or long-form content" />
|
|
205
|
+
|
|
206
|
+
// Auto-resize with CSS
|
|
207
|
+
<Textarea
|
|
208
|
+
style={{ minHeight: '100px', resize: 'vertical' }}
|
|
209
|
+
placeholder="Resizable textarea"
|
|
210
|
+
/>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### With Validation
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { Field } from '@discourser/design-system';
|
|
217
|
+
|
|
218
|
+
const [bio, setBio] = useState('');
|
|
219
|
+
const [error, setError] = useState('');
|
|
220
|
+
|
|
221
|
+
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
222
|
+
const value = e.target.value;
|
|
223
|
+
setBio(value);
|
|
224
|
+
|
|
225
|
+
if (value.length > 500) {
|
|
226
|
+
setError('Bio must be 500 characters or less');
|
|
227
|
+
} else {
|
|
228
|
+
setError('');
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
<Field.Root invalid={!!error}>
|
|
233
|
+
<Field.Label>Bio</Field.Label>
|
|
234
|
+
<Textarea
|
|
235
|
+
value={bio}
|
|
236
|
+
onChange={handleChange}
|
|
237
|
+
rows={5}
|
|
238
|
+
/>
|
|
239
|
+
{error ? (
|
|
240
|
+
<Field.ErrorText>{error}</Field.ErrorText>
|
|
241
|
+
) : (
|
|
242
|
+
<Field.HelperText>{bio.length} / 500 characters</Field.HelperText>
|
|
243
|
+
)}
|
|
244
|
+
</Field.Root>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Disabled and Read-only States
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Disabled textarea
|
|
251
|
+
<Textarea disabled placeholder="Cannot edit" value="Disabled content" />
|
|
252
|
+
|
|
253
|
+
// Read-only textarea (can select/copy text)
|
|
254
|
+
<Textarea readOnly value="Read-only content that can be selected" rows={3} />
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Required Field
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
<Field.Root>
|
|
261
|
+
<Field.Label>
|
|
262
|
+
Comments <span style={{ color: 'red' }}>*</span>
|
|
263
|
+
</Field.Label>
|
|
264
|
+
<Textarea required placeholder="Required field..." />
|
|
265
|
+
<Field.HelperText>This field is required</Field.HelperText>
|
|
266
|
+
</Field.Root>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Common Patterns
|
|
270
|
+
|
|
271
|
+
### Comment Input
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
const [comment, setComment] = useState('');
|
|
275
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
276
|
+
|
|
277
|
+
<div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
|
|
278
|
+
<Field.Root>
|
|
279
|
+
<Field.Label>Add Comment</Field.Label>
|
|
280
|
+
<Textarea
|
|
281
|
+
value={comment}
|
|
282
|
+
onChange={(e) => setComment(e.target.value)}
|
|
283
|
+
placeholder="Share your thoughts..."
|
|
284
|
+
rows={4}
|
|
285
|
+
/>
|
|
286
|
+
</Field.Root>
|
|
287
|
+
|
|
288
|
+
<div className={css({ display: 'flex', gap: 'sm', justifyContent: 'flex-end' })}>
|
|
289
|
+
<Button variant="text" onClick={() => setComment('')}>Cancel</Button>
|
|
290
|
+
<Button
|
|
291
|
+
variant="filled"
|
|
292
|
+
disabled={!comment.trim() || isSubmitting}
|
|
293
|
+
onClick={handleSubmit}
|
|
294
|
+
>
|
|
295
|
+
Post Comment
|
|
296
|
+
</Button>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Feedback Form
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
const [feedback, setFeedback] = useState('');
|
|
305
|
+
const charLimit = 1000;
|
|
306
|
+
const remaining = charLimit - feedback.length;
|
|
307
|
+
|
|
308
|
+
<Field.Root invalid={remaining < 0}>
|
|
309
|
+
<Field.Label>Feedback</Field.Label>
|
|
310
|
+
<Textarea
|
|
311
|
+
value={feedback}
|
|
312
|
+
onChange={(e) => setFeedback(e.target.value)}
|
|
313
|
+
rows={8}
|
|
314
|
+
size="lg"
|
|
315
|
+
placeholder="Tell us what you think..."
|
|
316
|
+
/>
|
|
317
|
+
<Field.HelperText>
|
|
318
|
+
{remaining >= 0
|
|
319
|
+
? `${remaining} characters remaining`
|
|
320
|
+
: `${Math.abs(remaining)} characters over limit`
|
|
321
|
+
}
|
|
322
|
+
</Field.HelperText>
|
|
323
|
+
</Field.Root>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Description Field
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
<Field.Root>
|
|
330
|
+
<Field.Label>Product Description</Field.Label>
|
|
331
|
+
<Textarea
|
|
332
|
+
variant="outline"
|
|
333
|
+
size="lg"
|
|
334
|
+
rows={6}
|
|
335
|
+
placeholder="Describe your product in detail..."
|
|
336
|
+
/>
|
|
337
|
+
<Field.HelperText>
|
|
338
|
+
Include key features, benefits, and specifications
|
|
339
|
+
</Field.HelperText>
|
|
340
|
+
</Field.Root>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## DO NOT
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
// ❌ Don't use native textarea element
|
|
347
|
+
<textarea className="..." placeholder="Message" /> // Use <Textarea> instead
|
|
348
|
+
|
|
349
|
+
// ❌ Don't use textarea without a label (accessibility issue)
|
|
350
|
+
<Textarea placeholder="Enter feedback" /> // Always use with Field.Label
|
|
351
|
+
|
|
352
|
+
// ❌ Don't use placeholder as the only label
|
|
353
|
+
<Textarea placeholder="Email Address" /> // Placeholder is not a label
|
|
354
|
+
|
|
355
|
+
// ❌ Don't make textarea too small for expected content
|
|
356
|
+
<Textarea rows={1} /> // For short input, use Input component instead
|
|
357
|
+
|
|
358
|
+
// ❌ Don't use inline styles for colors
|
|
359
|
+
<Textarea style={{ backgroundColor: 'blue', color: 'white' }} /> // Use variants
|
|
360
|
+
|
|
361
|
+
// ✅ Use Textarea with proper Field structure
|
|
362
|
+
<Field.Root>
|
|
363
|
+
<Field.Label>Feedback</Field.Label>
|
|
364
|
+
<Textarea placeholder="Share your thoughts..." rows={5} />
|
|
365
|
+
</Field.Root>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Accessibility
|
|
369
|
+
|
|
370
|
+
The Textarea component follows WCAG 2.1 Level AA standards:
|
|
371
|
+
|
|
372
|
+
- **Keyboard Navigation**: Full keyboard support (Tab, arrow keys, text selection)
|
|
373
|
+
- **Focus Indicator**: Clear focus ring on focus-visible
|
|
374
|
+
- **Disabled State**: Uses `disabled` attribute, grayed out appearance
|
|
375
|
+
- **Screen Readers**: Works with Field.Label for proper labeling
|
|
376
|
+
- **Touch Targets**: Minimum 44x44px with size md or larger
|
|
377
|
+
- **Color Contrast**: All variants meet 4.5:1 contrast ratio
|
|
378
|
+
|
|
379
|
+
### Accessibility Best Practices
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// ✅ Always provide a label
|
|
383
|
+
<Field.Root>
|
|
384
|
+
<Field.Label>Message</Field.Label>
|
|
385
|
+
<Textarea />
|
|
386
|
+
</Field.Root>
|
|
387
|
+
|
|
388
|
+
// ✅ Use helper text for guidance
|
|
389
|
+
<Field.Root>
|
|
390
|
+
<Field.Label>Bio</Field.Label>
|
|
391
|
+
<Textarea />
|
|
392
|
+
<Field.HelperText>Tell us about yourself (max 500 characters)</Field.HelperText>
|
|
393
|
+
</Field.Root>
|
|
394
|
+
|
|
395
|
+
// ✅ Show validation errors clearly
|
|
396
|
+
<Field.Root invalid={hasError}>
|
|
397
|
+
<Field.Label>Feedback</Field.Label>
|
|
398
|
+
<Textarea />
|
|
399
|
+
<Field.ErrorText>Feedback is required</Field.ErrorText>
|
|
400
|
+
</Field.Root>
|
|
401
|
+
|
|
402
|
+
// ✅ Mark required fields
|
|
403
|
+
<Field.Root>
|
|
404
|
+
<Field.Label>
|
|
405
|
+
Comments <span aria-label="required">*</span>
|
|
406
|
+
</Field.Label>
|
|
407
|
+
<Textarea required />
|
|
408
|
+
</Field.Root>
|
|
409
|
+
|
|
410
|
+
// ✅ Provide character count for limits
|
|
411
|
+
<Field.Root>
|
|
412
|
+
<Field.Label>Tweet</Field.Label>
|
|
413
|
+
<Textarea />
|
|
414
|
+
<Field.HelperText aria-live="polite">
|
|
415
|
+
{280 - text.length} characters remaining
|
|
416
|
+
</Field.HelperText>
|
|
417
|
+
</Field.Root>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Variant Selection Guide
|
|
421
|
+
|
|
422
|
+
| Scenario | Recommended Variant | Reasoning |
|
|
423
|
+
| ------------------------- | ------------------------ | ----------------------------------------------- |
|
|
424
|
+
| Form comments | `surface` | Default, good contrast with page background |
|
|
425
|
+
| Primary description field | `outline` | Emphasized, clear boundaries |
|
|
426
|
+
| Additional notes/metadata | `subtle` | Low emphasis, doesn't compete with main content |
|
|
427
|
+
| Inline editing | `flushed` | Minimal appearance, feels embedded in content |
|
|
428
|
+
| Feedback forms | `surface` or `outline` | Clear, prominent for important user input |
|
|
429
|
+
| Profile bio | `outline` with `lg` size | Prominent, allows longer content |
|
|
430
|
+
| Quick notes | `subtle` with `sm` size | Compact, unobtrusive |
|
|
431
|
+
|
|
432
|
+
## State Behaviors
|
|
433
|
+
|
|
434
|
+
| State | Visual Change | Behavior |
|
|
435
|
+
| ------------- | -------------------------------- | -------------------------------------- |
|
|
436
|
+
| **Hover** | Border color changes | Subtle indication of interactivity |
|
|
437
|
+
| **Focus** | Focus ring appears | Clear focus indication (inside border) |
|
|
438
|
+
| **Invalid** | Red border | Error state, shows validation issue |
|
|
439
|
+
| **Disabled** | Grayed out, reduced opacity | Cannot be edited or focused |
|
|
440
|
+
| **Read-only** | Normal appearance, no focus ring | Can select/copy text, but not edit |
|
|
441
|
+
|
|
442
|
+
## Responsive Considerations
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// Mobile-first: Larger textarea for touch
|
|
446
|
+
<Textarea size="lg" rows={5} />
|
|
447
|
+
|
|
448
|
+
// Desktop: Can use smaller sizes
|
|
449
|
+
<Textarea size={{ base: 'lg', md: 'md' }} rows={4} />
|
|
450
|
+
|
|
451
|
+
// Responsive rows
|
|
452
|
+
<Textarea rows={{ base: 6, md: 4 }} />
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Testing
|
|
456
|
+
|
|
457
|
+
When testing Textarea components:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
import { render, screen } from '@testing-library/react';
|
|
461
|
+
import userEvent from '@testing-library/user-event';
|
|
462
|
+
|
|
463
|
+
test('textarea accepts user input', async () => {
|
|
464
|
+
const handleChange = vi.fn();
|
|
465
|
+
render(
|
|
466
|
+
<Field.Root>
|
|
467
|
+
<Field.Label>Message</Field.Label>
|
|
468
|
+
<Textarea onChange={handleChange} />
|
|
469
|
+
</Field.Root>
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const textarea = screen.getByLabelText('Message');
|
|
473
|
+
await userEvent.type(textarea, 'Hello world');
|
|
474
|
+
|
|
475
|
+
expect(textarea).toHaveValue('Hello world');
|
|
476
|
+
expect(handleChange).toHaveBeenCalled();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test('disabled textarea cannot be edited', async () => {
|
|
480
|
+
render(<Textarea disabled value="Cannot edit" />);
|
|
481
|
+
|
|
482
|
+
const textarea = screen.getByDisplayValue('Cannot edit');
|
|
483
|
+
await userEvent.type(textarea, 'Try to type');
|
|
484
|
+
|
|
485
|
+
expect(textarea).toHaveValue('Cannot edit');
|
|
486
|
+
expect(textarea).toBeDisabled();
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Related Components
|
|
491
|
+
|
|
492
|
+
- **Input** - For single-line text input
|
|
493
|
+
- **Field** - For adding labels, helper text, and error messages
|
|
494
|
+
- **Select** - For choosing from predefined options
|
|
495
|
+
- **Button** - For form submission actions
|