Skip to content

Commit 4d9d5d7

Browse files
SisIvanovasimeonoffChronosSF
authored
fix(form-controls): unchecked & indeterminate invalid state (#12887)
* fix(form-controls): unchecked & indeterminate invalid state --------- Co-authored-by: Simeon Simeonoff <[email protected]> Co-authored-by: Stamen Stoychev <[email protected]>
1 parent 91669a6 commit 4d9d5d7

File tree

11 files changed

+124
-68
lines changed

11 files changed

+124
-68
lines changed

projects/igniteui-angular/src/lib/checkbox/checkbox.component.spec.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -377,18 +377,27 @@ describe('IgxCheckbox', () => {
377377
fixture.detectChanges();
378378

379379
const checkbox = fixture.componentInstance.cb;
380-
checkbox.checked = false;
380+
const cbxEl = fixture.debugElement.query(By.directive(IgxCheckboxComponent)).nativeElement;
381381
expect(checkbox.required).toBe(true);
382+
expect(checkbox.invalid).toBe(false);
383+
expect(cbxEl.classList.contains('igx-checkbox--invalid')).toBe(false);
382384
expect(checkbox.nativeElement.getAttribute('aria-required')).toEqual('true');
383385
expect(checkbox.nativeElement.getAttribute('aria-invalid')).toEqual('false');
384386

385-
fixture.debugElement.componentInstance.markAsTouched();
386-
fixture.detectChanges();
387+
dispatchCbEvent('keyup', cbxEl, fixture);
388+
expect(checkbox.focused).toBe(true);
389+
dispatchCbEvent('blur', cbxEl, fixture);
387390

388-
const invalidCheckbox = fixture.debugElement.nativeElement.querySelectorAll(`.igx-checkbox--invalid`);
389-
expect(invalidCheckbox.length).toBe(1);
391+
expect(cbxEl.classList.contains('igx-checkbox--invalid')).toBe(true);
390392
expect(checkbox.invalid).toBe(true);
391393
expect(checkbox.nativeElement.getAttribute('aria-invalid')).toEqual('true');
394+
395+
checkbox.checked = true;
396+
fixture.detectChanges();
397+
398+
expect(cbxEl.classList.contains('igx-checkbox--invalid')).toBe(false);
399+
expect(checkbox.invalid).toBe(false);
400+
expect(checkbox.nativeElement.getAttribute('aria-invalid')).toEqual('false');
392401
});
393402

394403
describe('EditorProvider', () => {
@@ -488,17 +497,6 @@ class CheckboxFormGroupComponent {
488497
public myForm = this.fb.group({ checkbox: ['', Validators.required] });
489498

490499
constructor(private fb: UntypedFormBuilder) {}
491-
492-
public markAsTouched() {
493-
if (!this.myForm.valid) {
494-
for (const key in this.myForm.controls) {
495-
if (this.myForm.controls[key]) {
496-
this.myForm.controls[key].markAsTouched();
497-
this.myForm.controls[key].updateValueAndValidity();
498-
}
499-
}
500-
}
501-
}
502500
}
503501
@Component({
504502
template: `

projects/igniteui-angular/src/lib/checkbox/checkbox.component.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -578,14 +578,10 @@ export class IgxCheckboxComponent implements EditorProvider, AfterViewInit, Cont
578578
*/
579579
protected updateValidityState() {
580580
if (this.ngControl) {
581-
if (!this.disabled && !this.indeterminate && !this.readonly &&
581+
if (!this.disabled && !this.readonly &&
582582
(this.ngControl.control.touched || this.ngControl.control.dirty)) {
583583
// the control is not disabled and is touched or dirty
584-
if (this.checked) {
585-
this._invalid = this.ngControl.invalid;
586-
} else {
587-
this._invalid = this.required ? true : false;
588-
}
584+
this._invalid = this.ngControl.invalid;
589585
} else {
590586
// if the control is untouched, pristine, or disabled, its state is initial. This is when the user did not interact
591587
// with the checkbox or when the form/control is reset
@@ -604,7 +600,7 @@ export class IgxCheckboxComponent implements EditorProvider, AfterViewInit, Cont
604600
* @internal
605601
*/
606602
private checkNativeValidity() {
607-
if (!this.disabled && this._required && !this.checked && !this.indeterminate && !this.readonly) {
603+
if (!this.disabled && this._required && !this.checked && !this.readonly) {
608604
this._invalid = true;
609605
} else {
610606
this._invalid = false;

projects/igniteui-angular/src/lib/core/styles/components/checkbox/_checkbox-component.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@
9595
@include e(composite) {
9696
@extend %cbx-composite--x--invalid !optional;
9797
}
98+
99+
&:hover {
100+
@include e(composite) {
101+
@extend %cbx-composite--x--invalid--fluent !optional;
102+
}
103+
104+
@include e(composite-mark) {
105+
@extend %cbx-composite-mark--x--fluent !optional;
106+
}
107+
}
98108
}
99109

100110
@include m(focused) {
@@ -154,6 +164,10 @@
154164
}
155165
}
156166

167+
@include mx(invalid, indeterminate) {
168+
@extend %igx-checkbox--indeterminate--invalid !optional;
169+
}
170+
157171
@include mx(focused, indeterminate) {
158172
@extend %igx-checkbox--focused-checked !optional;
159173
}

projects/igniteui-angular/src/lib/core/styles/components/checkbox/_checkbox-theme.scss

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@
213213

214214
%cbx-composite--x {
215215
border-color: var-get($theme, 'fill-color');
216-
background: var-get($theme, 'fill-color');
217216

218217
&::after {
219218
background: var-get($theme, 'fill-color');
@@ -232,7 +231,6 @@
232231

233232
%cbx-composite--x--invalid {
234233
border-color: var-get($theme, 'error-color');
235-
background: var-get($theme, 'error-color');
236234

237235
&::after {
238236
background: var-get($theme, 'error-color');
@@ -242,14 +240,23 @@
242240
%cbx-composite--x--fluent {
243241
@if $variant == 'fluent' {
244242
border-color: var-get($theme, 'fill-color-hover');
245-
background: var-get($theme, 'fill-color-hover');
246243

247244
&::after {
248245
background: var-get($theme, 'fill-color-hover');
249246
}
250247
}
251248
}
252249

250+
%cbx-composite--x--invalid--fluent {
251+
@if $variant == 'fluent' {
252+
border-color: var-get($theme, 'error-color-hover');
253+
254+
&::after {
255+
background: var-get($theme, 'error-color-hover');
256+
}
257+
}
258+
}
259+
253260
%cbx-composite--disabled {
254261
border-color: var-get($theme, 'disabled-color');
255262
background: transparent;
@@ -351,6 +358,40 @@
351358
}
352359
}
353360

361+
%igx-checkbox--indeterminate--invalid {
362+
%cbx-composite--x {
363+
&::after {
364+
background: var-get($theme, 'error-color');
365+
}
366+
}
367+
368+
@if $variant == 'fluent' {
369+
%cbx-composite {
370+
border-color: var-get($theme, 'error-color');
371+
372+
&::before {
373+
border-color: var-get($theme, 'error-color');
374+
}
375+
}
376+
377+
%cbx-composite--x {
378+
&::after {
379+
background: transparent;
380+
}
381+
}
382+
383+
&:hover {
384+
%cbx-composite {
385+
border-color: var-get($theme, 'error-color-hover');
386+
387+
&::before {
388+
border-color: var-get($theme, 'error-color-hover');
389+
}
390+
}
391+
}
392+
}
393+
}
394+
354395
%cbx-composite-mark--x {
355396
stroke-dashoffset: 0;
356397
opacity: 1;

projects/igniteui-angular/src/lib/core/styles/components/radio/_radio-theme.scss

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,23 @@
489489
}
490490

491491
%igx-radio--focused--invalid--checked {
492+
%radio-composite {
493+
&::after {
494+
border: $border-width $border-style var-get($theme, 'error-color');
495+
}
496+
497+
&::before {
498+
background: var-get($theme, 'error-color');
499+
border-color: var-get($theme, 'error-color');
500+
}
501+
502+
@if $variant == 'bootstrap' {
503+
&::before {
504+
background: white;
505+
}
506+
}
507+
}
508+
492509
@if $variant == 'fluent' {
493510
%radio-composite {
494511
&::after {

projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,15 @@ describe('IgxRadioGroupDirective', () => {
231231
const domRadio = fixture.debugElement.query(By.css('igx-radio')).nativeElement;
232232
expect(domRadio.classList.contains('igx-radio--invalid')).toBe(false);
233233
expect(radioGroup.selected).toBeUndefined;
234+
expect(radioGroup.invalid).toBe(false);
234235

235236
dispatchRadioEvent('keyup', domRadio, fixture);
236237
expect(domRadio.classList.contains('igx-radio--focused')).toBe(true);
237238
dispatchRadioEvent('blur', domRadio, fixture);
238239
fixture.detectChanges();
239240
tick();
240241

242+
expect(radioGroup.invalid).toBe(true);
241243
expect(domRadio.classList.contains('igx-radio--invalid')).toBe(true);
242244

243245
dispatchRadioEvent('keyup', domRadio, fixture);
@@ -248,6 +250,7 @@ describe('IgxRadioGroupDirective', () => {
248250
tick();
249251

250252
expect(domRadio.classList.contains('igx-radio--checked')).toBe(true);
253+
expect(radioGroup.invalid).toBe(false);
251254
expect(radioGroup.radioButtons.first.checked).toEqual(true);
252255
expect(domRadio.classList.contains('igx-radio--invalid')).toBe(false);
253256
}));

projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,10 @@ export class IgxRadioGroupDirective implements AfterContentInit, AfterViewInit,
198198
@HostListener('click', ['$event'])
199199
protected handleClick(event: MouseEvent) {
200200
event.stopPropagation();
201-
this.selected.nativeElement.focus();
201+
202+
if (this.selected) {
203+
this.selected.nativeElement.focus();
204+
}
202205
}
203206

204207
@HostListener('keydown', ['$event'])
@@ -355,7 +358,7 @@ export class IgxRadioGroupDirective implements AfterContentInit, AfterViewInit,
355358

356359
if (this.radioButtons) {
357360
this.radioButtons.forEach((button) => {
358-
fromEvent(button.nativeElement, 'blur')
361+
button.blurRadio
359362
.pipe(takeUntil(this.destroy$))
360363
.subscribe(() => {
361364
this.updateValidityOnBlur()
@@ -377,12 +380,11 @@ export class IgxRadioGroupDirective implements AfterContentInit, AfterViewInit,
377380
private updateValidityOnBlur() {
378381
this.radioButtons.forEach((button) => {
379382
button.focused = false;
380-
});
381383

382-
if (this.required) {
383-
const checked = this.radioButtons.find(x => x.checked);
384-
this.invalid = !checked;
385-
}
384+
if (button.invalid) {
385+
this.invalid = true;
386+
}
387+
});
386388
}
387389

388390
/**
@@ -513,7 +515,9 @@ export class IgxRadioGroupDirective implements AfterContentInit, AfterViewInit,
513515
private _selectedRadioButtonChanged(args: IChangeRadioEventArgs) {
514516
this.radioButtons.forEach((button) => {
515517
button.checked = button.id === args.radio.id;
516-
if (button.checked) {
518+
if (button.checked && button.ngControl) {
519+
this.invalid = button.ngControl.invalid;
520+
} else if (button.checked) {
517521
this.invalid = false;
518522
}
519523
});

projects/igniteui-angular/src/lib/radio/radio.component.spec.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,7 @@ describe('IgxRadio', () => {
209209
radioInstance.deselect();
210210
fixture.detectChanges();
211211

212-
dispatchRadioEvent('keyup', domRadio, fixture);
213-
expect(domRadio.classList.contains('igx-radio--focused')).toBe(true);
214-
dispatchRadioEvent('blur', domRadio, fixture);
215-
216212
expect(radioInstance.checked).toBe(false);
217-
expect(radioInstance.invalid).toBe(true);
218-
expect(domRadio.classList.contains('igx-radio--invalid')).toBe(true);
219213
});
220214

221215
it('Should work properly with ngModel', fakeAsync(() => {

projects/igniteui-angular/src/lib/radio/radio.component.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ export class IgxRadioComponent implements AfterViewInit, ControlValueAccessor, E
271271
@Output() public readonly change: EventEmitter<IChangeRadioEventArgs> = new EventEmitter<IChangeRadioEventArgs>();
272272

273273
/** @hidden @internal */
274-
private blurRadio = new EventEmitter();
274+
public blurRadio = new EventEmitter();
275275

276276
/**
277277
* Returns the class of the radio component.
@@ -471,7 +471,6 @@ export class IgxRadioComponent implements AfterViewInit, ControlValueAccessor, E
471471
public select() {
472472
if(!this.checked) {
473473
this.checked = true;
474-
this.invalid = false;
475474
this.change.emit({ value: this.value, radio: this });
476475
this._onChangeCallback(this.value);
477476
}
@@ -566,11 +565,7 @@ export class IgxRadioComponent implements AfterViewInit, ControlValueAccessor, E
566565
if (this.ngControl) {
567566
if (!this.disabled && (this.ngControl.control.touched || this.ngControl.control.dirty)) {
568567
// the control is not disabled and is touched or dirty
569-
if (this.checked) {
570-
this._invalid = this.ngControl.invalid;
571-
} else {
572-
this._invalid = this.required ? true : false;
573-
}
568+
this._invalid = this.ngControl.invalid;
574569
} else {
575570
// if control is untouched, pristine, or disabled its state is initial. This is when user did not interact
576571
// with the radio or when form/control is reset

projects/igniteui-angular/src/lib/switch/switch.component.spec.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -269,18 +269,27 @@ describe('IgxSwitch', () => {
269269
fixture.detectChanges();
270270

271271
const switchEl = fixture.componentInstance.switch;
272-
switchEl.checked = false;
272+
const switchNative = fixture.debugElement.query(By.directive(IgxSwitchComponent)).nativeElement;
273273
expect(switchEl.required).toBe(true);
274+
expect(switchEl.invalid).toBe(false);
275+
expect(switchNative.classList.contains('igx-switch--invalid')).toBe(false);
274276
expect(switchEl.nativeElement.getAttribute('aria-required')).toEqual('true');
275277
expect(switchEl.nativeElement.getAttribute('aria-invalid')).toEqual('false');
276278

277-
fixture.debugElement.componentInstance.markAsTouched();
278-
fixture.detectChanges();
279+
dispatchCbEvent('keyup', switchNative, fixture);
280+
expect(switchEl.focused).toBe(true);
281+
dispatchCbEvent('blur', switchNative, fixture);
279282

280-
const invalidSwitch = fixture.debugElement.nativeElement.querySelectorAll(`.igx-switch--invalid`);
281-
expect(invalidSwitch.length).toBe(1);
283+
expect(switchNative.classList.contains('igx-switch--invalid')).toBe(true);
282284
expect(switchEl.invalid).toBe(true);
283285
expect(switchEl.nativeElement.getAttribute('aria-invalid')).toEqual('true');
286+
287+
switchEl.checked = true;
288+
fixture.detectChanges();
289+
290+
expect(switchNative.classList.contains('igx-switch--invalid')).toBe(false);
291+
expect(switchEl.invalid).toBe(false);
292+
expect(switchEl.nativeElement.getAttribute('aria-invalid')).toEqual('false');
284293
});
285294

286295
describe('EditorProvider', () => {
@@ -350,17 +359,6 @@ class SwitchFormGroupComponent {
350359
public myForm = this.fb.group({ switch: ['', Validators.required] });
351360

352361
constructor(private fb: UntypedFormBuilder) {}
353-
354-
public markAsTouched() {
355-
if (!this.myForm.valid) {
356-
for (const key in this.myForm.controls) {
357-
if (this.myForm.controls[key]) {
358-
this.myForm.controls[key].markAsTouched();
359-
this.myForm.controls[key].updateValueAndValidity();
360-
}
361-
}
362-
}
363-
}
364362
}
365363

366364
@Component({

0 commit comments

Comments
 (0)