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..2470a33053df 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; /** @@ -546,14 +551,8 @@ export class MatSlider extends _MatSliderMixinBase @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); - } + this._setDisabled(coerceBooleanProperty(v)); + this._updateInputsDisabledState(); } private _disabled: boolean = false; @@ -709,6 +708,28 @@ export class MatSlider extends _MatSliderMixinBase : this._foundation.setValue(value); } + /** 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, + // 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); + } + } + + /** 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; + } + } + } + /** Whether this is a ranged slider. */ _isRange(): boolean { return this._inputs.length === 2; @@ -716,7 +737,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); } /** Gets the slider thumb input of the given thumb position. */