Skip to content

Commit a5bc2c1

Browse files
feat(cds-fluid-form): new wc component (#21951)
* feat(cds-fluid-form): new wc component * fix(fluid-components): add flex style * fix: hide modal-container overflow * feat: sync stories * feat(fluidform): storycontrols in react and wc * fix(fluid-password-input): remove excess bottom padding * test: add tests * fix: datepicker controls * chore: clean up imports * fix: passwordinput default alignment --------- Co-authored-by: Heloise Lui <71858203+heloiselui@users.noreply.github.com>
1 parent 7955675 commit a5bc2c1

14 files changed

Lines changed: 609 additions & 99 deletions

File tree

Lines changed: 158 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2023
2+
* Copyright IBM Corp. 2016, 2026
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -18,11 +18,13 @@ import FluidTextArea from '../FluidTextArea';
1818
import FluidTimePicker from '../FluidTimePicker';
1919
import FluidTimePickerSelect from '../FluidTimePickerSelect';
2020
import FluidNumberInput from '../FluidNumberInput';
21+
import FluidPasswordInput from '../FluidTextInput/FluidPasswordInput';
2122
import ModalWrapper from '../ModalWrapper';
2223
import mdx from './FluidForm.mdx';
2324

2425
const additionalProps = {
2526
className: 'some-class',
27+
'aria-label': 'sample form',
2628
};
2729

2830
const TextInputProps = {
@@ -39,11 +41,11 @@ const TextAreaProps = {
3941
placeholder: 'Placeholder text',
4042
};
4143

42-
const InvalidPasswordProps = {
44+
const PasswordInputProps = {
4345
className: 'some-class',
4446
id: 'test4',
4547
labelText: 'Password',
46-
value: '0000',
48+
defaultValue: '0000',
4749
};
4850

4951
export default {
@@ -54,99 +56,162 @@ export default {
5456
page: mdx,
5557
},
5658
},
59+
argTypes: {
60+
disabled: {
61+
control: {
62+
type: 'boolean',
63+
},
64+
description: 'Specify whether the fluid form inputs should be disabled',
65+
},
66+
readOnly: {
67+
control: {
68+
type: 'boolean',
69+
},
70+
description: 'Specify whether the fluid form inputs should be read-only',
71+
},
72+
invalid: {
73+
control: {
74+
type: 'boolean',
75+
},
76+
description:
77+
'Specify whether the fluid form inputs are in an invalid state',
78+
},
79+
invalidText: {
80+
control: {
81+
type: 'text',
82+
},
83+
description: 'Provide the text for the invalid state',
84+
},
85+
warn: {
86+
control: {
87+
type: 'boolean',
88+
},
89+
description:
90+
'Specify whether the fluid form inputs should display a warning',
91+
},
92+
warnText: {
93+
control: {
94+
type: 'text',
95+
},
96+
description: 'Provide the text for the warning state',
97+
},
98+
},
99+
args: {
100+
disabled: false,
101+
readOnly: false,
102+
invalid: false,
103+
invalidText:
104+
'Error message that is really long can wrap to more lines but should not be excessively long.',
105+
warn: false,
106+
warnText:
107+
'Warning message that is really long can wrap to more lines but should not be excessively long.',
108+
},
57109
};
58110

59-
export const Default = () => (
60-
<>
61-
<FluidForm aria-label="sample form" {...additionalProps}>
62-
<div style={{ display: 'flex' }}>
63-
<FluidTimePicker
64-
id="time-picker-1"
65-
labelText="Time"
66-
placeholder="hh:mm">
67-
<FluidTimePickerSelect id="select-01" labelText="Clock">
68-
<SelectItem value="am" text="AM" />
69-
<SelectItem value="pm" text="PM" />
70-
</FluidTimePickerSelect>
71-
<FluidTimePickerSelect id="select-02" labelText="Timezone">
72-
<SelectItem value="et" text="Eastern Time (ET)" />
73-
<SelectItem value="ct" text="Central Time (CT)" />
74-
<SelectItem value="mt" text="Mountain Time (MT)" />
75-
<SelectItem value="pt" text="Pacific Time (PT)" />
76-
</FluidTimePickerSelect>
77-
</FluidTimePicker>
78-
<FluidDatePicker datePickerType="range">
79-
<FluidDatePickerInput
80-
id="date-picker-input-id-start"
81-
placeholder="mm/dd/yyyy"
82-
labelText="Choose your dates"
83-
/>
84-
<FluidDatePickerInput
85-
id="date-picker-input-id-finish"
86-
placeholder="mm/dd/yyyy"
87-
labelText="End date"
88-
/>
89-
</FluidDatePicker>
90-
<FluidSelect
91-
id="select-1"
92-
defaultValue="placeholder-item"
93-
labelText="Choose an option">
94-
<SelectItem
95-
disabled
96-
hidden
97-
value="placeholder-item"
98-
text="Choose an option"
111+
export const Default = (args) => {
112+
const { disabled, readOnly, invalid, invalidText, warn, warnText } = args;
113+
return (
114+
<>
115+
<FluidForm {...additionalProps}>
116+
<div style={{ display: 'flex' }}>
117+
<FluidTimePicker
118+
id="time-picker-1"
119+
labelText="Time"
120+
placeholder="hh:mm"
121+
{...args}>
122+
<FluidTimePickerSelect id="select-01" labelText="Clock">
123+
<SelectItem value="am" text="AM" />
124+
<SelectItem value="pm" text="PM" />
125+
</FluidTimePickerSelect>
126+
<FluidTimePickerSelect id="select-02" labelText="Timezone">
127+
<SelectItem value="et" text="Eastern Time (ET)" />
128+
<SelectItem value="ct" text="Central Time (CT)" />
129+
<SelectItem value="mt" text="Mountain Time (MT)" />
130+
<SelectItem value="pt" text="Pacific Time (PT)" />
131+
</FluidTimePickerSelect>
132+
</FluidTimePicker>
133+
<FluidDatePicker
134+
datePickerType="range"
135+
invalid={invalid}
136+
warn={warn}
137+
disabled={disabled}
138+
readOnly={readOnly}>
139+
<FluidDatePickerInput
140+
id="date-picker-input-id-start"
141+
placeholder="mm/dd/yyyy"
142+
labelText="Choose your dates"
143+
disabled={disabled}
144+
readOnly={readOnly}
145+
invalidText={invalidText}
146+
warnText={warnText}
147+
/>
148+
<FluidDatePickerInput
149+
id="date-picker-input-id-finish"
150+
placeholder="mm/dd/yyyy"
151+
labelText="End date"
152+
disabled={disabled}
153+
readOnly={readOnly}
154+
invalidText={invalidText}
155+
warnText={warnText}
156+
/>
157+
</FluidDatePicker>
158+
<FluidSelect
159+
id="select-1"
160+
defaultValue="placeholder-item"
161+
labelText="Choose an option"
162+
{...args}>
163+
<SelectItem
164+
disabled
165+
hidden
166+
value="placeholder-item"
167+
text="Choose an option"
168+
/>
169+
<SelectItemGroup label="Category 1">
170+
<SelectItem value="option-1" text="Option 1" />
171+
<SelectItem value="option-2" text="Option 2" />
172+
</SelectItemGroup>
173+
<SelectItemGroup label="Category 2">
174+
<SelectItem value="option-3" text="Option 3" />
175+
<SelectItem value="option-4" text="Option 4" />
176+
</SelectItemGroup>
177+
</FluidSelect>
178+
</div>
179+
<div style={{ display: 'flex' }}>
180+
<FluidTextInput {...TextInputProps} {...args} />
181+
<FluidNumberInput
182+
label="Number Input Label"
183+
id="input-default"
184+
step={10}
185+
min={0}
186+
max={100}
187+
defaultValue={50}
188+
{...args}
99189
/>
100-
<SelectItemGroup label="Category 1">
101-
<SelectItem value="option-1" text="Option 1" />
102-
<SelectItem value="option-2" text="Option 2" />
103-
</SelectItemGroup>
104-
<SelectItemGroup label="Category 2">
105-
<SelectItem value="option-3" text="Option 3" />
106-
<SelectItem value="option-4" text="Option 4" />
107-
</SelectItemGroup>
108-
</FluidSelect>
109-
</div>
110-
<div style={{ display: 'flex' }}>
111-
<FluidTextInput {...TextInputProps} />
112-
<FluidNumberInput
113-
label="Number Input Label"
114-
labelText="Fluid Number Input"
115-
placeholder="Placeholder text"
116-
id="input-default"
117-
step={10}
118-
min={0}
119-
max={100}
120-
defaultValue={50}
121-
/>
122-
</div>
190+
</div>
123191

124-
<FluidTextInput
125-
type="password"
126-
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,}"
127-
{...InvalidPasswordProps}
128-
/>
129-
<FluidTextArea {...TextAreaProps} />
130-
</FluidForm>
192+
<FluidPasswordInput {...PasswordInputProps} {...args} />
193+
<FluidTextArea {...TextAreaProps} {...args} />
194+
</FluidForm>
131195

132-
<br />
196+
<br />
133197

134-
<ModalWrapper
135-
hasScrollingContent
136-
buttonTriggerText="Fluid form in modal"
137-
modalHeading="Modal heading"
138-
modalLabel="Label"
139-
handleSubmit={() => {}}
140-
size="md">
141-
<FluidForm {...additionalProps}>
142-
<FluidTextInput {...TextInputProps} />
143-
<FluidTextInput
144-
type="password"
145-
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,}"
146-
{...InvalidPasswordProps}
147-
/>
148-
<FluidTextArea {...TextAreaProps} />
149-
</FluidForm>
150-
</ModalWrapper>
151-
</>
152-
);
198+
<ModalWrapper
199+
hasScrollingContent
200+
buttonTriggerText="Fluid form in modal"
201+
modalHeading="Modal heading"
202+
modalLabel="Label"
203+
handleSubmit={() => {}}
204+
size="md">
205+
<FluidForm {...additionalProps}>
206+
<FluidTextInput {...TextInputProps} {...args} id="modal-test2" />
207+
<FluidPasswordInput
208+
{...PasswordInputProps}
209+
{...args}
210+
id="modal-test4"
211+
/>
212+
<FluidTextArea {...TextAreaProps} {...args} id="modal-test3" />
213+
</FluidForm>
214+
</ModalWrapper>
215+
</>
216+
);
217+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Copyright IBM Corp. 2026
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 { expect, fixture, html } from '@open-wc/testing';
9+
import '@carbon/web-components/es/components/fluid-form/index.js';
10+
11+
describe('cds-fluid-form', () => {
12+
describe('renders as expected - Component API', () => {
13+
it('should render children as expected', async () => {
14+
const el = await fixture(html`
15+
<cds-fluid-form>
16+
<div>Text 1</div>
17+
<div>Text 2</div>
18+
</cds-fluid-form>
19+
`);
20+
const children = el.querySelectorAll('div');
21+
expect(children).to.have.lengthOf(2);
22+
expect(children[0].textContent).to.equal('Text 1');
23+
expect(children[1].textContent).to.equal('Text 2');
24+
});
25+
26+
it('should be a fluid form', async () => {
27+
const el = await fixture(html`<cds-fluid-form></cds-fluid-form>`);
28+
const form = el.shadowRoot?.querySelector('form');
29+
expect(form).to.have.class('cds--form');
30+
expect(form).to.have.class('cds--form--fluid');
31+
});
32+
33+
it('should spread additional attributes on the host element', async () => {
34+
const el = await fixture(html`
35+
<cds-fluid-form data-testid="test-id"></cds-fluid-form>
36+
`);
37+
expect(el.getAttribute('data-testid')).to.equal('test-id');
38+
});
39+
40+
it('should support a custom class on the host element', async () => {
41+
const el = await fixture(html`
42+
<cds-fluid-form class="custom-class"></cds-fluid-form>
43+
`);
44+
expect(el).to.have.class('custom-class');
45+
});
46+
47+
it('should apply `in-modal` attribute when inside a modal-body', async () => {
48+
const el = await fixture(html`
49+
<cds-modal-body>
50+
<cds-fluid-form></cds-fluid-form>
51+
</cds-modal-body>
52+
`);
53+
const fluidForm = el.querySelector('cds-fluid-form');
54+
expect(fluidForm).to.have.attribute('in-modal');
55+
});
56+
57+
it('should not apply `in-modal` attribute when not inside a modal-body', async () => {
58+
const el = await fixture(html`<cds-fluid-form></cds-fluid-form>`);
59+
expect(el).to.not.have.attribute('in-modal');
60+
});
61+
});
62+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { ArgTypes, Canvas, Markdown, Meta } from '@storybook/addon-docs/blocks';
2+
import { cdnJs } from '../../globals/internal/storybook-cdn';
3+
import * as FluidFormStories from './fluid-form.stories';
4+
5+
<Meta of={FluidFormStories} component="cds-fluid-form" />
6+
7+
# Fluid Form
8+
9+
[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/web-components/src/components/fluid-form)
10+
&nbsp;|&nbsp;
11+
[Usage guidelines](https://www.carbondesignsystem.com/components/form/usage)
12+
&nbsp;|&nbsp;
13+
[Accessibility](https://www.carbondesignsystem.com/components/form/accessibility)
14+
15+
## Table of Contents
16+
17+
- [Overview](#overview)
18+
- [Accessibility Considerations](#accessibility-considerations)
19+
- [Accessible Name](#accessible-name)
20+
- [Component API](#component-api)
21+
- [Feedback](#feedback)
22+
23+
## Overview
24+
25+
<Canvas of={FluidFormStories.Default} />
26+
27+
## Accessibility Considerations
28+
29+
### Accessible Name
30+
31+
To comply with accessibility requirements, make sure to supply the component
32+
with an accessible name by providing either the `aria-label`, `aria-labelledby`
33+
or `title` attribute. Read more on the accessible naming rule
34+
[here](https://able.ibm.com/rules/archives/latest/doc/en-US/aria_accessiblename_exists.html).
35+
36+
## Component API
37+
38+
## `cds-fluid-form`
39+
40+
<ArgTypes of="cds-fluid-form" />
41+
42+
<Markdown>{`${cdnJs({ components: ['fluid-form'] })}`}</Markdown>
43+
44+
## Feedback
45+
46+
Help us improve this component by providing feedback, asking questions on Slack,
47+
or updating this file on
48+
[GitHub](https://github.com/carbon-design-system/carbon/edit/main/packages/web-components/src/components/fluid-form/fluid-form.mdx).

0 commit comments

Comments
 (0)