Skip to content

Commit ebfa052

Browse files
authored
fix(forms): Setting validation properties on a pristine non-dirty form (#1638)
* fix(forms): Setting validation properties on a pristine non-dirty form Setting validation properties on a pristine non-dirty form element does not apply invalid styles. Rewritten the synthetic test for invalidation on a single blur to follow the new behavior of form associated elements. Added tests for validation slots and removed some which are not really applicable. Some code refactoring in the component to follow the coding style guide. Closes #1632
1 parent 6bace83 commit ebfa052

File tree

8 files changed

+193
-156
lines changed

8 files changed

+193
-156
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
88
### Added
99
- New File Input Component(`igc-file-input`)
1010

11+
### Fixed
12+
- Setting validation properties on a pristine non-dirty form associated element does not apply invalid styles [#1632](https://github.com/IgniteUI/igniteui-webcomponents/issues/1632)
13+
1114
## [5.3.0] - 2025-03-13
1215
### Added
1316
- Tile manager component [#1402](https://github.com/IgniteUI/igniteui-webcomponents/pull/1402)

src/components/common/mixins/forms/associated.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ function BaseFormAssociated<T extends Constructor<LitElement>>(base: T) {
108108
}
109109

110110
private _setInvalidState(): void {
111-
if (this.hasUpdated || this._dirty) {
111+
if (this._dirty || !this._pristine) {
112112
this.invalid = !this.checkValidity();
113113
}
114114
}

src/components/common/utils.spec.ts

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -357,37 +357,6 @@ export function simulateWheel(node: Element, options?: WheelEventInit) {
357357
);
358358
}
359359

360-
/**
361-
* Simulates file upload for an input of type file.
362-
*
363-
* @param element - The custom element containing the file input
364-
* @param files - Array of File objects to upload
365-
* @param shadowRoot - Whether to look for the input in shadow DOM (default: true)
366-
* @returns Promise that resolves when element updates
367-
*/
368-
export async function simulateFileUpload(
369-
element: HTMLElement,
370-
files: File[],
371-
shadowRoot = true
372-
): Promise<void> {
373-
const input = shadowRoot
374-
? (element.shadowRoot!.querySelector(
375-
'input[type="file"]'
376-
) as HTMLInputElement)
377-
: (element.querySelector('input[type="file"]') as HTMLInputElement);
378-
379-
if (!input) {
380-
throw new Error('File input not found');
381-
}
382-
383-
const dataTransfer = new DataTransfer();
384-
files.forEach((file) => dataTransfer.items.add(file));
385-
386-
input.files = dataTransfer.files;
387-
input.dispatchEvent(new Event('change', { bubbles: true }));
388-
await elementUpdated(element);
389-
}
390-
391360
export function simulateDoubleClick(node: Element) {
392361
node.dispatchEvent(
393362
new PointerEvent('dblclick', { bubbles: true, composed: true })

src/components/file-input/file-input.spec.ts

Lines changed: 87 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { spy } from 'sinon';
33

44
import type { TemplateResult } from 'lit';
55
import { defineComponents } from '../common/definitions/defineComponents.js';
6+
import { first } from '../common/util.js';
67
import {
8+
type ValidationContainerTestsParams,
79
createFormAssociatedTestBed,
8-
simulateFileUpload,
10+
runValidationContainerTests,
911
} from '../common/utils.spec.js';
1012
import IgcFileInputComponent from './file-input.js';
1113

@@ -27,6 +29,10 @@ describe('File Input component', () => {
2729
input = element.renderRoot.querySelector('input')!;
2830
}
2931

32+
function getDOM(selector: string) {
33+
return element.renderRoot.querySelector(selector) as HTMLElement;
34+
}
35+
3036
describe('Properties', () => {
3137
it('sets the multiple property when input is of type file', async () => {
3238
await createFixture(html`<igc-file-input multiple></igc-file-input>`);
@@ -58,65 +64,61 @@ describe('File Input component', () => {
5864

5965
it('returns the uploaded files when input is of type file', async () => {
6066
await createFixture(html`<igc-file-input></igc-file-input>`);
61-
await simulateFileUpload(element, files);
6267

63-
expect(element.files).to.exist;
64-
expect(element.files!.length).to.equal(2);
65-
expect(element.files![0].name).to.equal('test.txt');
66-
expect(element.files![1].name).to.equal('image.png');
68+
simulateFileUpload(input, files);
69+
await elementUpdated(element);
70+
71+
expect(element.files).lengthOf(2);
72+
expect(element.files?.item(0)?.name).to.equal('test.txt');
73+
expect(element.files?.item(1)?.name).to.equal('image.png');
6774
});
6875

6976
it('should show placeholder text when no file is selected', async () => {
7077
await createFixture(html`<igc-file-input></igc-file-input>`);
7178

72-
expect(
73-
element
74-
.shadowRoot!.querySelector('[part="file-names"]')!
75-
.textContent!.trim()
76-
).to.equal('No file chosen');
79+
expect(getDOM('[part="file-names"]').innerText).to.equal(
80+
'No file chosen'
81+
);
7782

7883
element.placeholder = 'Select a document';
7984
await elementUpdated(element);
8085

81-
expect(
82-
element
83-
.shadowRoot!.querySelector('[part="file-names"]')!
84-
.textContent!.trim()
85-
).to.equal('Select a document');
86+
expect(getDOM('[part="file-names"]').innerText).to.equal(
87+
'Select a document'
88+
);
8689

87-
await simulateFileUpload(element, [files[0]]);
90+
element.focus();
91+
simulateFileUpload(input, [first(files)]);
92+
await elementUpdated(element);
8893

89-
expect(
90-
element
91-
.shadowRoot!.querySelector('[part="file-names"]')!
92-
.textContent!.trim()
93-
).to.equal('test.txt');
94+
expect(getDOM('[part="file-names"]').innerText).to.equal(
95+
first(files).name
96+
);
9497
});
9598

9699
it('resets the file selection when empty string is passed for value', async () => {
97-
const file = files[0];
98100
await createFixture(html`<igc-file-input></igc-file-input>`);
99-
await simulateFileUpload(element, [file]);
101+
const file = first(files);
102+
103+
simulateFileUpload(input, [file]);
104+
await elementUpdated(element);
100105

101106
expect(element.value).to.equal(`C:\\fakepath\\${file.name}`);
102-
expect(element.files!.length).to.equal(1);
103-
expect(element.files![0]).to.equal(file);
107+
expect(element.files).lengthOf(1);
108+
expect(element.files?.item(0)).to.equal(file);
104109

105110
element.value = '';
106-
expect(element.value).to.equal('');
107-
expect(element.files!.length).to.equal(0);
111+
expect(element.value).to.be.empty;
112+
expect(element.files).to.be.empty;
108113
});
109114
});
110115

111116
describe('File type layout', () => {
112117
it('renders publicly documented parts when the input is of type file', async () => {
113118
await createFixture(html`<igc-file-input></igc-file-input>`);
114119

115-
expect(
116-
element.shadowRoot!.querySelector('div[part="file-selector-button"]')
117-
).to.exist;
118-
expect(element.shadowRoot!.querySelector('div[part="file-names"]')).to
119-
.exist;
120+
expect(getDOM('div[part="file-selector-button"]')).is.not.null;
121+
expect(getDOM('div[part="file-names"]')).is.not.null;
120122
});
121123

122124
it('renders slotted contents when the input is of type file', async () => {
@@ -127,15 +129,17 @@ describe('File Input component', () => {
127129
</igc-file-input>
128130
`);
129131

130-
const selectorSlot = element.shadowRoot!.querySelector(
132+
const selectorSlot = getDOM(
131133
'slot[name="file-selector-text"]'
132134
) as HTMLSlotElement;
133-
const missingSlot = element.shadowRoot!.querySelector(
135+
const missingSlot = getDOM(
134136
'slot[name="file-missing-text"]'
135137
) as HTMLSlotElement;
136138

137-
expect(selectorSlot!.assignedNodes()[0].textContent).to.equal('Upload');
138-
expect(missingSlot!.assignedNodes()[0].textContent).to.equal(
139+
expect(first(selectorSlot.assignedNodes()).textContent).to.equal(
140+
'Upload'
141+
);
142+
expect(first(missingSlot.assignedNodes()).textContent).to.equal(
139143
'Choose a file'
140144
);
141145
});
@@ -150,7 +154,8 @@ describe('File Input component', () => {
150154
await createFixture(html`<igc-file-input></igc-file-input>`);
151155
const eventSpy = spy(element, 'emitEvent');
152156

153-
await simulateFileUpload(element, [files[0]]);
157+
simulateFileUpload(input, [first(files)]);
158+
await elementUpdated(element);
154159

155160
expect(eventSpy).calledWith('igcChange', {
156161
detail: element.value,
@@ -159,9 +164,6 @@ describe('File Input component', () => {
159164

160165
it('emits igcCancel', async () => {
161166
const eventSpy = spy(element, 'emitEvent');
162-
const input = element.shadowRoot!.querySelector(
163-
'input[type="file"]'
164-
) as HTMLInputElement;
165167

166168
input.dispatchEvent(new Event('cancel', { bubbles: true }));
167169
await elementUpdated(element);
@@ -171,76 +173,28 @@ describe('File Input component', () => {
171173
});
172174
});
173175

174-
it('should mark as dirty on focus', async () => {
175-
const input = element.shadowRoot!.querySelector(
176-
'input[type="file"]'
177-
) as HTMLInputElement;
178-
179-
input.dispatchEvent(new Event('focus', { bubbles: true }));
180-
await elementUpdated(element);
181-
182-
const eventSpy = spy(element as any, '_validate');
183-
input.dispatchEvent(new Event('blur', { bubbles: true }));
184-
185-
expect(eventSpy).called;
186-
});
187-
188-
it('should validate on blur', async () => {
176+
it('should update UI invalid state on blur when interacted', async () => {
189177
await createFixture(html`<igc-file-input required></igc-file-input>`);
190178

191-
const input = element.shadowRoot!.querySelector(
192-
'input[type="file"]'
193-
) as HTMLInputElement;
179+
// Internal invalid state, invalid UI is in initial state.
180+
expect(element.validity.valueMissing).to.be.true;
181+
expect(element.invalid).to.be.false;
194182

195-
input.dispatchEvent(new Event('blur', { bubbles: true }));
196-
await elementUpdated(element);
183+
// Internal invalid state, invalid UI is still in initial state.
184+
element.focus();
185+
expect(element.validity.valueMissing).to.be.true;
186+
expect(element.invalid).to.be.false;
197187

188+
// Internal invalid state, invalid UI is updated.
189+
element.blur();
190+
expect(element.validity.valueMissing).to.be.true;
198191
expect(element.invalid).to.be.true;
199192
});
200193
});
201194
});
202195

203-
describe('Form Validation', () => {
204-
const files = [
205-
new File(['test content'], 'test.txt', { type: 'text/plain' }),
206-
];
207-
const _expectedValidation = Symbol();
208-
209-
type TestBedInput = IgcFileInputComponent & {
210-
[_expectedValidation]: boolean;
211-
};
212-
213-
function validateInput(event: CustomEvent<string>) {
214-
const element = event.target as TestBedInput;
215-
expect(element.checkValidity()).to.equal(element[_expectedValidation]);
216-
}
217-
218-
function setExpectedValidationState(
219-
state: boolean,
220-
element: IgcFileInputComponent
221-
) {
222-
(element as TestBedInput)[_expectedValidation] = state;
223-
}
224-
225-
const spec = createFormAssociatedTestBed<IgcFileInputComponent>(
226-
html`<igc-file-input
227-
name="input"
228-
required
229-
@igcChange=${validateInput}
230-
></igc-file-input>`
231-
);
232-
233-
beforeEach(async () => {
234-
await spec.setup(IgcFileInputComponent.tagName);
235-
});
236-
237-
it('validates component', async () => {
238-
setExpectedValidationState(true, spec.element);
239-
await simulateFileUpload(spec.element, files);
240-
});
241-
});
242-
243196
describe('Form Integration', () => {
197+
let input: HTMLInputElement;
244198
const files = [
245199
new File(['test content'], 'test.txt', { type: 'text/plain' }),
246200
];
@@ -251,6 +205,7 @@ describe('Form Integration', () => {
251205

252206
beforeEach(async () => {
253207
await spec.setup(IgcFileInputComponent.tagName);
208+
input = spec.element.renderRoot.querySelector('input')!;
254209
});
255210

256211
it('correct initial state', () => {
@@ -267,12 +222,14 @@ describe('Form Integration', () => {
267222
});
268223

269224
it('is associated on submit', async () => {
270-
await simulateFileUpload(spec.element, files);
271-
spec.assertSubmitHasValue(files[0]);
225+
simulateFileUpload(input, files);
226+
await elementUpdated(spec.element);
227+
spec.assertSubmitHasValue(first(files));
272228
});
273229

274230
it('is correctly resets on form reset', async () => {
275-
await simulateFileUpload(spec.element, files);
231+
simulateFileUpload(input, files);
232+
await elementUpdated(spec.element);
276233
spec.reset();
277234

278235
expect(spec.element.value).to.be.empty;
@@ -289,7 +246,32 @@ describe('Form Integration', () => {
289246
it('fulfils required constraint', async () => {
290247
spec.assertSubmitFails();
291248

292-
await simulateFileUpload(spec.element, files);
249+
simulateFileUpload(input, files);
250+
await elementUpdated(spec.element);
251+
293252
spec.assertSubmitPasses();
294253
});
295254
});
255+
256+
describe('Validation message slots', () => {
257+
it('', async () => {
258+
const testParameters: ValidationContainerTestsParams<IgcFileInputComponent>[] =
259+
[
260+
{ slots: ['valueMissing'], props: { required: true } },
261+
{ slots: ['customError'] },
262+
];
263+
264+
runValidationContainerTests(IgcFileInputComponent, testParameters);
265+
});
266+
});
267+
268+
function simulateFileUpload(input: HTMLInputElement, files: File[]) {
269+
const dataTransfer = new DataTransfer();
270+
271+
for (const file of files) {
272+
dataTransfer.items.add(file);
273+
}
274+
275+
input.files = dataTransfer.files;
276+
input.dispatchEvent(new Event('change', { bubbles: true }));
277+
}

0 commit comments

Comments
 (0)