From 215632cf9888599439848f600ddf524b11deb53e Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Thu, 22 Apr 2021 12:46:16 -0700 Subject: [PATCH 1/2] test(material-experimental/mdc-slider): add custom form control tests * fix bug where setting the disabled state on the overall slider was not disabling the individual slider thumbs control value accessor disabled state. --- .../mdc-slider/slider.spec.ts | 245 +++++++++++++++++- .../mdc-slider/slider.ts | 38 ++- 2 files changed, 270 insertions(+), 13 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 05cba21cc314..90e8bb709e4e 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -21,7 +21,7 @@ import { tick, waitForAsync, } from '@angular/core/testing'; -import {FormsModule} from '@angular/forms'; +import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; import {Thumb} from '@material/slider'; import {MatSliderModule} from './module'; @@ -43,7 +43,7 @@ describe('MDC-based MatSlider' , () => { function createComponent(component: Type): ComponentFixture { TestBed.configureTestingModule({ - imports: [FormsModule, MatSliderModule], + imports: [FormsModule, MatSliderModule, ReactiveFormsModule], declarations: [component], }).compileComponents(); return TestBed.createComponent(component); @@ -1283,6 +1283,225 @@ describe('MDC-based MatSlider' , () => { })); }); + describe('slider as a custom form control', () => { + let fixture: ComponentFixture; + let testComponent: SliderWithFormControl; + let sliderInstance: MatSlider; + let inputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + fixture = createComponent(SliderWithFormControl); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + inputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should not update the control when the value is updated', () => { + expect(testComponent.control.value).toBe(0); + inputInstance.value = 11; + fixture.detectChanges(); + expect(testComponent.control.value).toBe(0); + }); + + it('should update the control on mouseup', () => { + expect(testComponent.control.value).toBe(0); + setValueByClick(sliderInstance, 76, platform.IOS); + expect(testComponent.control.value).toBe(76); + }); + + it('should update the control on slide', () => { + expect(testComponent.control.value).toBe(0); + slideToValue(sliderInstance, 19, Thumb.END, platform.IOS); + expect(testComponent.control.value).toBe(19); + }); + + it('should update the value when the control is set', () => { + expect(inputInstance.value).toBe(0); + testComponent.control.setValue(7); + expect(inputInstance.value).toBe(7); + }); + + it('should update the disabled state when control is disabled', () => { + expect(sliderInstance.disabled).toBe(false); + testComponent.control.disable(); + expect(sliderInstance.disabled).toBe(true); + }); + + it('should update the disabled state when the control is enabled', () => { + sliderInstance.disabled = true; + testComponent.control.enable(); + expect(sliderInstance.disabled).toBe(false); + }); + + it('should have the correct control state initially and after interaction', () => { + let sliderControl = testComponent.control; + + // The control should start off valid, pristine, and untouched. + expect(sliderControl.valid).toBe(true); + expect(sliderControl.pristine).toBe(true); + expect(sliderControl.touched).toBe(false); + + // After changing the value, the control should become dirty (not pristine), + // but remain untouched. + setValueByClick(sliderInstance, 50, platform.IOS); + + expect(sliderControl.valid).toBe(true); + expect(sliderControl.pristine).toBe(false); + expect(sliderControl.touched).toBe(false); + + // If the control has been visited due to interaction, the control should remain + // dirty and now also be touched. + inputInstance.blur(); + fixture.detectChanges(); + + expect(sliderControl.valid).toBe(true); + expect(sliderControl.pristine).toBe(false); + expect(sliderControl.touched).toBe(true); + }); + }); + + describe('slider as a custom form control', () => { + let fixture: ComponentFixture; + let testComponent: RangeSliderWithFormControl; + let sliderInstance: MatSlider; + let startInputInstance: MatSliderThumb; + let endInputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + fixture = createComponent(RangeSliderWithFormControl); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + startInputInstance = sliderInstance._getInput(Thumb.START); + endInputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should not update the start input control when the value is updated', () => { + expect(testComponent.startInputControl.value).toBe(0); + startInputInstance.value = 11; + fixture.detectChanges(); + expect(testComponent.startInputControl.value).toBe(0); + }); + + it('should not update the end input control when the value is updated', () => { + expect(testComponent.endInputControl.value).toBe(100); + endInputInstance.value = 11; + fixture.detectChanges(); + expect(testComponent.endInputControl.value).toBe(100); + }); + + it('should update the start input control on mouseup', () => { + expect(testComponent.startInputControl.value).toBe(0); + setValueByClick(sliderInstance, 20, platform.IOS); + expect(testComponent.startInputControl.value).toBe(20); + }); + + it('should update the end input control on mouseup', () => { + expect(testComponent.endInputControl.value).toBe(100); + setValueByClick(sliderInstance, 80, platform.IOS); + expect(testComponent.endInputControl.value).toBe(80); + }); + + it('should update the start input control on slide', () => { + expect(testComponent.startInputControl.value).toBe(0); + slideToValue(sliderInstance, 20, Thumb.START, platform.IOS); + expect(testComponent.startInputControl.value).toBe(20); + }); + + it('should update the end input control on slide', () => { + expect(testComponent.endInputControl.value).toBe(100); + slideToValue(sliderInstance, 80, Thumb.END, platform.IOS); + expect(testComponent.endInputControl.value).toBe(80); + }); + + it('should update the start input value when the start input control is set', () => { + expect(startInputInstance.value).toBe(0); + testComponent.startInputControl.setValue(10); + expect(startInputInstance.value).toBe(10); + }); + + it('should update the end input value when the end input control is set', () => { + expect(endInputInstance.value).toBe(100); + testComponent.endInputControl.setValue(90); + expect(endInputInstance.value).toBe(90); + }); + + it('should update the disabled state if the start input control is disabled', () => { + expect(sliderInstance.disabled).toBe(false); + testComponent.startInputControl.disable(); + expect(sliderInstance.disabled).toBe(true); + }); + + it('should update the disabled state if the end input control is disabled', () => { + expect(sliderInstance.disabled).toBe(false); + testComponent.endInputControl.disable(); + expect(sliderInstance.disabled).toBe(true); + }); + + it('should update the disabled state when both input controls are enabled', () => { + sliderInstance.disabled = true; + testComponent.startInputControl.enable(); + expect(sliderInstance.disabled).toBe(true); + testComponent.endInputControl.enable(); + expect(sliderInstance.disabled).toBe(false); + }); + + it('should have the correct start input control state initially and after interaction', () => { + let sliderControl = testComponent.startInputControl; + + // The control should start off valid, pristine, and untouched. + expect(sliderControl.valid).toBe(true); + expect(sliderControl.pristine).toBe(true); + expect(sliderControl.touched).toBe(false); + + // After changing the value, the control should become dirty (not pristine), + // but remain untouched. + setValueByClick(sliderInstance, 25, platform.IOS); + + expect(sliderControl.valid).toBe(true); + expect(sliderControl.pristine).toBe(false); + expect(sliderControl.touched).toBe(false); + + // If the control has been visited due to interaction, the control should remain + // dirty and now also be touched. + startInputInstance.blur(); + fixture.detectChanges(); + + expect(sliderControl.valid).toBe(true); + expect(sliderControl.pristine).toBe(false); + expect(sliderControl.touched).toBe(true); + }); + + it('should have the correct start input control state initially and after interaction', () => { + let sliderControl = testComponent.endInputControl; + + // The control should start off valid, pristine, and untouched. + expect(sliderControl.valid).toBe(true); + expect(sliderControl.pristine).toBe(true); + expect(sliderControl.touched).toBe(false); + + // After changing the value, the control should become dirty (not pristine), + // but remain untouched. + setValueByClick(sliderInstance, 75, platform.IOS); + + expect(sliderControl.valid).toBe(true); + expect(sliderControl.pristine).toBe(false); + expect(sliderControl.touched).toBe(false); + + // If the control has been visited due to interaction, the control should remain + // dirty and now also be touched. + endInputInstance.blur(); + fixture.detectChanges(); + + expect(sliderControl.valid).toBe(true); + expect(sliderControl.pristine).toBe(false); + expect(sliderControl.touched).toBe(true); + }); + }); + describe('slider with a two-way binding', () => { let fixture: ComponentFixture; let testComponent: SliderWithTwoWayBinding; @@ -1559,6 +1778,28 @@ class RangeSliderWithNgModel { endVal: number | undefined = 100; } +@Component({ + template: ` + + + `, +}) +class SliderWithFormControl { + control = new FormControl(0); +} + +@Component({ + template: ` + + + + `, +}) +class RangeSliderWithFormControl { + startInputControl = new FormControl(0); + endInputControl = new FormControl(100); +} + @Component({ template: ` diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 4370d038bc0e..9b581438f2a8 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -335,6 +335,11 @@ export class MatSliderThumb implements AfterViewInit, ControlValueAccessor, OnIn /** Event emitted on each value change that happens to the slider. */ @Output() readonly input: EventEmitter = new EventEmitter(); + /** + * Used to determine the disabled state of the MatSlider (ControlValueAccessor). + * For ranged sliders, the disabled state of the MatSlider depends on the combined state of the + * start and end inputs. See MatSlider._updateDisabled. + */ _disabled: boolean = false; /** @@ -545,16 +550,7 @@ export class MatSlider extends _MatSliderMixinBase /** Whether the slider is disabled. */ @Input() get disabled(): boolean { return this._disabled; } - set disabled(v: boolean) { - this._disabled = coerceBooleanProperty(v); - - // If we want to disable the slider after the foundation has been initialized, - // we need to inform the foundation by calling `setDisabled`. Also, we can't call - // this before initializing the foundation because it will throw errors. - if (this._initialized) { - this._foundation.setDisabled(v); - } - } + set disabled(v: boolean) { this._setDisabled(coerceBooleanProperty(v)); } private _disabled: boolean = false; /** Whether the slider displays a numeric value label upon pressing the thumb. */ @@ -709,6 +705,25 @@ export class MatSlider extends _MatSliderMixinBase : this._foundation.setValue(value); } + _setDisabled(value: boolean, updateControlValueAccessor: boolean = true) { + this._disabled = value; + + // If we want to disable the slider after the foundation has been initialized, + // we need to inform the foundation by calling `setDisabled`. Also, we can't call + // this before initializing the foundation because it will throw errors. + if (this._initialized) { + this._foundation.setDisabled(value); + + if (updateControlValueAccessor) { + // Set the disabled state of the individual slider thumb(s) (ControlValueAccessor). + this._getInput(Thumb.END)._disabled = true; + if (this._isRange()) { + this._getInput(Thumb.START)._disabled = true; + } + } + } + } + /** Whether this is a ranged slider. */ _isRange(): boolean { return this._inputs.length === 2; @@ -716,7 +731,8 @@ export class MatSlider extends _MatSliderMixinBase /** Sets the disabled state based on the disabled state of the inputs (ControlValueAccessor). */ _updateDisabled(): void { - this.disabled = this._inputs.some(input => input._disabled); + const disabled = this._inputs.some(input => input._disabled); + this._setDisabled(disabled, false); } /** Gets the slider thumb input of the given thumb position. */ From ae11ff6a6912cbf863dce86c70782b05f9cc6c97 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Fri, 23 Apr 2021 07:17:37 -0700 Subject: [PATCH 2/2] fixup! test(material-experimental/mdc-slider): add custom form control tests --- .../mdc-slider/slider.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 9b581438f2a8..2470a33053df 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -550,7 +550,10 @@ export class MatSlider extends _MatSliderMixinBase /** Whether the slider is disabled. */ @Input() get disabled(): boolean { return this._disabled; } - set disabled(v: boolean) { this._setDisabled(coerceBooleanProperty(v)); } + set disabled(v: boolean) { + this._setDisabled(coerceBooleanProperty(v)); + this._updateInputsDisabledState(); + } private _disabled: boolean = false; /** Whether the slider displays a numeric value label upon pressing the thumb. */ @@ -705,7 +708,8 @@ export class MatSlider extends _MatSliderMixinBase : this._foundation.setValue(value); } - _setDisabled(value: boolean, updateControlValueAccessor: boolean = true) { + /** Sets the disabled state of the MatSlider. */ + private _setDisabled(value: boolean) { this._disabled = value; // If we want to disable the slider after the foundation has been initialized, @@ -713,13 +717,15 @@ export class MatSlider extends _MatSliderMixinBase // this before initializing the foundation because it will throw errors. if (this._initialized) { this._foundation.setDisabled(value); + } + } - if (updateControlValueAccessor) { - // Set the disabled state of the individual slider thumb(s) (ControlValueAccessor). - this._getInput(Thumb.END)._disabled = true; - if (this._isRange()) { - this._getInput(Thumb.START)._disabled = true; - } + /** Sets the disabled state of the individual slider thumb(s) (ControlValueAccessor). */ + private _updateInputsDisabledState() { + if (this._initialized) { + this._getInput(Thumb.END)._disabled = true; + if (this._isRange()) { + this._getInput(Thumb.START)._disabled = true; } } } @@ -732,7 +738,7 @@ export class MatSlider extends _MatSliderMixinBase /** Sets the disabled state based on the disabled state of the inputs (ControlValueAccessor). */ _updateDisabled(): void { const disabled = this._inputs.some(input => input._disabled); - this._setDisabled(disabled, false); + this._setDisabled(disabled); } /** Gets the slider thumb input of the given thumb position. */