Skip to content

Commit e95c942

Browse files
authored
Merge branch 'master' into rkaraivanov/tooltip
2 parents 76109f3 + ebfa052 commit e95c942

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
@@ -365,37 +365,6 @@ export function simulateWheel(node: Element, options?: WheelEventInit) {
365365
);
366366
}
367367

368-
/**
369-
* Simulates file upload for an input of type file.
370-
*
371-
* @param element - The custom element containing the file input
372-
* @param files - Array of File objects to upload
373-
* @param shadowRoot - Whether to look for the input in shadow DOM (default: true)
374-
* @returns Promise that resolves when element updates
375-
*/
376-
export async function simulateFileUpload(
377-
element: HTMLElement,
378-
files: File[],
379-
shadowRoot = true
380-
): Promise<void> {
381-
const input = shadowRoot
382-
? (element.shadowRoot!.querySelector(
383-
'input[type="file"]'
384-
) as HTMLInputElement)
385-
: (element.querySelector('input[type="file"]') as HTMLInputElement);
386-
387-
if (!input) {
388-
throw new Error('File input not found');
389-
}
390-
391-
const dataTransfer = new DataTransfer();
392-
files.forEach((file) => dataTransfer.items.add(file));
393-
394-
input.files = dataTransfer.files;
395-
input.dispatchEvent(new Event('change', { bubbles: true }));
396-
await elementUpdated(element);
397-
}
398-
399368
export function simulateDoubleClick(node: Element) {
400369
node.dispatchEvent(
401370
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)