Skip to content

Commit 60b4737

Browse files
feat: number_input_fluid_components (#20837)
* feat: number_input_fluid_components * fix: number input styles * fix: added test cases number input --------- Co-authored-by: emyarod <[email protected]>
1 parent 154a42e commit 60b4737

File tree

8 files changed

+576
-14
lines changed

8 files changed

+576
-14
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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-number-input/index.js';
9+
import { fixture, html, expect, oneEvent } from '@open-wc/testing';
10+
11+
describe('<cds-number-input>', () => {
12+
it('should render a number input with correct type', async () => {
13+
const el = await fixture(
14+
html`<cds-fluid-number-input label="Label"></cds-fluid-number-input>`
15+
);
16+
expect(el.shadowRoot.querySelector('input').type).to.equal('number');
17+
});
18+
19+
it('should place custom class on host', async () => {
20+
const el = await fixture(
21+
html`<cds-fluid-number-input
22+
class="custom-class"
23+
label="Label"></cds-fluid-number-input>`
24+
);
25+
expect(el.classList.contains('custom-class')).to.be.true;
26+
});
27+
28+
it('should set min, max, step attributes', async () => {
29+
const el = await fixture(
30+
html`<cds-fluid-number-input
31+
min="1"
32+
max="10"
33+
step="2"
34+
label="Label"></cds-fluid-number-input>`
35+
);
36+
const input = el.shadowRoot.querySelector('input');
37+
expect(input.min).to.equal('1');
38+
expect(input.max).to.equal('10');
39+
expect(input.step).to.equal('2');
40+
});
41+
42+
it('should respect disabled and readonly attributes', async () => {
43+
const el = await fixture(
44+
html`<cds-fluid-number-input
45+
disabled
46+
readonly
47+
label="Label"></cds-fluid-number-input>`
48+
);
49+
const input = el.shadowRoot.querySelector('input');
50+
expect(el.hasAttribute('disabled') || input.disabled).to.be.true;
51+
expect(el.hasAttribute('readonly') || input.readOnly).to.be.true;
52+
});
53+
54+
it('should emit cds-number-input event with value and direction', async () => {
55+
const el = await fixture(
56+
html`<cds-fluid-number-input
57+
value="5"
58+
label="Label"></cds-fluid-number-input>`
59+
);
60+
const input = el.shadowRoot.querySelector('input');
61+
62+
setTimeout(() => {
63+
input.value = '6';
64+
input.dispatchEvent(
65+
new Event('input', { bubbles: true, composed: true })
66+
);
67+
});
68+
69+
const event = await oneEvent(el, 'cds-number-input');
70+
expect(event).to.exist;
71+
expect(event.detail.value).to.equal('6');
72+
expect(event.detail.direction).to.equal('up');
73+
});
74+
75+
// From React parity
76+
it('should show helper text and invalid text', async () => {
77+
const el = await fixture(html`
78+
<cds-fluid-number-input invalid label="Label">
79+
<span slot="invalid-text">Invalid</span>
80+
</cds-fluid-number-input>
81+
`);
82+
const invalid = el.querySelector('[slot="invalid-text"]');
83+
expect(invalid?.textContent).to.include('Invalid');
84+
});
85+
86+
it('should increment and decrement using buttons', async () => {
87+
const el = await fixture(
88+
html`<cds-fluid-number-input
89+
value="1"
90+
step="1"
91+
min="0"
92+
max="3"
93+
label="Label"></cds-fluid-number-input>`
94+
);
95+
const input = el.shadowRoot.querySelector('input');
96+
const [decrement, increment] = el.shadowRoot.querySelectorAll('button');
97+
98+
increment.click();
99+
await el.updateComplete;
100+
expect(input.value).to.equal('2');
101+
102+
decrement.click();
103+
await el.updateComplete;
104+
expect(input.value).to.equal('1');
105+
});
106+
107+
// Ensures step decimal precision works as expected
108+
it('should support decimal step values accurately', async () => {
109+
const el = await fixture(
110+
html`<cds-fluid-number-input
111+
value="1.1"
112+
step="0.1"
113+
label="Decimal"></cds-fluid-number-input>`
114+
);
115+
const input = el.shadowRoot.querySelector('input');
116+
const [, increment] = el.shadowRoot.querySelectorAll('button');
117+
118+
increment.click();
119+
await el.updateComplete;
120+
expect(input.value).to.equal('1.2');
121+
});
122+
123+
it('should respect allow-empty attribute', async () => {
124+
const el = await fixture(
125+
html`<cds-fluid-number-input
126+
allow-empty
127+
value=""
128+
label="Label"></cds-fluid-number-input>`
129+
);
130+
const input = el.shadowRoot.querySelector('input');
131+
expect(input.value).to.equal('');
132+
});
133+
134+
it('should hide the steppers when hide-steppers is set', async () => {
135+
const el = await fixture(
136+
html`<cds-fluid-number-input
137+
hide-steppers
138+
label="No steppers"></cds-fluid-number-input>`
139+
);
140+
const buttons = el.shadowRoot.querySelectorAll('button');
141+
expect(buttons.length).to.equal(0);
142+
});
143+
144+
it('should hide label visually when hide-label is set', async () => {
145+
const el = await fixture(
146+
html`<cds-fluid-number-input
147+
hide-label
148+
label="Hidden label"></cds-fluid-number-input>`
149+
);
150+
const label = el.shadowRoot.querySelector('label');
151+
const classList = label?.classList || [];
152+
expect(
153+
Array.from(classList).some((cls) => cls.includes('--visually-hidden'))
154+
).to.be.true;
155+
});
156+
157+
it('should respect autocomplete attribute', async () => {
158+
const el = await fixture(
159+
html`<cds-fluid-number-input
160+
autocomplete="on"
161+
label="Label"></cds-fluid-number-input>`
162+
);
163+
const input = el.shadowRoot.querySelector('input');
164+
expect(input.autocomplete).to.equal('on');
165+
});
166+
167+
// Checks native input event handling
168+
it('should emit input and blur events from inner input', async () => {
169+
const el = await fixture(
170+
html`<cds-fluid-number-input label="Label"></cds-fluid-number-input>`
171+
);
172+
const input = el.shadowRoot.querySelector('input');
173+
174+
let inputFired = false;
175+
let blurFired = false;
176+
177+
input.addEventListener('input', () => (inputFired = true));
178+
input.addEventListener('blur', () => (blurFired = true));
179+
180+
input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
181+
input.dispatchEvent(new Event('blur', { bubbles: true, composed: true }));
182+
183+
expect(inputFired).to.be.true;
184+
expect(blurFired).to.be.true;
185+
});
186+
187+
// Checks presence of basic ARIA attributes
188+
it('should have accessibility roles and attributes', async () => {
189+
const el = await fixture(
190+
html`<cds-fluid-number-input label="Label"></cds-fluid-number-input>`
191+
);
192+
const input = el.shadowRoot.querySelector('input');
193+
expect(input.getAttribute('role')).to.equal('alert');
194+
expect(input.getAttribute('aria-atomic')).to.equal('true');
195+
});
196+
197+
it('should disable step buttons when disabled', async () => {
198+
const el = await fixture(
199+
html`<cds-fluid-number-input
200+
disabled
201+
label="Label"></cds-fluid-number-input>`
202+
);
203+
const buttons = el.shadowRoot.querySelectorAll('button');
204+
buttons.forEach((btn) => expect(btn.disabled).to.be.true);
205+
});
206+
207+
it('should apply aria-labels to step buttons', async () => {
208+
const el = await fixture(
209+
html`<cds-fluid-number-input label="Label"></cds-fluid-number-input>`
210+
);
211+
const [decrement, increment] = el.shadowRoot.querySelectorAll('button');
212+
expect(decrement.getAttribute('aria-label')).to.equal(
213+
'decrease number input'
214+
);
215+
expect(increment.getAttribute('aria-label')).to.equal(
216+
'increase number input'
217+
);
218+
});
219+
220+
it('should render defaultValue when value is not set', async () => {
221+
const el = await fixture(
222+
html`<cds-fluid-number-input
223+
default-value="42"
224+
label="Label"></cds-fluid-number-input>`
225+
);
226+
const input = el.shadowRoot.querySelector('input');
227+
expect(input.value).to.equal('42');
228+
});
229+
230+
it('should apply custom assistive text for step buttons', async () => {
231+
const el = await fixture(
232+
html`<cds-fluid-number-input
233+
increment-button-assistive-text="More"
234+
decrement-button-assistive-text="Less"
235+
label="Label"></cds-fluid-number-input>`
236+
);
237+
const [decrement, increment] = el.shadowRoot.querySelectorAll('button');
238+
expect(decrement.getAttribute('aria-label')).to.equal('Less');
239+
expect(increment.getAttribute('aria-label')).to.equal('More');
240+
});
241+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 styles from './fluid-number-input.scss?lit';
12+
import CDSNumberInputSkeleton from '../number-input/number-input-skeleton';
13+
14+
/**
15+
* Fluid number input.
16+
*
17+
* @element cds-fluid-number-input-skeleton
18+
*/
19+
@customElement(`${prefix}-fluid-number-input-skeleton`)
20+
class CDSFluidNumberInputSkeleton extends CDSNumberInputSkeleton {
21+
render() {
22+
return html` ${super.render()} `;
23+
}
24+
25+
static styles = [CDSNumberInputSkeleton.styles, styles];
26+
}
27+
28+
export default CDSFluidNumberInputSkeleton;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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-number-input/index';
11+
@use '@carbon/styles/scss/components/fluid-text-input' as *;
12+
@use '@carbon/styles/scss/layout' as *;
13+
@use '@carbon/styles/scss/spacing' as *;
14+
@use '@carbon/styles/scss/theme';
15+
@use '@carbon/styles/scss/type' as *;
16+
@use '@carbon/styles/scss/utilities/skeleton' as *;
17+
@use '@carbon/styles/scss/components/number-input/index' as *;
18+
19+
:host(#{$prefix}-fluid-number-input) {
20+
@include emit-layout-tokens();
21+
}
22+
23+
:host(#{$prefix}-fluid-number-input-skeleton) {
24+
@extend .#{$prefix}--text-input--fluid__skeleton;
25+
26+
display: block;
27+
}

0 commit comments

Comments
 (0)