Skip to content

Commit 625559b

Browse files
feat: New fluid text area web component (#20420)
* feat: new fluid text area web component * fix: docs and test cases * fix: warn and invalid states * fix: docs and test * fix: test cases * fix: helper text test * fix: test cases * fix: story fix --------- Co-authored-by: Heloise Lui <71858203+heloiselui@users.noreply.github.com>
1 parent 1e41f3e commit 625559b

File tree

12 files changed

+601
-27
lines changed

12 files changed

+601
-27
lines changed

e2e/components/FluidTextArea/FluidTextArea-test.avt.e2e.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ test.describe('@avt FluidTextArea', () => {
3535
);
3636
});
3737

38-
test('@avt-advanced-states default-with-tooltip', async ({ page }) => {
38+
test('@avt-advanced-states default-with-toggletip', async ({ page }) => {
3939
await visitStory(page, {
4040
component: 'FluidTextArea',
41-
id: 'components-fluid-components-fluidtextarea--default-with-tooltip',
41+
id: 'components-fluid-components-fluidtextarea--default-with-toggletip',
4242
globals: {
4343
theme: 'white',
4444
},
4545
});
4646
await expect(page).toHaveNoACViolations(
47-
'FluidTextArea-default-with-tooltip'
47+
'FluidTextArea-default-with-toggletip'
4848
);
4949
});
5050

@@ -80,17 +80,17 @@ test.describe('@avt FluidTextArea', () => {
8080
await expect(page).toHaveNoACViolations('FluidTextArea default');
8181
});
8282

83-
test('@avt-keyboard-nav FluidTextArea with tooltip', async ({ page }) => {
83+
test('@avt-keyboard-nav FluidTextArea with toggletip', async ({ page }) => {
8484
await visitStory(page, {
8585
component: 'TextArea',
86-
id: 'components-fluid-components-fluidtextarea--default-with-tooltip',
86+
id: 'components-fluid-components-fluidtextarea--default-with-toggletip',
8787
globals: {
8888
theme: 'white',
8989
},
9090
});
9191
await expect(page.getByText('Text Area label')).toBeVisible();
9292

93-
// Checking tooltip
93+
// Checking toggletip
9494
await page.keyboard.press('Tab');
9595
await expect(page.getByLabel('Show information')).toBeFocused();
9696
await page.keyboard.press('Enter');
@@ -106,6 +106,6 @@ test.describe('@avt FluidTextArea', () => {
106106
// Writting a word to check functionality
107107
await textArea.fill('test');
108108
await expect(textArea).toHaveValue('test');
109-
await expect(page).toHaveNoACViolations('FluidTextArea with tooltip');
109+
await expect(page).toHaveNoACViolations('FluidTextArea with toggletip');
110110
});
111111
});

packages/react/src/components/FluidTextArea/FluidTextArea.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { stackblitzPrefillConfig } from '../../../previewer/codePreviewer';
1515

1616
- [Overview](#overview)
1717
- [Skeleton](#skeleton)
18-
- [Default With Tooltip](#default-with-tooltip)
18+
- [Default With Toggletip](#default-with-toggletip)
1919
- [Default With Layers](#default-with-layers)
2020
- [Accessibility Considerations](#accessibility-considerations)
2121
- [Accessible Name](#accessible-name)
@@ -48,14 +48,14 @@ import { stackblitzPrefillConfig } from '../../../previewer/codePreviewer';
4848
]}
4949
/>
5050

51-
### Default With Tooltip
51+
### Default With Toggletip
5252

5353
<Canvas
54-
of={FluidTextAreaStories.DefaultWithTooltip}
54+
of={FluidTextAreaStories.DefaultWithToggletip}
5555
additionalActions={[
5656
{
5757
title: 'Open in Stackblitz',
58-
onClick: () => stackblitzPrefillConfig(FluidTextAreaStories.DefaultWithTooltip),
58+
onClick: () => stackblitzPrefillConfig(FluidTextAreaStories.DefaultWithToggletip),
5959
},
6060
]}
6161
/>

packages/react/src/components/FluidTextArea/FluidTextArea.stories.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ const ToggleTip = (
160160
</>
161161
);
162162

163-
export const DefaultWithTooltip = () => (
163+
export const DefaultWithToggletip = () => (
164164
<FluidTextArea labelText={ToggleTip} placeholder="Placeholder text" />
165165
);
166166

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
* Copyright IBM Corp. 2025
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import '@carbon/web-components/es/components/fluid-textarea/index.js';
9+
import { html, fixture, expect, oneEvent } from '@open-wc/testing';
10+
11+
describe('cds-fluid-textarea', () => {
12+
it('should render correctly with label', async () => {
13+
const el = await fixture(html`
14+
<cds-fluid-textarea
15+
label="Textarea label"
16+
helper-text="Helper text"></cds-fluid-textarea>
17+
`);
18+
19+
const label = el.shadowRoot.querySelector('label');
20+
21+
expect(label).to.exist;
22+
expect(label.textContent).to.include('Textarea label');
23+
});
24+
25+
it('should reflect value to the textarea', async () => {
26+
const el = await fixture(html`
27+
<cds-fluid-textarea value="Initial content"></cds-fluid-textarea>
28+
`);
29+
30+
const textarea = el.shadowRoot.querySelector('textarea');
31+
expect(textarea.value).to.equal('Initial content');
32+
});
33+
34+
it('should emit input event and update value', async () => {
35+
const el = await fixture(html`<cds-fluid-textarea></cds-fluid-textarea>`);
36+
const textarea = el.shadowRoot.querySelector('textarea');
37+
textarea.value = 'Updated content';
38+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
39+
40+
await el.updateComplete;
41+
expect(el.value).to.equal('Updated content');
42+
});
43+
44+
it('should support readonly and disabled attributes', async () => {
45+
const el = await fixture(html`
46+
<cds-fluid-textarea readonly disabled></cds-fluid-textarea>
47+
`);
48+
49+
const textarea = el.shadowRoot.querySelector('textarea');
50+
expect(textarea.readOnly).to.be.true;
51+
expect(textarea.disabled).to.be.true;
52+
});
53+
54+
it('should show invalid text when invalid is set', async () => {
55+
const el = await fixture(html`
56+
<cds-fluid-textarea
57+
invalid
58+
invalid-text="Error occurred"></cds-fluid-textarea>
59+
`);
60+
61+
const error = el.shadowRoot.querySelector('.cds--form-requirement');
62+
expect(error.textContent).to.include('Error occurred');
63+
expect(el.hasAttribute('invalid')).to.be.true;
64+
});
65+
66+
it('should show warning text when warn is set', async () => {
67+
const el = await fixture(html`
68+
<cds-fluid-textarea
69+
warn
70+
warn-text="This is a warning"></cds-fluid-textarea>
71+
`);
72+
73+
const warning = el.shadowRoot.querySelector('.cds--form-requirement');
74+
expect(warning.textContent).to.include('This is a warning');
75+
expect(el.hasAttribute('warn')).to.be.true;
76+
});
77+
78+
it('should apply hide-label and visually hide the label', async () => {
79+
const el = await fixture(html`
80+
<cds-fluid-textarea label="Hidden label" hide-label></cds-fluid-textarea>
81+
`);
82+
const label = el.shadowRoot.querySelector('label');
83+
expect(label.classList.contains('cds--visually-hidden')).to.be.true;
84+
});
85+
86+
it('should reflect cols and rows attributes', async () => {
87+
const el = await fixture(html`
88+
<cds-fluid-textarea cols="50" rows="10"></cds-fluid-textarea>
89+
`);
90+
const textarea = el.shadowRoot.querySelector('textarea');
91+
expect(textarea.getAttribute('cols')).to.equal('50');
92+
expect(textarea.getAttribute('rows')).to.equal('10');
93+
});
94+
95+
it('should accept pattern and required attributes', async () => {
96+
const el = await fixture(html`
97+
<cds-fluid-textarea pattern="[A-Za-z]+" required></cds-fluid-textarea>
98+
`);
99+
const textarea = el.shadowRoot.querySelector('textarea');
100+
expect(textarea.getAttribute('pattern')).to.equal('[A-Za-z]+');
101+
expect(textarea.hasAttribute('required')).to.be.true;
102+
});
103+
104+
it('should forward data-* attributes', async () => {
105+
const el = await fixture(html`
106+
<cds-fluid-textarea data-testid="textarea-id"></cds-fluid-textarea>
107+
`);
108+
expect(el.getAttribute('data-testid')).to.equal('textarea-id');
109+
});
110+
111+
it('should support skeleton variant', async () => {
112+
const el = await fixture(html`
113+
<cds-fluid-textarea-skeleton></cds-fluid-textarea-skeleton>
114+
`);
115+
expect(el).to.exist;
116+
const skeleton = el.shadowRoot.querySelector('.cds--skeleton');
117+
expect(skeleton).to.exist;
118+
});
119+
120+
it('should be accessible', async () => {
121+
const el = await fixture(html`
122+
<cds-fluid-textarea
123+
label="Label"
124+
helper-text="Help"
125+
value="value"></cds-fluid-textarea>
126+
`);
127+
await expect(el).to.be.accessible();
128+
});
129+
130+
// Additional parity tests with React
131+
132+
describe('counter mode behaviors', () => {
133+
// Test for switching counter mode from "word" to "character"
134+
it('should apply maxlength only in character mode', async () => {
135+
const el = await fixture(html`
136+
<cds-fluid-textarea
137+
enable-counter
138+
counter-mode="character"
139+
max-count="100"></cds-fluid-textarea>
140+
`);
141+
const textarea = el.shadowRoot.querySelector('textarea');
142+
expect(textarea.getAttribute('maxlength')).to.equal('100');
143+
});
144+
145+
// Test for switching counter mode from "word" to "character"
146+
it('should remove maxlength when switching to word mode', async () => {
147+
const el = await fixture(html`
148+
<cds-fluid-textarea
149+
enable-counter
150+
counter-mode="character"
151+
max-count="100"></cds-fluid-textarea>
152+
`);
153+
el.counterMode = 'word';
154+
await el.updateComplete;
155+
const textarea = el.shadowRoot.querySelector('textarea');
156+
expect(textarea.hasAttribute('maxlength')).to.be.false;
157+
});
158+
159+
// Test for switching back to character mode
160+
it('should add maxlength when switching back to character mode', async () => {
161+
const el = await fixture(html`
162+
<cds-fluid-textarea
163+
enable-counter
164+
counter-mode="word"
165+
max-count="100"></cds-fluid-textarea>
166+
`);
167+
el.counterMode = 'character';
168+
await el.updateComplete;
169+
const textarea = el.shadowRoot.querySelector('textarea');
170+
expect(textarea.getAttribute('maxlength')).to.equal('100');
171+
});
172+
});
173+
174+
// Slot support tests (label-text, helper-text, invalid-text, warn-text)
175+
describe('slot support', () => {
176+
it('renders slotted label-text', async () => {
177+
const el = await fixture(html`
178+
<cds-fluid-textarea>
179+
<span slot="label-text">Slotted Label</span>
180+
</cds-fluid-textarea>
181+
`);
182+
await el.updateComplete;
183+
const slot = el.shadowRoot.querySelector('slot[name="label-text"]');
184+
const content = slot.assignedNodes({ flatten: true })[0];
185+
expect(content.textContent.trim()).to.equal('Slotted Label');
186+
});
187+
188+
it('renders slotted invalid-text', async () => {
189+
const el = await fixture(html`
190+
<cds-fluid-textarea invalid>
191+
<span slot="invalid-text">Slotted Invalid</span>
192+
</cds-fluid-textarea>
193+
`);
194+
await el.updateComplete;
195+
const slot = el.shadowRoot.querySelector('slot[name="invalid-text"]');
196+
const content = slot.assignedNodes({ flatten: true })[0];
197+
expect(content.textContent.trim()).to.equal('Slotted Invalid');
198+
});
199+
200+
it('renders slotted warn-text', async () => {
201+
const el = await fixture(html`
202+
<cds-fluid-textarea warn>
203+
<span slot="warn-text">Slotted Warning</span>
204+
</cds-fluid-textarea>
205+
`);
206+
await el.updateComplete;
207+
const slot = el.shadowRoot.querySelector('slot[name="warn-text"]');
208+
const content = slot.assignedNodes({ flatten: true })[0];
209+
expect(content.textContent.trim()).to.equal('Slotted Warning');
210+
});
211+
});
212+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright IBM Corp.2025
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { prefix } from '../../globals/settings';
9+
import { html } from 'lit';
10+
import { carbonElement as customElement } from '../../globals/decorators/carbon-element';
11+
import CDSTextareaSkeleton from '../textarea/textarea-skeleton';
12+
import styles from './fluid-textarea.scss?lit';
13+
14+
/**
15+
* Fluid text area input.
16+
*
17+
* @element cds-fluid-textarea
18+
*/
19+
@customElement(`${prefix}-fluid-textarea-skeleton`)
20+
class CDSFluidTextareaSkeleton extends CDSTextareaSkeleton {
21+
render() {
22+
return html`
23+
<div class="${prefix}--text-area--fluid__skeleton ${prefix}--form-item">
24+
${super.render()}
25+
</div>
26+
`;
27+
}
28+
29+
static styles = [CDSTextareaSkeleton.styles, styles];
30+
}
31+
32+
export default CDSFluidTextareaSkeleton;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ArgTypes, Canvas, Markdown, Meta } from '@storybook/addon-docs/blocks';
2+
import { cdnJs } from '../../globals/internal/storybook-cdn';
3+
import * as FluidTextAreaStories from './fluid-textarea.stories';
4+
5+
<Meta of={FluidTextAreaStories} />
6+
7+
# Text Area
8+
9+
[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/web-components/src/components/fluid-textarea)
10+
11+
## Table of Contents
12+
13+
- [Overview](#overview)
14+
- [Skeleton](#skeleton)
15+
- [Default With Tooltip](#default-with-tooltip)
16+
- [Component API](#component-api)
17+
- [CDN](#cdn)
18+
- [Feedback](#feedback)
19+
20+
## Overview
21+
22+
<Canvas of={FluidTextAreaStories.Default} />
23+
24+
## Skeleton
25+
26+
<Canvas of={FluidTextAreaStories.Skeleton} />
27+
28+
## Default With Toggletip
29+
30+
<Canvas of={FluidTextAreaStories.DefaultWithToggletip} />
31+
32+
## Component API
33+
34+
## `cds-fluid-textarea`
35+
36+
<ArgTypes of="cds-textarea" />
37+
38+
<Markdown>{`${cdnJs({ components: ['fluid-textarea'] })}`}</Markdown>
39+
40+
## Feedback
41+
42+
Help us improve this component by providing feedback, asking questions on Slack,
43+
or updating this file on
44+
[GitHub](https://github.com/carbon-design-system/carbon/edit/main/packages/web-components/src/components/fluid-textarea/fluid-textarea.mdx).
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Copyright IBM Corp.2025
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
$css--plex: true !default;
9+
@use '@carbon/styles/scss/config' as *;
10+
@use '@carbon/styles/scss/components/fluid-text-area/index';
11+
@use '@carbon/styles/scss/layout' as *;
12+
@use '@carbon/styles/scss/spacing' as *;
13+
14+
:host(#{$prefix}-fluid-textarea) {
15+
@include emit-layout-tokens();
16+
}

0 commit comments

Comments
 (0)