@tpzdsp/next-toolkit 1.2.9 → 1.4.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.
Files changed (35) hide show
  1. package/package.json +14 -1
  2. package/src/components/Card/Card.stories.tsx +21 -0
  3. package/src/components/Card/Card.test.tsx +46 -7
  4. package/src/components/Card/Card.tsx +1 -1
  5. package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +89 -0
  6. package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +75 -0
  7. package/src/components/ErrorBoundary/ErrorBoundary.tsx +28 -0
  8. package/src/components/ErrorBoundary/ErrorFallback.stories.tsx +70 -0
  9. package/src/components/ErrorBoundary/ErrorFallback.test.tsx +54 -0
  10. package/src/components/ErrorBoundary/ErrorFallback.tsx +30 -0
  11. package/src/components/Paragraph/Paragraph.tsx +9 -5
  12. package/src/components/backToTop/BackToTop.stories.tsx +409 -0
  13. package/src/components/backToTop/BackToTop.test.tsx +57 -0
  14. package/src/components/backToTop/BackToTop.tsx +131 -0
  15. package/src/components/chip/Chip.stories.tsx +40 -0
  16. package/src/components/chip/Chip.test.tsx +31 -0
  17. package/src/components/chip/Chip.tsx +20 -0
  18. package/src/components/cookieBanner/CookieBanner.stories.tsx +258 -0
  19. package/src/components/cookieBanner/CookieBanner.test.tsx +68 -0
  20. package/src/components/cookieBanner/CookieBanner.tsx +73 -0
  21. package/src/components/form/Input.stories.tsx +435 -0
  22. package/src/components/form/Input.test.tsx +214 -0
  23. package/src/components/form/Input.tsx +24 -0
  24. package/src/components/form/TextArea.stories.tsx +465 -0
  25. package/src/components/form/TextArea.test.tsx +236 -0
  26. package/src/components/form/TextArea.tsx +24 -0
  27. package/src/components/googleAnalytics/GlobalVars.tsx +31 -0
  28. package/src/components/googleAnalytics/GoogleAnalytics.tsx +15 -0
  29. package/src/components/index.ts +15 -0
  30. package/src/components/skipLink/SkipLink.stories.tsx +346 -0
  31. package/src/components/skipLink/SkipLink.test.tsx +22 -0
  32. package/src/components/skipLink/SkipLink.tsx +19 -0
  33. package/src/errors/ApiError.ts +71 -0
  34. package/src/errors/index.ts +1 -0
  35. package/src/utils/utils.ts +3 -0
@@ -0,0 +1,465 @@
1
+ /* eslint-disable storybook/no-renderer-packages */
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import { TextArea } from './TextArea';
5
+
6
+ const meta = {
7
+ title: 'Components/Form/TextArea',
8
+ component: TextArea,
9
+ parameters: {
10
+ layout: 'centered',
11
+ docs: {
12
+ description: {
13
+ component: 'A styled textarea component with error state support and customizable styling.',
14
+ },
15
+ },
16
+ },
17
+ tags: ['autodocs'],
18
+ argTypes: {
19
+ hasError: {
20
+ control: 'boolean',
21
+ description: 'Whether the textarea should display in error state',
22
+ defaultValue: false,
23
+ },
24
+ rows: {
25
+ control: { type: 'number', min: 1, max: 20 },
26
+ description: 'Number of visible text lines',
27
+ defaultValue: 4,
28
+ },
29
+ cols: {
30
+ control: { type: 'number', min: 10, max: 100 },
31
+ description: 'Number of visible character widths',
32
+ },
33
+ placeholder: {
34
+ control: 'text',
35
+ description: 'Placeholder text for the textarea',
36
+ },
37
+ disabled: {
38
+ control: 'boolean',
39
+ description: 'Whether the textarea is disabled',
40
+ defaultValue: false,
41
+ },
42
+ required: {
43
+ control: 'boolean',
44
+ description: 'Whether the textarea is required',
45
+ defaultValue: false,
46
+ },
47
+ maxLength: {
48
+ control: { type: 'number', min: 0 },
49
+ description: 'Maximum number of characters allowed',
50
+ },
51
+ },
52
+ } satisfies Meta<typeof TextArea>;
53
+
54
+ export default meta;
55
+ type Story = StoryObj<typeof meta>;
56
+
57
+ export const Default: Story = {
58
+ args: {
59
+ placeholder: 'Enter your message...',
60
+ rows: 4,
61
+ },
62
+ parameters: {
63
+ docs: {
64
+ description: {
65
+ story: 'Default textarea with standard styling.',
66
+ },
67
+ },
68
+ },
69
+ };
70
+
71
+ export const WithError: Story = {
72
+ args: {
73
+ hasError: true,
74
+ placeholder: 'This textarea has an error',
75
+ value: 'Invalid content that caused an error',
76
+ rows: 4,
77
+ },
78
+ parameters: {
79
+ docs: {
80
+ description: {
81
+ story: 'Textarea in error state with red border styling.',
82
+ },
83
+ },
84
+ },
85
+ };
86
+
87
+ export const Disabled: Story = {
88
+ args: {
89
+ disabled: true,
90
+ placeholder: 'This textarea is disabled',
91
+ value: 'This content cannot be edited',
92
+ rows: 4,
93
+ },
94
+ parameters: {
95
+ docs: {
96
+ description: {
97
+ story: 'Disabled textarea with reduced opacity and gray background.',
98
+ },
99
+ },
100
+ },
101
+ };
102
+
103
+ export const Required: Story = {
104
+ render: () => (
105
+ <div className="w-96">
106
+ <label htmlFor="required-textarea" className="block text-sm font-medium mb-1">
107
+ Description <span className="text-red-500">*</span>
108
+ </label>
109
+
110
+ <TextArea
111
+ id="required-textarea"
112
+ required
113
+ placeholder="Please provide a detailed description"
114
+ rows={5}
115
+ aria-describedby="required-help"
116
+ />
117
+
118
+ <p id="required-help" className="text-gray-600 text-xs mt-1">
119
+ * This field is required
120
+ </p>
121
+ </div>
122
+ ),
123
+ parameters: {
124
+ docs: {
125
+ description: {
126
+ story: 'Required textarea field with visual indicator (red asterisk) and helper text.',
127
+ },
128
+ },
129
+ },
130
+ };
131
+
132
+ export const WithCharacterLimit: Story = {
133
+ render: () => (
134
+ <div className="w-96">
135
+ <label htmlFor="limited-textarea" className="block text-sm font-medium mb-1">
136
+ Comment (max 280 characters)
137
+ </label>
138
+
139
+ <TextArea
140
+ id="limited-textarea"
141
+ placeholder="What's on your mind?"
142
+ maxLength={280}
143
+ rows={4}
144
+ aria-describedby="char-count"
145
+ />
146
+
147
+ <p id="char-count" className="text-gray-600 text-xs mt-1">
148
+ 280 characters remaining
149
+ </p>
150
+ </div>
151
+ ),
152
+ parameters: {
153
+ docs: {
154
+ description: {
155
+ story: 'Textarea with character limit and counter (note: counter is static in this demo).',
156
+ },
157
+ },
158
+ },
159
+ };
160
+
161
+ export const DifferentSizes: Story = {
162
+ render: () => (
163
+ <div className="space-y-6 w-96">
164
+ <div>
165
+ <label htmlFor="small-textarea" className="block text-sm font-medium mb-1">
166
+ Small (3 rows)
167
+ </label>
168
+
169
+ <TextArea id="small-textarea" placeholder="Short description..." rows={3} />
170
+ </div>
171
+
172
+ <div>
173
+ <label htmlFor="medium-textarea" className="block text-sm font-medium mb-1">
174
+ Medium (5 rows)
175
+ </label>
176
+
177
+ <TextArea id="medium-textarea" placeholder="Medium description..." rows={5} />
178
+ </div>
179
+
180
+ <div>
181
+ <label htmlFor="large-textarea" className="block text-sm font-medium mb-1">
182
+ Large (8 rows)
183
+ </label>
184
+
185
+ <TextArea id="large-textarea" placeholder="Long description..." rows={8} />
186
+ </div>
187
+ </div>
188
+ ),
189
+ parameters: {
190
+ docs: {
191
+ description: {
192
+ story: 'Textareas with different row heights for various use cases.',
193
+ },
194
+ },
195
+ },
196
+ };
197
+
198
+ export const FormExample: Story = {
199
+ render: () => (
200
+ <form className="space-y-6 w-96">
201
+ <div>
202
+ <label htmlFor="subject" className="block text-sm font-medium mb-1">
203
+ Subject <span className="text-red-500">*</span>
204
+ </label>
205
+
206
+ <input
207
+ id="subject"
208
+ type="text"
209
+ className="w-full rounded-md border p-2"
210
+ placeholder="Brief subject line"
211
+ required
212
+ />
213
+ </div>
214
+
215
+ <div>
216
+ <label htmlFor="message" className="block text-sm font-medium mb-1">
217
+ Message <span className="text-red-500">*</span>
218
+ </label>
219
+
220
+ <TextArea
221
+ id="message"
222
+ placeholder="Enter your detailed message here..."
223
+ rows={6}
224
+ required
225
+ aria-describedby="message-help"
226
+ />
227
+
228
+ <p id="message-help" className="text-gray-600 text-xs mt-1">
229
+ Please provide as much detail as possible
230
+ </p>
231
+ </div>
232
+
233
+ <div>
234
+ <label htmlFor="notes" className="block text-sm font-medium mb-1">
235
+ Additional Notes
236
+ </label>
237
+
238
+ <TextArea id="notes" placeholder="Any additional information (optional)" rows={3} />
239
+ </div>
240
+
241
+ <button
242
+ type="submit"
243
+ className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700
244
+ focus:outline-none focus:ring-2 focus:ring-blue-500"
245
+ >
246
+ Send Message
247
+ </button>
248
+ </form>
249
+ ),
250
+ parameters: {
251
+ docs: {
252
+ description: {
253
+ story:
254
+ 'Complete form example showing textareas in a realistic context with proper labels and validation.',
255
+ },
256
+ },
257
+ },
258
+ };
259
+
260
+ export const ErrorStates: Story = {
261
+ render: () => (
262
+ <div className="space-y-6 w-96">
263
+ <div>
264
+ <label htmlFor="error-required" className="block text-sm font-medium mb-1">
265
+ Required Field <span className="text-red-500">*</span>
266
+ </label>
267
+
268
+ <TextArea
269
+ id="error-required"
270
+ hasError
271
+ placeholder="This field is required"
272
+ rows={4}
273
+ aria-invalid="true"
274
+ aria-describedby="required-error"
275
+ />
276
+
277
+ <p id="required-error" className="text-red-600 text-xs mt-1" role="alert">
278
+ This field is required.
279
+ </p>
280
+ </div>
281
+
282
+ <div>
283
+ <label htmlFor="error-length" className="block text-sm font-medium mb-1">
284
+ Description (min 10 characters)
285
+ </label>
286
+
287
+ <TextArea
288
+ id="error-length"
289
+ hasError
290
+ value="Too short"
291
+ rows={4}
292
+ minLength={10}
293
+ aria-invalid="true"
294
+ aria-describedby="length-error"
295
+ />
296
+
297
+ <p id="length-error" className="text-red-600 text-xs mt-1" role="alert">
298
+ Description must be at least 10 characters long.
299
+ </p>
300
+ </div>
301
+
302
+ <div>
303
+ <label htmlFor="error-content" className="block text-sm font-medium mb-1">
304
+ Content Review
305
+ </label>
306
+
307
+ <TextArea
308
+ id="error-content"
309
+ hasError
310
+ value="This content contains inappropriate language..."
311
+ rows={4}
312
+ aria-invalid="true"
313
+ aria-describedby="content-error"
314
+ />
315
+
316
+ <p id="content-error" className="text-red-600 text-xs mt-1" role="alert">
317
+ Please review your content and remove inappropriate language.
318
+ </p>
319
+ </div>
320
+ </div>
321
+ ),
322
+ parameters: {
323
+ docs: {
324
+ description: {
325
+ story:
326
+ 'Examples of textarea error states with proper ARIA attributes and error messages for accessibility.',
327
+ },
328
+ },
329
+ },
330
+ };
331
+
332
+ export const CustomStyling: Story = {
333
+ render: () => (
334
+ <div className="space-y-6 w-96">
335
+ <div>
336
+ <label htmlFor="large-textarea" className="block text-sm font-medium mb-1">
337
+ Large Text
338
+ </label>
339
+
340
+ <TextArea
341
+ id="large-textarea"
342
+ placeholder="Large text size"
343
+ className="text-lg p-3"
344
+ rows={4}
345
+ />
346
+ </div>
347
+
348
+ <div>
349
+ <label htmlFor="custom-border" className="block text-sm font-medium mb-1">
350
+ Custom Border
351
+ </label>
352
+
353
+ <TextArea
354
+ id="custom-border"
355
+ placeholder="Custom border color"
356
+ className="border-2 border-purple-500 focus:border-purple-700"
357
+ rows={4}
358
+ />
359
+ </div>
360
+
361
+ <div>
362
+ <label htmlFor="no-rounded" className="block text-sm font-medium mb-1">
363
+ Square Corners
364
+ </label>
365
+
366
+ <TextArea
367
+ id="no-rounded"
368
+ placeholder="No border radius"
369
+ className="rounded-none"
370
+ rows={4}
371
+ />
372
+ </div>
373
+
374
+ <div>
375
+ <label htmlFor="full-width" className="block text-sm font-medium mb-1">
376
+ Full Width
377
+ </label>
378
+
379
+ <TextArea
380
+ id="full-width"
381
+ placeholder="Full width textarea"
382
+ className="w-full resize-none"
383
+ rows={4}
384
+ />
385
+ </div>
386
+ </div>
387
+ ),
388
+ parameters: {
389
+ docs: {
390
+ description: {
391
+ story:
392
+ 'Examples of custom styling using className prop. The component uses tailwind-merge to properly handle class overrides.',
393
+ },
394
+ },
395
+ },
396
+ };
397
+
398
+ export const Accessibility: Story = {
399
+ render: () => (
400
+ <div className="space-y-6 w-96">
401
+ <div className="bg-blue-50 border border-blue-200 rounded p-4">
402
+ <h3 className="font-bold text-blue-800 mb-2">Accessibility Features:</h3>
403
+
404
+ <ul className="list-disc list-inside space-y-1 text-blue-700 text-sm">
405
+ <li>Proper label association with htmlFor/id</li>
406
+
407
+ <li>ARIA attributes for error states</li>
408
+
409
+ <li>Descriptive error messages</li>
410
+
411
+ <li>Keyboard navigation support</li>
412
+
413
+ <li>Screen reader compatibility</li>
414
+
415
+ <li>Character count announcements</li>
416
+ </ul>
417
+ </div>
418
+
419
+ <div>
420
+ <label htmlFor="accessible-textarea" className="block text-sm font-medium mb-1">
421
+ Accessible Textarea Example
422
+ </label>
423
+
424
+ <TextArea
425
+ id="accessible-textarea"
426
+ placeholder="Enter your feedback..."
427
+ rows={5}
428
+ aria-describedby="textarea-help"
429
+ maxLength={500}
430
+ />
431
+
432
+ <p id="textarea-help" className="text-gray-600 text-xs mt-1">
433
+ Your feedback helps us improve our services. Maximum 500 characters.
434
+ </p>
435
+ </div>
436
+
437
+ <div>
438
+ <label htmlFor="accessible-error" className="block text-sm font-medium mb-1">
439
+ Textarea with Error
440
+ </label>
441
+
442
+ <TextArea
443
+ id="accessible-error"
444
+ hasError
445
+ value="This content needs review..."
446
+ rows={4}
447
+ aria-invalid="true"
448
+ aria-describedby="accessible-error-msg"
449
+ />
450
+
451
+ <p id="accessible-error-msg" className="text-red-600 text-xs mt-1" role="alert">
452
+ Please provide more specific details in your description.
453
+ </p>
454
+ </div>
455
+ </div>
456
+ ),
457
+ parameters: {
458
+ docs: {
459
+ description: {
460
+ story:
461
+ 'Comprehensive accessibility example showing proper ARIA attributes, error handling, and screen reader support.',
462
+ },
463
+ },
464
+ },
465
+ };
@@ -0,0 +1,236 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { render, screen } from '@testing-library/react';
4
+
5
+ import { TextArea } from './TextArea';
6
+
7
+ // Constants for repeated class names
8
+ const ROUNDED_MD = 'rounded-md';
9
+ const BORDER = 'border';
10
+ const P_1 = 'p-1';
11
+ const DISABLED_OPACITY = 'disabled:opacity-60';
12
+ const DISABLED_BG = 'disabled:bg-gray-100';
13
+ const BORDER_ERROR = 'border-error';
14
+
15
+ describe('TextArea', () => {
16
+ describe('general', () => {
17
+ it('renders a textarea element', () => {
18
+ render(<TextArea />);
19
+
20
+ const textarea = screen.getByRole('textbox');
21
+
22
+ expect(textarea).toBeInTheDocument();
23
+ expect(textarea.tagName).toBe('TEXTAREA');
24
+ });
25
+
26
+ it('applies default classes', () => {
27
+ render(<TextArea />);
28
+
29
+ const textarea = screen.getByRole('textbox');
30
+
31
+ expect(textarea).toHaveClass(ROUNDED_MD, BORDER, P_1, DISABLED_OPACITY, DISABLED_BG);
32
+ });
33
+
34
+ it('applies error styling when hasError is true', () => {
35
+ render(<TextArea hasError />);
36
+
37
+ const textarea = screen.getByRole('textbox');
38
+
39
+ expect(textarea).toHaveClass(BORDER_ERROR);
40
+ });
41
+
42
+ it('does not apply error styling when hasError is false', () => {
43
+ render(<TextArea hasError={false} />);
44
+
45
+ const textarea = screen.getByRole('textbox');
46
+
47
+ expect(textarea).not.toHaveClass(BORDER_ERROR);
48
+ });
49
+
50
+ it('does not apply error styling when hasError is undefined', () => {
51
+ render(<TextArea />);
52
+
53
+ const textarea = screen.getByRole('textbox');
54
+
55
+ expect(textarea).not.toHaveClass(BORDER_ERROR);
56
+ });
57
+
58
+ it('merges custom className with default classes', () => {
59
+ render(<TextArea className="custom-class" />);
60
+
61
+ const textarea = screen.getByRole('textbox');
62
+
63
+ expect(textarea).toHaveClass(ROUNDED_MD, BORDER, P_1, 'custom-class');
64
+ });
65
+
66
+ it('allows custom className to override default classes', () => {
67
+ render(<TextArea className="p-4" />);
68
+
69
+ const textarea = screen.getByRole('textbox');
70
+
71
+ expect(textarea).toHaveClass('p-4');
72
+ expect(textarea).toHaveClass(BORDER); // Base border class remains
73
+ expect(textarea).not.toHaveClass(P_1); // Overridden by p-4
74
+ });
75
+
76
+ it('forwards standard textarea props', () => {
77
+ render(
78
+ <TextArea
79
+ placeholder="Enter description"
80
+ value="test content"
81
+ name="description"
82
+ id="description-textarea"
83
+ rows={5}
84
+ cols={50}
85
+ readOnly
86
+ />,
87
+ );
88
+
89
+ const textarea = screen.getByRole('textbox');
90
+
91
+ expect(textarea).toHaveAttribute('placeholder', 'Enter description');
92
+ expect(textarea).toHaveValue('test content'); // Use toHaveValue instead of toHaveAttribute
93
+ expect(textarea).toHaveAttribute('name', 'description');
94
+ expect(textarea).toHaveAttribute('id', 'description-textarea');
95
+ expect(textarea).toHaveAttribute('rows', '5');
96
+ expect(textarea).toHaveAttribute('cols', '50');
97
+ expect(textarea).toHaveAttribute('readonly');
98
+ });
99
+
100
+ it('applies disabled styling when disabled', () => {
101
+ render(<TextArea disabled />);
102
+
103
+ const textarea = screen.getByRole('textbox');
104
+
105
+ expect(textarea).toBeDisabled();
106
+ expect(textarea).toHaveClass(DISABLED_OPACITY, DISABLED_BG);
107
+ });
108
+
109
+ it('combines hasError with custom className', () => {
110
+ render(<TextArea hasError className="w-full" />);
111
+
112
+ const textarea = screen.getByRole('textbox');
113
+
114
+ expect(textarea).toHaveClass(BORDER_ERROR, 'w-full', ROUNDED_MD, BORDER, P_1);
115
+ });
116
+
117
+ it('supports required attribute', () => {
118
+ render(<TextArea required />);
119
+
120
+ const textarea = screen.getByRole('textbox');
121
+
122
+ expect(textarea).toBeRequired();
123
+ });
124
+
125
+ it('supports aria attributes', () => {
126
+ render(
127
+ <TextArea
128
+ aria-label="Description"
129
+ aria-describedby="description-help"
130
+ aria-invalid="true"
131
+ />,
132
+ );
133
+
134
+ const textarea = screen.getByRole('textbox');
135
+
136
+ expect(textarea).toHaveAttribute('aria-label', 'Description');
137
+ expect(textarea).toHaveAttribute('aria-describedby', 'description-help');
138
+ expect(textarea).toHaveAttribute('aria-invalid', 'true');
139
+ });
140
+
141
+ it('handles complex className override scenarios', () => {
142
+ render(<TextArea hasError className="border-green-500 p-4" />);
143
+
144
+ const textarea = screen.getByRole('textbox');
145
+
146
+ // Custom classes should override defaults due to twMerge
147
+ expect(textarea).toHaveClass('border-green-500', 'p-4');
148
+ expect(textarea).toHaveClass(ROUNDED_MD); // Default class preserved
149
+ expect(textarea).toHaveClass(BORDER); // Base border class is still present
150
+ expect(textarea).not.toHaveClass(P_1); // Overridden by p-4
151
+ expect(textarea).not.toHaveClass(BORDER_ERROR); // Overridden by border-green-500
152
+ });
153
+
154
+ it('supports textarea-specific attributes', () => {
155
+ render(<TextArea rows={10} cols={80} wrap="soft" maxLength={500} minLength={10} />);
156
+
157
+ const textarea = screen.getByRole('textbox');
158
+
159
+ expect(textarea).toHaveAttribute('rows', '10');
160
+ expect(textarea).toHaveAttribute('cols', '80');
161
+ expect(textarea).toHaveAttribute('wrap', 'soft');
162
+ expect(textarea).toHaveAttribute('maxlength', '500');
163
+ expect(textarea).toHaveAttribute('minlength', '10');
164
+ });
165
+ });
166
+
167
+ describe('accessibility', () => {
168
+ it('is accessible by role', () => {
169
+ render(<TextArea />);
170
+
171
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
172
+ });
173
+
174
+ it('supports form labels via id', () => {
175
+ render(
176
+ <>
177
+ <label htmlFor="test-textarea">Description</label>
178
+
179
+ <TextArea id="test-textarea" />
180
+ </>,
181
+ );
182
+
183
+ const textarea = screen.getByLabelText('Description');
184
+
185
+ expect(textarea).toBeInTheDocument();
186
+ expect(textarea.tagName).toBe('TEXTAREA');
187
+ });
188
+
189
+ it('supports error state for screen readers', () => {
190
+ render(<TextArea hasError aria-invalid="true" />);
191
+
192
+ const textarea = screen.getByRole('textbox');
193
+
194
+ expect(textarea).toHaveAttribute('aria-invalid', 'true');
195
+ expect(textarea).toHaveClass(BORDER_ERROR);
196
+ });
197
+
198
+ it('supports textarea with associated help text', () => {
199
+ render(
200
+ <>
201
+ <label htmlFor="description">Description</label>
202
+
203
+ <TextArea id="description" aria-describedby="description-help" />
204
+
205
+ <div id="description-help">Please provide a detailed description</div>
206
+ </>,
207
+ );
208
+
209
+ const textarea = screen.getByLabelText('Description');
210
+
211
+ expect(textarea).toHaveAttribute('aria-describedby', 'description-help');
212
+ });
213
+ });
214
+
215
+ describe('form integration', () => {
216
+ it('works with controlled components', () => {
217
+ const { rerender } = render(<TextArea value="initial value" readOnly />);
218
+
219
+ let textarea = screen.getByRole('textbox');
220
+
221
+ expect(textarea).toHaveValue('initial value');
222
+
223
+ rerender(<TextArea value="updated value" readOnly />);
224
+ textarea = screen.getByRole('textbox');
225
+ expect(textarea).toHaveValue('updated value');
226
+ });
227
+
228
+ it('supports placeholder text', () => {
229
+ render(<TextArea placeholder="Enter your thoughts here..." />);
230
+
231
+ const textarea = screen.getByPlaceholderText('Enter your thoughts here...');
232
+
233
+ expect(textarea).toBeInTheDocument();
234
+ });
235
+ });
236
+ });