From 1b9b5064b3c84fd65191fc4852939427d3d75c01 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Fri, 15 Jan 2021 11:36:59 -0800 Subject: [PATCH 01/35] feat(material-experimental/mdc-slider): delete old code * clearing the outdated code from mdc-slider * this will also make future commit messages easier to read --- src/dev-app/mdc-slider/mdc-slider-demo.html | 53 - src/dev-app/mdc-slider/mdc-slider-demo.ts | 8 +- .../mdc-slider/BUILD.bazel | 1 + .../mdc-slider/module.ts | 2 +- .../mdc-slider/public-api.ts | 4 +- .../mdc-slider/slider.e2e.spec.ts | 22 +- .../mdc-slider/slider.html | 1 - .../mdc-slider/slider.spec.ts | 1473 +---------------- .../mdc-slider/slider.ts | 516 +----- .../mdc-slider/testing/public-api.ts | 2 +- .../mdc-slider/testing/slider-harness.spec.ts | 24 +- .../mdc-slider/testing/slider-harness.ts | 129 +- 12 files changed, 48 insertions(+), 2187 deletions(-) diff --git a/src/dev-app/mdc-slider/mdc-slider-demo.html b/src/dev-app/mdc-slider/mdc-slider-demo.html index bb8e3de17870..e69de29bb2d1 100644 --- a/src/dev-app/mdc-slider/mdc-slider-demo.html +++ b/src/dev-app/mdc-slider/mdc-slider-demo.html @@ -1,53 +0,0 @@ -

Default Slider

-Label -{{slidey.value}} - -

Colors

- - - - -

Slider with Min and Max

- - - -{{slider2.value}} - - -

Disabled Slider

- - - - -

Slider with set value

- - -

Slider with step defined

- -{{slider5.value}} - -

Slider with set tick interval

- - - -

Slider with Thumb Label

- - -

Slider with one-way binding

- - - -

Slider with two-way binding

- - - -

Set/lost focus to show thumblabel programmatically

- - - - - - - - - diff --git a/src/dev-app/mdc-slider/mdc-slider-demo.ts b/src/dev-app/mdc-slider/mdc-slider-demo.ts index fab41d01fc41..f339793bbf02 100644 --- a/src/dev-app/mdc-slider/mdc-slider-demo.ts +++ b/src/dev-app/mdc-slider/mdc-slider-demo.ts @@ -13,10 +13,4 @@ import {Component} from '@angular/core'; selector: 'mdc-slider-demo', templateUrl: 'mdc-slider-demo.html', }) -export class MdcSliderDemo { - demo: number; - val: number = 50; - min: number = 0; - max: number = 100; - disabledValue = 0; -} +export class MdcSliderDemo {} diff --git a/src/material-experimental/mdc-slider/BUILD.bazel b/src/material-experimental/mdc-slider/BUILD.bazel index 5d1fe42b70c2..1e9289c09207 100644 --- a/src/material-experimental/mdc-slider/BUILD.bazel +++ b/src/material-experimental/mdc-slider/BUILD.bazel @@ -84,6 +84,7 @@ ng_e2e_test_library( name = "e2e_test_sources", srcs = glob(["**/*.e2e.spec.ts"]), deps = [ + ":mdc-slider", "//src/cdk/testing/private/e2e", ], ) diff --git a/src/material-experimental/mdc-slider/module.ts b/src/material-experimental/mdc-slider/module.ts index b77814532ae8..4b9a77e6d9ab 100644 --- a/src/material-experimental/mdc-slider/module.ts +++ b/src/material-experimental/mdc-slider/module.ts @@ -13,7 +13,7 @@ import {MatSlider} from './slider'; @NgModule({ imports: [MatCommonModule, CommonModule], - exports: [MatSlider, MatCommonModule], + exports: [MatSlider], declarations: [MatSlider], }) export class MatSliderModule { diff --git a/src/material-experimental/mdc-slider/public-api.ts b/src/material-experimental/mdc-slider/public-api.ts index e6307b9bf1c3..47d22e540254 100644 --- a/src/material-experimental/mdc-slider/public-api.ts +++ b/src/material-experimental/mdc-slider/public-api.ts @@ -6,5 +6,5 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './slider'; -export * from './module'; +export {MatSlider} from './slider'; +export {MatSliderModule} from './module'; diff --git a/src/material-experimental/mdc-slider/slider.e2e.spec.ts b/src/material-experimental/mdc-slider/slider.e2e.spec.ts index e82f8ebde4b4..fd79d7568fee 100644 --- a/src/material-experimental/mdc-slider/slider.e2e.spec.ts +++ b/src/material-experimental/mdc-slider/slider.e2e.spec.ts @@ -1,14 +1,16 @@ -import {browser, by, element} from 'protractor'; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ -// TODO: disabled until we implement the new MDC slider. -describe('mat-slider dummy' , () => it('', () => {})); +/* tslint:disable-next-line:no-unused-variable */ +import {MatSlider} from './index'; -// tslint:disable-next-line:ban -xdescribe('mat-slider', () => { - beforeEach(async () => await browser.get('/mdc-slider')); - - it('should show a slider', async () => { - expect(await element(by.tagName('mat-slider')).isPresent()).toBe(true); - }); +// TODO(wagnermaciel): Implement this in a separate PR +describe('MDC-based MatSlider' , () => { + it('does nothing yet', async () => {}); }); diff --git a/src/material-experimental/mdc-slider/slider.html b/src/material-experimental/mdc-slider/slider.html index f925043d4f5c..e69de29bb2d1 100644 --- a/src/material-experimental/mdc-slider/slider.html +++ b/src/material-experimental/mdc-slider/slider.html @@ -1 +0,0 @@ - diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 0d0398232c4a..e702f5243e95 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -1,1468 +1,19 @@ -import {BidiModule} from '@angular/cdk/bidi'; -import { - BACKSPACE, - DOWN_ARROW, - END, - HOME, - LEFT_ARROW, - PAGE_DOWN, - PAGE_UP, - RIGHT_ARROW, - UP_ARROW, -} from '@angular/cdk/keycodes'; -import {Platform} from '@angular/cdk/platform'; -import { - createKeyboardEvent, - createMouseEvent, - createPointerEvent, - dispatchEvent, - dispatchFakeEvent, - dispatchKeyboardEvent, - dispatchMouseEvent, -} from '@angular/cdk/testing/private'; -import {Component, DebugElement, Type, ViewChild} from '@angular/core'; -import {ComponentFixture, fakeAsync, flush, TestBed, tick, inject} from '@angular/core/testing'; -import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {By} from '@angular/platform-browser'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {MatSlider, MatSliderModule} from './index'; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ -// TODO: disabled until we implement the new MDC slider. -// TODO: once the tests are re-enabled, we should remove `mdc-slider` -// from the `skippedPackages` in `check-mdc-tests-config.ts`. -describe('MDC-based MatSlider dummy' , () => it('', () => {})); -// tslint:disable-next-line:ban -xdescribe('MDC-based MatSlider', () => { - function createComponent(component: Type): ComponentFixture { - TestBed.configureTestingModule({ - imports: [ - MatSliderModule, - ReactiveFormsModule, - FormsModule, - BidiModule, - NoopAnimationsModule, - ], - declarations: [component], - }).compileComponents(); +/* tslint:disable-next-line:no-unused-variable */ +import {MatSlider} from './index'; - return TestBed.createComponent(component); - } +// TODO(wagnermaciel): Implement this in a separate PR +describe('MDC-based MatSlider' , () => { describe('standard slider', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: MatSlider; - - beforeEach(() => { - fixture = createComponent(StandardSlider); - fixture.detectChanges(); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - sliderInstance = sliderDebugElement.componentInstance; - }); - - it('should set the default values', () => { - expect(sliderInstance.value).toBe(0); - expect(sliderInstance.min).toBe(0); - expect(sliderInstance.max).toBe(100); - }); - - it('should update the value on mousedown', () => { - expect(sliderInstance.value).toBe(0); - - dispatchMousedownEventSequence(sliderNativeElement, 0.19); - - expect(sliderInstance.value).toBe(19); - }); - - // TODO(devversion): MDC slider updates values with right mouse button. - // tslint:disable-next-line:ban - xit('should not update when pressing the right mouse button', () => { - expect(sliderInstance.value).toBe(0); - - dispatchMousedownEventSequence(sliderNativeElement, 0.19, 1); - - expect(sliderInstance.value).toBe(0); - }); - - it('should update the value on a slide', () => { - expect(sliderInstance.value).toBe(0); - - dispatchSlideEventSequence(sliderNativeElement, 0, 0.89); - - expect(sliderInstance.value).toBe(89); - }); - - it('should set the value as min when sliding before the track', () => { - expect(sliderInstance.value).toBe(0); - - dispatchSlideEventSequence(sliderNativeElement, 0, -1.33); - - expect(sliderInstance.value).toBe(0); - }); - - it('should set the value as max when sliding past the track', () => { - expect(sliderInstance.value).toBe(0); - - dispatchSlideEventSequence(sliderNativeElement, 0, 1.75); - - expect(sliderInstance.value).toBe(100); - }); - - it('should not change value without emitting a change event', () => { - const onChangeSpy = jasmine.createSpy('slider onChange'); - - sliderInstance.change.subscribe(onChangeSpy); - sliderInstance.value = 50; - fixture.detectChanges(); - - dispatchSlideEventSequence(sliderNativeElement, 0, 0.1); - - expect(onChangeSpy).toHaveBeenCalledTimes(1); - }); - - it('should have aria-orientation horizontal', () => { - expect(sliderNativeElement.getAttribute('aria-orientation')).toEqual('horizontal'); - }); - - it('should slide to the max value when the steps do not divide evenly into it', () => { - sliderInstance.min = 5; - sliderInstance.max = 100; - sliderInstance.step = 15; - - dispatchSlideEventSequence(sliderNativeElement, 0, 1); - fixture.detectChanges(); - - expect(sliderInstance.value).toBe(100); - }); - - it('should have a focus indicator', () => { - expect(sliderNativeElement.classList.contains('mat-mdc-focus-indicator')).toBe(true); - }); - - }); - - describe('disabled slider', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: MatSlider; - - beforeEach(() => { - fixture = createComponent(DisabledSlider); - fixture.detectChanges(); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - sliderInstance = sliderDebugElement.componentInstance; - }); - - it('should be disabled', () => { - expect(sliderInstance.disabled).toBeTruthy(); - }); - - it('should not change the value on mousedown when disabled', () => { - expect(sliderInstance.value).toBe(0); - - dispatchMousedownEventSequence(sliderNativeElement, 0.63); - - expect(sliderInstance.value).toBe(0); - }); - - it('should not change the value on slide when disabled', () => { - expect(sliderInstance.value).toBe(0); - - dispatchSlideEventSequence(sliderNativeElement, 0, 0.5); - - expect(sliderInstance.value).toBe(0); - }); - - it('should not emit change when disabled', () => { - const onChangeSpy = jasmine.createSpy('slider onChange'); - sliderInstance.change.subscribe(onChangeSpy); - - dispatchSlideEventSequence(sliderNativeElement, 0, 0.5); - - expect(onChangeSpy).toHaveBeenCalledTimes(0); - }); - - it('should not add the mat-slider-active class on mousedown when disabled', () => { - expect(sliderNativeElement.classList).not.toContain('mat-slider-active'); - - dispatchMousedownEventSequence(sliderNativeElement, 0.43); - fixture.detectChanges(); - - expect(sliderNativeElement.classList).not.toContain('mat-slider-active'); - }); - - it('should disable tabbing to the slider', inject([Platform], (platform: Platform) => { - expect(sliderNativeElement.hasAttribute('tabindex')).toBe(false); - // The "tabIndex" property returns an incorrect value in Edge 17. - // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4365703/ - if (!platform.EDGE) { - expect(sliderNativeElement.tabIndex).toBe(-1); - } - })); - }); - - describe('slider with set min and max', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: MatSlider; - let thumbContainerEl: HTMLElement; - - beforeEach(fakeAsync(() => { - fixture = createComponent(SliderWithMinAndMax); - fixture.detectChanges(); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - sliderInstance = sliderDebugElement.injector.get(MatSlider); - thumbContainerEl = sliderNativeElement - .querySelector('.mdc-slider__thumb-container'); - - // Flush the "requestAnimationFrame" timer that performs the initial - // rendering of the MDC slider. - flushRequestAnimationFrame(); - })); - - it('should set the default values from the attributes', () => { - expect(sliderInstance.value).toBe(4); - expect(sliderInstance.min).toBe(4); - expect(sliderInstance.max).toBe(6); - }); - - it('should set the correct value on mousedown', () => { - dispatchMousedownEventSequence(sliderNativeElement, 0.09); - fixture.detectChanges(); - - // Computed by multiplying the difference between the min and the max by the percentage from - // the mousedown and adding that to the minimum. - let value = Math.round(4 + (0.09 * (6 - 4))); - expect(sliderInstance.value).toBe(value); - }); - - it('should set the correct value on slide', () => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.62); - fixture.detectChanges(); - - // Computed by multiplying the difference between the min and the max by the percentage from - // the mousedown and adding that to the minimum. - let value = Math.round(4 + (0.62 * (6 - 4))); - expect(sliderInstance.value).toBe(value); - }); - - it('should snap the fill to the nearest value on mousedown', fakeAsync(() => { - dispatchMousedownEventSequence(sliderNativeElement, 0.68); - fixture.detectChanges(); - flushRequestAnimationFrame(); - - // The closest snap is halfway on the slider. - expect(thumbContainerEl.style.transform).toContain('translateX(50px)'); - })); - - it('should snap the fill to the nearest value on slide', fakeAsync(() => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.74); - fixture.detectChanges(); - flushRequestAnimationFrame(); - - // The closest snap is at the halfway point on the slider. - expect(thumbContainerEl.style.transform).toContain('translateX(50px)'); - })); - }); - - describe('slider with set value', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: MatSlider; - - beforeEach(() => { - fixture = createComponent(SliderWithValue); - fixture.detectChanges(); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - sliderInstance = sliderDebugElement.injector.get(MatSlider); - }); - - it('should set the default value from the attribute', () => { - expect(sliderInstance.value).toBe(26); - }); - - it('should set the correct value on mousedown', () => { - dispatchMousedownEventSequence(sliderNativeElement, 0.92); - fixture.detectChanges(); - - // On a slider with default max and min the value should be approximately equal to the - // percentage clicked. This should be the case regardless of what the original set value was. - expect(sliderInstance.value).toBe(92); - }); - - it('should set the correct value on slide', () => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.32); - fixture.detectChanges(); - - expect(sliderInstance.value).toBe(32); - }); - }); - - describe('slider with set step', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: MatSlider; - let thumbContainerEl: HTMLElement; - - beforeEach(fakeAsync(() => { - fixture = createComponent(SliderWithStep); - fixture.detectChanges(); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - sliderInstance = sliderDebugElement.injector.get(MatSlider); - thumbContainerEl = sliderNativeElement - .querySelector('.mdc-slider__thumb-container'); - - // Flush the "requestAnimationFrame" timer that performs the initial - // rendering of the MDC slider. - flushRequestAnimationFrame(); - })); - - it('should set the correct step value on mousedown', () => { - expect(sliderInstance.value).toBe(0); - - dispatchMousedownEventSequence(sliderNativeElement, 0.13); - fixture.detectChanges(); - - expect(sliderInstance.value).toBe(25); - }); - - it('should set the correct step value on keydown', () => { - expect(sliderInstance.value).toBe(0); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', UP_ARROW); - fixture.detectChanges(); - - expect(sliderInstance.value).toBe(25); - }); - - it('should snap the fill to a step on mousedown', fakeAsync(() => { - dispatchMousedownEventSequence(sliderNativeElement, 0.66); - fixture.detectChanges(); - flushRequestAnimationFrame(); - - // The closest step is at 75% of the slider. - expect(thumbContainerEl.style.transform).toContain('translateX(75px)'); - })); - - it('should set the correct step value on slide', () => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.07); - fixture.detectChanges(); - - expect(sliderInstance.value).toBe(0); - }); - - it('should snap the thumb and fill to a step on slide', fakeAsync(() => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.88); - fixture.detectChanges(); - flushRequestAnimationFrame(); - - // The closest snap is at the end of the slider. - expect(thumbContainerEl.style.transform).toContain('translateX(100px)'); - })); - - it('should not add decimals to the value if it is a whole number', () => { - fixture.componentInstance.step = 0.1; - fixture.detectChanges(); - - dispatchSlideEventSequence(sliderNativeElement, 0, 1); - - expect(sliderDebugElement.componentInstance.displayValue).toBe('100'); - }); - - // TODO(devversion): MDC slider does not support decimal steps. - // tslint:disable-next-line:ban - xit('should truncate long decimal values when using a decimal step and the arrow keys', () => { - fixture.componentInstance.step = 0.1; - fixture.detectChanges(); - - for (let i = 0; i < 3; i++) { - dispatchKeyboardEvent(sliderNativeElement, 'keydown', UP_ARROW); - } - - expect(sliderInstance.value).toBe(0.3); - }); - }); - - describe('slider with set tick interval', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let ticksContainerElement: HTMLElement; - - beforeEach(() => { - fixture = createComponent(SliderWithSetTickInterval); - fixture.detectChanges(); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - ticksContainerElement = - sliderNativeElement.querySelector('.mdc-slider__track-marker-container'); - }); - - it('should set the correct tick separation', () => { - const step = 3; - const tickInterval = fixture.componentInstance.tickInterval; - // Since the step value is set to "3", a slider with a maximum of 100 will have - // (100/3) visual steps. Of those visual steps, only each 6th (tickInterval) visual - // step will have a tick on the track. Resulting in ((100/3)/6) ticks on the track. - const sizeOfTick = (100 / step) / tickInterval; - // Similarly this equals to 18% of a 100px track as every 18th (3 * 6) - // pixel will be a tick. - const ticksPerTrackPercentage = (tickInterval * step); - // iOS evaluates the "background" expression for the ticks to the exact number, - // Firefox, Edge, Safari 12.1 evaluate to a percentage value. Chrome evaluates to - // a rounded five-digit decimal number and Safari 13.1 evaluates to a decimal - // representing the percentage. - const expectationRegex = new RegExp( - `(${sizeOfTick}|${ticksPerTrackPercentage}%|${sizeOfTick.toFixed(5)}|` + - `${ticksPerTrackPercentage / 100})`); - expect(ticksContainerElement.style.background) - .toMatch(expectationRegex); - }); - - it('should be able to reset the tick interval after it has been set', () => { - expect(sliderNativeElement.classList) - .toContain('mat-slider-has-ticks', 'Expected element to have ticks initially.'); - - fixture.componentInstance.tickInterval = 0; - fixture.detectChanges(); - - expect(sliderNativeElement.classList) - .not.toContain('mat-slider-has-ticks', 'Expected element not to have ticks after reset.'); - }); - }); - - describe('slider with thumb label', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: MatSlider; - let thumbLabelTextElement: Element; - - beforeEach(() => { - fixture = createComponent(SliderWithThumbLabel); - fixture.detectChanges(); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - sliderInstance = sliderDebugElement.componentInstance; - thumbLabelTextElement = sliderNativeElement.querySelector('.mdc-slider__pin-value-marker')!; - }); - - it('should add the thumb label class to the slider container', () => { - expect(sliderNativeElement.classList).toContain('mat-slider-thumb-label-showing'); - }); - - it('should update the thumb label text on mousedown', () => { - expect(thumbLabelTextElement.textContent).toBe('0'); - - dispatchMousedownEventSequence(sliderNativeElement, 0.13); - fixture.detectChanges(); - - // The thumb label text is set to the slider's value. These should always be the same. - expect(thumbLabelTextElement.textContent).toBe('13'); - }); - - it('should update the thumb label text on slide', () => { - expect(thumbLabelTextElement.textContent).toBe('0'); - - dispatchSlideEventSequence(sliderNativeElement, 0, 0.56); - fixture.detectChanges(); - - // The thumb label text is set to the slider's value. These should always be the same. - expect(thumbLabelTextElement.textContent).toBe(`${sliderInstance.value}`); - }); - }); - - describe('slider with custom thumb label formatting', () => { - let fixture: ComponentFixture; - let sliderNativeElement: HTMLElement; - let thumbLabelTextElement: Element; - - beforeEach(() => { - fixture = createComponent(SliderWithCustomThumbLabelFormatting); - fixture.detectChanges(); - - const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - thumbLabelTextElement = sliderNativeElement.querySelector('.mdc-slider__pin-value-marker')!; - }); - - it('should invoke the passed-in `displayWith` function with the value', () => { - spyOn(fixture.componentInstance, 'displayWith').and.callThrough(); - - dispatchMousedownEventSequence(sliderNativeElement, 0); - fixture.detectChanges(); - - expect(fixture.componentInstance.displayWith).toHaveBeenCalledWith(1); - }); - - // TODO(devversion): MDC does not refresh value pin if value changes programmatically. - // tslint:disable-next-line:ban - xit('should format the thumb label based on the passed-in `displayWith` function if value ' + - 'is updated through binding', () => { - fixture.componentInstance.value = 200000; - fixture.detectChanges(); - - expect(thumbLabelTextElement.textContent).toBe('200k'); - }); - - it('should format the thumb label based on the passed-in `displayWith` function', () => { - dispatchMousedownEventSequence(sliderNativeElement, 1); - fixture.detectChanges(); - - expect(thumbLabelTextElement.textContent).toBe('100k'); - }); - }); - - describe('slider with value property binding', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: MatSlider; - let testComponent: SliderWithOneWayBinding; - let thumbContainerEl: HTMLElement; - - beforeEach(fakeAsync(() => { - fixture = createComponent(SliderWithOneWayBinding); - fixture.detectChanges(); - - testComponent = fixture.debugElement.componentInstance; - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - sliderInstance = sliderDebugElement.injector.get(MatSlider); - thumbContainerEl = sliderNativeElement - .querySelector('.mdc-slider__thumb-container') as HTMLElement; - - // Flush the "requestAnimationFrame" timer that performs the initial - // rendering of the MDC slider. - flushRequestAnimationFrame(); - })); - - it('should initialize based on bound value', () => { - expect(sliderInstance.value).toBe(50); - expect(thumbContainerEl.style.transform).toContain('translateX(50px)'); - }); - - it('should update when bound value changes', fakeAsync(() => { - testComponent.val = 75; - fixture.detectChanges(); - flushRequestAnimationFrame(); - - expect(sliderInstance.value).toBe(75); - expect(thumbContainerEl.style.transform).toContain('translateX(75px)'); - })); - }); - - describe('slider with set min and max and a value smaller than min', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: MatSlider; - let thumbContainerEl: HTMLElement; - - beforeEach(fakeAsync(() => { - fixture = createComponent(SliderWithValueSmallerThanMin); - fixture.detectChanges(); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - sliderInstance = sliderDebugElement.componentInstance; - thumbContainerEl = sliderNativeElement - .querySelector('.mdc-slider__thumb-container'); - - // Flush the "requestAnimationFrame" timer that performs the initial - // rendering of the MDC slider. - flushRequestAnimationFrame(); - })); - - it('should set the value smaller than the min value', () => { - expect(sliderInstance.value).toBe(3); - expect(sliderInstance.min).toBe(4); - expect(sliderInstance.max).toBe(6); - }); - - it('should set the fill to the min value', () => { - expect(thumbContainerEl.style.transform).toContain('translateX(0px)'); - }); - }); - - describe('slider with set min and max and a value greater than max', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: MatSlider; - let thumbContainerEl: HTMLElement; - - beforeEach(fakeAsync(() => { - fixture = createComponent(SliderWithValueGreaterThanMax); - fixture.detectChanges(); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - sliderInstance = sliderDebugElement.componentInstance; - thumbContainerEl = sliderNativeElement - .querySelector('.mdc-slider__thumb-container'); - - // Flush the "requestAnimationFrame" timer that performs the initial - // rendering of the MDC slider. - flushRequestAnimationFrame(); - })); - - it('should set the value greater than the max value', () => { - expect(sliderInstance.value).toBe(7); - expect(sliderInstance.min).toBe(4); - expect(sliderInstance.max).toBe(6); - }); - - it('should set the fill to the max value', () => { - expect(thumbContainerEl.style.transform).toContain('translateX(100px)'); - }); - }); - - describe('slider with change handler', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let testComponent: SliderWithChangeHandler; - - beforeEach(() => { - fixture = createComponent(SliderWithChangeHandler); - fixture.detectChanges(); - - testComponent = fixture.debugElement.componentInstance; - spyOn(testComponent, 'onChange'); - spyOn(testComponent, 'onInput'); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - }); - - it('should emit change on mousedown', () => { - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchMousedownEventSequence(sliderNativeElement, 0.2); - fixture.detectChanges(); - - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - }); - - it('should emit change on slide', () => { - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchSlideEventSequence(sliderNativeElement, 0, 0.4); - fixture.detectChanges(); - - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - }); - - // TODO(devversion): MDC slider always emits change event on mouseup (regardless of value) - // Bug tracked with: https://github.com/material-components/material-components-web/issues/5018 - // tslint:disable-next-line:ban - xit('should not emit multiple changes for same value', () => { - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchMousedownEventSequence(sliderNativeElement, 0.6); - dispatchSlideEventSequence(sliderNativeElement, 0.6, 0.6); - fixture.detectChanges(); - - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - }); - - it('should dispatch events when changing back to previously emitted value after ' + - 'programmatically setting value', () => { - expect(testComponent.onChange).not.toHaveBeenCalled(); - expect(testComponent.onInput).not.toHaveBeenCalled(); - - dispatchMousedownEventSequence(sliderNativeElement, 0.2); - fixture.detectChanges(); - - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - expect(testComponent.onInput).toHaveBeenCalledTimes(1); - - testComponent.value = 0; - fixture.detectChanges(); - - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - expect(testComponent.onInput).toHaveBeenCalledTimes(1); - - dispatchMousedownEventSequence(sliderNativeElement, 0.2); - fixture.detectChanges(); - - expect(testComponent.onChange).toHaveBeenCalledTimes(2); - expect(testComponent.onInput).toHaveBeenCalledTimes(2); - }); - }); - - describe('slider with input event', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let testComponent: SliderWithChangeHandler; - - beforeEach(() => { - fixture = createComponent(SliderWithChangeHandler); - fixture.detectChanges(); - - testComponent = fixture.debugElement.componentInstance; - spyOn(testComponent, 'onInput'); - spyOn(testComponent, 'onChange'); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - }); - - it('should emit an input event while sliding', () => { - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchSliderMouseEvent(sliderNativeElement, 'down', 0); - dispatchSliderMouseEvent(sliderNativeElement, 'move', 0.5); - dispatchSliderMouseEvent(sliderNativeElement, 'move', 1); - dispatchSliderMouseEvent(sliderNativeElement, 'up', 1); - - fixture.detectChanges(); - - // The input event should fire twice, because the slider changed two times. - expect(testComponent.onInput).toHaveBeenCalledTimes(2); - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - }); - - it('should emit an input event when clicking', () => { - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchMousedownEventSequence(sliderNativeElement, 0.75); - fixture.detectChanges(); - - // The `onInput` event should be emitted once due to a single click. - expect(testComponent.onInput).toHaveBeenCalledTimes(1); - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - }); - - }); - - describe('slider with auto ticks', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let ticksContainerElement: HTMLElement; - - beforeEach(fakeAsync(() => { - fixture = createComponent(SliderWithAutoTickInterval); - fixture.detectChanges(); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - ticksContainerElement = - sliderNativeElement.querySelector('.mdc-slider__track-marker-container'); - - flushRequestAnimationFrame(); - })); - - it('should set the correct tick separation', () => { - expect(ticksContainerElement.style.background).toContain('30px'); - }); - }); - - describe('keyboard support', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let testComponent: SliderWithChangeHandler; - let sliderInstance: MatSlider; - - beforeEach(() => { - fixture = createComponent(SliderWithChangeHandler); - fixture.detectChanges(); - - testComponent = fixture.debugElement.componentInstance; - spyOn(testComponent, 'onInput'); - spyOn(testComponent, 'onChange'); - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - sliderInstance = sliderDebugElement.injector.get(MatSlider); - }); - - it('should increment slider by 1 on up arrow pressed', () => { - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', UP_ARROW); - fixture.detectChanges(); - - // The `onInput` event should be emitted once due to a single keyboard press. - expect(testComponent.onInput).toHaveBeenCalledTimes(1); - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - expect(sliderInstance.value).toBe(1); - }); - - it('should increment slider by 1 on right arrow pressed', () => { - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', RIGHT_ARROW); - fixture.detectChanges(); - - // The `onInput` event should be emitted once due to a single keyboard press. - expect(testComponent.onInput).toHaveBeenCalledTimes(1); - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - expect(sliderInstance.value).toBe(1); - }); - - it('should decrement slider by 1 on down arrow pressed', () => { - fixture.componentInstance.value = 100; - fixture.detectChanges(); - - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', DOWN_ARROW); - fixture.detectChanges(); - - // The `onInput` event should be emitted once due to a single keyboard press. - expect(testComponent.onInput).toHaveBeenCalledTimes(1); - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - expect(sliderInstance.value).toBe(99); - }); - - it('should decrement slider by 1 on left arrow pressed', () => { - fixture.componentInstance.value = 100; - fixture.detectChanges(); - - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', LEFT_ARROW); - fixture.detectChanges(); - - // The `onInput` event should be emitted once due to a single keyboard press. - expect(testComponent.onInput).toHaveBeenCalledTimes(1); - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - expect(sliderInstance.value).toBe(99); - }); - - // TODO(devversion): MDC increments the slider by "4" on page up. The standard - // Material slider increments by "10". - it('should increment slider by 4 on page up pressed', () => { - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', PAGE_UP); - fixture.detectChanges(); - - // The `onInput` event should be emitted once due to a single keyboard press. - expect(testComponent.onInput).toHaveBeenCalledTimes(1); - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - expect(sliderInstance.value).toBe(4); - }); - - // TODO(devversion): MDC decrements the slider by "4" on page up. The standard - // Material slider decrements by "10". - it('should decrement slider by 4 on page down pressed', () => { - fixture.componentInstance.value = 100; - fixture.detectChanges(); - - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', PAGE_DOWN); - fixture.detectChanges(); - - // The `onInput` event should be emitted once due to a single keyboard press. - expect(testComponent.onInput).toHaveBeenCalledTimes(1); - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - expect(sliderInstance.value).toBe(96); - }); - - it('should set slider to max on end pressed', () => { - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', END); - fixture.detectChanges(); - - // The `onInput` event should be emitted once due to a single keyboard press. - expect(testComponent.onInput).toHaveBeenCalledTimes(1); - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - expect(sliderInstance.value).toBe(100); - }); - - it('should set slider to min on home pressed', () => { - fixture.componentInstance.value = 100; - fixture.detectChanges(); - - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', HOME); - fixture.detectChanges(); - - // The `onInput` event should be emitted once due to a single keyboard press. - expect(testComponent.onInput).toHaveBeenCalledTimes(1); - expect(testComponent.onChange).toHaveBeenCalledTimes(1); - expect(sliderInstance.value).toBe(0); - }); - - it(`should take no action for presses of keys it doesn't care about`, () => { - fixture.componentInstance.value = 50; - fixture.detectChanges(); - - expect(testComponent.onChange).not.toHaveBeenCalled(); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', BACKSPACE); - fixture.detectChanges(); - - // The `onInput` event should be emitted once due to a single keyboard press. - expect(testComponent.onInput).not.toHaveBeenCalled(); - expect(testComponent.onChange).not.toHaveBeenCalled(); - expect(sliderInstance.value).toBe(50); - }); - - // TODO: MDC slider does not respect modifier keys. - // tslint:disable-next-line:ban - xit('should ignore events modifier keys', () => { - sliderInstance.value = 0; - - [ - UP_ARROW, DOWN_ARROW, RIGHT_ARROW, - LEFT_ARROW, PAGE_DOWN, PAGE_UP, HOME, END - ].forEach(key => { - const event = createKeyboardEvent('keydown', key, undefined, {alt: true}); - dispatchEvent(sliderNativeElement, event); - fixture.detectChanges(); - expect(event.defaultPrevented).toBe(false); - }); - - expect(testComponent.onInput).not.toHaveBeenCalled(); - expect(testComponent.onChange).not.toHaveBeenCalled(); - expect(sliderInstance.value).toBe(0); - }); - }); - - describe('slider with direction', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: MatSlider; - let testComponent: SliderWithDir; - - beforeEach(() => { - fixture = createComponent(SliderWithDir); - fixture.detectChanges(); - - testComponent = fixture.debugElement.componentInstance; - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderInstance = sliderDebugElement.injector.get(MatSlider); - sliderNativeElement = sliderDebugElement.nativeElement; - }); - - it('works in RTL languages', fakeAsync(() => { - testComponent.dir = 'rtl'; - fixture.detectChanges(); - flushRequestAnimationFrame(); - - dispatchMousedownEventSequence(sliderNativeElement, 0.3); - fixture.detectChanges(); - - expect(sliderInstance.value).toBe(70); - })); - - it('should re-render slider with updated style upon directionality change', fakeAsync(() => { - testComponent.dir = 'rtl'; - fixture.detectChanges(); - flushRequestAnimationFrame(); - - const thumbContainerEl = sliderNativeElement - .querySelector('.mdc-slider__thumb-container'); - - expect(thumbContainerEl.style.transform).toContain('translateX(100px)'); - - testComponent.dir = 'ltr'; - fixture.detectChanges(); - flushRequestAnimationFrame(); - - expect(thumbContainerEl.style.transform).toContain('translateX(0px)'); - })); - - it('should decrement RTL slider by 1 on right arrow pressed', () => { - testComponent.dir = 'rtl'; - testComponent.value = 100; - fixture.detectChanges(); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', RIGHT_ARROW); - fixture.detectChanges(); - - expect(sliderInstance.value).toBe(99); - }); - - it('should increment RTL slider by 1 on left arrow pressed', () => { - testComponent.dir = 'rtl'; - fixture.detectChanges(); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', LEFT_ARROW); - fixture.detectChanges(); - - expect(sliderInstance.value).toBe(1); - }); - }); - - describe('tabindex', () => { - - it('should allow setting the tabIndex through binding', () => { - const fixture = createComponent(SliderWithTabIndexBinding); - fixture.detectChanges(); - - const sliderNativeEl = fixture.debugElement.query(By.directive(MatSlider)).nativeElement; - expect(sliderNativeEl.tabIndex).toBe(0, 'Expected the tabIndex to be set to 0 by default.'); - - fixture.componentInstance.tabIndex = 3; - fixture.detectChanges(); - - expect(sliderNativeEl.tabIndex).toBe(3, 'Expected the tabIndex to have been changed.'); - }); - - it('should detect the native tabindex attribute', () => { - const fixture = createComponent(SliderWithNativeTabindexAttr); - fixture.detectChanges(); - - const slider = fixture.debugElement.query(By.directive(MatSlider)).componentInstance; - - expect(slider.tabIndex) - .toBe(5, 'Expected the tabIndex to be set to the value of the native attribute.'); - }); + it('does nothing yet', () => {}); }); - - describe('slider with ngModel', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let testComponent: SliderWithNgModel; - - beforeEach(() => { - fixture = createComponent(SliderWithNgModel); - fixture.detectChanges(); - - testComponent = fixture.debugElement.componentInstance; - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - }); - - it('should update the model on mousedown', () => { - expect(testComponent.val).toBe(0); - - dispatchMousedownEventSequence(sliderNativeElement, 0.76); - fixture.detectChanges(); - - expect(testComponent.val).toBe(76); - }); - - it('should update the model on slide', () => { - expect(testComponent.val).toBe(0); - - dispatchSlideEventSequence(sliderNativeElement, 0, 0.19); - fixture.detectChanges(); - - expect(testComponent.val).toBe(19); - }); - - it('should update the model on keydown', () => { - expect(testComponent.val).toBe(0); - - dispatchKeyboardEvent(sliderNativeElement, 'keydown', UP_ARROW); - fixture.detectChanges(); - - expect(testComponent.val).toBe(1); - }); - - it('should be able to reset a slider by setting the model back to undefined', fakeAsync(() => { - expect(testComponent.slider.value).toBe(0); - - testComponent.val = 5; - fixture.detectChanges(); - flush(); - - expect(testComponent.slider.value).toBe(5); - - testComponent.val = undefined; - fixture.detectChanges(); - flush(); - - expect(testComponent.slider.value).toBe(0); - })); - - }); - - describe('slider as a custom form control', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: MatSlider; - let testComponent: SliderWithFormControl; - - beforeEach(() => { - fixture = createComponent(SliderWithFormControl); - fixture.detectChanges(); - - testComponent = fixture.debugElement.componentInstance; - - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - sliderInstance = sliderDebugElement.injector.get(MatSlider); - }); - - it('should not update the control when the value is updated', () => { - expect(testComponent.control.value).toBe(0); - - sliderInstance.value = 11; - fixture.detectChanges(); - - expect(testComponent.control.value).toBe(0); - }); - - it('should update the control on mousedown', () => { - expect(testComponent.control.value).toBe(0); - - dispatchMousedownEventSequence(sliderNativeElement, 0.76); - fixture.detectChanges(); - - expect(testComponent.control.value).toBe(76); - }); - - it('should update the control on slide', () => { - expect(testComponent.control.value).toBe(0); - - dispatchSlideEventSequence(sliderNativeElement, 0, 0.19); - fixture.detectChanges(); - - expect(testComponent.control.value).toBe(19); - }); - - it('should update the value when the control is set', () => { - expect(sliderInstance.value).toBe(0); - - testComponent.control.setValue(7); - fixture.detectChanges(); - - expect(sliderInstance.value).toBe(7); - }); - - it('should update the disabled state when control is disabled', () => { - expect(sliderInstance.disabled).toBe(false); - - testComponent.control.disable(); - fixture.detectChanges(); - - expect(sliderInstance.disabled).toBe(true); - }); - - it('should update the disabled state when the control is enabled', () => { - sliderInstance.disabled = true; - - testComponent.control.enable(); - fixture.detectChanges(); - - 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. - dispatchMousedownEventSequence(sliderNativeElement, 0.5); - fixture.detectChanges(); - - 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. - dispatchFakeEvent(sliderNativeElement, '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; - let sliderNativeElement: HTMLElement; - - beforeEach(() => { - fixture = createComponent(SliderWithTwoWayBinding); - fixture.detectChanges(); - - testComponent = fixture.componentInstance; - let sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); - sliderNativeElement = sliderDebugElement.nativeElement; - }); - - it('should sync the value binding in both directions', () => { - expect(testComponent.value).toBe(0); - expect(testComponent.slider.value).toBe(0); - - dispatchMousedownEventSequence(sliderNativeElement, 0.1); - dispatchMouseEvent(sliderNativeElement, 'mouseup'); - fixture.detectChanges(); - - expect(testComponent.value).toBe(10); - expect(testComponent.slider.value).toBe(10); - - testComponent.value = 20; - fixture.detectChanges(); - - expect(testComponent.value).toBe(20); - expect(testComponent.slider.value).toBe(20); - }); - }); - }); - -function flushRequestAnimationFrame() { - // Flush the "requestAnimationFrame" timer that performs the rendering of - // the MDC slider. Zone uses 16ms for "requestAnimationFrame". - tick(16); -} - -// Disable animations and make the slider an even 100px, so that we get -// nice round values in tests. -const styles = ` - .mat-mdc-slider { - min-width: 100px !important; - width: 100px; - } -`; - -@Component({ - template: ``, - styles: [styles], -}) -class StandardSlider { } - -@Component({ - template: ``, - styles: [styles], -}) -class DisabledSlider { } - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithMinAndMax { - min = 4; - max = 6; -} - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithValue { } - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithStep { - step = 25; - max = 100; -} - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithAutoTickInterval { } - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithSetTickInterval { - tickInterval = 6; -} - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithThumbLabel { } - - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithCustomThumbLabelFormatting { - value = 0; - - displayWith(value: number | null) { - if (!value) { - return 0; - } - - if (value >= 1000) { - return (value / 1000) + 'k'; - } - - return value; - } -} - - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithOneWayBinding { - val = 50; -} - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithFormControl { - control = new FormControl(0); -} - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithNgModel { - @ViewChild(MatSlider) slider: MatSlider; - val: number | undefined = 0; -} - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithValueSmallerThanMin { } - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithValueGreaterThanMax { } - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithChangeHandler { - value = 0; - onChange() { } - onInput() { } - - @ViewChild(MatSlider) slider: MatSlider; -} - -@Component({ - template: `
`, - styles: [styles], -}) -class SliderWithDir { - value = 0; - dir = 'ltr'; -} - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithTabIndexBinding { - tabIndex: number; -} - -@Component({ - template: ``, - styles: [styles], -}) -class SliderWithNativeTabindexAttr { - tabIndex: number; -} - -@Component({ - template: '', - styles: [styles], -}) -class SliderWithTwoWayBinding { - @ViewChild(MatSlider) slider: MatSlider; - value = 0; -} - -/** - * Dispatches a mousedown event sequence (consisting of mousedown, mouseup) from an element. - * Note: The mouse event truncates the position for the event. - * @param sliderElement The mat-slider element from which the event will be dispatched. - * @param percentage The percentage of the slider where the event should occur. Used to find the - * physical location of the pointer. - * @param button Button that should be held down when starting to drag the slider. - */ -function dispatchMousedownEventSequence(sliderElement: HTMLElement, percentage: number, - button = 0): void { - dispatchSliderMouseEvent(sliderElement, 'down', percentage, button); - dispatchSliderMouseEvent(sliderElement, 'up', percentage, button); -} - -/** - * Dispatches a slide event sequence (consisting of slidestart, slide, slideend) from an element. - * @param sliderElement The mat-slider element from which the event will be dispatched. - * @param startPercent The percentage of the slider where the slide will begin. - * @param endPercent The percentage of the slider where the slide will end. - */ -function dispatchSlideEventSequence(sliderElement: HTMLElement, startPercent: number, - endPercent: number): void { - dispatchSliderMouseEvent(sliderElement, 'down', startPercent); - dispatchSliderMouseEvent(sliderElement, 'move', startPercent); - dispatchSliderMouseEvent(sliderElement, 'move', endPercent); - dispatchSliderMouseEvent(sliderElement, 'up', endPercent); -} - -/** - * Dispatches a mouse event from an element at a given position based on the percentage. - * @param sliderElement The mat-slider element from which the event will be dispatched. - * @param type Type of mouse interaction. - * @param percent The percentage of the slider where the event will happen. - * @param button Button that should be held for this event. - */ -function dispatchSliderMouseEvent(sliderElement: HTMLElement, type: 'up' | 'down' | 'move', - percent: number, button = 0): void { - const trackElement = sliderElement.querySelector('.mdc-slider__track-container')!; - const dimensions = trackElement.getBoundingClientRect(); - const clientX = dimensions.left + (dimensions.width * percent); - const clientY = dimensions.top + (dimensions.height * percent); - - // The latest versions of all browsers we support have the new `PointerEvent` API. - // Though since we capture the two most recent versions of these browsers, we also - // need to support Safari 12 at time of writing. Safari 12 does not have support for this, - // so we need to conditionally create and dispatch these events based on feature detection. - if (window.PointerEvent !== undefined) { - dispatchEvent(sliderElement, createPointerEvent(`pointer${type}`, clientX, clientY)); - } - dispatchEvent(sliderElement, createMouseEvent(`mouse${type}`, clientX, clientY, 0)); -} diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 024be32ab32b..9b457e7172d7 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -6,523 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directionality} from '@angular/cdk/bidi'; -import { - BooleanInput, - coerceBooleanProperty, - coerceNumberProperty, - NumberInput -} from '@angular/cdk/coercion'; -import {normalizePassiveListenerOptions, Platform} from '@angular/cdk/platform'; -import { - AfterViewInit, - Attribute, - ChangeDetectionStrategy, - Component, - ElementRef, - EventEmitter, - forwardRef, - Inject, - Input, - NgZone, - OnChanges, - OnDestroy, - Optional, - Output, - SimpleChanges, - ViewChild, - ViewEncapsulation -} from '@angular/core'; -import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; -import {ThemePalette} from '@angular/material-experimental/mdc-core'; -import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; -import {MDCSliderAdapter, MDCSliderFoundation, Thumb} from '@material/slider'; -import {Subscription} from 'rxjs'; - -/** - * Visually, a 30px separation between tick marks looks best. This is very subjective but it is - * the default separation we chose. - */ -const MIN_AUTO_TICK_SEPARATION = 30; - -/** - * Size of a tick marker for a slider. The size of a tick is based on the Material - * Design guidelines and the MDC slider implementation. - * TODO(devversion): ideally MDC would expose the tick marker size as constant - */ -const TICK_MARKER_SIZE = 2; - -// TODO: disabled until we implement the new MDC slider. -/** Event options used to bind passive listeners. */ -// tslint:disable-next-line:no-unused-variable -const passiveListenerOptions = normalizePassiveListenerOptions({passive: true}); - -// TODO: disabled until we implement the new MDC slider. -/** Event options used to bind active listeners. */ -// tslint:disable-next-line:no-unused-variable -const activeListenerOptions = normalizePassiveListenerOptions({passive: false}); - -/** - * Provider Expression that allows mat-slider to register as a ControlValueAccessor. - * This allows it to support [(ngModel)] and [formControl]. - * @docs-private - */ -export const MAT_SLIDER_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MatSlider), - multi: true -}; - -/** A simple change event emitted by the MatSlider component. */ -export class MatSliderChange { - /** The MatSlider that changed. */ - source: MatSlider; - - /** The new value of the source slider. */ - value: number; -} +import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; @Component({ selector: 'mat-slider', templateUrl: 'slider.html', styleUrls: ['slider.css'], - host: { - 'class': 'mat-mdc-slider mdc-slider mat-mdc-focus-indicator', - 'role': 'slider', - 'aria-orientation': 'horizontal', - // The tabindex if the slider turns disabled is managed by the MDC foundation which - // dynamically updates and restores the "tabindex" attribute. - '[attr.tabindex]': 'tabIndex || 0', - '[class.mdc-slider--discrete]': 'thumbLabel', - '[class.mat-slider-has-ticks]': 'tickInterval !== 0', - '[class.mdc-slider--display-markers]': 'tickInterval !== 0', - '[class.mat-slider-thumb-label-showing]': 'thumbLabel', - // Class binding which is only used by the test harness as there is no other - // way for the harness to detect if mouse coordinates need to be inverted. - '[class.mat-slider-invert-mouse-coords]': '_isRtl()', - '[class.mat-slider-disabled]': 'disabled', - '[class.mat-primary]': 'color == "primary"', - '[class.mat-accent]': 'color == "accent"', - '[class.mat-warn]': 'color == "warn"', - '[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"', - '(blur)': '_markAsTouched()', - }, - exportAs: 'matSlider', - encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, - providers: [MAT_SLIDER_VALUE_ACCESSOR], + encapsulation: ViewEncapsulation.None, }) -export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlValueAccessor { - /** Event emitted when the slider value has changed. */ - @Output() readonly change: EventEmitter = new EventEmitter(); - - /** Event emitted when the slider thumb moves. */ - @Output() readonly input: EventEmitter = new EventEmitter(); - - /** - * Emits when the raw value of the slider changes. This is here primarily - * to facilitate the two-way binding for the `value` input. - * @docs-private - */ - @Output() readonly valueChange: EventEmitter = new EventEmitter(); - - /** Tabindex for the slider. */ - @Input() tabIndex: number = 0; - - /** The color palette for this slider. */ - @Input() color: ThemePalette = 'accent'; - - /** - * Function that will be used to format the value before it is displayed - * in the thumb label. Can be used to format very large number in order - * for them to fit into the slider thumb. - */ - @Input() displayWith: (value: number) => string | number; - - /** The minimum value that the slider can have. */ - @Input() - get min(): number { - return this._min; - } - set min(value: number) { - this._min = coerceNumberProperty(value); - } - private _min = 0; - - /** The maximum value that the slider can have. */ - @Input() - get max(): number { - return this._max; - } - set max(value: number) { - this._max = coerceNumberProperty(value); - } - private _max = 100; - - /** Value of the slider. */ - @Input() - get value(): number|null { - // If the value needs to be read and it is still uninitialized, initialize - // it to the current minimum value. - if (this._value === null) { - this.value = this.min; - } - return this._value; - } - set value(value: number|null) { - this._value = coerceNumberProperty(value); - } - private _value: number|null = null; - - /** The values at which the thumb will snap. */ - @Input() - get step(): number { - return this._step; - } - set step(v: number) { - this._step = coerceNumberProperty(v, this._step); - } - private _step: number = 1; - - /** - * How often to show ticks. Relative to the step so that a tick always appears on a step. - * Ex: Tick interval of 4 with a step of 3 will draw a tick every 4 steps (every 12 values). - */ - @Input() - get tickInterval() { - return this._tickInterval; - } - set tickInterval(value: number|'auto') { - if (value === 'auto') { - this._tickInterval = 'auto'; - } else if (typeof value === 'number' || typeof value === 'string') { - this._tickInterval = coerceNumberProperty(value, this._tickInterval); - } else { - this._tickInterval = 0; - } - } - private _tickInterval: number|'auto' = 0; - - /** Whether or not to show the thumb label. */ - @Input() - get thumbLabel(): boolean { - return this._thumbLabel; - } - set thumbLabel(value: boolean) { - this._thumbLabel = coerceBooleanProperty(value); - } - private _thumbLabel: boolean = false; - - /** Whether the slider is disabled. */ - @Input() - get disabled(): boolean { - return this._disabled; - } - set disabled(disabled) { - this._disabled = coerceBooleanProperty(disabled); - } - private _disabled = false; - - /** Adapter for the MDC slider foundation. */ - private _sliderAdapter: MDCSliderAdapter = { - hasClass: (_className: string) => false, - addClass: (_className: string) => {}, - removeClass: (_className: string) => {}, - getAttribute: (_attribute: string) => null, - addThumbClass: (_className: string, _thumb: Thumb) => {}, - removeThumbClass: (_className: string, _thumb: Thumb) => {}, - getThumbKnobWidth: (_thumb: Thumb) => 0, - getThumbBoundingClientRect: (_thumb: Thumb) => null!, - getBoundingClientRect: () => null!, - isRTL: () => false, - setThumbStyleProperty: (_propertyName: string, _value: string, _thumb: Thumb) => {}, - removeThumbStyleProperty: (_propertyName: string, _thumb: Thumb) => {}, - setTrackActiveStyleProperty: (_propertyName: string, _value: string) => {}, - setValueIndicatorText: (_value: number, _thumb: Thumb) => {}, - updateTickMarks: () => {}, - setPointerCapture: (_pointerId: number) => {}, - emitChangeEvent: (_value: number, _thumb: Thumb) => {}, - emitInputEvent: (_value: number, _thumb: Thumb) => {}, - registerEventHandler: () => {}, - deregisterEventHandler: () => {}, - registerThumbEventHandler: () => {}, - deregisterThumbEventHandler: () => {}, - registerBodyEventHandler: () => {}, - deregisterBodyEventHandler: () => {}, - registerWindowEventHandler: () => {}, - deregisterWindowEventHandler: () => {}, - removeTrackActiveStyleProperty: (_propertyName: string) => {}, - emitDragStartEvent: (_value: number, _thumb: Thumb) => {}, - emitDragEndEvent: (_value: number, _thumb: Thumb) => {}, - getValueToAriaValueTextFn: () => null, - getInputValue: () => '', - setInputValue: (_value: string, _thumb: Thumb) => {}, - getInputAttribute: (_attribute: string, _thumb: Thumb) => null, - setInputAttribute: (_attribute: string, _value: string) => {}, - removeInputAttribute: (_attribute: string) => {}, - focusInput: () => {}, - isInputFocused: (_thumb: Thumb) => false, - registerInputEventHandler: (_thumb: Thumb, _evtType: string, _handler: any) => {}, - deregisterInputEventHandler: (_thumb: Thumb, _evtType: string, _handler: any) => {}, - }; - - /** Instance of the MDC slider foundation for this slider. */ - private _foundation = new MDCSliderFoundation(this._sliderAdapter); - - /** Whether the MDC foundation has been initialized. */ - private _isInitialized = false; - - /** Function that notifies the control value accessor about a value change. */ - private _controlValueAccessorChangeFn: (value: number) => void = () => {}; - - /** Subscription to the Directionality change EventEmitter. */ - private _dirChangeSubscription = Subscription.EMPTY; - - /** Function that marks the slider as touched. Registered via "registerOnTouch". */ - _markAsTouched: () => any = () => {}; - - @ViewChild('thumbContainer') _thumbContainer: ElementRef; - @ViewChild('track') _track: ElementRef; - @ViewChild('pinValueMarker') _pinValueMarker: ElementRef; - @ViewChild('trackMarker') _trackMarker: ElementRef; - - constructor( - private _elementRef: ElementRef, - private _ngZone: NgZone, - private _platform: Platform, - @Optional() private _dir: Directionality, - @Attribute('tabindex') tabIndex: string, - @Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) { - this.tabIndex = parseInt(tabIndex) || 0; - - if (this._dir) { - this._dirChangeSubscription = this._dir.change.subscribe(() => { - // In case the directionality changes, we need to refresh the rendered MDC slider. - // Note that we need to wait until the page actually updated as otherwise the - // client rectangle wouldn't reflect the new directionality. - // TODO(devversion): ideally the MDC slider would just compute dimensions similarly - // to the standard Material slider on "mouseenter". - this._ngZone.runOutsideAngular(() => setTimeout(() => this._foundation.layout())); - }); - } - } - - ngAfterViewInit() { - this._isInitialized = true; - - if (this._platform.isBrowser) { - // The MDC slider foundation accesses DOM globals, so we cannot initialize the - // foundation on the server. The foundation would be needed to move the thumb - // to the proper position and to render the ticks. - // this._foundation.init(); - - // The standard Angular Material slider is always using discrete values. We always - // want to enable discrete values and support ticks, but want to still provide - // non-discrete slider visual looks if thumb label is disabled. - // TODO(devversion): check if we can get a public API for this. - // Tracked with: https://github.com/material-components/material-components-web/issues/5020 - (this._foundation as any).isDiscrete_ = true; - - // These bindings cannot be synced in the foundation, as the foundation is not - // initialized and they cause DOM globals to be accessed (to move the thumb) - this._syncStep(); - this._syncMax(); - this._syncMin(); - - // Note that "value" needs to be synced after "max" and "min" because otherwise - // the value will be clamped by the MDC foundation implementation. - this._syncValue(); - } - - this._syncDisabled(); - } - - ngOnChanges(changes: SimpleChanges) { - if (!this._isInitialized) { - return; - } - - if (changes['step']) { - this._syncStep(); - } - if (changes['max']) { - this._syncMax(); - } - if (changes['min']) { - this._syncMin(); - } - if (changes['disabled']) { - this._syncDisabled(); - } - if (changes['value']) { - this._syncValue(); - } - if (changes['tickInterval']) { - this._refreshTrackMarkers(); - } - } - - ngOnDestroy() { - this._dirChangeSubscription.unsubscribe(); - // The foundation cannot be destroyed on the server, as the foundation - // has not be initialized on the server. - if (this._platform.isBrowser) { - this._foundation.destroy(); - } - } - - /** Focuses the slider. */ - focus(options?: FocusOptions) { - this._elementRef.nativeElement.focus(options); - } - - /** Blurs the slider. */ - blur() { - this._elementRef.nativeElement.blur(); - } - - /** Gets the display text of the current value. */ - get displayValue() { - if (this.displayWith) { - return this.displayWith(this.value!).toString(); - } - return this.value!.toString() || '0'; - } - - /** Creates a slider change object from the specified value. */ - private _createChangeEvent(newValue: number): MatSliderChange { - const event = new MatSliderChange(); - event.source = this; - event.value = newValue; - return event; - } - - // TODO: disabled until we implement the new MDC slider. - /** Emits a change event and notifies the control value accessor. */ - // tslint:disable-next-line:no-unused-variable - private _emitChangeEvent(newValue: number) { - this._controlValueAccessorChangeFn(newValue); - this.valueChange.emit(newValue); - this.change.emit(this._createChangeEvent(newValue)); - } - - // TODO: disabled until we implement the new MDC slider. - /** Computes the CSS background value for the track markers (aka ticks). */ - // tslint:disable-next-line:no-unused-variable - private _getTrackMarkersBackground(min: number, max: number, step: number) { - if (!this.tickInterval) { - return ''; - } - - const markerWidth = `${TICK_MARKER_SIZE}px`; - const markerBackground = - `linear-gradient(to right, currentColor ${markerWidth}, transparent 0)`; - - if (this.tickInterval === 'auto') { - const trackSize = this._elementRef.nativeElement.getBoundingClientRect().width; - const pixelsPerStep = trackSize * step / (max - min); - const stepsPerTick = Math.ceil(MIN_AUTO_TICK_SEPARATION / pixelsPerStep); - const pixelsPerTick = stepsPerTick * step; - return `${markerBackground} 0 center / ${pixelsPerTick}px 100% repeat-x`; - } - - // keep calculation in css for better rounding/subpixel behavior - const markerAmount = `(((${max} - ${min}) / ${step}) / ${this.tickInterval})`; - const markerBkgdLayout = - `0 center / calc((100% - ${markerWidth}) / ${markerAmount}) 100% repeat-x`; - return `${markerBackground} ${markerBkgdLayout}`; - } - - /** Method that ensures that track markers are refreshed. */ - private _refreshTrackMarkers() { - // MDC only checks whether the slider has markers once on init by looking for the - // `mdc-slider--display-markers` class in the DOM, whereas we support changing and hiding - // the markers dynamically. This is a workaround until we can get a public API for it. See: - // https://github.com/material-components/material-components-web/issues/5020 - (this._foundation as any).hasTrackMarker_ = this.tickInterval !== 0; - - // TODO: disabled until we implement the new MDC slider. - // this._foundation.setupTrackMarker(); - } - - /** Syncs the "step" input value with the MDC foundation. */ - private _syncStep() { - // TODO: disabled until we implement the new MDC slider. - // this._foundation.setStep(this.step); - } - - /** Syncs the "max" input value with the MDC foundation. */ - private _syncMax() { - // TODO: disabled until we implement the new MDC slider. - // this._foundation.setMax(this.max); - } - - /** Syncs the "min" input value with the MDC foundation. */ - private _syncMin() { - // TODO: disabled until we implement the new MDC slider. - // this._foundation.setMin(this.min); - } - - /** Syncs the "value" input binding with the MDC foundation. */ - private _syncValue() { - // TODO: disabled until we implement the new MDC slider. - // this._foundation.setValue(this.value!); - } - - /** Syncs the "disabled" input value with the MDC foundation. */ - private _syncDisabled() { - // TODO: disabled until we implement the new MDC slider. - // this._foundation.setDisabled(this.disabled); - } - - /** Whether the slider is displayed in RTL-mode. */ - _isRtl(): boolean { - return this._dir && this._dir.value === 'rtl'; - } - - /** - * Registers a callback to be triggered when the value has changed. - * Implemented as part of ControlValueAccessor. - * @param fn Callback to be registered. - */ - registerOnChange(fn: any) { - this._controlValueAccessorChangeFn = fn; - } - - /** - * Registers a callback to be triggered when the component is touched. - * Implemented as part of ControlValueAccessor. - * @param fn Callback to be registered. - */ - registerOnTouched(fn: any) { - this._markAsTouched = fn; - } - - /** - * Sets whether the component should be disabled. - * Implemented as part of ControlValueAccessor. - * @param isDisabled - */ - setDisabledState(isDisabled: boolean) { - this.disabled = isDisabled; - this._syncDisabled(); - } - - /** - * Sets the model value. - * Implemented as part of ControlValueAccessor. - * @param value - */ - writeValue(value: any) { - this.value = value; - this._syncValue(); - } - - static ngAcceptInputType_min: NumberInput; - static ngAcceptInputType_max: NumberInput; - static ngAcceptInputType_value: NumberInput; - static ngAcceptInputType_step: NumberInput; - static ngAcceptInputType_tickInterval: NumberInput; - static ngAcceptInputType_thumbLabel: BooleanInput; - static ngAcceptInputType_disabled: BooleanInput; -} +export class MatSlider {} diff --git a/src/material-experimental/mdc-slider/testing/public-api.ts b/src/material-experimental/mdc-slider/testing/public-api.ts index e26601667fe1..3b5b997b0d34 100644 --- a/src/material-experimental/mdc-slider/testing/public-api.ts +++ b/src/material-experimental/mdc-slider/testing/public-api.ts @@ -6,5 +6,5 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './slider-harness'; export {SliderHarnessFilters} from '@angular/material/slider/testing'; +export {MatSliderHarness} from './slider-harness'; diff --git a/src/material-experimental/mdc-slider/testing/slider-harness.spec.ts b/src/material-experimental/mdc-slider/testing/slider-harness.spec.ts index 7738add5be2a..cbc3d0bf18d0 100644 --- a/src/material-experimental/mdc-slider/testing/slider-harness.spec.ts +++ b/src/material-experimental/mdc-slider/testing/slider-harness.spec.ts @@ -1,14 +1,16 @@ -import {runHarnessTests} from '@angular/material/slider/testing/shared.spec'; -import {MatSliderModule} from '../index'; -import {MatSliderHarness} from './slider-harness'; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ -// TODO: disabled until we implement the new MDC slider. -describe('MDC-based MatSliderHarness dummy' , () => it('', () => {})); +/* tslint:disable-next-line:no-unused-variable */ +import {MatSlider} from '../index'; -// tslint:disable-next-line:ban -xdescribe('MDC-based MatSliderHarness', () => { - runHarnessTests(MatSliderModule, MatSliderHarness as any, { - supportsVertical: false, - supportsInvert: false, - }); +// TODO(wagnermaciel): Implement this in a separate PR + +describe('MDC-based MatSliderHarness', () => { + it('does nothing yet', () => {}); }); diff --git a/src/material-experimental/mdc-slider/testing/slider-harness.ts b/src/material-experimental/mdc-slider/testing/slider-harness.ts index 6b32d54cde53..02ab59c0288d 100644 --- a/src/material-experimental/mdc-slider/testing/slider-harness.ts +++ b/src/material-experimental/mdc-slider/testing/slider-harness.ts @@ -6,134 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion'; -import {ComponentHarness, HarnessPredicate, parallel} from '@angular/cdk/testing'; -import {SliderHarnessFilters} from '@angular/material/slider/testing'; +import {ComponentHarness} from '@angular/cdk/testing'; /** Harness for interacting with a MDC mat-slider in tests. */ export class MatSliderHarness extends ComponentHarness { - static hostSelector = '.mat-mdc-slider'; - - /** - * Gets a `HarnessPredicate` that can be used to search for a mat-slider with - * specific attributes. - * @param options Options for narrowing the search: - * - `selector` finds a slider whose host element matches the given selector. - * - `id` finds a slider with specific id. - * @return a `HarnessPredicate` configured with the given options. - */ - static with(options: SliderHarnessFilters = {}): HarnessPredicate { - return new HarnessPredicate(MatSliderHarness, options); - } - - private _textLabel = this.locatorForOptional('.mdc-slider__pin-value-marker'); - private _trackContainer = this.locatorFor('.mdc-slider__track-container'); - - /** Gets the slider's id. */ - async getId(): Promise { - const id = await (await this.host()).getProperty('id'); - // In case no id has been specified, the "id" property always returns - // an empty string. To make this method more explicit, we return null. - return id !== '' ? id : null; - } - - /** - * Gets the current display value of the slider. Returns null if the thumb - * label is disabled. - */ - async getDisplayValue(): Promise { - const textLabelEl = await this._textLabel(); - return textLabelEl ? textLabelEl.text() : null; - } - - /** Gets the current percentage value of the slider. */ - async getPercentage(): Promise { - return this._calculatePercentage(await this.getValue()); - } - - /** Gets the current value of the slider. */ - async getValue(): Promise { - return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuenow')); - } - - /** Gets the maximum value of the slider. */ - async getMaxValue(): Promise { - return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuemax')); - } - - /** Gets the minimum value of the slider. */ - async getMinValue(): Promise { - return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuemin')); - } - - /** Whether the slider is disabled. */ - async isDisabled(): Promise { - const disabled = (await this.host()).getAttribute('aria-disabled'); - return coerceBooleanProperty(await disabled); - } - - /** Gets the orientation of the slider. */ - async getOrientation(): Promise<'horizontal'> { - // "aria-orientation" will always be set to "horizontal" for the MDC - // slider as there is no vertical slider support yet. - return (await this.host()).getAttribute('aria-orientation') as Promise<'horizontal'>; - } - - /** - * Sets the value of the slider by clicking on the slider track. - * - * Note that in rare cases the value cannot be set to the exact specified value. This - * can happen if not every value of the slider maps to a single pixel that could be - * clicked using mouse interaction. In such cases consider using the keyboard to - * select the given value or expand the slider's size for a better user experience. - */ - async setValue(value: number): Promise { - // Need to wait for async tasks outside Angular to complete. This is necessary because - // whenever directionality changes, the slider updates the element dimensions in the next - // tick (in a timer outside of the NgZone). Since this method relies on the element - // dimensions to be updated, we wait for the delayed calculation task to complete. - await this.waitForTasksOutsideAngular(); - - const [sliderEl, trackContainer] = - await parallel(() => [this.host(), this._trackContainer()]); - let percentage = await this._calculatePercentage(value); - const {width} = await trackContainer.getDimensions(); - - // In case the slider is displayed in RTL mode, we need to invert the - // percentage so that the proper value is set. - if (await sliderEl.hasClass('mat-slider-invert-mouse-coords')) { - percentage = 1 - percentage; - } - - // We need to round the new coordinates because creating fake DOM - // events will cause the coordinates to be rounded down. - await sliderEl.click(Math.round(width * percentage), 0); - } - - /** - * Focuses the slider and returns a void promise that indicates when the - * action is complete. - */ - async focus(): Promise { - return (await this.host()).focus(); - } - - /** - * Blurs the slider and returns a void promise that indicates when the - * action is complete. - */ - async blur(): Promise { - return (await this.host()).blur(); - } - - /** Whether the slider is focused. */ - async isFocused(): Promise { - return (await this.host()).isFocused(); - } - - /** Calculates the percentage of the given value. */ - private async _calculatePercentage(value: number) { - const [min, max] = await parallel(() => [this.getMinValue(), this.getMaxValue()]); - return (value - min) / (max - min); - } + // TODO(wagnermaciel): Implement this in a separate PR } From e55ee1d03dc9b0b8d5cea22be922fec8ab646605 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Fri, 15 Jan 2021 11:59:17 -0800 Subject: [PATCH 02/35] build: add mdc-slider to the mdc exports configs skipped packages --- scripts/check-mdc-exports-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-mdc-exports-config.ts b/scripts/check-mdc-exports-config.ts index 5e66c55a612c..aa7559c5e717 100644 --- a/scripts/check-mdc-exports-config.ts +++ b/scripts/check-mdc-exports-config.ts @@ -1,6 +1,6 @@ export const config = { // The MDC sidenav hasn't been implemented yet. - skippedPackages: ['mdc-sidenav'], + skippedPackages: ['mdc-sidenav', 'mdc-slider'], skippedExports: { 'mdc-chips': [ // These components haven't been implemented for MDC due to a different accessibility pattern. From 3a6d0b3b7ea1cd199ea794e0bac3311edd8f0a51 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Wed, 20 Jan 2021 16:51:43 -0500 Subject: [PATCH 03/35] feat(material-experimental/mdc-slider): add skeleton code for MatSliderAdapter (#21645) * create slider-adapter.ts * add method stubs for MDCSliderAdapter implementation * add MDCSliderFoundation class variable to MatSlider --- .../mdc-slider/slider-adapter.ts | 138 ++++++++++++++++++ .../mdc-slider/slider.ts | 20 ++- 2 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 src/material-experimental/mdc-slider/slider-adapter.ts diff --git a/src/material-experimental/mdc-slider/slider-adapter.ts b/src/material-experimental/mdc-slider/slider-adapter.ts new file mode 100644 index 000000000000..a3144d73f0d6 --- /dev/null +++ b/src/material-experimental/mdc-slider/slider-adapter.ts @@ -0,0 +1,138 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {SpecificEventListener, EventType} from '@material/base'; +import {MDCSliderAdapter, Thumb, TickMark} from '@material/slider'; + +export class SliderAdapter implements MDCSliderAdapter { + hasClass(className: string): boolean { + throw Error('Method not implemented.'); + } + addClass(className: string): void { + throw Error('Method not implemented.'); + } + removeClass(className: string): void { + throw Error('Method not implemented.'); + } + getAttribute(attribute: string): string | null { + throw Error('Method not implemented.'); + } + addThumbClass(className: string, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + removeThumbClass(className: string, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + getInputValue(thumb: Thumb): string { + throw Error('Method not implemented.'); + } + setInputValue(value: string, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + getInputAttribute(attribute: string, thumb: Thumb): string | null { + throw Error('Method not implemented.'); + } + setInputAttribute(attribute: string, value: string, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + removeInputAttribute(attribute: string, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + focusInput(thumb: Thumb): void { + throw Error('Method not implemented.'); + } + isInputFocused(thumb: Thumb): boolean { + throw Error('Method not implemented.'); + } + getThumbKnobWidth(thumb: Thumb): number { + throw Error('Method not implemented.'); + } + getThumbBoundingClientRect(thumb: Thumb): ClientRect { + throw Error('Method not implemented.'); + } + getBoundingClientRect(): ClientRect { + throw Error('Method not implemented.'); + } + isRTL(): boolean { + throw Error('Method not implemented.'); + } + setThumbStyleProperty(propertyName: string, value: string, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + removeThumbStyleProperty(propertyName: string, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + setTrackActiveStyleProperty(propertyName: string, value: string): void { + throw Error('Method not implemented.'); + } + removeTrackActiveStyleProperty(propertyName: string): void { + throw Error('Method not implemented.'); + } + setValueIndicatorText(value: number, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + getValueToAriaValueTextFn(): ((value: number) => string) | null { + throw Error('Method not implemented.'); + } + updateTickMarks(tickMarks: TickMark[]): void { + throw Error('Method not implemented.'); + } + setPointerCapture(pointerId: number): void { + throw Error('Method not implemented.'); + } + emitChangeEvent(value: number, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + emitInputEvent(value: number, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + emitDragStartEvent(value: number, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + emitDragEndEvent(value: number, thumb: Thumb): void { + throw Error('Method not implemented.'); + } + registerEventHandler(evtType: K, handler: SpecificEventListener): void { + throw Error('Method not implemented.'); + } + deregisterEventHandler(evtType: K, handler: SpecificEventListener): void { + throw Error('Method not implemented.'); + } + registerThumbEventHandler + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { + throw Error('Method not implemented.'); + } + deregisterThumbEventHandler + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { + throw Error('Method not implemented.'); + } + registerInputEventHandler + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { + throw Error('Method not implemented.'); + } + deregisterInputEventHandler + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { + throw Error('Method not implemented.'); + } + registerBodyEventHandler + (evtType: K, handler: SpecificEventListener): void { + throw Error('Method not implemented.'); + } + deregisterBodyEventHandler + (evtType: K, handler: SpecificEventListener): void { + throw Error('Method not implemented.'); + } + registerWindowEventHandler + (evtType: K, handler: SpecificEventListener): void { + throw Error('Method not implemented.'); + } + deregisterWindowEventHandler + (evtType: K, handler: SpecificEventListener): void { + throw Error('Method not implemented.'); + } +} diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 9b457e7172d7..b82a61f60447 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -6,8 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ViewEncapsulation, +} from '@angular/core'; +import {MDCSliderFoundation} from '@material/slider'; +import {SliderAdapter} from './slider-adapter'; +/** + * Allows users to select from a range of values by moving the slider thumb. It is similar in + * behavior to the native `` element. + */ @Component({ selector: 'mat-slider', templateUrl: 'slider.html', @@ -15,4 +25,10 @@ import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/co changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) -export class MatSlider {} +export class MatSlider { + + /** Instance of the MDC slider foundation for this slider. */ + + // tslint:disable-next-line:no-unused-variable + private _foundation = new MDCSliderFoundation(new SliderAdapter()); +} From fcb6d933807c151e39f118811bb86d45924ea0a1 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Thu, 21 Jan 2021 12:29:08 -0500 Subject: [PATCH 04/35] feat(material-experimental/mdc-slider): add skeleton code for MatSliderThumb (#21655) * created slider-thumb.ts * created MatSliderThumb directive for the mdc-slider input --- .../mdc-slider/module.ts | 6 +- .../mdc-slider/public-api.ts | 1 + .../mdc-slider/slider-thumb.ts | 80 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/material-experimental/mdc-slider/slider-thumb.ts diff --git a/src/material-experimental/mdc-slider/module.ts b/src/material-experimental/mdc-slider/module.ts index 4b9a77e6d9ab..6604a90985f3 100644 --- a/src/material-experimental/mdc-slider/module.ts +++ b/src/material-experimental/mdc-slider/module.ts @@ -10,11 +10,15 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import {MatCommonModule} from '@angular/material-experimental/mdc-core'; import {MatSlider} from './slider'; +import {MatSliderThumb} from './slider-thumb'; @NgModule({ imports: [MatCommonModule, CommonModule], exports: [MatSlider], - declarations: [MatSlider], + declarations: [ + MatSlider, + MatSliderThumb, + ], }) export class MatSliderModule { } diff --git a/src/material-experimental/mdc-slider/public-api.ts b/src/material-experimental/mdc-slider/public-api.ts index 47d22e540254..ee39eb6b8adf 100644 --- a/src/material-experimental/mdc-slider/public-api.ts +++ b/src/material-experimental/mdc-slider/public-api.ts @@ -7,4 +7,5 @@ */ export {MatSlider} from './slider'; +export {MatSliderThumb} from './slider-thumb'; export {MatSliderModule} from './module'; diff --git a/src/material-experimental/mdc-slider/slider-thumb.ts b/src/material-experimental/mdc-slider/slider-thumb.ts new file mode 100644 index 000000000000..6c1f7bb2ea1c --- /dev/null +++ b/src/material-experimental/mdc-slider/slider-thumb.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NumberInput} from '@angular/cdk/coercion'; +import {DOCUMENT} from '@angular/common'; +import {Directive, ElementRef, EventEmitter, Inject, Input, Output} from '@angular/core'; +import {Thumb} from '@material/slider'; + +/** + * The native input used by the MatSlider. + */ +@Directive({ + selector: 'input[mat-slider-thumb]', + host: { + 'class': 'mdc-slider__input', + 'type': 'range', + '[min]': 'min', + '[max]': 'max', + '[step]': 'step', + '[attr.value]': 'value', + '(blur)': '_blur.emit()', + '(focus)': '_focus.emit()', + } +}) export class MatSliderThumb { + /** The current value of this slider input. */ + @Input() + get value(): number { return this._value; } + set value(v: number) { this._value = v; } + private _value: number; + + /** The minimum value that this slider input can have. */ + @Input() + get min(): number { return 0; } + set min(v: number) { throw Error('Invalid attribute "min" on MatSliderThumb.'); } + + /** The maximum value that this slider input can have. */ + @Input() + get max(): number { return 100; } + set max(v: number) { throw Error('Invalid attribute "max" on MatSliderThumb.'); } + + /** The size of each increment between the values of the slider. */ + @Input() + get step(): number { return 1; } + set step(v: number) { throw Error('Invalid attribute "step" on MatSliderThumb.'); } + + /** MDC Slider does not use the disabled attribute it's native inputs. */ + @Input() + set disabled(v: boolean) { throw Error('Invalid attribute "disabled" on MatSliderThumb.'); } + + /** Event emitted every time the MatSliderThumb is blurred. */ + @Output() readonly _blur: EventEmitter = new EventEmitter(); + + /** Event emitted every time the MatSliderThumb is focused. */ + @Output() readonly _focus: EventEmitter = new EventEmitter(); + + /** Indicates which slider thumb this input corresponds to. */ + thumb: Thumb; + + constructor( + @Inject(DOCUMENT) private readonly _document: Document, + private readonly _elementRef: ElementRef, + ) {} + + /** Returns the hosts native HTML element. */ + _getHostElement(): HTMLInputElement { + return this._elementRef.nativeElement; + } + + /** Returns true if this slider input currently has focus. */ + _isFocused(): boolean { + return this._document.activeElement === this._getHostElement(); + } + + static ngAcceptInputType_value: NumberInput; +} From 41c12aace03fa72af48e2c7261fa29c3fb5ffa55 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Mon, 8 Feb 2021 09:11:03 -0800 Subject: [PATCH 05/35] feat(material-experimental/mdc-slider): implement MatSlider (#21680) --- .../mdc-slider/module.ts | 2 +- .../mdc-slider/slider-thumb.ts | 35 ++- .../mdc-slider/slider.html | 25 ++ .../mdc-slider/slider.ts | 221 +++++++++++++++++- .../kitchen-sink-mdc/kitchen-sink-mdc.html | 4 - 5 files changed, 271 insertions(+), 16 deletions(-) diff --git a/src/material-experimental/mdc-slider/module.ts b/src/material-experimental/mdc-slider/module.ts index 6604a90985f3..4beea1a3b390 100644 --- a/src/material-experimental/mdc-slider/module.ts +++ b/src/material-experimental/mdc-slider/module.ts @@ -14,7 +14,7 @@ import {MatSliderThumb} from './slider-thumb'; @NgModule({ imports: [MatCommonModule, CommonModule], - exports: [MatSlider], + exports: [MatSlider, MatSliderThumb], declarations: [ MatSlider, MatSliderThumb, diff --git a/src/material-experimental/mdc-slider/slider-thumb.ts b/src/material-experimental/mdc-slider/slider-thumb.ts index 6c1f7bb2ea1c..e9780a90ff2f 100644 --- a/src/material-experimental/mdc-slider/slider-thumb.ts +++ b/src/material-experimental/mdc-slider/slider-thumb.ts @@ -10,6 +10,24 @@ import {NumberInput} from '@angular/cdk/coercion'; import {DOCUMENT} from '@angular/common'; import {Directive, ElementRef, EventEmitter, Inject, Input, Output} from '@angular/core'; import {Thumb} from '@material/slider'; +import {MatSlider} from './slider'; + +/** + * Represents a drag event emitted by the MatSlider component. + */ +export interface MatSliderDragEvent { + /** The MatSliderThumb that was interacted with. */ + source: MatSliderThumb; + + /** The parent MatSlider that was interacted with. */ + parent: MatSlider; + + /** The current value of the slider. */ + value: number; + + /** The thumb that was interacted with. */ + thumb: Thumb; +} /** * The native input used by the MatSlider. @@ -52,6 +70,14 @@ import {Thumb} from '@material/slider'; @Input() set disabled(v: boolean) { throw Error('Invalid attribute "disabled" on MatSliderThumb.'); } + /** Event emitted when the slider thumb starts being dragged. */ + @Output() readonly dragStart: EventEmitter + = new EventEmitter(); + + /** Event emitted when the slider thumb stops being dragged. */ + @Output() readonly dragEnd: EventEmitter + = new EventEmitter(); + /** Event emitted every time the MatSliderThumb is blurred. */ @Output() readonly _blur: EventEmitter = new EventEmitter(); @@ -63,17 +89,12 @@ import {Thumb} from '@material/slider'; constructor( @Inject(DOCUMENT) private readonly _document: Document, - private readonly _elementRef: ElementRef, + readonly _elementRef: ElementRef, ) {} - /** Returns the hosts native HTML element. */ - _getHostElement(): HTMLInputElement { - return this._elementRef.nativeElement; - } - /** Returns true if this slider input currently has focus. */ _isFocused(): boolean { - return this._document.activeElement === this._getHostElement(); + return this._document.activeElement === this._elementRef.nativeElement; } static ngAcceptInputType_value: NumberInput; diff --git a/src/material-experimental/mdc-slider/slider.html b/src/material-experimental/mdc-slider/slider.html index e69de29bb2d1..991a22bc27c4 100644 --- a/src/material-experimental/mdc-slider/slider.html +++ b/src/material-experimental/mdc-slider/slider.html @@ -0,0 +1,25 @@ + + + + +
+
+
+
+
+
+
+
+
+ + +
+
+
+ + {{_getValueIndicatorTextByThumb(thumb)}} + +
+
+
+
diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index b82a61f60447..2c06e7b736a8 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -7,12 +7,31 @@ */ import { + BooleanInput, + coerceBooleanProperty, + coerceNumberProperty, + NumberInput +} from '@angular/cdk/coercion'; +import {Platform} from '@angular/cdk/platform'; +import {DOCUMENT} from '@angular/common'; +import { + AfterViewInit, ChangeDetectionStrategy, + ChangeDetectorRef, Component, + ContentChildren, + ElementRef, + Inject, + Input, + OnDestroy, + QueryList, + ViewChild, + ViewChildren, ViewEncapsulation, } from '@angular/core'; -import {MDCSliderFoundation} from '@material/slider'; +import {MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; import {SliderAdapter} from './slider-adapter'; +import {MatSliderThumb} from './slider-thumb'; /** * Allows users to select from a range of values by moving the slider thumb. It is similar in @@ -22,13 +41,207 @@ import {SliderAdapter} from './slider-adapter'; selector: 'mat-slider', templateUrl: 'slider.html', styleUrls: ['slider.css'], + host: { + 'class': 'mat-mdc-slider mdc-slider', + '[class.mdc-slider--range]': '_isRange()', + '[class.mdc-slider--disabled]': 'disabled', + '[class.mdc-slider--discrete]': 'discrete', + '[class.mdc-slider--tick-marks]': 'showTickMarks', + }, + exportAs: 'matSlider', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) -export class MatSlider { +export class MatSlider implements AfterViewInit, OnDestroy { + /** The slider thumb(s). */ + @ViewChildren('thumb') _thumbs: QueryList>; - /** Instance of the MDC slider foundation for this slider. */ + /** The slider thumb knob(s) */ + @ViewChildren('knob') _knobs: QueryList>; + + /** The active section of the slider track. */ + @ViewChild('trackActive') _trackActive: ElementRef; + + /** The sliders hidden range input(s). */ + @ContentChildren(MatSliderThumb, {descendants: false}) _inputs: QueryList; + + /** 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); + } + } + private _disabled: boolean = false; + + /** Whether the slider displays a numeric value label upon pressing the thumb. */ + @Input() + get discrete(): boolean { return this._discrete; } + set discrete(v: boolean) { this._discrete = coerceBooleanProperty(v); } + private _discrete: boolean = false; + + /** Whether the slider displays tick marks along the slider track. */ + @Input() + get showTickMarks(): boolean { return this._showTickMarks; } + set showTickMarks(v: boolean) { + this._showTickMarks = coerceBooleanProperty(v); + } + private _showTickMarks: boolean = false; + + /** The minimum value that the slider can have. */ + @Input() + get min(): number { return this._min; } + set min(v: number) { this._min = coerceNumberProperty(v, this._min); } + private _min = 0; - // tslint:disable-next-line:no-unused-variable + /** The maximum value that the slider can have. */ + @Input() + get max(): number { return this._max; } + set max(v: number) { this._max = coerceNumberProperty(v, this._max); } + private _max = 100; + + /** The values at which the thumb will snap. */ + @Input() + get step(): number { return this._step; } + set step(v: number) { this._step = coerceNumberProperty(v, this._step); } + private _step: number = 1; + + /** + * Function that will be used to format the value before it is displayed + * in the thumb label. Can be used to format very large number in order + * for them to fit into the slider thumb. + */ + @Input() displayWith: ((value: number) => string) | null; + + /** Instance of the MDC slider foundation for this slider. */ private _foundation = new MDCSliderFoundation(new SliderAdapter()); + + /** Whether the foundation has been initialized. */ + _initialized: boolean = false; + + /** The string representation of the start thumbs value. */ + _startValueIndicatorText: string; + + /** The string representation of the end thumbs value. */ + _endValueIndicatorText: string; + + /** The injected document if available or fallback to the global document reference. */ + _document: Document; + + /** + * The defaultView of the injected document if + * available or fallback to global window reference. + */ + _window: Window; + + /** The hosts native HTML element. */ + _hostElement: HTMLElement; + + /** Used to keep track of & render the active & inactive tick marks on the slider track. */ + _tickMarks: TickMark[]; + + constructor( + readonly _cdr: ChangeDetectorRef, + private readonly _elementRef: ElementRef, + private readonly _platform: Platform, + @Inject(DOCUMENT) document: any) { + this._document = document; + this._window = this._document.defaultView || window; + this._hostElement = this._elementRef.nativeElement; + } + + ngAfterViewInit() { + this._foundation.init(); + if (this._platform.isBrowser) { + this._foundation.layout(); + } + this._initialized = true; + } + + ngOnDestroy() { + if (this._platform.isBrowser) { + this._foundation.destroy(); + } + } + + /** Gets the current value of given slider thumb. */ + _getValue(thumb: Thumb): number { + return thumb === Thumb.START + ? this._foundation.getValueStart() + : this._foundation.getValue(); + } + + /** Sets the value of a slider thumb. */ + _setValue(value: number, thumb: Thumb): void { + thumb === Thumb.START + ? this._foundation.setValueStart(value) + : this._foundation.setValue(value); + } + + /** Whether this is a ranged slider. */ + _isRange(): boolean { + return this._inputs.length === 2; + } + + /** Gets the slider thumb input of the given thumb. */ + _getInput(thumb: Thumb): MatSliderThumb { + return thumb === Thumb.END ? this._inputs.get(this._inputs.length - 1)! : this._inputs.get(0)!; + } + + /** Gets the slider thumb HTML input element of the given thumb. */ + _getInputElement(thumb: Thumb): HTMLInputElement { + return this._getInput(thumb)._elementRef.nativeElement; + } + + /** Gets the slider thumb HTML element of the given thumb. */ + _getThumbElement(thumb: Thumb): HTMLElement { + const thumbs = this._thumbs.toArray().map(e => e.nativeElement); + return thumb === Thumb.END ? thumbs[thumbs.length - 1] : thumbs[0]; + } + + /** Gets the slider knob HTML element of the given thumb. */ + _getKnobElement(thumb: Thumb): HTMLElement { + const knobs = this._knobs.toArray().map(e => e.nativeElement); + return thumb === Thumb.END ? knobs[knobs.length - 1] : knobs[0]; + } + + /** + * Gets the text representation of the given value. + * + * Uses the `displayWith` function if one has been provided. Otherwise, it just returns the + * current numeric value as a string. + */ + _getValueIndicatorText(value: number): string { + return this.displayWith ? this.displayWith(value) : value.toString(); + } + + /** Gets the text representation of the current value of the given thumb. */ + _getValueIndicatorTextByThumb(thumb: Thumb): string { + return this._getValueIndicatorText(this._getValue(thumb)); + } + + /** Determines the class name for a HTML element. */ + _getTickMarkClass(tickMark: TickMark): string { + return tickMark === TickMark.ACTIVE + ? 'mdc-slider__tick-mark--active' + : 'mdc-slider__tick-mark--inactive'; + } + + /** Returns an array of the thumb types that exist on the current slider instance. */ + _getThumbTypes(): Thumb[] { + return this._isRange() ? [Thumb.START, Thumb.END] : [Thumb.END]; + } + + static ngAcceptInputType_disabled: BooleanInput; + static ngAcceptInputType_discrete: BooleanInput; + static ngAcceptInputType_showTickMarks: BooleanInput; + static ngAcceptInputType_min: NumberInput; + static ngAcceptInputType_max: NumberInput; + static ngAcceptInputType_step: NumberInput; } diff --git a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html index 0e698b8d6cab..78bd1b80548a 100644 --- a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html +++ b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html @@ -104,10 +104,6 @@

MDC slide-toggle

with a label

MDC Slider

- - - -

MDC Tabs

From 800e11a57e5ba93813226f59d1f81776936afb47 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Wed, 17 Feb 2021 15:44:49 -0500 Subject: [PATCH 06/35] feat(material-experimental/mdc-slider): implement the SliderAdapter (#21844) * feat(material-experimental/mdc-slider): implement the SliderAdapter * complete the core logic for MatSliderThumb and MatSlider * collapse slider-thumb.ts and slider-adapter.ts into slider.ts --- src/dev-app/mdc-slider/mdc-slider-demo.html | 4 + .../mdc-slider/module.ts | 3 +- .../mdc-slider/public-api.ts | 3 +- .../mdc-slider/slider-adapter.ts | 138 ------ .../mdc-slider/slider-thumb.ts | 101 ---- .../mdc-slider/slider.html | 4 +- .../mdc-slider/slider.scss | 5 +- .../mdc-slider/slider.ts | 469 +++++++++++++++--- 8 files changed, 423 insertions(+), 304 deletions(-) delete mode 100644 src/material-experimental/mdc-slider/slider-adapter.ts delete mode 100644 src/material-experimental/mdc-slider/slider-thumb.ts diff --git a/src/dev-app/mdc-slider/mdc-slider-demo.html b/src/dev-app/mdc-slider/mdc-slider-demo.html index e69de29bb2d1..7d170e878b0b 100644 --- a/src/dev-app/mdc-slider/mdc-slider-demo.html +++ b/src/dev-app/mdc-slider/mdc-slider-demo.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/material-experimental/mdc-slider/module.ts b/src/material-experimental/mdc-slider/module.ts index 4beea1a3b390..ba3e3367ad34 100644 --- a/src/material-experimental/mdc-slider/module.ts +++ b/src/material-experimental/mdc-slider/module.ts @@ -9,8 +9,7 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import {MatCommonModule} from '@angular/material-experimental/mdc-core'; -import {MatSlider} from './slider'; -import {MatSliderThumb} from './slider-thumb'; +import {MatSlider, MatSliderThumb} from './slider'; @NgModule({ imports: [MatCommonModule, CommonModule], diff --git a/src/material-experimental/mdc-slider/public-api.ts b/src/material-experimental/mdc-slider/public-api.ts index ee39eb6b8adf..294dbb8a1be7 100644 --- a/src/material-experimental/mdc-slider/public-api.ts +++ b/src/material-experimental/mdc-slider/public-api.ts @@ -6,6 +6,5 @@ * found in the LICENSE file at https://angular.io/license */ -export {MatSlider} from './slider'; -export {MatSliderThumb} from './slider-thumb'; +export {MatSlider, MatSliderThumb, MatSliderDragEvent} from './slider'; export {MatSliderModule} from './module'; diff --git a/src/material-experimental/mdc-slider/slider-adapter.ts b/src/material-experimental/mdc-slider/slider-adapter.ts deleted file mode 100644 index a3144d73f0d6..000000000000 --- a/src/material-experimental/mdc-slider/slider-adapter.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {SpecificEventListener, EventType} from '@material/base'; -import {MDCSliderAdapter, Thumb, TickMark} from '@material/slider'; - -export class SliderAdapter implements MDCSliderAdapter { - hasClass(className: string): boolean { - throw Error('Method not implemented.'); - } - addClass(className: string): void { - throw Error('Method not implemented.'); - } - removeClass(className: string): void { - throw Error('Method not implemented.'); - } - getAttribute(attribute: string): string | null { - throw Error('Method not implemented.'); - } - addThumbClass(className: string, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - removeThumbClass(className: string, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - getInputValue(thumb: Thumb): string { - throw Error('Method not implemented.'); - } - setInputValue(value: string, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - getInputAttribute(attribute: string, thumb: Thumb): string | null { - throw Error('Method not implemented.'); - } - setInputAttribute(attribute: string, value: string, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - removeInputAttribute(attribute: string, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - focusInput(thumb: Thumb): void { - throw Error('Method not implemented.'); - } - isInputFocused(thumb: Thumb): boolean { - throw Error('Method not implemented.'); - } - getThumbKnobWidth(thumb: Thumb): number { - throw Error('Method not implemented.'); - } - getThumbBoundingClientRect(thumb: Thumb): ClientRect { - throw Error('Method not implemented.'); - } - getBoundingClientRect(): ClientRect { - throw Error('Method not implemented.'); - } - isRTL(): boolean { - throw Error('Method not implemented.'); - } - setThumbStyleProperty(propertyName: string, value: string, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - removeThumbStyleProperty(propertyName: string, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - setTrackActiveStyleProperty(propertyName: string, value: string): void { - throw Error('Method not implemented.'); - } - removeTrackActiveStyleProperty(propertyName: string): void { - throw Error('Method not implemented.'); - } - setValueIndicatorText(value: number, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - getValueToAriaValueTextFn(): ((value: number) => string) | null { - throw Error('Method not implemented.'); - } - updateTickMarks(tickMarks: TickMark[]): void { - throw Error('Method not implemented.'); - } - setPointerCapture(pointerId: number): void { - throw Error('Method not implemented.'); - } - emitChangeEvent(value: number, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - emitInputEvent(value: number, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - emitDragStartEvent(value: number, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - emitDragEndEvent(value: number, thumb: Thumb): void { - throw Error('Method not implemented.'); - } - registerEventHandler(evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - deregisterEventHandler(evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - registerThumbEventHandler - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - deregisterThumbEventHandler - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - registerInputEventHandler - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - deregisterInputEventHandler - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - registerBodyEventHandler - (evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - deregisterBodyEventHandler - (evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - registerWindowEventHandler - (evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - deregisterWindowEventHandler - (evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } -} diff --git a/src/material-experimental/mdc-slider/slider-thumb.ts b/src/material-experimental/mdc-slider/slider-thumb.ts deleted file mode 100644 index e9780a90ff2f..000000000000 --- a/src/material-experimental/mdc-slider/slider-thumb.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {NumberInput} from '@angular/cdk/coercion'; -import {DOCUMENT} from '@angular/common'; -import {Directive, ElementRef, EventEmitter, Inject, Input, Output} from '@angular/core'; -import {Thumb} from '@material/slider'; -import {MatSlider} from './slider'; - -/** - * Represents a drag event emitted by the MatSlider component. - */ -export interface MatSliderDragEvent { - /** The MatSliderThumb that was interacted with. */ - source: MatSliderThumb; - - /** The parent MatSlider that was interacted with. */ - parent: MatSlider; - - /** The current value of the slider. */ - value: number; - - /** The thumb that was interacted with. */ - thumb: Thumb; -} - -/** - * The native input used by the MatSlider. - */ -@Directive({ - selector: 'input[mat-slider-thumb]', - host: { - 'class': 'mdc-slider__input', - 'type': 'range', - '[min]': 'min', - '[max]': 'max', - '[step]': 'step', - '[attr.value]': 'value', - '(blur)': '_blur.emit()', - '(focus)': '_focus.emit()', - } -}) export class MatSliderThumb { - /** The current value of this slider input. */ - @Input() - get value(): number { return this._value; } - set value(v: number) { this._value = v; } - private _value: number; - - /** The minimum value that this slider input can have. */ - @Input() - get min(): number { return 0; } - set min(v: number) { throw Error('Invalid attribute "min" on MatSliderThumb.'); } - - /** The maximum value that this slider input can have. */ - @Input() - get max(): number { return 100; } - set max(v: number) { throw Error('Invalid attribute "max" on MatSliderThumb.'); } - - /** The size of each increment between the values of the slider. */ - @Input() - get step(): number { return 1; } - set step(v: number) { throw Error('Invalid attribute "step" on MatSliderThumb.'); } - - /** MDC Slider does not use the disabled attribute it's native inputs. */ - @Input() - set disabled(v: boolean) { throw Error('Invalid attribute "disabled" on MatSliderThumb.'); } - - /** Event emitted when the slider thumb starts being dragged. */ - @Output() readonly dragStart: EventEmitter - = new EventEmitter(); - - /** Event emitted when the slider thumb stops being dragged. */ - @Output() readonly dragEnd: EventEmitter - = new EventEmitter(); - - /** Event emitted every time the MatSliderThumb is blurred. */ - @Output() readonly _blur: EventEmitter = new EventEmitter(); - - /** Event emitted every time the MatSliderThumb is focused. */ - @Output() readonly _focus: EventEmitter = new EventEmitter(); - - /** Indicates which slider thumb this input corresponds to. */ - thumb: Thumb; - - constructor( - @Inject(DOCUMENT) private readonly _document: Document, - readonly _elementRef: ElementRef, - ) {} - - /** Returns true if this slider input currently has focus. */ - _isFocused(): boolean { - return this._document.activeElement === this._elementRef.nativeElement; - } - - static ngAcceptInputType_value: NumberInput; -} diff --git a/src/material-experimental/mdc-slider/slider.html b/src/material-experimental/mdc-slider/slider.html index 991a22bc27c4..b8f42c85be02 100644 --- a/src/material-experimental/mdc-slider/slider.html +++ b/src/material-experimental/mdc-slider/slider.html @@ -16,9 +16,7 @@
- - {{_getValueIndicatorTextByThumb(thumb)}} - + {{_getValueIndicatorText(thumb)}}
diff --git a/src/material-experimental/mdc-slider/slider.scss b/src/material-experimental/mdc-slider/slider.scss index dc38ea9472a2..eea151a252f8 100644 --- a/src/material-experimental/mdc-slider/slider.scss +++ b/src/material-experimental/mdc-slider/slider.scss @@ -1,7 +1,8 @@ -// TODO: disabled until we implement the new MDC slider. -// @use '@material/slider' as mdc-slider; @use '../../cdk/a11y'; +@import '@material/slider/slider'; +@include core-styles; + $mat-slider-min-size: 128px !default; $mat-slider-horizontal-margin: 8px !default; diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 2c06e7b736a8..5283903f42c2 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -20,18 +20,181 @@ import { ChangeDetectorRef, Component, ContentChildren, + Directive, ElementRef, + EventEmitter, Inject, Input, OnDestroy, + Output, QueryList, ViewChild, ViewChildren, ViewEncapsulation, } from '@angular/core'; -import {MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; -import {SliderAdapter} from './slider-adapter'; -import {MatSliderThumb} from './slider-thumb'; +import {SpecificEventListener, EventType} from '@material/base'; +import {MDCSliderAdapter, MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; + +/** Represents a drag event emitted by the MatSlider component. */ +export interface MatSliderDragEvent { + /** The MatSliderThumb that was interacted with. */ + source: MatSliderThumb; + + /** The MatSlider that was interacted with. */ + parent: MatSlider; + + /** The current value of the slider. */ + value: number; +} + +/** + * Directive that adds slider-specific behaviors to an input element inside ``. + * Up to two may be placed inside of a ``. + * + * If one is used, the selector `matSliderThumb` must be used, and the outcome will be a normal + * slider. If two are used, the selectors `matSliderStartThumb` and `matSliderEndThumb` must be + * used, and the outcome will be a range slider with two slider thumbs. + */ +@Directive({ + selector: 'input[matSliderThumb], input[matSliderStartThumb], input[matSliderEndThumb]', + host: { + 'class': 'mdc-slider__input', + 'type': 'range', + '(blur)': '_blur.emit()', + '(focus)': '_focus.emit()', + }, +}) +export class MatSliderThumb implements AfterViewInit { + + // ** IMPORTANT NOTE ** + // + // The way `value` is implemented for MatSliderThumb doesn't follow typical Angular conventions. + // Normally we would define a private variable `_value` as the source of truth for the value of + // the slider thumb input. The source of truth for the value of the slider inputs has already + // been decided for us by MDC to be the value attribute on the slider thumb inputs. This is + // because the MDC foundation and adapter expect that the value attribute is the source of truth + // for the slider inputs. + // + // Also, note that the value attribute is completely disconnected from the value property. + + /** The current value of this slider input. */ + @Input() + get value(): number { + return coerceNumberProperty(this._elementRef.nativeElement.getAttribute('value')); + } + set value(v: number) { + const value = coerceNumberProperty(v); + + // If the foundation has already been initialized, we need to + // relay any value updates to it so that it can update the UI. + if (this._slider._initialized) { + this._slider._setValue(value, this._thumbPosition); + } else { + // Setup for the MDC foundation. + this._elementRef.nativeElement.setAttribute('value', `${value}`); + } + } + + /** Event emitted when the slider thumb starts being dragged. */ + @Output() readonly dragStart: EventEmitter + = new EventEmitter(); + + /** Event emitted when the slider thumb stops being dragged. */ + @Output() readonly dragEnd: EventEmitter + = new EventEmitter(); + + /** Event emitted every time the MatSliderThumb is blurred. */ + @Output() readonly _blur: EventEmitter = new EventEmitter(); + + /** Event emitted every time the MatSliderThumb is focused. */ + @Output() readonly _focus: EventEmitter = new EventEmitter(); + + /** Indicates which slider thumb this input corresponds to. */ + private _thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') + ? Thumb.START + : Thumb.END; + + private _document: Document; + + constructor( + @Inject(DOCUMENT) document: any, + private readonly _slider: MatSlider, + readonly _elementRef: ElementRef, + ) { + this._document = document; + // By calling this in the constructor we guarantee that the sibling sliders initial value by + // has already been set by the time we reach ngAfterViewInit(). + this._initializeInputValueAttribute(); + } + + ngAfterViewInit() { + this._initializeInputMinMax(); + this._initializeInputValueProperty(); + + // Setup for the MDC foundation. + if (this._slider.disabled) { + this._elementRef.nativeElement.disabled = true; + } + } + + /** Returns true if this slider input currently has focus. */ + _isFocused(): boolean { + return this._document.activeElement === this._elementRef.nativeElement; + } + + /** + * Sets the min and max properties on the slider thumb input. + * + * Must be called AFTER the sibling slider thumb input is guaranteed to have had its value + * attribute value set. For a range slider, the min and max of the slider thumb input depends on + * the value of its sibling slider thumb inputs value. + * + * Must be called BEFORE the value property is set. In the case where the min and max have not + * yet been set and we are setting the input value property to a value outside of the native + * inputs default min or max. The value property would not be set to our desired value, but + * instead be capped at either the default min or max. + * + */ + private _initializeInputMinMax(): void { + const min = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') + ? this._slider._getInput(Thumb.START).value + : this._slider.min; + const max = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') + ? this._slider._getInput(Thumb.END).value + : this._slider.max; + this._elementRef.nativeElement.min = `${min}`; + this._elementRef.nativeElement.max = `${max}`; + } + + /** + * Sets the value property on the slider thumb input. + * + * Must be called AFTER the min and max have been set. In the case where the min and max have not + * yet been set and we are setting the input value property to a value outside of the native + * inputs default min or max. The value property would not be set to our desired value, but + * instead be capped at either the default min or max. + */ + private _initializeInputValueProperty(): void { + this._elementRef.nativeElement.value = `${this.value}`; + } + + /** + * Ensures the value attribute is initialized. + * + * Must be called BEFORE the min and max are set. For a range slider, the min and max of the + * slider thumb input depends on the value of its sibling slider thumb inputs value. + */ + private _initializeInputValueAttribute(): void { + // Only set the default value if an initial value has not already been provided. + if (!this._elementRef.nativeElement.hasAttribute('value')) { + this.value = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') + ? this._slider.max + : this._slider.min; + } + } + + static ngAcceptInputType_value: NumberInput; +} /** * Allows users to select from a range of values by moving the slider thumb. It is similar in @@ -59,11 +222,16 @@ export class MatSlider implements AfterViewInit, OnDestroy { /** The slider thumb knob(s) */ @ViewChildren('knob') _knobs: QueryList>; + /** The span containing the slider thumb value indicator text */ + @ViewChildren('valueIndicatorTextElement') + _valueIndicatorTextElements: QueryList>; + /** The active section of the slider track. */ @ViewChild('trackActive') _trackActive: ElementRef; /** The sliders hidden range input(s). */ - @ContentChildren(MatSliderThumb, {descendants: false}) _inputs: QueryList; + @ContentChildren(MatSliderThumb, {descendants: false}) + _inputs: QueryList; /** Whether the slider is disabled. */ @Input() @@ -89,22 +257,20 @@ export class MatSlider implements AfterViewInit, OnDestroy { /** Whether the slider displays tick marks along the slider track. */ @Input() get showTickMarks(): boolean { return this._showTickMarks; } - set showTickMarks(v: boolean) { - this._showTickMarks = coerceBooleanProperty(v); - } + set showTickMarks(v: boolean) { this._showTickMarks = coerceBooleanProperty(v); } private _showTickMarks: boolean = false; /** The minimum value that the slider can have. */ @Input() get min(): number { return this._min; } set min(v: number) { this._min = coerceNumberProperty(v, this._min); } - private _min = 0; + private _min: number = 0; /** The maximum value that the slider can have. */ @Input() get max(): number { return this._max; } set max(v: number) { this._max = coerceNumberProperty(v, this._max); } - private _max = 100; + private _max: number = 100; /** The values at which the thumb will snap. */ @Input() @@ -120,17 +286,11 @@ export class MatSlider implements AfterViewInit, OnDestroy { @Input() displayWith: ((value: number) => string) | null; /** Instance of the MDC slider foundation for this slider. */ - private _foundation = new MDCSliderFoundation(new SliderAdapter()); + private _foundation = new MDCSliderFoundation(new SliderAdapter(this)); /** Whether the foundation has been initialized. */ _initialized: boolean = false; - /** The string representation of the start thumbs value. */ - _startValueIndicatorText: string; - - /** The string representation of the end thumbs value. */ - _endValueIndicatorText: string; - /** The injected document if available or fallback to the global document reference. */ _document: Document; @@ -140,28 +300,48 @@ export class MatSlider implements AfterViewInit, OnDestroy { */ _window: Window; - /** The hosts native HTML element. */ - _hostElement: HTMLElement; - /** Used to keep track of & render the active & inactive tick marks on the slider track. */ _tickMarks: TickMark[]; + /** The display value of the start thumb. */ + private _startValueIndicatorText: string; + + /** The display value of the end thumb. */ + private _endValueIndicatorText: string; + constructor( readonly _cdr: ChangeDetectorRef, - private readonly _elementRef: ElementRef, + readonly _elementRef: ElementRef, private readonly _platform: Platform, @Inject(DOCUMENT) document: any) { this._document = document; this._window = this._document.defaultView || window; - this._hostElement = this._elementRef.nativeElement; } ngAfterViewInit() { - this._foundation.init(); + if (typeof ngDevMode === 'undefined' || ngDevMode) { + _validateInputs( + this._isRange(), + this._getInputElement(Thumb.START), + this._getInputElement(Thumb.END), + ); + } if (this._platform.isBrowser) { + this._foundation.init(); this._foundation.layout(); + this._initialized = true; } - this._initialized = true; + // The MDC foundation requires access to the view and content children of the MatSlider. In + // order to access the view and content children of MatSlider we need to wait until change + // detection runs and materializes them. That is why we call init() and layout() in + // ngAfterViewInit(). + // + // The MDC foundation then uses the information it gathers from the DOM to compute an initial + // value for the tickMarks array. It then tries to update the component data, but because it is + // updating the component data AFTER change detection already ran, we will get a changed after + // checked error. Because of this, we need to force change detection to update the UI with the + // new state. + this._cdr.detectChanges(); } ngOnDestroy() { @@ -170,16 +350,9 @@ export class MatSlider implements AfterViewInit, OnDestroy { } } - /** Gets the current value of given slider thumb. */ - _getValue(thumb: Thumb): number { - return thumb === Thumb.START - ? this._foundation.getValueStart() - : this._foundation.getValue(); - } - /** Sets the value of a slider thumb. */ - _setValue(value: number, thumb: Thumb): void { - thumb === Thumb.START + _setValue(value: number, thumbPosition: Thumb): void { + thumbPosition === Thumb.START ? this._foundation.setValueStart(value) : this._foundation.setValue(value); } @@ -189,41 +362,45 @@ export class MatSlider implements AfterViewInit, OnDestroy { return this._inputs.length === 2; } - /** Gets the slider thumb input of the given thumb. */ - _getInput(thumb: Thumb): MatSliderThumb { - return thumb === Thumb.END ? this._inputs.get(this._inputs.length - 1)! : this._inputs.get(0)!; + /** Gets the slider thumb input of the given thumb position. */ + _getInput(thumbPosition: Thumb): MatSliderThumb { + return thumbPosition === Thumb.END ? this._inputs.last! : this._inputs.first!; + } + + /** Gets the slider thumb HTML input element of the given thumb position. */ + _getInputElement(thumbPosition: Thumb): HTMLInputElement { + return this._getInput(thumbPosition)._elementRef.nativeElement; } - /** Gets the slider thumb HTML input element of the given thumb. */ - _getInputElement(thumb: Thumb): HTMLInputElement { - return this._getInput(thumb)._elementRef.nativeElement; + /** Gets the slider thumb HTML element of the given thumb position. */ + _getThumbElement(thumbPosition: Thumb): HTMLElement { + const thumbElementRef = thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first; + return thumbElementRef.nativeElement; } - /** Gets the slider thumb HTML element of the given thumb. */ - _getThumbElement(thumb: Thumb): HTMLElement { - const thumbs = this._thumbs.toArray().map(e => e.nativeElement); - return thumb === Thumb.END ? thumbs[thumbs.length - 1] : thumbs[0]; + /** Gets the slider knob HTML element of the given thumb position. */ + _getKnobElement(thumbPosition: Thumb): HTMLElement { + const knobElementRef = thumbPosition === Thumb.END ? this._knobs.last : this._knobs.first; + return knobElementRef.nativeElement; } - /** Gets the slider knob HTML element of the given thumb. */ - _getKnobElement(thumb: Thumb): HTMLElement { - const knobs = this._knobs.toArray().map(e => e.nativeElement); - return thumb === Thumb.END ? knobs[knobs.length - 1] : knobs[0]; + _getValueIndicatorText(thumbPosition: Thumb) { + return thumbPosition === Thumb.START + ? this._startValueIndicatorText + : this._endValueIndicatorText; } /** - * Gets the text representation of the given value. + * Sets the value indicator text of the given thumb position using the given value. * - * Uses the `displayWith` function if one has been provided. Otherwise, it just returns the - * current numeric value as a string. + * Uses the `displayWith` function if one has been provided. Otherwise, it just uses the + * numeric value as a string. */ - _getValueIndicatorText(value: number): string { - return this.displayWith ? this.displayWith(value) : value.toString(); - } - - /** Gets the text representation of the current value of the given thumb. */ - _getValueIndicatorTextByThumb(thumb: Thumb): string { - return this._getValueIndicatorText(this._getValue(thumb)); + _setValueIndicatorText(value: number, thumbPosition: Thumb) { + const valueText = this.displayWith ? this.displayWith(value) : `${value}`; + thumbPosition === Thumb.START + ? this._startValueIndicatorText = valueText + : this._endValueIndicatorText = valueText; } /** Determines the class name for a HTML element. */ @@ -245,3 +422,183 @@ export class MatSlider implements AfterViewInit, OnDestroy { static ngAcceptInputType_max: NumberInput; static ngAcceptInputType_step: NumberInput; } + +/** The MDCSliderAdapter implementation. */ +class SliderAdapter implements MDCSliderAdapter { + constructor(private readonly _delegate: MatSlider) {} + + // We manually assign functions instead of using prototype methods because + // MDC clobbers the values otherwise. + // See https://github.com/material-components/material-components-web/pull/6256 + + hasClass = (className: string): boolean => { + return this._delegate._elementRef.nativeElement.classList.contains(className); + } + addClass = (className: string): void => { + this._delegate._elementRef.nativeElement.classList.add(className); + } + removeClass = (className: string): void => { + this._delegate._elementRef.nativeElement.classList.remove(className); + } + getAttribute = (attribute: string): string | null => { + return this._delegate._elementRef.nativeElement.getAttribute(attribute); + } + addThumbClass = (className: string, thumbPosition: Thumb): void => { + this._delegate._getThumbElement(thumbPosition).classList.add(className); + } + removeThumbClass = (className: string, thumbPosition: Thumb): void => { + this._delegate._getThumbElement(thumbPosition).classList.remove(className); + } + getInputValue = (thumbPosition: Thumb): string => { + return this._delegate._getInputElement(thumbPosition).value; + } + setInputValue = (value: string, thumbPosition: Thumb): void => { + this._delegate._getInputElement(thumbPosition).value = value; + } + getInputAttribute = (attribute: string, thumbPosition: Thumb): string | null => { + return this._delegate._getInputElement(thumbPosition).getAttribute(attribute); + } + setInputAttribute = (attribute: string, value: string, thumbPosition: Thumb): void => { + this._delegate._getInputElement(thumbPosition).setAttribute(attribute, value); + } + removeInputAttribute = (attribute: string, thumbPosition: Thumb): void => { + this._delegate._getInputElement(thumbPosition).removeAttribute(attribute); + } + focusInput = (thumbPosition: Thumb): void => { + this._delegate._getInputElement(thumbPosition).focus(); + } + isInputFocused = (thumbPosition: Thumb): boolean => { + return this._delegate._getInput(thumbPosition)._isFocused(); + } + getThumbKnobWidth = (thumbPosition: Thumb): number => { + // TODO(wagnermaciel): Check if this causes issues for SSR + // once the mdc-slider is added back to the kitchen sink SSR app. + return this._delegate._getKnobElement(thumbPosition).getBoundingClientRect().width; + } + getThumbBoundingClientRect = (thumbPosition: Thumb): ClientRect => { + return this._delegate._getThumbElement(thumbPosition).getBoundingClientRect(); + } + getBoundingClientRect = (): ClientRect => { + return this._delegate._elementRef.nativeElement.getBoundingClientRect(); + } + isRTL = (): boolean => { + // TODO(wagnermaciel): Actually implementing this. + return false; + } + setThumbStyleProperty = (propertyName: string, value: string, thumbPosition: Thumb): void => { + this._delegate._getThumbElement(thumbPosition).style.setProperty(propertyName, value); + } + removeThumbStyleProperty = (propertyName: string, thumbPosition: Thumb): void => { + this._delegate._getThumbElement(thumbPosition).style.removeProperty(propertyName); + } + setTrackActiveStyleProperty = (propertyName: string, value: string): void => { + this._delegate._trackActive.nativeElement.style.setProperty(propertyName, value); + } + removeTrackActiveStyleProperty = (propertyName: string): void => { + this._delegate._trackActive.nativeElement.style.removeProperty(propertyName); + } + setValueIndicatorText = (value: number, thumbPosition: Thumb): void => { + this._delegate._setValueIndicatorText(value, thumbPosition); + } + getValueToAriaValueTextFn = (): ((value: number) => string) | null => { + return this._delegate.displayWith; + } + updateTickMarks = (tickMarks: TickMark[]): void => { + this._delegate._tickMarks = tickMarks; + this._delegate._cdr.markForCheck(); + } + setPointerCapture = (pointerId: number): void => { + this._delegate._elementRef.nativeElement.setPointerCapture(pointerId); + } + // We ignore emitChangeEvent and emitInputEvent because the slider inputs + // are already exposed so users can just listen for those events directly themselves. + emitChangeEvent = (value: number, thumbPosition: Thumb): void => {}; + emitInputEvent = (value: number, thumbPosition: Thumb): void => {}; + emitDragStartEvent = (value: number, thumbPosition: Thumb): void => { + const input = this._delegate._getInput(thumbPosition); + input.dragStart.emit({ source: input, parent: this._delegate, value }); + } + emitDragEndEvent = (value: number, thumbPosition: Thumb): void => { + const input = this._delegate._getInput(thumbPosition); + input.dragEnd.emit({ source: input, parent: this._delegate, value }); + } + registerEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._elementRef.nativeElement.addEventListener(evtType, handler); + } + deregisterEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._elementRef.nativeElement.removeEventListener(evtType, handler); + } + registerThumbEventHandler = + (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getThumbElement(thumbPosition).addEventListener(evtType, handler); + } + deregisterThumbEventHandler = + (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getThumbElement(thumbPosition).removeEventListener(evtType, handler); + } + registerInputEventHandler = + (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getInputElement(thumbPosition).addEventListener(evtType, handler); + } + deregisterInputEventHandler = + (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getInputElement(thumbPosition).removeEventListener(evtType, handler); + } + registerBodyEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._document.body.addEventListener(evtType, handler); + } + deregisterBodyEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._document.body.removeEventListener(evtType, handler); + } + registerWindowEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._window.addEventListener(evtType, handler); + } + deregisterWindowEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._window.removeEventListener(evtType, handler); + } +} + +/** + * Ensures that there is not an invalid configuration for the slider thumb inputs. + */ +function _validateInputs( + isRange: boolean, + startInputElement: HTMLInputElement, + endInputElement: HTMLInputElement): void { + if (isRange) { + if (!startInputElement.hasAttribute('matSliderStartThumb')) { + _throwInvalidInputConfigurationError(); + } + if (!endInputElement.hasAttribute('matSliderEndThumb')) { + _throwInvalidInputConfigurationError(); + } + } else { + if (!endInputElement.hasAttribute('matSliderThumb')) { + _throwInvalidInputConfigurationError(); + } + } +} + +function _throwInvalidInputConfigurationError(): void { + throw Error(`Invalid slider thumb input configuration! + + Valid configurations are as follows: + + + + + + or + + + + + + `); +} From b5adcdf1b0eefd868d8a5c13ae2b9a0f43a13efc Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Mon, 22 Feb 2021 12:48:35 -0500 Subject: [PATCH 07/35] fix(material-experimental/mdc-slider): init step on thumb inputs (#21971) * fix(material-experimental/mdc-slider): init step on thumb inputs --- .../mdc-slider/slider.ts | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 5283903f42c2..ed6a5073cc70 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -80,7 +80,7 @@ export class MatSliderThumb implements AfterViewInit { /** The current value of this slider input. */ @Input() get value(): number { - return coerceNumberProperty(this._elementRef.nativeElement.getAttribute('value')); + return coerceNumberProperty(this._hostElement.getAttribute('value')); } set value(v: number) { const value = coerceNumberProperty(v); @@ -91,7 +91,7 @@ export class MatSliderThumb implements AfterViewInit { this._slider._setValue(value, this._thumbPosition); } else { // Setup for the MDC foundation. - this._elementRef.nativeElement.setAttribute('value', `${value}`); + this._hostElement.setAttribute('value', `${value}`); } } @@ -114,36 +114,41 @@ export class MatSliderThumb implements AfterViewInit { ? Thumb.START : Thumb.END; + /** The injected document if available or fallback to the global document reference. */ private _document: Document; + /** The host native HTML input element. */ + _hostElement: HTMLInputElement; + constructor( @Inject(DOCUMENT) document: any, private readonly _slider: MatSlider, - readonly _elementRef: ElementRef, + private readonly _elementRef: ElementRef, ) { this._document = document; + this._hostElement = _elementRef.nativeElement; // By calling this in the constructor we guarantee that the sibling sliders initial value by // has already been set by the time we reach ngAfterViewInit(). this._initializeInputValueAttribute(); } ngAfterViewInit() { - this._initializeInputMinMax(); + this._initializeInputState(); this._initializeInputValueProperty(); // Setup for the MDC foundation. if (this._slider.disabled) { - this._elementRef.nativeElement.disabled = true; + this._hostElement.disabled = true; } } /** Returns true if this slider input currently has focus. */ _isFocused(): boolean { - return this._document.activeElement === this._elementRef.nativeElement; + return this._document.activeElement === this._hostElement; } /** - * Sets the min and max properties on the slider thumb input. + * Sets the min, max, and step properties on the slider thumb input. * * Must be called AFTER the sibling slider thumb input is guaranteed to have had its value * attribute value set. For a range slider, the min and max of the slider thumb input depends on @@ -155,15 +160,16 @@ export class MatSliderThumb implements AfterViewInit { * instead be capped at either the default min or max. * */ - private _initializeInputMinMax(): void { - const min = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') + private _initializeInputState(): void { + const min = this._hostElement.hasAttribute('matSliderEndThumb') ? this._slider._getInput(Thumb.START).value : this._slider.min; - const max = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') + const max = this._hostElement.hasAttribute('matSliderStartThumb') ? this._slider._getInput(Thumb.END).value : this._slider.max; - this._elementRef.nativeElement.min = `${min}`; - this._elementRef.nativeElement.max = `${max}`; + this._hostElement.min = `${min}`; + this._hostElement.max = `${max}`; + this._hostElement.step = `${this._slider.step}`; } /** @@ -175,7 +181,7 @@ export class MatSliderThumb implements AfterViewInit { * instead be capped at either the default min or max. */ private _initializeInputValueProperty(): void { - this._elementRef.nativeElement.value = `${this.value}`; + this._hostElement.value = `${this.value}`; } /** @@ -186,8 +192,8 @@ export class MatSliderThumb implements AfterViewInit { */ private _initializeInputValueAttribute(): void { // Only set the default value if an initial value has not already been provided. - if (!this._elementRef.nativeElement.hasAttribute('value')) { - this.value = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') + if (!this._hostElement.hasAttribute('value')) { + this.value = this._hostElement.hasAttribute('matSliderEndThumb') ? this._slider.max : this._slider.min; } @@ -369,7 +375,7 @@ export class MatSlider implements AfterViewInit, OnDestroy { /** Gets the slider thumb HTML input element of the given thumb position. */ _getInputElement(thumbPosition: Thumb): HTMLInputElement { - return this._getInput(thumbPosition)._elementRef.nativeElement; + return this._getInput(thumbPosition)._hostElement; } /** Gets the slider thumb HTML element of the given thumb position. */ From 51c0ff3f365132881cf2791721ec607a041813d7 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Mon, 22 Feb 2021 13:53:56 -0500 Subject: [PATCH 08/35] feat(material-experimental/mdc-slider): add slider styles (#21934) * feat(material-experimental/mdc-slider): add slider styles * implement _MatSliderMixinBase * add color input to MatSlider * extend _MatSliderMixinBase from MatSlider * use without-ripple mixin for slider.scss * @include all other mdc-slider mixins except thumb-ripple-color in _slider-theme.scss * implement primary, accent, and warn colors in _slider-theme.scss --- src/dev-app/mdc-slider/mdc-slider-demo.html | 34 +++++++- src/dev-app/mdc-slider/mdc-slider-demo.ts | 1 + .../mdc-slider/_slider-theme.scss | 84 ++++++++++++++++--- .../mdc-slider/slider.scss | 5 +- .../mdc-slider/slider.ts | 15 +++- 5 files changed, 123 insertions(+), 16 deletions(-) diff --git a/src/dev-app/mdc-slider/mdc-slider-demo.html b/src/dev-app/mdc-slider/mdc-slider-demo.html index 7d170e878b0b..b6bd930eac65 100644 --- a/src/dev-app/mdc-slider/mdc-slider-demo.html +++ b/src/dev-app/mdc-slider/mdc-slider-demo.html @@ -1,4 +1,32 @@ - - - +

Color: Primary

+ + + + + + + + + + +

Color: Accent

+ + + + + + + + + + +

Color: Warn

+ + + + + + + + diff --git a/src/dev-app/mdc-slider/mdc-slider-demo.ts b/src/dev-app/mdc-slider/mdc-slider-demo.ts index f339793bbf02..3cbd5ebe85cd 100644 --- a/src/dev-app/mdc-slider/mdc-slider-demo.ts +++ b/src/dev-app/mdc-slider/mdc-slider-demo.ts @@ -12,5 +12,6 @@ import {Component} from '@angular/core'; @Component({ selector: 'mdc-slider-demo', templateUrl: 'mdc-slider-demo.html', + styles: ['.mat-mdc-slider { width: 300px; }'], }) export class MdcSliderDemo {} diff --git a/src/material-experimental/mdc-slider/_slider-theme.scss b/src/material-experimental/mdc-slider/_slider-theme.scss index bf9f432751da..7800907da6cf 100644 --- a/src/material-experimental/mdc-slider/_slider-theme.scss +++ b/src/material-experimental/mdc-slider/_slider-theme.scss @@ -1,24 +1,46 @@ -// TODO: disabled until we implement the new MDC slider. -// @use '@material/slider' as mdc-slider; +@use 'sass:map'; + +@use '@material/slider/slider' as mdc-slider; +@use '@material/slider/slider-theme'; +@use '@material/theme/variables' as theme-variables; @use '../mdc-helpers/mdc-helpers'; @use '../../material/core/typography/typography'; +@use '../../material/core/ripple/ripple-theme'; @use '../../material/core/theming/theming'; @mixin color($config-or-theme) { $config: theming.get-color-config($config-or-theme); @include mdc-helpers.mat-using-mdc-theme($config) { - // TODO: disabled until we implement the new MDC slider. - // @include mdc-slider-core-styles($query: $mat-theme-styles-query); + @include mdc-slider.without-ripple($query: mdc-helpers.$mat-theme-styles-query); .mat-mdc-slider { + &.mat-primary, &.mat-accent, &.mat-warn { + $is-dark: map-get($config, is-dark); + $indicator-color: if($is-dark, white, black); + $indicator-text-color: if($is-dark, black, white); + $indicator-opacity: if($is-dark, 0.9, 0.6); + + @include slider-theme.value-indicator-color( + $color: $indicator-color, + $opacity: $indicator-opacity, + $query: mdc-helpers.$mat-theme-styles-query + ); + @include slider-theme.value-indicator-text-color( + $color: $indicator-text-color, + $query: mdc-helpers.$mat-theme-styles-query + ); + } + &.mat-primary { - // TODO: disabled until we implement the new MDC slider. - // @include mdc-slider-color-accessible(primary, $mat-theme-styles-query); + @include _custom-slider-color(primary, on-primary); + } + + &.mat-accent { + @include _custom-slider-color(secondary, on-secondary); } &.mat-warn { - // TODO: disabled until we implement the new MDC slider. - // @include mdc-slider-color-accessible(error, $mat-theme-styles-query); + @include _custom-slider-color(error, on-error); } } } @@ -28,8 +50,7 @@ $config: typography.private-typography-to-2018-config( theming.get-typography-config($config-or-theme)); @include mdc-helpers.mat-using-mdc-typography($config) { - // TODO: disabled until we implement the new MDC slider. - // @include mdc-slider-core-styles($query: $mat-typography-styles-query); + @include mdc-slider.without-ripple($query: mdc-helpers.$mat-typography-styles-query); } } @@ -54,3 +75,46 @@ } } +@mixin _custom-slider-color($color, $on-color) { + @include slider-theme.thumb-color( + $color-or-map: ( + default: $color, + disabled: on-surface, + ), + $query: mdc-helpers.$mat-theme-styles-query + ); + @include slider-theme.track-active-color( + $color-or-map: ( + default: $color, + disabled: on-surface, + ), + $query: mdc-helpers.$mat-theme-styles-query + ); + @include slider-theme.track-inactive-color( + $color-or-map: ( + default: $color, + disabled: on-surface, + ), + $query: mdc-helpers.$mat-theme-styles-query + ); + @include slider-theme.tick-mark-active-color( + $color-or-map: ( + default: $on-color, + disabled: surface, + ), + $query: mdc-helpers.$mat-theme-styles-query + ); + @include slider-theme.tick-mark-inactive-color( + $color-or-map: ( + default: $color, + disabled: on-surface, + ), + $query: mdc-helpers.$mat-theme-styles-query + ); + $ripple-color: map.get(theme-variables.$property-values, $color); + @include ripple-theme.color(( + foreground: ( + base: $ripple-color + ), + )); +} diff --git a/src/material-experimental/mdc-slider/slider.scss b/src/material-experimental/mdc-slider/slider.scss index eea151a252f8..e8d05b58bda9 100644 --- a/src/material-experimental/mdc-slider/slider.scss +++ b/src/material-experimental/mdc-slider/slider.scss @@ -1,7 +1,8 @@ +@use '@material/slider/slider' as mdc-slider; @use '../../cdk/a11y'; +@use '../mdc-helpers/mdc-helpers'; -@import '@material/slider/slider'; -@include core-styles; +@include mdc-slider.without-ripple($query: mdc-helpers.$mat-base-styles-query); $mat-slider-min-size: 128px !default; $mat-slider-horizontal-margin: 8px !default; diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index ed6a5073cc70..acab4ab27792 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -32,6 +32,7 @@ import { ViewChildren, ViewEncapsulation, } from '@angular/core'; +import {CanColorCtor, mixinColor} from '@angular/material/core'; import {SpecificEventListener, EventType} from '@material/base'; import {MDCSliderAdapter, MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; @@ -202,6 +203,16 @@ export class MatSliderThumb implements AfterViewInit { static ngAcceptInputType_value: NumberInput; } +// Boilerplate for applying mixins to MatSlider. +/** @docs-private */ +class MatSliderBase { + constructor(public _elementRef: ElementRef) {} +} +const _MatSliderMixinBase: + CanColorCtor & + typeof MatSliderBase = + mixinColor(MatSliderBase, 'primary'); + /** * Allows users to select from a range of values by moving the slider thumb. It is similar in * behavior to the native `` element. @@ -220,8 +231,9 @@ export class MatSliderThumb implements AfterViewInit { exportAs: 'matSlider', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, + inputs: ['color'], }) -export class MatSlider implements AfterViewInit, OnDestroy { +export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnDestroy { /** The slider thumb(s). */ @ViewChildren('thumb') _thumbs: QueryList>; @@ -320,6 +332,7 @@ export class MatSlider implements AfterViewInit, OnDestroy { readonly _elementRef: ElementRef, private readonly _platform: Platform, @Inject(DOCUMENT) document: any) { + super(_elementRef); this._document = document; this._window = this._document.defaultView || window; } From 4074c632e6f07772e5ef7c6ebc09d7d2da9af444 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Wed, 24 Feb 2021 19:31:40 -0500 Subject: [PATCH 09/35] feat(material-experimental/mdc-slider): implement slider thumb ripples (#21979) * create MatSliderVisualThumb * create slider-thumb.html & slider-thumb.scss --- .../mdc-slider/BUILD.bazel | 11 +- .../mdc-slider/_slider-theme.scss | 6 + .../mdc-slider/module.ts | 7 +- .../mdc-slider/slider-thumb.html | 7 + .../mdc-slider/slider-thumb.scss | 4 + .../mdc-slider/slider.html | 14 +- .../mdc-slider/slider.ts | 242 +++++++++++++++--- 7 files changed, 247 insertions(+), 44 deletions(-) create mode 100644 src/material-experimental/mdc-slider/slider-thumb.html create mode 100644 src/material-experimental/mdc-slider/slider-thumb.scss diff --git a/src/material-experimental/mdc-slider/BUILD.bazel b/src/material-experimental/mdc-slider/BUILD.bazel index 1e9289c09207..5aeb7db3c082 100644 --- a/src/material-experimental/mdc-slider/BUILD.bazel +++ b/src/material-experimental/mdc-slider/BUILD.bazel @@ -17,7 +17,10 @@ ng_module( ["**/*.ts"], exclude = ["**/*.spec.ts"], ), - assets = [":slider_scss"] + glob(["**/*.html"]), + assets = [ + ":slider_scss", + ":slider_thumb_scss", + ] + glob(["**/*.html"]), module_name = "@angular/material-experimental/mdc-slider", deps = [ "//src/cdk/bidi", @@ -35,6 +38,7 @@ sass_library( deps = [ "//src/cdk/a11y:a11y_scss_lib", "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib", + "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib", ], ) @@ -50,6 +54,11 @@ sass_binary( ], ) +sass_binary( + name = "slider_thumb_scss", + src = "slider-thumb.scss", +) + ########### # Testing ########### diff --git a/src/material-experimental/mdc-slider/_slider-theme.scss b/src/material-experimental/mdc-slider/_slider-theme.scss index 7800907da6cf..d24550461686 100644 --- a/src/material-experimental/mdc-slider/_slider-theme.scss +++ b/src/material-experimental/mdc-slider/_slider-theme.scss @@ -117,4 +117,10 @@ base: $ripple-color ), )); + .mat-mdc-slider-hover-ripple { + background-color: rgba(theme-variables.prop-value($color), 0.05); + } + .mat-mdc-slider-focus-ripple, .mat-mdc-slider-active-ripple { + background-color: rgba(theme-variables.prop-value($color), 0.2); + } } diff --git a/src/material-experimental/mdc-slider/module.ts b/src/material-experimental/mdc-slider/module.ts index ba3e3367ad34..1f06dffadb88 100644 --- a/src/material-experimental/mdc-slider/module.ts +++ b/src/material-experimental/mdc-slider/module.ts @@ -8,15 +8,16 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; -import {MatCommonModule} from '@angular/material-experimental/mdc-core'; -import {MatSlider, MatSliderThumb} from './slider'; +import {MatCommonModule, MatRippleModule} from '@angular/material-experimental/mdc-core'; +import {MatSlider, MatSliderThumb, MatSliderVisualThumb} from './slider'; @NgModule({ - imports: [MatCommonModule, CommonModule], + imports: [MatCommonModule, CommonModule, MatRippleModule], exports: [MatSlider, MatSliderThumb], declarations: [ MatSlider, MatSliderThumb, + MatSliderVisualThumb, ], }) export class MatSliderModule { diff --git a/src/material-experimental/mdc-slider/slider-thumb.html b/src/material-experimental/mdc-slider/slider-thumb.html new file mode 100644 index 000000000000..da92e3e64b9c --- /dev/null +++ b/src/material-experimental/mdc-slider/slider-thumb.html @@ -0,0 +1,7 @@ +
+
+ {{valueIndicatorText}} +
+
+
+
diff --git a/src/material-experimental/mdc-slider/slider-thumb.scss b/src/material-experimental/mdc-slider/slider-thumb.scss new file mode 100644 index 000000000000..e5cd1605936a --- /dev/null +++ b/src/material-experimental/mdc-slider/slider-thumb.scss @@ -0,0 +1,4 @@ +.mat-mdc-slider-visual-thumb .mat-ripple { + height: 100%; + width: 100%; +} diff --git a/src/material-experimental/mdc-slider/slider.html b/src/material-experimental/mdc-slider/slider.html index b8f42c85be02..037735923859 100644 --- a/src/material-experimental/mdc-slider/slider.html +++ b/src/material-experimental/mdc-slider/slider.html @@ -13,11 +13,9 @@
-
-
-
- {{_getValueIndicatorText(thumb)}} -
-
-
-
+ + diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index acab4ab27792..59f4f91aa527 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -25,6 +25,7 @@ import { EventEmitter, Inject, Input, + NgZone, OnDestroy, Output, QueryList, @@ -32,7 +33,14 @@ import { ViewChildren, ViewEncapsulation, } from '@angular/core'; -import {CanColorCtor, mixinColor} from '@angular/material/core'; +import { + CanColorCtor, + MatRipple, + mixinColor, + RippleAnimationConfig, + RippleRef, + RippleState, +} from '@angular/material/core'; import {SpecificEventListener, EventType} from '@material/base'; import {MDCSliderAdapter, MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; @@ -48,6 +56,186 @@ export interface MatSliderDragEvent { value: number; } +/** + * The visual slider thumb. + * + * Handles the slider thumb ripple states (hover, focus, and active), + * and displaying the value tooltip on discrete sliders. + * @docs-private + */ +@Component({ + selector: 'mat-slider-visual-thumb', + templateUrl: './slider-thumb.html', + styleUrls: ['slider-thumb.css'], + host: { + 'class': 'mdc-slider__thumb mat-mdc-slider-visual-thumb', + }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, +}) +export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { + /** Whether the slider displays a numeric value label upon pressing the thumb. */ + @Input() discrete: boolean; + + /** Indicates which slider thumb this input corresponds to. */ + @Input() thumbPosition: Thumb; + + /** The display value of the slider thumb. */ + @Input() valueIndicatorText: string; + + /** The MatRipple for this slider thumb. */ + @ViewChild(MatRipple) private readonly _ripple: MatRipple; + + /** The slider thumb knob */ + @ViewChild('knob') _knob: ElementRef; + + /** The slider input corresponding to this slider thumb. */ + private _sliderInput: MatSliderThumb; + + /** The RippleRef for the slider thumbs hover state. */ + private _hoverRippleRef: RippleRef; + + /** The RippleRef for the slider thumbs focus state. */ + private _focusRippleRef: RippleRef; + + /** The RippleRef for the slider thumbs active state. */ + private _activeRippleRef: RippleRef; + + /** Whether the slider thumb is currently being pressed. */ + private _isActive: boolean = false; + + /** Whether the slider thumb is currently being hovered. */ + private _isHovered: boolean = false; + + constructor( + private readonly _ngZone: NgZone, + private readonly _slider: MatSlider, + private readonly _elementRef: ElementRef) {} + + ngAfterViewInit() { + this._ripple.radius = 24; + this._sliderInput = this._slider._getInput(this.thumbPosition); + + this._sliderInput.dragStart.subscribe((e: MatSliderDragEvent) => this._onDragStart(e)); + this._sliderInput.dragEnd.subscribe((e: MatSliderDragEvent) => this._onDragEnd(e)); + + this._sliderInput._focus.subscribe(() => this._onFocus()); + this._sliderInput._blur.subscribe(() => this._onBlur()); + + // These two listeners don't update any data bindings so we bind them + // outside of the NgZone to pervent angular from needlessly running change detection. + this._ngZone.runOutsideAngular(() => { + this._elementRef.nativeElement.addEventListener('mouseenter', this._onMouseEnter.bind(this)); + this._elementRef.nativeElement.addEventListener('mouseleave', this._onMouseLeave.bind(this)); + }); + } + + ngOnDestroy() { + this._sliderInput.dragStart.unsubscribe(); + this._sliderInput.dragEnd.unsubscribe(); + this._sliderInput._focus.unsubscribe(); + this._sliderInput._blur.unsubscribe(); + this._elementRef.nativeElement.removeEventListener('mouseenter', this._onMouseEnter); + this._elementRef.nativeElement.removeEventListener('mouseleave', this._onMouseLeave); + } + + private _onMouseEnter(): void { + this._isHovered = true; + // We don't want to show the hover ripple on top of the focus ripple. + // This can happen if the user tabs to a thumb and then the user moves their cursor over it. + if (!this._isShowingRipple(this._focusRippleRef)) { + this._showHoverRipple(); + } + } + + private _onMouseLeave(): void { + this._isHovered = false; + this._hoverRippleRef?.fadeOut(); + } + + private _onFocus(): void { + // We don't want to show the hover ripple on top of the focus ripple. + // Happen when the users cursor is over a thumb and then the user tabs to it. + this._hoverRippleRef?.fadeOut(); + this._showFocusRipple(); + } + + private _onBlur(): void { + // Happens when the user tabs away while still dragging a thumb. + if (!this._isActive) { + this._focusRippleRef?.fadeOut(); + } + // Happens when the user tabs away from a thumb but their cursor is still over it. + if (this._isHovered) { + this._showHoverRipple(); + } + } + + private _onDragStart(event: MatSliderDragEvent): void { + if (event.source._thumbPosition === this.thumbPosition) { + this._isActive = true; + this._showActiveRipple(); + } + } + + private _onDragEnd(event: MatSliderDragEvent): void { + if (event.source._thumbPosition === this.thumbPosition) { + this._isActive = false; + this._activeRippleRef?.fadeOut(); + // Happens when the user starts dragging a thumb, tabs away, and then stops dragging. + if (!this._sliderInput._isFocused()) { + this._focusRippleRef?.fadeOut(); + } + } + } + + /** Handles displaying the hover ripple. */ + private _showHoverRipple(): void { + if (!this._isShowingRipple(this._hoverRippleRef)) { + this._hoverRippleRef = this._showRipple({ enterDuration: 0, exitDuration: 0 }); + this._hoverRippleRef.element.classList.add('mat-mdc-slider-hover-ripple'); + } + } + + /** Handles displaying the focus ripple. */ + private _showFocusRipple(): void { + if (!this._isShowingRipple(this._focusRippleRef)) { + this._focusRippleRef = this._showRipple({ enterDuration: 0, exitDuration: 0 }); + this._focusRippleRef.element.classList.add('mat-mdc-slider-focus-ripple'); + } + } + + /** Handles displaying the active ripple. */ + private _showActiveRipple(): void { + if (!this._isShowingRipple(this._activeRippleRef)) { + this._activeRippleRef = this._showRipple({ enterDuration: 225, exitDuration: 400 }); + this._activeRippleRef.element.classList.add('mat-mdc-slider-active-ripple'); + } + } + + /** Whether the given rippleRef is currently fading in or visible. */ + private _isShowingRipple(rippleRef?: RippleRef): boolean { + return rippleRef?.state === RippleState.FADING_IN || rippleRef?.state === RippleState.VISIBLE; + } + + /** Manually launches the slider thumb ripple using the specified ripple animation config. */ + private _showRipple(animation: RippleAnimationConfig): RippleRef { + return this._ripple.launch( + {animation, centered: true, persistent: true}, + ); + } + + /** Gets the hosts native HTML element. */ + _getHostElement(): HTMLElement { + return this._elementRef.nativeElement; + } + + /** Gets the native HTML element of the slider thumb knob. */ + _getKnob(): HTMLElement { + return this._knob.nativeElement; + } +} + /** * Directive that adds slider-specific behaviors to an input element inside ``. * Up to two may be placed inside of a ``. @@ -111,7 +299,7 @@ export class MatSliderThumb implements AfterViewInit { @Output() readonly _focus: EventEmitter = new EventEmitter(); /** Indicates which slider thumb this input corresponds to. */ - private _thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') + _thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') ? Thumb.START : Thumb.END; @@ -235,14 +423,7 @@ const _MatSliderMixinBase: }) export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnDestroy { /** The slider thumb(s). */ - @ViewChildren('thumb') _thumbs: QueryList>; - - /** The slider thumb knob(s) */ - @ViewChildren('knob') _knobs: QueryList>; - - /** The span containing the slider thumb value indicator text */ - @ViewChildren('valueIndicatorTextElement') - _valueIndicatorTextElements: QueryList>; + @ViewChildren(MatSliderVisualThumb) _thumbs: QueryList; /** The active section of the slider track. */ @ViewChild('trackActive') _trackActive: ElementRef; @@ -301,7 +482,7 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD * in the thumb label. Can be used to format very large number in order * for them to fit into the slider thumb. */ - @Input() displayWith: ((value: number) => string) | null; + @Input() displayWith: ((value: number) => string) = (value: number) => `${value}`; /** Instance of the MDC slider foundation for this slider. */ private _foundation = new MDCSliderFoundation(new SliderAdapter(this)); @@ -322,10 +503,10 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD _tickMarks: TickMark[]; /** The display value of the start thumb. */ - private _startValueIndicatorText: string; + _startValueIndicatorText: string; /** The display value of the end thumb. */ - private _endValueIndicatorText: string; + _endValueIndicatorText: string; constructor( readonly _cdr: ChangeDetectorRef, @@ -383,7 +564,7 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD /** Gets the slider thumb input of the given thumb position. */ _getInput(thumbPosition: Thumb): MatSliderThumb { - return thumbPosition === Thumb.END ? this._inputs.last! : this._inputs.first!; + return thumbPosition === Thumb.END ? this._inputs.last : this._inputs.first; } /** Gets the slider thumb HTML input element of the given thumb position. */ @@ -391,22 +572,18 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD return this._getInput(thumbPosition)._hostElement; } + private _getThumb(thumbPosition: Thumb): MatSliderVisualThumb { + return thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first; + } + /** Gets the slider thumb HTML element of the given thumb position. */ _getThumbElement(thumbPosition: Thumb): HTMLElement { - const thumbElementRef = thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first; - return thumbElementRef.nativeElement; + return this._getThumb(thumbPosition)._getHostElement(); } /** Gets the slider knob HTML element of the given thumb position. */ _getKnobElement(thumbPosition: Thumb): HTMLElement { - const knobElementRef = thumbPosition === Thumb.END ? this._knobs.last : this._knobs.first; - return knobElementRef.nativeElement; - } - - _getValueIndicatorText(thumbPosition: Thumb) { - return thumbPosition === Thumb.START - ? this._startValueIndicatorText - : this._endValueIndicatorText; + return this._getThumb(thumbPosition)._getKnob(); } /** @@ -416,10 +593,16 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD * numeric value as a string. */ _setValueIndicatorText(value: number, thumbPosition: Thumb) { - const valueText = this.displayWith ? this.displayWith(value) : `${value}`; thumbPosition === Thumb.START - ? this._startValueIndicatorText = valueText - : this._endValueIndicatorText = valueText; + ? this._startValueIndicatorText = this.displayWith(value) + : this._endValueIndicatorText = this.displayWith(value); + } + + /** Gets the value indicator text for the given thumb position. */ + _getValueIndicatorText(thumbPosition: Thumb): string { + return thumbPosition === Thumb.START + ? this._startValueIndicatorText + : this._endValueIndicatorText; } /** Determines the class name for a HTML element. */ @@ -429,11 +612,6 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD : 'mdc-slider__tick-mark--inactive'; } - /** Returns an array of the thumb types that exist on the current slider instance. */ - _getThumbTypes(): Thumb[] { - return this._isRange() ? [Thumb.START, Thumb.END] : [Thumb.END]; - } - static ngAcceptInputType_disabled: BooleanInput; static ngAcceptInputType_discrete: BooleanInput; static ngAcceptInputType_showTickMarks: BooleanInput; From 5fdea862376193989b4d33ebdf48b7db49108f5e Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Mon, 1 Mar 2021 16:01:53 -0500 Subject: [PATCH 10/35] feat(material-experimental/mdc-slider): implement control value accessor (#22016) * feat(material-experimental/mdc-slider): implement control value accessor --- .../mdc-slider/slider.ts | 74 ++++++++++++++++++- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 59f4f91aa527..4d3c96343a08 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -33,6 +33,7 @@ import { ViewChildren, ViewEncapsulation, } from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import { CanColorCtor, MatRipple, @@ -249,11 +250,16 @@ export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { host: { 'class': 'mdc-slider__input', 'type': 'range', - '(blur)': '_blur.emit()', + '(blur)': '_onBlur()', '(focus)': '_focus.emit()', }, + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: MatSliderThumb, + multi: true + }], }) -export class MatSliderThumb implements AfterViewInit { +export class MatSliderThumb implements AfterViewInit, ControlValueAccessor { // ** IMPORTANT NOTE ** // @@ -298,6 +304,20 @@ export class MatSliderThumb implements AfterViewInit { /** Event emitted every time the MatSliderThumb is focused. */ @Output() readonly _focus: EventEmitter = new EventEmitter(); + _disabled: boolean = false; + + /** + * A callback function that is called when the + * control's value changes in the UI (ControlValueAccessor). + */ + _onChange: (value: any) => void = () => {}; + + /** + * A callback function that is called by the forms API on + * initialization to update the form model on blur (ControlValueAccessor). + */ + private _onTouched: () => void = () => {}; + /** Indicates which slider thumb this input corresponds to. */ _thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') ? Thumb.START @@ -331,6 +351,47 @@ export class MatSliderThumb implements AfterViewInit { } } + _onBlur(): void { + this._onTouched(); + this._blur.emit(); + } + + /** + * Sets the model value. Implemented as part of ControlValueAccessor. + * @param value + */ + writeValue(value: any): void { + this.value = value; + } + + /** + * Registers a callback to be triggered when the value has changed. + * Implemented as part of ControlValueAccessor. + * @param fn Callback to be registered. + */ + registerOnChange(fn: any): void { + this._onChange = fn; + } + + /** + * Registers a callback to be triggered when the component is touched. + * Implemented as part of ControlValueAccessor. + * @param fn Callback to be registered. + */ + registerOnTouched(fn: any): void { + this._onTouched = fn; + } + + /** + * Sets whether the component should be disabled. + * Implemented as part of ControlValueAccessor. + * @param isDisabled + */ + setDisabledState(isDisabled: boolean): void { + this._disabled = isDisabled; + this._slider._updateDisabled(); + } + /** Returns true if this slider input currently has focus. */ _isFocused(): boolean { return this._document.activeElement === this._hostElement; @@ -562,6 +623,11 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD return this._inputs.length === 2; } + /** Sets the disabled state based on the disabled state of the inputs (ControlValueAccessor). */ + _updateDisabled(): void { + this.disabled = this._inputs.some(input => input._disabled); + } + /** Gets the slider thumb input of the given thumb position. */ _getInput(thumbPosition: Thumb): MatSliderThumb { return thumbPosition === Thumb.END ? this._inputs.last : this._inputs.first; @@ -709,7 +775,9 @@ class SliderAdapter implements MDCSliderAdapter { } // We ignore emitChangeEvent and emitInputEvent because the slider inputs // are already exposed so users can just listen for those events directly themselves. - emitChangeEvent = (value: number, thumbPosition: Thumb): void => {}; + emitChangeEvent = (value: number, thumbPosition: Thumb): void => { + this._delegate._getInput(thumbPosition)._onChange(value); + } emitInputEvent = (value: number, thumbPosition: Thumb): void => {}; emitDragStartEvent = (value: number, thumbPosition: Thumb): void => { const input = this._delegate._getInput(thumbPosition); From 20a4c7d5dbba9567d22261934114b27865d0e2b8 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Tue, 9 Mar 2021 14:41:27 -0800 Subject: [PATCH 11/35] feat(material-experimental/mdc-slider): implement some basic unit tests (#22072) * feat(material-experimental/mdc-slider): implement some basic unit tests * implement unit tests for the standard slider, standard range slider, and for the slider ripple states * add mdc-slider theme to all-theme * use #waitForAsync to wait for foundation to finish initializing & layout * use forwardRef to avoid injection errors that only throw on ci * disable the mat ripple on the slider thumbs to prevent the automatic launch that happens on click/touch the problem is easily reproduced if you undo this change and test it out on a mobile device. * note: we use touch events instead of pointer events when testing on ios because pointerdown, pointerup, and pointermove are not supported --- .../testbed/fake-events/dispatch-events.ts | 5 +- .../testbed/fake-events/event-objects.ts | 4 +- .../mdc-slider/BUILD.bazel | 2 + .../mdc-slider/slider-thumb.html | 2 +- .../mdc-slider/slider.spec.ts | 365 +++++++++++++++++- .../mdc-slider/slider.ts | 10 +- .../mdc-theming/_all-theme.scss | 2 + 7 files changed, 375 insertions(+), 15 deletions(-) diff --git a/src/cdk/testing/testbed/fake-events/dispatch-events.ts b/src/cdk/testing/testbed/fake-events/dispatch-events.ts index ee4759ac0679..4063386d1215 100644 --- a/src/cdk/testing/testbed/fake-events/dispatch-events.ts +++ b/src/cdk/testing/testbed/fake-events/dispatch-events.ts @@ -65,6 +65,7 @@ export function dispatchPointerEvent(node: Node, type: string, clientX = 0, clie * Shorthand to dispatch a touch event on the specified coordinates. * @docs-private */ -export function dispatchTouchEvent(node: Node, type: string, x = 0, y = 0) { - return dispatchEvent(node, createTouchEvent(type, x, y)); +export function dispatchTouchEvent(node: Node, type: string, pageX = 0, pageY = 0, clientX = 0, + clientY = 0) { + return dispatchEvent(node, createTouchEvent(type, pageX, pageY, clientX, clientY)); } diff --git a/src/cdk/testing/testbed/fake-events/event-objects.ts b/src/cdk/testing/testbed/fake-events/event-objects.ts index f211df2920f6..a22cb64be43c 100644 --- a/src/cdk/testing/testbed/fake-events/event-objects.ts +++ b/src/cdk/testing/testbed/fake-events/event-objects.ts @@ -79,11 +79,11 @@ export function createPointerEvent(type: string, clientX = 0, clientY = 0, * Creates a browser TouchEvent with the specified pointer coordinates. * @docs-private */ -export function createTouchEvent(type: string, pageX = 0, pageY = 0) { +export function createTouchEvent(type: string, pageX = 0, pageY = 0, clientX = 0, clientY = 0) { // In favor of creating events that work for most of the browsers, the event is created // as a basic UI Event. The necessary details for the event will be set manually. const event = document.createEvent('UIEvent'); - const touchDetails = {pageX, pageY}; + const touchDetails = {pageX, pageY, clientX, clientY}; // TS3.6 removes the initUIEvent method and suggests porting to "new UIEvent()". (event as any).initUIEvent(type, true, true, window, 0); diff --git a/src/material-experimental/mdc-slider/BUILD.bazel b/src/material-experimental/mdc-slider/BUILD.bazel index 5aeb7db3c082..cb14d0a3ba2c 100644 --- a/src/material-experimental/mdc-slider/BUILD.bazel +++ b/src/material-experimental/mdc-slider/BUILD.bazel @@ -75,8 +75,10 @@ ng_test_library( "//src/cdk/keycodes", "//src/cdk/platform", "//src/cdk/testing/private", + "//src/material/core", "@npm//@angular/forms", "@npm//@angular/platform-browser", + "@npm//@material/slider", ], ) diff --git a/src/material-experimental/mdc-slider/slider-thumb.html b/src/material-experimental/mdc-slider/slider-thumb.html index da92e3e64b9c..595c373fe1f7 100644 --- a/src/material-experimental/mdc-slider/slider-thumb.html +++ b/src/material-experimental/mdc-slider/slider-thumb.html @@ -4,4 +4,4 @@
-
+
diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index e702f5243e95..8ae842f776e4 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -6,14 +6,369 @@ * found in the LICENSE file at https://angular.io/license */ +import {Platform} from '@angular/cdk/platform'; +import { + dispatchMouseEvent, + dispatchPointerEvent, + dispatchTouchEvent, +} from '@angular/cdk/testing/private'; +import {Component, DebugElement, Type} from '@angular/core'; +import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {Thumb} from '@material/slider'; +import {MatSliderModule} from './module'; +import {MatSlider, MatSliderThumb, MatSliderVisualThumb} from './slider'; -/* tslint:disable-next-line:no-unused-variable */ -import {MatSlider} from './index'; +describe('MDC-based MatSlider' , () => { + let platform: Platform; + + beforeAll(() => { + platform = TestBed.inject(Platform); + // Mock #setPointerCapture as it throws errors on pointerdown without a real pointerId. + spyOn(Element.prototype, 'setPointerCapture'); + }); -// TODO(wagnermaciel): Implement this in a separate PR + function createComponent(component: Type): ComponentFixture { + TestBed.configureTestingModule({ + imports: [MatSliderModule], + declarations: [component], + }).compileComponents(); + return TestBed.createComponent(component); + } -describe('MDC-based MatSlider' , () => { describe('standard slider', () => { - it('does nothing yet', () => {}); + let fixture: ComponentFixture; + let sliderDebugElement: DebugElement; + let sliderInstance: MatSlider; + let inputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + fixture = createComponent(StandardSlider); + fixture.detectChanges(); + sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + inputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should set the default values', () => { + expect(inputInstance.value).toBe(0); + expect(sliderInstance.min).toBe(0); + expect(sliderInstance.max).toBe(100); + }); + + it('should update the value on mousedown', () => { + setValueByClick(sliderInstance, 19, platform.IOS); + expect(inputInstance.value).toBe(19); + }); + + it('should update the value on a slide', () => { + slideToValue(sliderInstance, 77, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(77); + }); + + it('should set the value as min when sliding before the track', () => { + slideToValue(sliderInstance, -1, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(0); + }); + + it('should set the value as max when sliding past the track', () => { + slideToValue(sliderInstance, 101, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(100); + }); + + it('should focus the slider input when clicking on the slider', () => { + expect(document.activeElement).not.toBe(inputInstance._hostElement); + setValueByClick(sliderInstance, 0, platform.IOS); + expect(document.activeElement).toBe(inputInstance._hostElement); + }); + }); + + describe('standard range slider', () => { + let fixture: ComponentFixture; + let sliderDebugElement: DebugElement; + let sliderInstance: MatSlider; + let startInputInstance: MatSliderThumb; + let endInputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + fixture = createComponent(StandardRangeSlider); + fixture.detectChanges(); + sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + startInputInstance = sliderInstance._getInput(Thumb.START); + endInputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should set the default values', () => { + expect(startInputInstance.value).toBe(0); + expect(endInputInstance.value).toBe(100); + expect(sliderInstance.min).toBe(0); + expect(sliderInstance.max).toBe(100); + }); + + it('should update the start value on a slide', () => { + slideToValue(sliderInstance, 19, Thumb.START, platform.IOS); + expect(startInputInstance.value).toBe(19); + }); + + it('should update the end value on a slide', () => { + slideToValue(sliderInstance, 27, Thumb.END, platform.IOS); + expect(endInputInstance.value).toBe(27); + }); + + it('should update the start value on mousedown behind the start thumb', () => { + sliderInstance._setValue(19, Thumb.START); + setValueByClick(sliderInstance, 12, platform.IOS); + expect(startInputInstance.value).toBe(12); + }); + + it('should update the end value on mousedown in front of the end thumb', () => { + sliderInstance._setValue(27, Thumb.END); + setValueByClick(sliderInstance, 55, platform.IOS); + expect(endInputInstance.value).toBe(55); + }); + + it('should set the start value as min when sliding before the track', () => { + slideToValue(sliderInstance, -1, Thumb.START, platform.IOS); + expect(startInputInstance.value).toBe(0); + }); + + it('should set the end value as max when sliding past the track', () => { + slideToValue(sliderInstance, 101, Thumb.START, platform.IOS); + expect(startInputInstance.value).toBe(100); + }); + + it('should not let the start thumb slide past the end thumb', () => { + sliderInstance._setValue(50, Thumb.END); + slideToValue(sliderInstance, 75, Thumb.START, platform.IOS); + expect(startInputInstance.value).toBe(50); + }); + + it('should not let the end thumb slide before the start thumb', () => { + sliderInstance._setValue(50, Thumb.START); + slideToValue(sliderInstance, 25, Thumb.END, platform.IOS); + expect(startInputInstance.value).toBe(50); + }); + }); + + describe('ripple states', () => { + let fixture: ComponentFixture; + let inputInstance: MatSliderThumb; + let thumbInstance: MatSliderVisualThumb; + let thumbElement: HTMLElement; + let thumbX: number; + let thumbY: number; + + beforeEach(waitForAsync(() => { + fixture = createComponent(StandardSlider); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + const sliderInstance = sliderDebugElement.componentInstance; + inputInstance = sliderInstance._getInput(Thumb.END); + thumbInstance = sliderInstance._getThumb(Thumb.END); + thumbElement = thumbInstance._getHostElement(); + const thumbDimensions = thumbElement.getBoundingClientRect(); + thumbX = thumbDimensions.left - (thumbDimensions.width / 2); + thumbY = thumbDimensions.top - (thumbDimensions.height / 2); + })); + + function isRippleVisible(selector: string) { + tick(500); + return !!document.querySelector(`.mat-mdc-slider-${selector}-ripple`); + } + + function blur() { + inputInstance._hostElement.blur(); + } + + function mouseenter() { + dispatchMouseEvent(thumbElement, 'mouseenter', thumbX, thumbY); + } + + function mouseleave() { + dispatchMouseEvent(thumbElement, 'mouseleave', thumbX, thumbY); + } + + function pointerdown() { + dispatchPointerOrTouchEvent( + thumbElement, PointerEventType.POINTER_DOWN, thumbX, thumbY, platform.IOS + ); + } + + function pointerup() { + dispatchPointerOrTouchEvent( + thumbElement, PointerEventType.POINTER_UP, thumbX, thumbY, platform.IOS + ); + } + + it('should show the hover ripple on mouseenter', fakeAsync(() => { + expect(isRippleVisible('hover')).toBe(false); + mouseenter(); + expect(isRippleVisible('hover')).toBe(true); + })); + + it('should hide the hover ripple on mouseleave', fakeAsync(() => { + mouseenter(); + mouseleave(); + expect(isRippleVisible('hover')).toBe(false); + })); + + it('should show the focus ripple on pointerdown', fakeAsync(() => { + expect(isRippleVisible('focus')).toBe(false); + pointerdown(); + expect(isRippleVisible('focus')).toBe(true); + })); + + it('should continue to show the focus ripple on pointerup', fakeAsync(() => { + pointerdown(); + pointerup(); + expect(isRippleVisible('focus')).toBe(true); + })); + + it('should hide the focus ripple on blur', fakeAsync(() => { + pointerdown(); + pointerup(); + blur(); + expect(isRippleVisible('focus')).toBe(false); + })); + + it('should show the active ripple on pointerdown', fakeAsync(() => { + expect(isRippleVisible('active')).toBe(false); + pointerdown(); + expect(isRippleVisible('active')).toBe(true); + })); + + it('should hide the active ripple on pointerup', fakeAsync(() => { + pointerdown(); + pointerup(); + expect(isRippleVisible('active')).toBe(false); + })); + + // Edge cases. + + it('should not show the hover ripple if the thumb is already focused', fakeAsync(() => { + pointerdown(); + mouseenter(); + expect(isRippleVisible('hover')).toBe(false); + })); + + it('should hide the hover ripple if the thumb is focused', fakeAsync(() => { + mouseenter(); + pointerdown(); + expect(isRippleVisible('hover')).toBe(false); + })); + + it('should not hide the focus ripple if the thumb is pressed', fakeAsync(() => { + pointerdown(); + blur(); + expect(isRippleVisible('focus')).toBe(true); + })); + + it('should not hide the hover ripple on blur if the thumb is hovered', fakeAsync(() => { + mouseenter(); + pointerdown(); + pointerup(); + blur(); + expect(isRippleVisible('hover')).toBe(true); + })); + + it('should hide the focus ripple on drag end if the thumb already lost focus', fakeAsync(() => { + pointerdown(); + blur(); + pointerup(); + expect(isRippleVisible('focus')).toBe(false); + })); }); }); + + +@Component({ + template: ` + + + + `, +}) +class StandardSlider {} + +@Component({ + template: ` + + + + + `, +}) +class StandardRangeSlider {} + +/** The pointer event types used by the MDC Slider. */ +const enum PointerEventType { + POINTER_DOWN = 'pointerdown', + POINTER_UP = 'pointerup', + POINTER_MOVE = 'pointermove', +} + +/** The touch event types used by the MDC Slider. */ +const enum TouchEventType { + TOUCH_START = 'touchstart', + TOUCH_END = 'touchend', + TOUCH_MOVE = 'touchmove', +} + +/** Clicks on the MatSlider at the coordinates corresponding to the given value. */ +function setValueByClick(slider: MatSlider, value: number, isIOS: boolean) { + const {min, max} = slider; + const percent = (value - min) / (max - min); + + const sliderElement = slider._elementRef.nativeElement; + const {top, left, width, height} = sliderElement.getBoundingClientRect(); + const x = left + (width * percent); + const y = top + (height / 2); + + dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_DOWN, x, y, isIOS); + dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, x, y, isIOS); +} + +/** Slides the MatSlider's thumb to the given value. */ +function slideToValue(slider: MatSlider, value: number, thumbPosition: Thumb, isIOS: boolean) { + const {min, max} = slider; + const percent = (value - min) / (max - min); + + const sliderElement = slider._elementRef.nativeElement; + const thumbElement = slider._getThumbElement(thumbPosition); + + const sliderDimensions = sliderElement.getBoundingClientRect(); + let thumbDimensions = thumbElement.getBoundingClientRect(); + + const startX = thumbDimensions.left + (thumbDimensions.width / 2); + const startY = thumbDimensions.top + (thumbDimensions.height / 2); + + const endX = sliderDimensions.left + (sliderDimensions.width * percent); + const endY = sliderDimensions.top + (sliderDimensions.height / 2); + + dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_DOWN, startX, startY, isIOS); + dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_MOVE, endX, endY, isIOS); + dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, endX, endY, isIOS); +} + +/** Dispatch a pointerdown or pointerup event if supported, otherwise dispatch the touch event. */ +function dispatchPointerOrTouchEvent( + node: Node, type: PointerEventType, x: number, y: number, isIOS: boolean) { + if (isIOS) { + dispatchTouchEvent(node, pointerEventTypeToTouchEventType(type), x, y, x, y); + } else { + dispatchPointerEvent(node, type, x, y); + } +} + +/** Returns the touch event equivalent of the given pointer event. */ +function pointerEventTypeToTouchEventType(pointerEventType: PointerEventType) { + switch (pointerEventType) { + case PointerEventType.POINTER_DOWN: + return TouchEventType.TOUCH_START; + case PointerEventType.POINTER_UP: + return TouchEventType.TOUCH_END; + case PointerEventType.POINTER_MOVE: + return TouchEventType.TOUCH_MOVE; + } +} diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 4d3c96343a08..90753fbaf329 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -23,6 +23,7 @@ import { Directive, ElementRef, EventEmitter, + forwardRef, Inject, Input, NgZone, @@ -110,7 +111,7 @@ export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { constructor( private readonly _ngZone: NgZone, - private readonly _slider: MatSlider, + @Inject(forwardRef(() => MatSlider)) private readonly _slider: MatSlider, private readonly _elementRef: ElementRef) {} ngAfterViewInit() { @@ -331,9 +332,8 @@ export class MatSliderThumb implements AfterViewInit, ControlValueAccessor { constructor( @Inject(DOCUMENT) document: any, - private readonly _slider: MatSlider, - private readonly _elementRef: ElementRef, - ) { + @Inject(forwardRef(() => MatSlider)) private readonly _slider: MatSlider, + private readonly _elementRef: ElementRef) { this._document = document; this._hostElement = _elementRef.nativeElement; // By calling this in the constructor we guarantee that the sibling sliders initial value by @@ -638,7 +638,7 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD return this._getInput(thumbPosition)._hostElement; } - private _getThumb(thumbPosition: Thumb): MatSliderVisualThumb { + _getThumb(thumbPosition: Thumb): MatSliderVisualThumb { return thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first; } diff --git a/src/material-experimental/mdc-theming/_all-theme.scss b/src/material-experimental/mdc-theming/_all-theme.scss index b50de0f52882..4d9068e3afd4 100644 --- a/src/material-experimental/mdc-theming/_all-theme.scss +++ b/src/material-experimental/mdc-theming/_all-theme.scss @@ -10,6 +10,7 @@ @use '../mdc-radio/radio-theme'; @use '../mdc-select/select-theme'; @use '../mdc-slide-toggle/slide-toggle-theme'; +@use '../mdc-slider/slider-theme'; @use '../mdc-snack-bar/snack-bar-theme'; @use '../mdc-tabs/tabs-theme'; @use '../mdc-table/table-theme'; @@ -41,6 +42,7 @@ @include radio-theme.theme($theme-or-color-config); @include select-theme.theme($theme-or-color-config); @include slide-toggle-theme.theme($theme-or-color-config); + @include slider-theme.theme($theme-or-color-config); @include snack-bar-theme.theme($theme-or-color-config); @include table-theme.theme($theme-or-color-config); @include form-field-theme.theme($theme-or-color-config); From 005a76ca09a3f2f44bfebfea082f209339826452 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Tue, 9 Mar 2021 15:20:33 -0800 Subject: [PATCH 12/35] feat(material-experimental/mdc-slider): add unit tests for disabled slider (#22168) --- .../mdc-slider/slider.spec.ts | 134 ++++++++++++++++-- 1 file changed, 119 insertions(+), 15 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 8ae842f776e4..340e1186b96b 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -151,6 +151,91 @@ describe('MDC-based MatSlider' , () => { }); }); + describe('disabled slider', () => { + let sliderInstance: MatSlider; + let inputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + const fixture = createComponent(DisabledSlider); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + inputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should be disabled', () => { + expect(sliderInstance.disabled).toBeTrue(); + }); + + it('should have the disabled class on the root element', () => { + expect(sliderInstance._elementRef.nativeElement.classList).toContain('mdc-slider--disabled'); + }); + + it('should set the disabled attribute on the input element', () => { + expect(inputInstance._hostElement.disabled).toBeTrue(); + }); + + it('should not update the value on mousedown', () => { + setValueByClick(sliderInstance, 19, platform.IOS); + expect(inputInstance.value).toBe(0); + }); + + it('should not update the value on a slide', () => { + slideToValue(sliderInstance, 77, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(0); + }); + }); + + describe('disabled range slider', () => { + let sliderInstance: MatSlider; + let startInputInstance: MatSliderThumb; + let endInputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + const fixture = createComponent(DisabledRangeSlider); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + startInputInstance = sliderInstance._getInput(Thumb.START); + endInputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should be disabled', () => { + expect(sliderInstance.disabled).toBeTrue(); + }); + + it('should have the disabled class on the root element', () => { + expect(sliderInstance._elementRef.nativeElement.classList).toContain('mdc-slider--disabled'); + }); + + it('should set the disabled attribute on the input elements', () => { + expect(startInputInstance._hostElement.disabled).toBeTrue(); + expect(endInputInstance._hostElement.disabled).toBeTrue(); + }); + + it('should not update the start value on a slide', () => { + slideToValue(sliderInstance, 19, Thumb.START, platform.IOS); + expect(startInputInstance.value).toBe(0); + }); + + it('should not update the end value on a slide', () => { + slideToValue(sliderInstance, 27, Thumb.END, platform.IOS); + expect(endInputInstance.value).toBe(100); + }); + + it('should not update the start value on mousedown behind the start thumb', () => { + sliderInstance._setValue(19, Thumb.START); + setValueByClick(sliderInstance, 12, platform.IOS); + expect(startInputInstance.value).toBe(19); + }); + + it('should update the end value on mousedown in front of the end thumb', () => { + sliderInstance._setValue(27, Thumb.END); + setValueByClick(sliderInstance, 55, platform.IOS); + expect(endInputInstance.value).toBe(27); + }); + }); + describe('ripple states', () => { let fixture: ComponentFixture; let inputInstance: MatSliderThumb; @@ -202,46 +287,46 @@ describe('MDC-based MatSlider' , () => { } it('should show the hover ripple on mouseenter', fakeAsync(() => { - expect(isRippleVisible('hover')).toBe(false); + expect(isRippleVisible('hover')).toBeFalse(); mouseenter(); - expect(isRippleVisible('hover')).toBe(true); + expect(isRippleVisible('hover')).toBeTrue(); })); it('should hide the hover ripple on mouseleave', fakeAsync(() => { mouseenter(); mouseleave(); - expect(isRippleVisible('hover')).toBe(false); + expect(isRippleVisible('hover')).toBeFalse(); })); it('should show the focus ripple on pointerdown', fakeAsync(() => { - expect(isRippleVisible('focus')).toBe(false); + expect(isRippleVisible('focus')).toBeFalse(); pointerdown(); - expect(isRippleVisible('focus')).toBe(true); + expect(isRippleVisible('focus')).toBeTrue(); })); it('should continue to show the focus ripple on pointerup', fakeAsync(() => { pointerdown(); pointerup(); - expect(isRippleVisible('focus')).toBe(true); + expect(isRippleVisible('focus')).toBeTrue(); })); it('should hide the focus ripple on blur', fakeAsync(() => { pointerdown(); pointerup(); blur(); - expect(isRippleVisible('focus')).toBe(false); + expect(isRippleVisible('focus')).toBeFalse(); })); it('should show the active ripple on pointerdown', fakeAsync(() => { - expect(isRippleVisible('active')).toBe(false); + expect(isRippleVisible('active')).toBeFalse(); pointerdown(); - expect(isRippleVisible('active')).toBe(true); + expect(isRippleVisible('active')).toBeTrue(); })); it('should hide the active ripple on pointerup', fakeAsync(() => { pointerdown(); pointerup(); - expect(isRippleVisible('active')).toBe(false); + expect(isRippleVisible('active')).toBeFalse(); })); // Edge cases. @@ -249,19 +334,19 @@ describe('MDC-based MatSlider' , () => { it('should not show the hover ripple if the thumb is already focused', fakeAsync(() => { pointerdown(); mouseenter(); - expect(isRippleVisible('hover')).toBe(false); + expect(isRippleVisible('hover')).toBeFalse(); })); it('should hide the hover ripple if the thumb is focused', fakeAsync(() => { mouseenter(); pointerdown(); - expect(isRippleVisible('hover')).toBe(false); + expect(isRippleVisible('hover')).toBeFalse(); })); it('should not hide the focus ripple if the thumb is pressed', fakeAsync(() => { pointerdown(); blur(); - expect(isRippleVisible('focus')).toBe(true); + expect(isRippleVisible('focus')).toBeTrue(); })); it('should not hide the hover ripple on blur if the thumb is hovered', fakeAsync(() => { @@ -269,14 +354,14 @@ describe('MDC-based MatSlider' , () => { pointerdown(); pointerup(); blur(); - expect(isRippleVisible('hover')).toBe(true); + expect(isRippleVisible('hover')).toBeTrue(); })); it('should hide the focus ripple on drag end if the thumb already lost focus', fakeAsync(() => { pointerdown(); blur(); pointerup(); - expect(isRippleVisible('focus')).toBe(false); + expect(isRippleVisible('focus')).toBeFalse(); })); }); }); @@ -301,6 +386,25 @@ class StandardSlider {} }) class StandardRangeSlider {} +@Component({ + template: ` + + + + `, +}) +class DisabledSlider {} + +@Component({ + template: ` + + + + + `, +}) +class DisabledRangeSlider {} + /** The pointer event types used by the MDC Slider. */ const enum PointerEventType { POINTER_DOWN = 'pointerdown', From 9706ec4de7c39fb97071d12d06eb9be02858a061 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Wed, 10 Mar 2021 10:05:13 -0800 Subject: [PATCH 13/35] test(material-experimental/mdc-slider): add unit tests for sliders with set min and max * add support for changing the min, max, or step after the component has already been initialized --- .../mdc-slider/slider.spec.ts | 122 +++++++++++++++++- .../mdc-slider/slider.ts | 36 +++++- 2 files changed, 149 insertions(+), 9 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 340e1186b96b..6b961acd1ac9 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -37,15 +37,13 @@ describe('MDC-based MatSlider' , () => { } describe('standard slider', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; let sliderInstance: MatSlider; let inputInstance: MatSliderThumb; beforeEach(waitForAsync(() => { - fixture = createComponent(StandardSlider); + const fixture = createComponent(StandardSlider); fixture.detectChanges(); - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); sliderInstance = sliderDebugElement.componentInstance; inputInstance = sliderInstance._getInput(Thumb.END); })); @@ -364,6 +362,101 @@ describe('MDC-based MatSlider' , () => { expect(isRippleVisible('focus')).toBeFalse(); })); }); + + describe('slider with set min and max', () => { + let fixture: ComponentFixture; + let sliderInstance: MatSlider; + let inputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + fixture = createComponent(SliderWithMinAndMax); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + inputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should set the default values from the attributes', () => { + expect(inputInstance.value).toBe(25); + expect(sliderInstance.min).toBe(25); + expect(sliderInstance.max).toBe(75); + }); + + it('should set the correct value on mousedown', () => { + setValueByClick(sliderInstance, 33, platform.IOS); + expect(inputInstance.value).toBe(33); + }); + + it('should set the correct value on slide', () => { + slideToValue(sliderInstance, 55, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(55); + }); + + it('should be able to set the min and max values when they are more precise ' + + 'than the step', () => { + sliderInstance.step = 10; + fixture.detectChanges(); + slideToValue(sliderInstance, 25, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(25); + slideToValue(sliderInstance, 75, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(75); + }); + }); + + describe('range slider with set min and max', () => { + let fixture: ComponentFixture; + let sliderInstance: MatSlider; + let startInputInstance: MatSliderThumb; + let endInputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + fixture = createComponent(RangeSliderWithMinAndMax); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + startInputInstance = sliderInstance._getInput(Thumb.START); + endInputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should set the default values from the attributes', () => { + expect(startInputInstance.value).toBe(25); + expect(endInputInstance.value).toBe(75); + expect(sliderInstance.min).toBe(25); + expect(sliderInstance.max).toBe(75); + }); + + it('should set the correct start value on mousedown behind the start thumb', () => { + sliderInstance._setValue(50, Thumb.START); + setValueByClick(sliderInstance, 33, platform.IOS); + expect(startInputInstance.value).toBe(33); + }); + + it('should set the correct end value on mousedown behind the end thumb', () => { + sliderInstance._setValue(50, Thumb.END); + setValueByClick(sliderInstance, 66, platform.IOS); + expect(endInputInstance.value).toBe(66); + }); + + it('should set the correct start value on slide', () => { + slideToValue(sliderInstance, 40, Thumb.START, platform.IOS); + expect(startInputInstance.value).toBe(40); + }); + + it('should set the correct end value on slide', () => { + slideToValue(sliderInstance, 60, Thumb.END, platform.IOS); + expect(endInputInstance.value).toBe(60); + }); + + it('should be able to set the min and max values when they are more precise ' + + 'than the step', () => { + sliderInstance.step = 10; + fixture.detectChanges(); + slideToValue(sliderInstance, 25, Thumb.START, platform.IOS); + expect(startInputInstance.value).toBe(25); + slideToValue(sliderInstance, 75, Thumb.END, platform.IOS); + expect(endInputInstance.value).toBe(75); + }); + }); }); @@ -405,6 +498,25 @@ class DisabledSlider {} }) class DisabledRangeSlider {} +@Component({ + template: ` + + + + `, +}) +class SliderWithMinAndMax {} + +@Component({ + template: ` + + + + + `, +}) +class RangeSliderWithMinAndMax {} + /** The pointer event types used by the MDC Slider. */ const enum PointerEventType { POINTER_DOWN = 'pointerdown', @@ -442,7 +554,7 @@ function slideToValue(slider: MatSlider, value: number, thumbPosition: Thumb, is const thumbElement = slider._getThumbElement(thumbPosition); const sliderDimensions = sliderElement.getBoundingClientRect(); - let thumbDimensions = thumbElement.getBoundingClientRect(); + const thumbDimensions = thumbElement.getBoundingClientRect(); const startX = thumbDimensions.left + (thumbDimensions.width / 2); const startY = thumbDimensions.top + (thumbDimensions.height / 2); diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 90753fbaf329..c371cba070ac 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -410,7 +410,7 @@ export class MatSliderThumb implements AfterViewInit, ControlValueAccessor { * instead be capped at either the default min or max. * */ - private _initializeInputState(): void { + _initializeInputState(): void { const min = this._hostElement.hasAttribute('matSliderEndThumb') ? this._slider._getInput(Thumb.START).value : this._slider.min; @@ -523,19 +523,28 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD /** The minimum value that the slider can have. */ @Input() get min(): number { return this._min; } - set min(v: number) { this._min = coerceNumberProperty(v, this._min); } + set min(v: number) { + this._min = coerceNumberProperty(v, this._min); + this._reinitialize(); + } private _min: number = 0; /** The maximum value that the slider can have. */ @Input() get max(): number { return this._max; } - set max(v: number) { this._max = coerceNumberProperty(v, this._max); } + set max(v: number) { + this._max = coerceNumberProperty(v, this._max); + this._reinitialize(); + } private _max: number = 100; /** The values at which the thumb will snap. */ @Input() get step(): number { return this._step; } - set step(v: number) { this._step = coerceNumberProperty(v, this._step); } + set step(v: number) { + this._step = coerceNumberProperty(v, this._step); + this._reinitialize(); + } private _step: number = 1; /** @@ -611,6 +620,25 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD } } + /** + * Reinitializes the slider foundation and input state(s). + * + * The MDC Foundation does not support changing some slider attributes after it has been + * initialized (e.g. min, max, and step). To continue supporting this feature, we need to + * destroy the foundation and re-initialize everything whenever we make these changes. + */ + private _reinitialize(): void { + if (this._initialized) { + this._foundation.destroy(); + if (this._isRange()) { + this._getInput(Thumb.START)._initializeInputState(); + } + this._getInput(Thumb.END)._initializeInputState(); + this._foundation.init(); + this._foundation.layout(); + } + } + /** Sets the value of a slider thumb. */ _setValue(value: number, thumbPosition: Thumb): void { thumbPosition === Thumb.START From 51c4218ce6fdd19e47f0dfe63d707f880192707f Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Wed, 10 Mar 2021 15:28:50 -0800 Subject: [PATCH 14/35] fix(material-experimental/mdc-slider): fix VE bug * in view engine, MatSliders inputs are not initialized before MatSliderThumbs constructor is called. This means we cannot initialize the slider value attribute in the constructor. To fix this, we are initializing the value attribute in ngOnInit which is still before ngAfterViewInit but after MatSliders inputs are initialized --- src/material-experimental/mdc-slider/slider.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index c371cba070ac..1b9b45c4fd44 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -28,6 +28,7 @@ import { Input, NgZone, OnDestroy, + OnInit, Output, QueryList, ViewChild, @@ -260,7 +261,7 @@ export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { multi: true }], }) -export class MatSliderThumb implements AfterViewInit, ControlValueAccessor { +export class MatSliderThumb implements AfterViewInit, ControlValueAccessor, OnInit { // ** IMPORTANT NOTE ** // @@ -336,11 +337,14 @@ export class MatSliderThumb implements AfterViewInit, ControlValueAccessor { private readonly _elementRef: ElementRef) { this._document = document; this._hostElement = _elementRef.nativeElement; - // By calling this in the constructor we guarantee that the sibling sliders initial value by - // has already been set by the time we reach ngAfterViewInit(). - this._initializeInputValueAttribute(); } + ngOnInit() { + // By calling this in ngOnInit() we guarantee that the sibling sliders initial value by + // has already been set by the time we reach ngAfterViewInit(). + this._initializeInputValueAttribute(); + } + ngAfterViewInit() { this._initializeInputState(); this._initializeInputValueProperty(); From 6dd2e7e1ca1307408a3c03d78b3d4b43697ecfd9 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Thu, 11 Mar 2021 16:51:39 -0500 Subject: [PATCH 15/35] test(material-experimental/mdc-slider): add unit tests for sliders with values (#22193) --- .../mdc-slider/slider.spec.ts | 97 +++++++++++++++++-- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 6b961acd1ac9..e7821e37c4ff 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -12,7 +12,7 @@ import { dispatchPointerEvent, dispatchTouchEvent, } from '@angular/cdk/testing/private'; -import {Component, DebugElement, Type} from '@angular/core'; +import {Component, Type} from '@angular/core'; import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {Thumb} from '@material/slider'; @@ -82,16 +82,14 @@ describe('MDC-based MatSlider' , () => { }); describe('standard range slider', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; let sliderInstance: MatSlider; let startInputInstance: MatSliderThumb; let endInputInstance: MatSliderThumb; beforeEach(waitForAsync(() => { - fixture = createComponent(StandardRangeSlider); + const fixture = createComponent(StandardRangeSlider); fixture.detectChanges(); - sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); sliderInstance = sliderDebugElement.componentInstance; startInputInstance = sliderInstance._getInput(Thumb.START); endInputInstance = sliderInstance._getInput(Thumb.END); @@ -235,7 +233,6 @@ describe('MDC-based MatSlider' , () => { }); describe('ripple states', () => { - let fixture: ComponentFixture; let inputInstance: MatSliderThumb; let thumbInstance: MatSliderVisualThumb; let thumbElement: HTMLElement; @@ -243,7 +240,7 @@ describe('MDC-based MatSlider' , () => { let thumbY: number; beforeEach(waitForAsync(() => { - fixture = createComponent(StandardSlider); + const fixture = createComponent(StandardSlider); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); const sliderInstance = sliderDebugElement.componentInstance; @@ -457,6 +454,73 @@ describe('MDC-based MatSlider' , () => { expect(endInputInstance.value).toBe(75); }); }); + + describe('slider with set value', () => { + let sliderInstance: MatSlider; + let inputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + const fixture = createComponent(SliderWithValue); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + inputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should set the default value from the attribute', () => { + expect(inputInstance.value).toBe(50); + }); + + it('should set the correct value on mousedown', () => { + setValueByClick(sliderInstance, 19, platform.IOS); + expect(inputInstance.value).toBe(19); + }); + + it('should set the correct value on slide', () => { + slideToValue(sliderInstance, 77, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(77); + }); + }); + + describe('range slider with set value', () => { + let sliderInstance: MatSlider; + let startInputInstance: MatSliderThumb; + let endInputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + const fixture = createComponent(RangeSliderWithValue); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + startInputInstance = sliderInstance._getInput(Thumb.START); + endInputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should set the default value from the attribute', () => { + expect(startInputInstance.value).toBe(25); + expect(endInputInstance.value).toBe(75); + }); + + it('should set the correct start value on mousedown behind the start thumb', () => { + setValueByClick(sliderInstance, 19, platform.IOS); + expect(startInputInstance.value).toBe(19); + }); + + it('should set the correct start value on mousedown in front of the end thumb', () => { + setValueByClick(sliderInstance, 77, platform.IOS); + expect(endInputInstance.value).toBe(77); + }); + + it('should set the correct start value on slide', () => { + slideToValue(sliderInstance, 73, Thumb.START, platform.IOS); + expect(startInputInstance.value).toBe(73); + }); + + it('should set the correct end value on slide', () => { + slideToValue(sliderInstance, 99, Thumb.END, platform.IOS); + expect(endInputInstance.value).toBe(99); + }); + }); }); @@ -517,6 +581,25 @@ class SliderWithMinAndMax {} }) class RangeSliderWithMinAndMax {} +@Component({ + template: ` + + + + `, +}) +class SliderWithValue {} + +@Component({ + template: ` + + + + + `, +}) +class RangeSliderWithValue {} + /** The pointer event types used by the MDC Slider. */ const enum PointerEventType { POINTER_DOWN = 'pointerdown', From 9a38cb2cd613bc49eb655f42e0a267b549ee9e6a Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Thu, 11 Mar 2021 16:51:52 -0500 Subject: [PATCH 16/35] fix(material-experimental/mdc-slider): dedup mdc-slider styles (#22195) * remove mdc-slider theme from theme.scss since it is already included in all-theme now * avoid using deprecated mdc-theme prop-value function in _slider-theme.scss --- src/dev-app/theme.scss | 2 -- src/material-experimental/mdc-slider/_slider-theme.scss | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/dev-app/theme.scss b/src/dev-app/theme.scss index 55444d37b6f3..8ae0901816f4 100644 --- a/src/dev-app/theme.scss +++ b/src/dev-app/theme.scss @@ -32,8 +32,6 @@ $candy-app-theme: mat.define-light-theme(( @include experimental.all-mdc-component-themes($candy-app-theme); @include experimental.column-resize-theme($candy-app-theme); @include experimental.popover-edit-theme($candy-app-theme); -// We add this in manually for now, because it isn't included in `all-mdc-component-themes`. -@include mdc-slider-theme.theme($candy-app-theme); .demo-strong-focus { // Include base styles for strong focus indicators. diff --git a/src/material-experimental/mdc-slider/_slider-theme.scss b/src/material-experimental/mdc-slider/_slider-theme.scss index d24550461686..d465538fa955 100644 --- a/src/material-experimental/mdc-slider/_slider-theme.scss +++ b/src/material-experimental/mdc-slider/_slider-theme.scss @@ -118,9 +118,9 @@ ), )); .mat-mdc-slider-hover-ripple { - background-color: rgba(theme-variables.prop-value($color), 0.05); + background-color: rgba($ripple-color, 0.05); } .mat-mdc-slider-focus-ripple, .mat-mdc-slider-active-ripple { - background-color: rgba(theme-variables.prop-value($color), 0.2); + background-color: rgba($ripple-color, 0.2); } } From 7b1b4ba233f942272f4dc2f92cf063a6661f7ac2 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Thu, 11 Mar 2021 16:52:00 -0500 Subject: [PATCH 17/35] feat(material-experimental/mdc-slider): add support for rtl/ltr toggle (#22196) * feat(material-experimental/mdc-slider): add support for rtl/ltr toggle --- src/material-experimental/mdc-slider/slider.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 1b9b45c4fd44..0568077ee4d5 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {Directionality} from '@angular/cdk/bidi'; import { BooleanInput, coerceBooleanProperty, @@ -29,6 +30,7 @@ import { NgZone, OnDestroy, OnInit, + Optional, Output, QueryList, ViewChild, @@ -46,6 +48,7 @@ import { } from '@angular/material/core'; import {SpecificEventListener, EventType} from '@material/base'; import {MDCSliderAdapter, MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; +import {Subscription} from 'rxjs'; /** Represents a drag event emitted by the MatSlider component. */ export interface MatSliderDragEvent { @@ -582,14 +585,19 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD /** The display value of the end thumb. */ _endValueIndicatorText: string; + /** Subscription to changes to the directionality (LTR / RTL) context for the application. */ + private _dirChangeSubscription: Subscription; + constructor( readonly _cdr: ChangeDetectorRef, readonly _elementRef: ElementRef, private readonly _platform: Platform, + @Optional() private _dir: Directionality, @Inject(DOCUMENT) document: any) { super(_elementRef); this._document = document; this._window = this._document.defaultView || window; + this._dirChangeSubscription = this._dir.change.subscribe(() => this._reinitialize()); } ngAfterViewInit() { @@ -622,6 +630,12 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD if (this._platform.isBrowser) { this._foundation.destroy(); } + this._dirChangeSubscription.unsubscribe(); + } + + /** Returns true if the language direction for this slider element is right to left. */ + _isRTL() { + return this._dir && this._dir.value === 'rtl'; } /** @@ -777,8 +791,7 @@ class SliderAdapter implements MDCSliderAdapter { return this._delegate._elementRef.nativeElement.getBoundingClientRect(); } isRTL = (): boolean => { - // TODO(wagnermaciel): Actually implementing this. - return false; + return this._delegate._isRTL(); } setThumbStyleProperty = (propertyName: string, value: string, thumbPosition: Thumb): void => { this._delegate._getThumbElement(thumbPosition).style.setProperty(propertyName, value); From 635a8ae88a5105c3790ffc4d7dcd800a7a21d1fa Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Thu, 11 Mar 2021 17:36:50 -0500 Subject: [PATCH 18/35] =?UTF-8?q?feat(material-experimental/mdc-slider):?= =?UTF-8?q?=20add=20support=20for=20disabling=20rip=E2=80=A6=20(#22199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(material-experimental/mdc-slider): add support for disabling ripples --- .../mdc-slider/slider.html | 1 + .../mdc-slider/slider.ts | 43 ++++++++++++++----- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.html b/src/material-experimental/mdc-slider/slider.html index 037735923859..caa46f348407 100644 --- a/src/material-experimental/mdc-slider/slider.html +++ b/src/material-experimental/mdc-slider/slider.html @@ -16,6 +16,7 @@ diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 0568077ee4d5..3b86bebc857c 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -40,9 +40,14 @@ import { import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import { CanColorCtor, + CanDisableRipple, + CanDisableRippleCtor, MatRipple, + MAT_RIPPLE_GLOBAL_OPTIONS, mixinColor, + mixinDisableRipple, RippleAnimationConfig, + RippleGlobalOptions, RippleRef, RippleState, } from '@angular/material/core'; @@ -89,6 +94,9 @@ export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { /** The display value of the slider thumb. */ @Input() valueIndicatorText: string; + /** Whether ripples on the slider thumb should be disabled. */ + @Input() disableRipple: boolean = false; + /** The MatRipple for this slider thumb. */ @ViewChild(MatRipple) private readonly _ripple: MatRipple; @@ -99,13 +107,13 @@ export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { private _sliderInput: MatSliderThumb; /** The RippleRef for the slider thumbs hover state. */ - private _hoverRippleRef: RippleRef; + private _hoverRippleRef: RippleRef | undefined; /** The RippleRef for the slider thumbs focus state. */ - private _focusRippleRef: RippleRef; + private _focusRippleRef: RippleRef | undefined; /** The RippleRef for the slider thumbs active state. */ - private _activeRippleRef: RippleRef; + private _activeRippleRef: RippleRef | undefined; /** Whether the slider thumb is currently being pressed. */ private _isActive: boolean = false; @@ -199,7 +207,7 @@ export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { private _showHoverRipple(): void { if (!this._isShowingRipple(this._hoverRippleRef)) { this._hoverRippleRef = this._showRipple({ enterDuration: 0, exitDuration: 0 }); - this._hoverRippleRef.element.classList.add('mat-mdc-slider-hover-ripple'); + this._hoverRippleRef?.element.classList.add('mat-mdc-slider-hover-ripple'); } } @@ -207,7 +215,7 @@ export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { private _showFocusRipple(): void { if (!this._isShowingRipple(this._focusRippleRef)) { this._focusRippleRef = this._showRipple({ enterDuration: 0, exitDuration: 0 }); - this._focusRippleRef.element.classList.add('mat-mdc-slider-focus-ripple'); + this._focusRippleRef?.element.classList.add('mat-mdc-slider-focus-ripple'); } } @@ -215,7 +223,7 @@ export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { private _showActiveRipple(): void { if (!this._isShowingRipple(this._activeRippleRef)) { this._activeRippleRef = this._showRipple({ enterDuration: 225, exitDuration: 400 }); - this._activeRippleRef.element.classList.add('mat-mdc-slider-active-ripple'); + this._activeRippleRef?.element.classList.add('mat-mdc-slider-active-ripple'); } } @@ -225,7 +233,10 @@ export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { } /** Manually launches the slider thumb ripple using the specified ripple animation config. */ - private _showRipple(animation: RippleAnimationConfig): RippleRef { + private _showRipple(animation: RippleAnimationConfig): RippleRef | undefined { + if (this.disableRipple) { + return; + } return this._ripple.launch( {animation, centered: true, persistent: true}, ); @@ -466,8 +477,9 @@ class MatSliderBase { } const _MatSliderMixinBase: CanColorCtor & + CanDisableRippleCtor & typeof MatSliderBase = - mixinColor(MatSliderBase, 'primary'); + mixinColor(mixinDisableRipple(MatSliderBase), 'primary'); /** * Allows users to select from a range of values by moving the slider thumb. It is similar in @@ -487,9 +499,10 @@ const _MatSliderMixinBase: exportAs: 'matSlider', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, - inputs: ['color'], + inputs: ['color', 'disableRipple'], }) -export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnDestroy { +export class MatSlider extends _MatSliderMixinBase + implements AfterViewInit, CanDisableRipple, OnDestroy { /** The slider thumb(s). */ @ViewChildren(MatSliderVisualThumb) _thumbs: QueryList; @@ -592,8 +605,10 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD readonly _cdr: ChangeDetectorRef, readonly _elementRef: ElementRef, private readonly _platform: Platform, + @Inject(DOCUMENT) document: any, @Optional() private _dir: Directionality, - @Inject(DOCUMENT) document: any) { + @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) + readonly _globalRippleOptions?: RippleGlobalOptions) { super(_elementRef); this._document = document; this._window = this._document.defaultView || window; @@ -724,12 +739,18 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD : 'mdc-slider__tick-mark--inactive'; } + /** Whether the slider thumb ripples should be disabled. */ + _isRippleDisabled(): boolean { + return this.disabled || this.disableRipple || !!this._globalRippleOptions?.disabled; + } + static ngAcceptInputType_disabled: BooleanInput; static ngAcceptInputType_discrete: BooleanInput; static ngAcceptInputType_showTickMarks: BooleanInput; static ngAcceptInputType_min: NumberInput; static ngAcceptInputType_max: NumberInput; static ngAcceptInputType_step: NumberInput; + static ngAcceptInputType_disableRipple: BooleanInput; } /** The MDCSliderAdapter implementation. */ From f3619288e2170e2c42cc57f7e96661fa0615ff38 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Mon, 15 Mar 2021 09:26:25 -0400 Subject: [PATCH 19/35] fix(material-experimental/mdc-slider): add slider to mdc_scss_deps_lib (#22216) * fix(material-experimental/mdc-slider): add slider to mdc_scss_deps_lib --- src/material-experimental/mdc-helpers/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/src/material-experimental/mdc-helpers/BUILD.bazel b/src/material-experimental/mdc-helpers/BUILD.bazel index b62d5232f6d9..8a84ac110801 100644 --- a/src/material-experimental/mdc-helpers/BUILD.bazel +++ b/src/material-experimental/mdc-helpers/BUILD.bazel @@ -23,6 +23,7 @@ npm_sass_library( "@npm//@material/list", "@npm//@material/menu-surface", "@npm//@material/radio", + "@npm//@material/slider", "@npm//@material/snackbar", "@npm//@material/switch", "@npm//@material/tab", From 93358793c870c6a9ad4b9fc2e38e8e55d3f9048c Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Mon, 15 Mar 2021 12:14:36 -0400 Subject: [PATCH 20/35] =?UTF-8?q?test(material-experimental/mdc-slider):?= =?UTF-8?q?=20add=20tests=20for=20sliders=20with=20se=E2=80=A6=20(#22214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(material-experimental/mdc-slider): add tests for sliders with set steps --- .../mdc-slider/slider.spec.ts | 162 +++++++++++++++++- 1 file changed, 161 insertions(+), 1 deletion(-) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index e7821e37c4ff..b4347d8ca6ae 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -6,8 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ +import {LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes'; import {Platform} from '@angular/cdk/platform'; import { + dispatchFakeEvent, dispatchMouseEvent, dispatchPointerEvent, dispatchTouchEvent, @@ -392,7 +394,6 @@ describe('MDC-based MatSlider' , () => { it('should be able to set the min and max values when they are more precise ' + 'than the step', () => { sliderInstance.step = 10; - fixture.detectChanges(); slideToValue(sliderInstance, 25, Thumb.END, platform.IOS); expect(inputInstance.value).toBe(25); slideToValue(sliderInstance, 75, Thumb.END, platform.IOS); @@ -521,6 +522,130 @@ describe('MDC-based MatSlider' , () => { expect(endInputInstance.value).toBe(99); }); }); + + describe('slider with set step', () => { + let sliderInstance: MatSlider; + let inputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + const fixture = createComponent(SliderWithStep); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + inputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should set the correct step value on mousedown', () => { + expect(inputInstance.value).toBe(0); + setValueByClick(sliderInstance, 13, platform.IOS); + expect(inputInstance.value).toBe(25); + }); + + it('should set the correct step value on slide', () => { + slideToValue(sliderInstance, 12, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(0); + }); + + it('should not add decimals to the value if it is a whole number', () => { + sliderInstance.step = 0.1; + slideToValue(sliderInstance, 100, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(100); + }); + + it('should truncate long decimal values when using a decimal step', () => { + // TODO(wagnermaciel): Uncomment this test once b/182504575 is resolved. + // sliderInstance.step = 0.1; + // slideToValue(sliderInstance, 33.3333, Thumb.END, platform.IOS); + // expect(inputInstance.value).toBe(33); + }); + + it('should truncate long decimal values when using a decimal step and the arrow keys', () => { + sliderInstance.step = 0.1; + changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.END); + changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.END); + changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.END); + expect(inputInstance.value).toBe(0.3); + }); + }); + + describe('range slider with set step', () => { + let sliderInstance: MatSlider; + let startInputInstance: MatSliderThumb; + let endInputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + const fixture = createComponent(RangeSliderWithStep); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + startInputInstance = sliderInstance._getInput(Thumb.START); + endInputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should set the correct step value on mousedown behind the start thumb', () => { + sliderInstance._setValue(50, Thumb.START); + setValueByClick(sliderInstance, 13, platform.IOS); + expect(startInputInstance.value).toBe(25); + }); + + it('should set the correct step value on mousedown in front of the end thumb', () => { + sliderInstance._setValue(50, Thumb.END); + setValueByClick(sliderInstance, 63, platform.IOS); + expect(endInputInstance.value).toBe(75); + }); + + it('should set the correct start thumb step value on slide', () => { + slideToValue(sliderInstance, 26, Thumb.START, platform.IOS); + expect(startInputInstance.value).toBe(25); + }); + + it('should set the correct end thumb step value on slide', () => { + slideToValue(sliderInstance, 45, Thumb.END, platform.IOS); + expect(endInputInstance.value).toBe(50); + }); + + it('should not add decimals to the end value if it is a whole number', () => { + sliderInstance.step = 0.1; + slideToValue(sliderInstance, 100, Thumb.END, platform.IOS); + expect(endInputInstance.value).toBe(100); + }); + + it('should not add decimals to the start value if it is a whole number', () => { + sliderInstance.step = 0.1; + slideToValue(sliderInstance, 100, Thumb.END, platform.IOS); + expect(endInputInstance.value).toBe(100); + }); + + it('should truncate long decimal start values when using a decimal step', () => { + // TODO(wagnermaciel): Uncomment this test once b/182504575 is resolved. + // sliderInstance.step = 0.1; + // slideToValue(sliderInstance, 33.3333, Thumb.START, platform.IOS); + // expect(startInputInstance.value).toBe(33); + }); + + it('should truncate long decimal end values when using a decimal step', () => { + // TODO(wagnermaciel): Uncomment this test once b/182504575 is resolved. + // sliderInstance.step = 0.1; + // slideToValue(sliderInstance, 66.6666, Thumb.END, platform.IOS); + // expect(endInputInstance.value).toBe(66); + }); + + it('should truncate long decimal start values when using a decimal step arrow keys', () => { + sliderInstance.step = 0.1; + changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.START); + changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.START); + changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.START); + expect(startInputInstance.value).toBe(0.3); + }); + + it('should truncate long decimal end values when using a decimal step arrow keys', () => { + sliderInstance.step = 0.1; + changeValueUsingArrowKeys(sliderInstance, LEFT_ARROW, Thumb.END); + changeValueUsingArrowKeys(sliderInstance, LEFT_ARROW, Thumb.END); + changeValueUsingArrowKeys(sliderInstance, LEFT_ARROW, Thumb.END); + expect(endInputInstance.value).toBe(99.7); + }); + }); }); @@ -600,6 +725,25 @@ class SliderWithValue {} }) class RangeSliderWithValue {} +@Component({ + template: ` + + + + `, +}) +class SliderWithStep {} + +@Component({ + template: ` + + + + + `, +}) +class RangeSliderWithStep {} + /** The pointer event types used by the MDC Slider. */ const enum PointerEventType { POINTER_DOWN = 'pointerdown', @@ -650,6 +794,22 @@ function slideToValue(slider: MatSlider, value: number, thumbPosition: Thumb, is dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, endX, endY, isIOS); } +/** + * Mimics changing the slider value using arrow keys. + * + * Dispatching keydown events on inputs do not trigger value changes. Thus, to mimic this behavior, + * we manually change the slider inputs value and then dispatch a change event (which is what the + * MDC Foundation is listening for & how it handles these updates). + */ +function changeValueUsingArrowKeys(slider: MatSlider, arrow: number, thumbPosition: Thumb) { + const input = slider._getInput(thumbPosition); + const value = arrow === RIGHT_ARROW + ? input.value + slider.step + : input.value - slider.step; + input._hostElement.value = value.toString(); + dispatchFakeEvent(input._hostElement, 'change'); +} + /** Dispatch a pointerdown or pointerup event if supported, otherwise dispatch the touch event. */ function dispatchPointerOrTouchEvent( node: Node, type: PointerEventType, x: number, y: number, isIOS: boolean) { From 7fbc1abbdb14b415e17d2bf1112b61b1d7f08795 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Mon, 15 Mar 2021 15:40:48 -0400 Subject: [PATCH 21/35] =?UTF-8?q?test(material-experimental/mdc-slider):?= =?UTF-8?q?=20add=20tests=20for=20slider=20with=20set=E2=80=A6=20(#22238)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(material-experimental/mdc-slider): add tests for slider with set displayWith * fix(material-experimental/mdc-slider): trigger change detection when the value indicator text changes --- .../mdc-slider/slider.spec.ts | 111 ++++++++++++++++++ .../mdc-slider/slider.ts | 1 + 2 files changed, 112 insertions(+) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index b4347d8ca6ae..c0cabd835ae7 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -646,6 +646,88 @@ describe('MDC-based MatSlider' , () => { expect(endInputInstance.value).toBe(99.7); }); }); + + describe('slider with custom thumb label formatting', () => { + let fixture: ComponentFixture; + let sliderInstance: MatSlider; + let valueIndicatorTextElement: Element; + + beforeEach(() => { + fixture = createComponent(DiscreteSliderWithDisplayWith); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider))!; + const sliderNativeElement = sliderDebugElement.nativeElement; + sliderInstance = sliderDebugElement.componentInstance; + valueIndicatorTextElement = + sliderNativeElement.querySelector('.mdc-slider__value-indicator-text')!; + }); + + it('should invoke the passed-in `displayWith` function with the value', () => { + spyOn(fixture.componentInstance, 'displayWith').and.callThrough(); + sliderInstance._setValue(1337, Thumb.END); + fixture.whenStable().then(() => { + expect(fixture.componentInstance.displayWith).toHaveBeenCalledWith(1337); + }); + }); + + it('should format the thumb label based on the passed-in `displayWith` function', () => { + sliderInstance._setValue(200000, Thumb.END); + fixture.whenStable().then(() => { + expect(valueIndicatorTextElement.textContent).toBe('200k'); + }); + }); + }); + + describe('range slider with custom thumb label formatting', () => { + let fixture: ComponentFixture; + let sliderInstance: MatSlider; + let startValueIndicatorTextElement: Element; + let endValueIndicatorTextElement: Element; + + beforeEach(() => { + fixture = createComponent(DiscreteRangeSliderWithDisplayWith); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider))!; + sliderInstance = sliderDebugElement.componentInstance; + + const startThumbElement = sliderInstance._getThumbElement(Thumb.START); + const endThumbElement = sliderInstance._getThumbElement(Thumb.END); + startValueIndicatorTextElement = + startThumbElement.querySelector('.mdc-slider__value-indicator-text')!; + endValueIndicatorTextElement = + endThumbElement.querySelector('.mdc-slider__value-indicator-text')!; + }); + + it('should invoke the passed-in `displayWith` function with the start value', () => { + spyOn(fixture.componentInstance, 'displayWith').and.callThrough(); + sliderInstance._setValue(1337, Thumb.START); + fixture.whenStable().then(() => { + expect(fixture.componentInstance.displayWith).toHaveBeenCalledWith(1337); + }); + }); + + it('should invoke the passed-in `displayWith` function with the end value', () => { + spyOn(fixture.componentInstance, 'displayWith').and.callThrough(); + sliderInstance._setValue(5996, Thumb.END); + fixture.whenStable().then(() => { + expect(fixture.componentInstance.displayWith).toHaveBeenCalledWith(5996); + }); + }); + + it('should format the start thumb label based on the passed-in `displayWith` function', () => { + sliderInstance._setValue(200000, Thumb.START); + fixture.whenStable().then(() => { + expect(startValueIndicatorTextElement.textContent).toBe('200k'); + }); + }); + + it('should format the end thumb label based on the passed-in `displayWith` function', () => { + sliderInstance._setValue(700000, Thumb.END); + fixture.whenStable().then(() => { + expect(endValueIndicatorTextElement.textContent).toBe('700k'); + }); + }); + }); }); @@ -744,6 +826,35 @@ class SliderWithStep {} }) class RangeSliderWithStep {} +@Component({ + template: ` + + + + `, +}) +class DiscreteSliderWithDisplayWith { + displayWith(v: number) { + if (v >= 1000) { return `$${v / 1000}k`; } + return `$${v}`; + } +} + +@Component({ + template: ` + + + + + `, +}) +class DiscreteRangeSliderWithDisplayWith { + displayWith(v: number) { + if (v >= 1000) { return `$${v / 1000}k`; } + return `$${v}`; + } +} + /** The pointer event types used by the MDC Slider. */ const enum PointerEventType { POINTER_DOWN = 'pointerdown', diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 3b86bebc857c..4e2929ec2f2e 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -723,6 +723,7 @@ export class MatSlider extends _MatSliderMixinBase thumbPosition === Thumb.START ? this._startValueIndicatorText = this.displayWith(value) : this._endValueIndicatorText = this.displayWith(value); + this._cdr.markForCheck(); } /** Gets the value indicator text for the given thumb position. */ From 32b2c0e7ef69c0fe99525ce702659cff467fd57f Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Tue, 16 Mar 2021 16:15:10 -0400 Subject: [PATCH 22/35] test(material-experimental/mdc-slider): add tests for slider with one-way value binding (#22242) --- .../mdc-slider/slider.spec.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index c0cabd835ae7..d6cab30d3368 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -728,6 +728,56 @@ describe('MDC-based MatSlider' , () => { }); }); }); + + describe('slider with value property binding', () => { + let fixture: ComponentFixture; + let testComponent: SliderWithOneWayBinding; + let inputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + fixture = createComponent(SliderWithOneWayBinding); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + const sliderInstance = sliderDebugElement.componentInstance; + inputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should update when bound value changes', () => { + testComponent.value = 75; + fixture.detectChanges(); + expect(inputInstance.value).toBe(75); + }); + }); + + describe('range slider with value property binding', () => { + let fixture: ComponentFixture; + let testComponent: RangeSliderWithOneWayBinding; + let startInputInstance: MatSliderThumb; + let endInputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + fixture = createComponent(RangeSliderWithOneWayBinding); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + const sliderInstance = sliderDebugElement.componentInstance; + startInputInstance = sliderInstance._getInput(Thumb.START); + endInputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should update when bound start value changes', () => { + testComponent.startValue = 30; + fixture.detectChanges(); + expect(startInputInstance.value).toBe(30); + }); + + it('should update when bound end value changes', () => { + testComponent.endValue = 70; + fixture.detectChanges(); + expect(endInputInstance.value).toBe(70); + }); + }); }); @@ -855,6 +905,30 @@ class DiscreteRangeSliderWithDisplayWith { } } +@Component({ + template: ` + + + + `, +}) +class SliderWithOneWayBinding { + value = 50; +} + +@Component({ + template: ` + + + + + `, +}) +class RangeSliderWithOneWayBinding { + startValue = 25; + endValue = 75; +} + /** The pointer event types used by the MDC Slider. */ const enum PointerEventType { POINTER_DOWN = 'pointerdown', From ce802d367bba3cec95d92d8be9d13e5cf5168c0d Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Tue, 23 Mar 2021 12:00:24 -0400 Subject: [PATCH 23/35] =?UTF-8?q?fix(material-experimental/mdc-slider):=20?= =?UTF-8?q?fix=20change=20events=20on=20slider=20in=E2=80=A6=20(#22286)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(material-experimental/mdc-slider): fix change events on slider inputs * create GlobalChangeAndInputListener to handle listening for change events that occur on the document * stop all of the slider inputs change events from reaching users * dispatch our own fake change events from #emitChangeEvent in the slider adapter * use the GlobalChangeAndInputListener for change events instead of adding our own event listener in #registerInputEventHandler * keep track of and unsubscribe from the GlobalChangeAndInputListener in #deregisterInputEventHandler --- .../global-change-and-input-listener.ts | 70 +++++++++++++++++++ .../mdc-slider/slider.ts | 64 +++++++++++++++-- 2 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 src/material-experimental/mdc-slider/global-change-and-input-listener.ts diff --git a/src/material-experimental/mdc-slider/global-change-and-input-listener.ts b/src/material-experimental/mdc-slider/global-change-and-input-listener.ts new file mode 100644 index 000000000000..3fcfc8b8b04d --- /dev/null +++ b/src/material-experimental/mdc-slider/global-change-and-input-listener.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {DOCUMENT} from '@angular/common'; +import {Inject, Injectable, NgZone} from '@angular/core'; +import {SpecificEventListener} from '@material/base'; +import {Subject, Subscription} from 'rxjs'; +import {finalize} from 'rxjs/operators'; + +/** + * Handles listening for all change and input events that occur on the document. + * + * This service exposes a single method #listen to allow users to subscribe to change and input + * events that occur on the document. Since listening for these events on the document can be + * expensive, we lazily attach listeners to the document when the first subscription is made, and + * remove the listeners once the last observer unsubscribes. + */ +@Injectable({providedIn: 'root'}) +export class GlobalChangeAndInputListener { + + /** The injected document if available or fallback to the global document reference. */ + private _document: Document; + + /** Stores the subjects that emit the events that occur on the global document. */ + private _subjects = new Map>(); + + /** Stores the event handlers that emit the events that occur on the global document. */ + private _handlers = new Map void)>(); + + constructor(@Inject(DOCUMENT) document: any, private _ngZone: NgZone) { + this._document = document; + } + + /** Returns a function for handling the given type of event. */ + private _createHandlerFn(type: K): ((event: Event) => void) { + return (event: Event) => { + this._subjects.get(type)!.next(event); + }; + } + + /** Returns a subscription to global change or input events. */ + listen(type: K, callback: SpecificEventListener): Subscription { + // This is the first subscription to these events. + if (!this._subjects.get(type)) { + const handlerFn = this._createHandlerFn(type).bind(this); + this._subjects.set(type, new Subject()); + this._handlers.set(type, handlerFn); + this._ngZone.runOutsideAngular(() => { + this._document.addEventListener(type, handlerFn, true); + }); + } + + const subject = this._subjects.get(type)!; + const handler = this._handlers.get(type)!; + + return subject.pipe(finalize(() => { + // This is the last event listener unsubscribing. + if (subject.observers.length === 1) { + this._document.removeEventListener(type, handler, true); + this._subjects.delete(type); + this._handlers.delete(type); + } + })).subscribe(callback); + } +} diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 4e2929ec2f2e..01db485fe637 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -54,6 +54,7 @@ import { import {SpecificEventListener, EventType} from '@material/base'; import {MDCSliderAdapter, MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; import {Subscription} from 'rxjs'; +import {GlobalChangeAndInputListener} from './global-change-and-input-listener'; /** Represents a drag event emitted by the MatSlider component. */ export interface MatSliderDragEvent { @@ -320,6 +321,12 @@ export class MatSliderThumb implements AfterViewInit, ControlValueAccessor, OnIn /** Event emitted every time the MatSliderThumb is focused. */ @Output() readonly _focus: EventEmitter = new EventEmitter(); + /** Event emitted on pointer up or after left or right arrow key presses. */ + @Output() readonly change: EventEmitter = new EventEmitter(); + + /** Event emitted on each value change that happens to the slider. */ + @Output() readonly input: EventEmitter = new EventEmitter(); + _disabled: boolean = false; /** @@ -374,6 +381,13 @@ export class MatSliderThumb implements AfterViewInit, ControlValueAccessor, OnIn this._blur.emit(); } + _emitFakeEvent(type: 'change'|'input') { + const event = new Event(type) as any; + event.isFake = true; + const emitter = type === 'change' ? this.change : this.input; + emitter.emit(event); + } + /** * Sets the model value. Implemented as part of ControlValueAccessor. * @param value @@ -605,10 +619,11 @@ export class MatSlider extends _MatSliderMixinBase readonly _cdr: ChangeDetectorRef, readonly _elementRef: ElementRef, private readonly _platform: Platform, + readonly _globalChangeAndInputListener: GlobalChangeAndInputListener<'input'|'change'>, @Inject(DOCUMENT) document: any, @Optional() private _dir: Directionality, @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) - readonly _globalRippleOptions?: RippleGlobalOptions) { + readonly _globalRippleOptions?: RippleGlobalOptions) { super(_elementRef); this._document = document; this._window = this._document.defaultView || window; @@ -756,6 +771,10 @@ export class MatSlider extends _MatSliderMixinBase /** The MDCSliderAdapter implementation. */ class SliderAdapter implements MDCSliderAdapter { + + /** The global change listener subscription used to handle change events on the slider inputs. */ + changeSubscription: Subscription; + constructor(private readonly _delegate: MatSlider) {} // We manually assign functions instead of using prototype methods because @@ -840,12 +859,22 @@ class SliderAdapter implements MDCSliderAdapter { setPointerCapture = (pointerId: number): void => { this._delegate._elementRef.nativeElement.setPointerCapture(pointerId); } - // We ignore emitChangeEvent and emitInputEvent because the slider inputs - // are already exposed so users can just listen for those events directly themselves. emitChangeEvent = (value: number, thumbPosition: Thumb): void => { - this._delegate._getInput(thumbPosition)._onChange(value); + // We block all real slider input change events and emit fake change events from here, instead. + // We do this because the mdc implementation of the slider does not trigger real change events + // on pointer up (only on left or right arrow key down). + // + // By stopping real change events from reaching users, and dispatching fake change events + // (which we allow to reach the user) the slider inputs change events are triggered at the + // appropriate times. This allows users to listen for change events directly on the slider + // input as they would with a native range input. + const input = this._delegate._getInput(thumbPosition); + input._emitFakeEvent('change'); + input._onChange(value); + } + emitInputEvent = (value: number, thumbPosition: Thumb): void => { + this._delegate._getInput(thumbPosition)._emitFakeEvent('input'); } - emitInputEvent = (value: number, thumbPosition: Thumb): void => {}; emitDragStartEvent = (value: number, thumbPosition: Thumb): void => { const input = this._delegate._getInput(thumbPosition); input.dragStart.emit({ source: input, parent: this._delegate, value }); @@ -872,11 +901,32 @@ class SliderAdapter implements MDCSliderAdapter { } registerInputEventHandler = (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { - this._delegate._getInputElement(thumbPosition).addEventListener(evtType, handler); + if (evtType === 'change' || evtType === 'input') { + this.changeSubscription = this._delegate._globalChangeAndInputListener + .listen(evtType as 'change'|'input', (event: Event) => { + // We block all real change and input events and emit fake events from #emitChangeEvent + // and #emitInputEvent, instead. We do this because interacting with the MDC slider + // won't trigger all of the correct change and input events, but it will call + // #emitChangeEvent and #emitInputEvent at the correct times. This allows users to + // listen for these events directly on the slider input as they would with a native + // range input. + if (event.target === this._delegate._getInputElement(thumbPosition)) { + if ((event as any).isFake) { return; } + event.stopImmediatePropagation(); + handler(event as GlobalEventHandlersEventMap[K]); + } + }); + } else { + this._delegate._getInputElement(thumbPosition).addEventListener(evtType, handler); + } } deregisterInputEventHandler = (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { - this._delegate._getInputElement(thumbPosition).removeEventListener(evtType, handler); + if (evtType === 'change') { + this.changeSubscription.unsubscribe(); + } else { + this._delegate._getInputElement(thumbPosition).removeEventListener(evtType, handler); + } } registerBodyEventHandler = (evtType: K, handler: SpecificEventListener): void => { From e18dd79d1d21c0821747481a14189f7fed7dcfb2 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Thu, 8 Apr 2021 10:33:19 -0700 Subject: [PATCH 24/35] fix(material-experimental/mdc-slider): fix change and input events on the mdc slider * use #fromEvent to simplify the global change and input listener * go back to dispatching real events instead of using Angular's event emitter system * fix how the global change and input listener is used in the slider adapter * Rename fake event indicator boolean * Simplified change & input event logic to make things more readable --- .../global-change-and-input-listener.ts | 63 +++++------ .../mdc-slider/slider.spec.ts | 71 +++--------- .../mdc-slider/slider.ts | 105 ++++++++++++++---- 3 files changed, 131 insertions(+), 108 deletions(-) diff --git a/src/material-experimental/mdc-slider/global-change-and-input-listener.ts b/src/material-experimental/mdc-slider/global-change-and-input-listener.ts index 3fcfc8b8b04d..7f56b7cc1741 100644 --- a/src/material-experimental/mdc-slider/global-change-and-input-listener.ts +++ b/src/material-experimental/mdc-slider/global-change-and-input-listener.ts @@ -7,64 +7,61 @@ */ import {DOCUMENT} from '@angular/common'; -import {Inject, Injectable, NgZone} from '@angular/core'; +import {Inject, Injectable, NgZone, OnDestroy} from '@angular/core'; import {SpecificEventListener} from '@material/base'; -import {Subject, Subscription} from 'rxjs'; -import {finalize} from 'rxjs/operators'; +import {fromEvent, Observable, Subject, Subscription} from 'rxjs'; +import {finalize, share, takeUntil} from 'rxjs/operators'; /** * Handles listening for all change and input events that occur on the document. * * This service exposes a single method #listen to allow users to subscribe to change and input - * events that occur on the document. Since listening for these events on the document can be - * expensive, we lazily attach listeners to the document when the first subscription is made, and - * remove the listeners once the last observer unsubscribes. + * events that occur on the document. Since listening for these events can be expensive, we use + * #fromEvent which will lazily attach a listener when the first subscription is made and remove the + * listener once the last observer unsubscribes. */ @Injectable({providedIn: 'root'}) -export class GlobalChangeAndInputListener { +export class GlobalChangeAndInputListener implements OnDestroy { /** The injected document if available or fallback to the global document reference. */ private _document: Document; /** Stores the subjects that emit the events that occur on the global document. */ - private _subjects = new Map>(); + private _observables = new Map>(); - /** Stores the event handlers that emit the events that occur on the global document. */ - private _handlers = new Map void)>(); + /** The notifier that triggers the global event observables to stop emitting and complete. */ + private _destroyed = new Subject(); constructor(@Inject(DOCUMENT) document: any, private _ngZone: NgZone) { this._document = document; } - /** Returns a function for handling the given type of event. */ - private _createHandlerFn(type: K): ((event: Event) => void) { - return (event: Event) => { - this._subjects.get(type)!.next(event); - }; + ngOnDestroy() { + this._destroyed.next(); + this._destroyed.complete(); + this._observables.clear(); } /** Returns a subscription to global change or input events. */ listen(type: K, callback: SpecificEventListener): Subscription { - // This is the first subscription to these events. - if (!this._subjects.get(type)) { - const handlerFn = this._createHandlerFn(type).bind(this); - this._subjects.set(type, new Subject()); - this._handlers.set(type, handlerFn); - this._ngZone.runOutsideAngular(() => { - this._document.addEventListener(type, handlerFn, true); - }); + // If this is the first time we are listening to this event, create the observable for it. + if (!this._observables.has(type)) { + this._observables.set(type, this._createGlobalEventObservable(type)); } - const subject = this._subjects.get(type)!; - const handler = this._handlers.get(type)!; + return this._ngZone.runOutsideAngular(() => + this._observables.get(type)!.subscribe((event: Event) => + this._ngZone.run(() => callback(event)) + ) + ); + } - return subject.pipe(finalize(() => { - // This is the last event listener unsubscribing. - if (subject.observers.length === 1) { - this._document.removeEventListener(type, handler, true); - this._subjects.delete(type); - this._handlers.delete(type); - } - })).subscribe(callback); + /** Creates an observable that emits all events of the given type. */ + private _createGlobalEventObservable(type: K) { + return fromEvent(this._document, type, {capture: true, passive: true}).pipe( + takeUntil(this._destroyed), + finalize(() => this._observables.delete(type)), + share(), + ); } } diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index d6cab30d3368..4dae4dded28c 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -6,10 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes'; import {Platform} from '@angular/cdk/platform'; import { - dispatchFakeEvent, dispatchMouseEvent, dispatchPointerEvent, dispatchTouchEvent, @@ -524,11 +522,12 @@ describe('MDC-based MatSlider' , () => { }); describe('slider with set step', () => { + let fixture: ComponentFixture; let sliderInstance: MatSlider; let inputInstance: MatSliderThumb; beforeEach(waitForAsync(() => { - const fixture = createComponent(SliderWithStep); + fixture = createComponent(SliderWithStep); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); sliderInstance = sliderDebugElement.componentInstance; @@ -553,28 +552,20 @@ describe('MDC-based MatSlider' , () => { }); it('should truncate long decimal values when using a decimal step', () => { - // TODO(wagnermaciel): Uncomment this test once b/182504575 is resolved. - // sliderInstance.step = 0.1; - // slideToValue(sliderInstance, 33.3333, Thumb.END, platform.IOS); - // expect(inputInstance.value).toBe(33); - }); - - it('should truncate long decimal values when using a decimal step and the arrow keys', () => { sliderInstance.step = 0.1; - changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.END); - changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.END); - changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.END); - expect(inputInstance.value).toBe(0.3); + slideToValue(sliderInstance, 66.3333, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(66.3); }); }); describe('range slider with set step', () => { + let fixture: ComponentFixture; let sliderInstance: MatSlider; let startInputInstance: MatSliderThumb; let endInputInstance: MatSliderThumb; beforeEach(waitForAsync(() => { - const fixture = createComponent(RangeSliderWithStep); + fixture = createComponent(RangeSliderWithStep); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); sliderInstance = sliderDebugElement.componentInstance; @@ -617,33 +608,23 @@ describe('MDC-based MatSlider' , () => { }); it('should truncate long decimal start values when using a decimal step', () => { - // TODO(wagnermaciel): Uncomment this test once b/182504575 is resolved. - // sliderInstance.step = 0.1; - // slideToValue(sliderInstance, 33.3333, Thumb.START, platform.IOS); - // expect(startInputInstance.value).toBe(33); + sliderInstance.step = 0.1; + slideToValue(sliderInstance, 66.3333, Thumb.START, platform.IOS); + expect(startInputInstance.value).toBe(66.3); }); it('should truncate long decimal end values when using a decimal step', () => { - // TODO(wagnermaciel): Uncomment this test once b/182504575 is resolved. - // sliderInstance.step = 0.1; - // slideToValue(sliderInstance, 66.6666, Thumb.END, platform.IOS); - // expect(endInputInstance.value).toBe(66); - }); - - it('should truncate long decimal start values when using a decimal step arrow keys', () => { sliderInstance.step = 0.1; - changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.START); - changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.START); - changeValueUsingArrowKeys(sliderInstance, RIGHT_ARROW, Thumb.START); - expect(startInputInstance.value).toBe(0.3); - }); + slideToValue(sliderInstance, 66.3333, Thumb.END, platform.IOS); + expect(endInputInstance.value).toBe(66.3); - it('should truncate long decimal end values when using a decimal step arrow keys', () => { - sliderInstance.step = 0.1; - changeValueUsingArrowKeys(sliderInstance, LEFT_ARROW, Thumb.END); - changeValueUsingArrowKeys(sliderInstance, LEFT_ARROW, Thumb.END); - changeValueUsingArrowKeys(sliderInstance, LEFT_ARROW, Thumb.END); - expect(endInputInstance.value).toBe(99.7); + // NOTE(wagnermaciel): Different browsers treat the clientX dispatched by us differently. + // Below is an example of a case that should work but because Firefox rounds the clientX + // down, the clientX that gets dispatched (1695.998...) is not the same clientX that the MDC + // Foundation receives (1695). This means the test will pass on chromium but fail on Firefox. + // + // slideToValue(sliderInstance, 66.66, Thumb.END, platform.IOS); + // expect(endInputInstance.value).toBe(66.7); }); }); @@ -979,22 +960,6 @@ function slideToValue(slider: MatSlider, value: number, thumbPosition: Thumb, is dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, endX, endY, isIOS); } -/** - * Mimics changing the slider value using arrow keys. - * - * Dispatching keydown events on inputs do not trigger value changes. Thus, to mimic this behavior, - * we manually change the slider inputs value and then dispatch a change event (which is what the - * MDC Foundation is listening for & how it handles these updates). - */ -function changeValueUsingArrowKeys(slider: MatSlider, arrow: number, thumbPosition: Thumb) { - const input = slider._getInput(thumbPosition); - const value = arrow === RIGHT_ARROW - ? input.value + slider.step - : input.value - slider.step; - input._hostElement.value = value.toString(); - dispatchFakeEvent(input._hostElement, 'change'); -} - /** Dispatch a pointerdown or pointerup event if supported, otherwise dispatch the touch event. */ function dispatchPointerOrTouchEvent( node: Node, type: PointerEventType, x: number, y: number, isIOS: boolean) { diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 01db485fe637..b738580ec183 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -383,9 +383,8 @@ export class MatSliderThumb implements AfterViewInit, ControlValueAccessor, OnIn _emitFakeEvent(type: 'change'|'input') { const event = new Event(type) as any; - event.isFake = true; - const emitter = type === 'change' ? this.change : this.input; - emitter.emit(event); + event._matIsHandled = true; + this._hostElement.dispatchEvent(event); } /** @@ -772,10 +771,85 @@ export class MatSlider extends _MatSliderMixinBase /** The MDCSliderAdapter implementation. */ class SliderAdapter implements MDCSliderAdapter { - /** The global change listener subscription used to handle change events on the slider inputs. */ - changeSubscription: Subscription; + /** The global event listener subscription used to handle events on the slider inputs. */ + private _globalEventSubscriptions = new Subscription(); - constructor(private readonly _delegate: MatSlider) {} + /** The MDC Foundations handler function for start input change events. */ + private _startInputChangeEventHandler: SpecificEventListener; + + /** The MDC Foundations handler function for end input change events. */ + private _endInputChangeEventHandler: SpecificEventListener; + + constructor(private readonly _delegate: MatSlider) { + this._globalEventSubscriptions.add(this._subscribeToSliderInputEvents('change')); + this._globalEventSubscriptions.add(this._subscribeToSliderInputEvents('input')); + } + + /** + * Handles "change" and "input" events on the slider inputs. + * + * Exposes a callback to allow the MDC Foundations "change" event handler to be called for "real" + * events. + * + * ** IMPORTANT NOTE ** + * + * We block all "real" change and input events and emit fake events from #emitChangeEvent and + * #emitInputEvent, instead. We do this because interacting with the MDC slider won't trigger all + * of the correct change and input events, but it will call #emitChangeEvent and #emitInputEvent + * at the correct times. This allows users to listen for these events directly on the slider + * input as they would with a native range input. + */ + private _subscribeToSliderInputEvents(type: 'change'|'input') { + return this._delegate._globalChangeAndInputListener.listen(type, (event: Event) => { + const thumbPosition = this._getInputThumbPosition(event.target); + + // Do nothing if the event isn't from a thumb input. + if (thumbPosition === null) { return; } + + // Do nothing if the event is "fake". + if ((event as any)._matIsHandled) { return ; } + + // Prevent "real" events from reaching end users. + event.stopImmediatePropagation(); + + // Relay "real" change events to the MDC Foundation. + if (type === 'change') { + this._callChangeEventHandler(event, thumbPosition); + } + }); + } + + /** Calls the MDC Foundations change event handler for the specified thumb position. */ + private _callChangeEventHandler(event: Event, thumbPosition: Thumb) { + if (thumbPosition === Thumb.START) { + this._startInputChangeEventHandler(event); + } else { + this._endInputChangeEventHandler(event); + } + } + + /** Save the event handler so it can be used in our global change event listener subscription. */ + private _saveChangeEventHandler(thumbPosition: Thumb, handler: SpecificEventListener) { + if (thumbPosition === Thumb.START) { + this._startInputChangeEventHandler = handler; + } else { + this._endInputChangeEventHandler = handler; + } + } + + /** + * Returns the thumb position of the given event target. + * Returns null if the given event target does not correspond to a slider thumb input. + */ + private _getInputThumbPosition(target: EventTarget | null): Thumb | null { + if (target === this._delegate._getInputElement(Thumb.END)) { + return Thumb.END; + } + if (this._delegate._isRange() && target === this._delegate._getInputElement(Thumb.START)) { + return Thumb.START; + } + return null; + } // We manually assign functions instead of using prototype methods because // MDC clobbers the values otherwise. @@ -901,21 +975,8 @@ class SliderAdapter implements MDCSliderAdapter { } registerInputEventHandler = (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { - if (evtType === 'change' || evtType === 'input') { - this.changeSubscription = this._delegate._globalChangeAndInputListener - .listen(evtType as 'change'|'input', (event: Event) => { - // We block all real change and input events and emit fake events from #emitChangeEvent - // and #emitInputEvent, instead. We do this because interacting with the MDC slider - // won't trigger all of the correct change and input events, but it will call - // #emitChangeEvent and #emitInputEvent at the correct times. This allows users to - // listen for these events directly on the slider input as they would with a native - // range input. - if (event.target === this._delegate._getInputElement(thumbPosition)) { - if ((event as any).isFake) { return; } - event.stopImmediatePropagation(); - handler(event as GlobalEventHandlersEventMap[K]); - } - }); + if (evtType === 'change') { + this._saveChangeEventHandler(thumbPosition, handler as SpecificEventListener); } else { this._delegate._getInputElement(thumbPosition).addEventListener(evtType, handler); } @@ -923,7 +984,7 @@ class SliderAdapter implements MDCSliderAdapter { deregisterInputEventHandler = (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { if (evtType === 'change') { - this.changeSubscription.unsubscribe(); + this._globalEventSubscriptions.unsubscribe(); } else { this._delegate._getInputElement(thumbPosition).removeEventListener(evtType, handler); } From d36f8fd7bdf01932284e285b8a7dde2487d47a22 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Fri, 9 Apr 2021 11:00:05 -0700 Subject: [PATCH 25/35] feat(material-experimental/mdc-slider): rebuild the mdc-slider demo (#22445) * add exportAs: matSliderThumb to MatSliderThumb * create focus and blur methods in MatSliderThumb to match the old demo --- src/dev-app/mdc-slider/mdc-slider-demo.html | 98 +++++++++++++++---- src/dev-app/mdc-slider/mdc-slider-demo.ts | 8 +- .../mdc-slider/slider.ts | 9 ++ 3 files changed, 93 insertions(+), 22 deletions(-) diff --git a/src/dev-app/mdc-slider/mdc-slider-demo.html b/src/dev-app/mdc-slider/mdc-slider-demo.html index b6bd930eac65..68db534874f7 100644 --- a/src/dev-app/mdc-slider/mdc-slider-demo.html +++ b/src/dev-app/mdc-slider/mdc-slider-demo.html @@ -1,32 +1,88 @@ -

Color: Primary

- - - +

Default Slider

+Label + + +{{slidey.value}} - - - +

Colors

+ + + + + + + + + + +

Slider with Min and Max

+ + + + +{{slider2.value}} + + +

Disabled Slider

+ + + + + +

Slider with set value

+ + -

Color: Accent

- - - +

Slider with step defined

+ + +{{slider5.value}} - - - +

Slider with set tick interval

+ + + + + + +

Slider with Thumb Label

+ + + + +

Slider with one-way binding

+ + + + + +

Slider with two-way binding

+ + + + -

Color: Warn

- - - +

Set/lost focus to show thumblabel programmatically

+ + + + + + + + + + + + - - - +

Range slider

+ + + diff --git a/src/dev-app/mdc-slider/mdc-slider-demo.ts b/src/dev-app/mdc-slider/mdc-slider-demo.ts index 3cbd5ebe85cd..2ccd963c7744 100644 --- a/src/dev-app/mdc-slider/mdc-slider-demo.ts +++ b/src/dev-app/mdc-slider/mdc-slider-demo.ts @@ -14,4 +14,10 @@ import {Component} from '@angular/core'; templateUrl: 'mdc-slider-demo.html', styles: ['.mat-mdc-slider { width: 300px; }'], }) -export class MdcSliderDemo {} +export class MdcSliderDemo { + demo: number; + val: number = 50; + min: number = 0; + max: number = 100; + disabledValue = 0; +} diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index b738580ec183..927ce8ba1dc8 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -264,6 +264,7 @@ export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { */ @Directive({ selector: 'input[matSliderThumb], input[matSliderStartThumb], input[matSliderEndThumb]', + exportAs: 'matSliderThumb', host: { 'class': 'mdc-slider__input', 'type': 'range', @@ -423,6 +424,14 @@ export class MatSliderThumb implements AfterViewInit, ControlValueAccessor, OnIn this._slider._updateDisabled(); } + focus(): void { + this._hostElement.focus(); + } + + blur(): void { + this._hostElement.blur(); + } + /** Returns true if this slider input currently has focus. */ _isFocused(): boolean { return this._document.activeElement === this._hostElement; From eb29373e3f2bc474a392e6f9356b62fd53dcec05 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Mon, 12 Apr 2021 14:30:00 -0700 Subject: [PATCH 26/35] test(material-experimental/mdc-slider): create e2e tests (#22463) * create basic tests for standard, disabled, and range sliders --- src/e2e-app/mdc-slider/mdc-slider-e2e.ts | 15 ++- .../mdc-slider/BUILD.bazel | 1 + .../mdc-slider/slider.e2e.spec.ts | 124 +++++++++++++++++- 3 files changed, 134 insertions(+), 6 deletions(-) diff --git a/src/e2e-app/mdc-slider/mdc-slider-e2e.ts b/src/e2e-app/mdc-slider/mdc-slider-e2e.ts index ec5fe2d2b71d..fe9f07e698dd 100644 --- a/src/e2e-app/mdc-slider/mdc-slider-e2e.ts +++ b/src/e2e-app/mdc-slider/mdc-slider-e2e.ts @@ -10,7 +10,20 @@ import {Component} from '@angular/core'; @Component({ selector: 'mdc-slider-e2e', - template: ``, + template: ` + + + + + + + + + + + + + `, }) export class MdcSliderE2e { } diff --git a/src/material-experimental/mdc-slider/BUILD.bazel b/src/material-experimental/mdc-slider/BUILD.bazel index cb14d0a3ba2c..2c3fa6c98228 100644 --- a/src/material-experimental/mdc-slider/BUILD.bazel +++ b/src/material-experimental/mdc-slider/BUILD.bazel @@ -97,6 +97,7 @@ ng_e2e_test_library( deps = [ ":mdc-slider", "//src/cdk/testing/private/e2e", + "@npm//@material/slider", ], ) diff --git a/src/material-experimental/mdc-slider/slider.e2e.spec.ts b/src/material-experimental/mdc-slider/slider.e2e.spec.ts index fd79d7568fee..d6aca9fdd198 100644 --- a/src/material-experimental/mdc-slider/slider.e2e.spec.ts +++ b/src/material-experimental/mdc-slider/slider.e2e.spec.ts @@ -6,11 +6,125 @@ * found in the LICENSE file at https://angular.io/license */ -/* tslint:disable-next-line:no-unused-variable */ -import {MatSlider} from './index'; - -// TODO(wagnermaciel): Implement this in a separate PR +import {clickElementAtPoint, getElement, Point} from '@angular/cdk/testing/private/e2e'; +import {Thumb} from '@material/slider'; +import {browser, by, element, ElementFinder} from 'protractor'; describe('MDC-based MatSlider' , () => { - it('does nothing yet', async () => {}); + const getStandardSlider = () => element(by.id('standard-slider')); + const getDisabledSlider = () => element(by.id('disabled-slider')); + const getRangeSlider = () => element(by.id('range-slider')); + + beforeEach(async () => await browser.get('mdc-slider')); + + describe('standard slider', async () => { + let slider: ElementFinder; + beforeEach(() => { slider = getStandardSlider(); }); + + it('should update the value on click', async () => { + await setValueByClick(slider, 15); + expect(await getSliderValue(slider, Thumb.END)).toBe(15); + }); + + it('should update the value on slide', async () => { + await slideToValue(slider, 35, Thumb.END); + expect(await getSliderValue(slider, Thumb.END)).toBe(35); + }); + }); + + describe('disabled slider', async () => { + let slider: ElementFinder; + beforeEach(() => { slider = getDisabledSlider(); }); + + it('should not update the value on click', async () => { + await setValueByClick(slider, 15); + expect(await getSliderValue(slider, Thumb.END)).not.toBe(15); + }); + + it('should not update the value on slide', async () => { + await slideToValue(slider, 35, Thumb.END); + expect(await getSliderValue(slider, Thumb.END)).not.toBe(35); + }); + }); + + describe('range slider', async () => { + let slider: ElementFinder; + beforeEach(() => { slider = getRangeSlider(); }); + + it('should update the start thumb value on slide', async () => { + await slideToValue(slider, 35, Thumb.START); + expect(await getSliderValue(slider, Thumb.START)).toBe(35); + }); + + it('should update the end thumb value on slide', async () => { + console.log('value:', await getSliderValue(slider, Thumb.END)); + await slideToValue(slider, 55, Thumb.END); + console.log('value:', await getSliderValue(slider, Thumb.END)); + expect(await getSliderValue(slider, Thumb.END)).toBe(55); + }); + + it('should update the start thumb value on click between thumbs ' + + 'but closer to the start thumb', async () => { + await setValueByClick(slider, 49); + expect(await getSliderValue(slider, Thumb.START)).toBe(49); + expect(await getSliderValue(slider, Thumb.END)).toBe(100); + }); + + it('should update the end thumb value on click between thumbs ' + + 'but closer to the end thumb', async () => { + await setValueByClick(slider, 51); + expect(await getSliderValue(slider, Thumb.START)).toBe(0); + expect(await getSliderValue(slider, Thumb.END)).toBe(51); + }); + }); }); + +/** Returns the current value of the slider. */ +async function getSliderValue(slider: ElementFinder, thumbPosition: Thumb): Promise { + const inputs = await slider.all(by.css('.mdc-slider__input')); + return thumbPosition === Thumb.END + ? Number(await inputs[inputs.length - 1].getAttribute('value')) + : Number(await inputs[0].getAttribute('value')); +} + +/** Clicks on the MatSlider at the coordinates corresponding to the given value. */ +async function setValueByClick(slider: ElementFinder, value: number): Promise { + return clickElementAtPoint(slider, await getCoordsForValue(slider, value)); +} + +/** Clicks on the MatSlider at the coordinates corresponding to the given value. */ +async function slideToValue + (slider: ElementFinder, value: number, thumbPosition: Thumb): Promise { + const webElement = await getElement(slider).getWebElement(); + const startCoords = await getCoordsForValue( + slider, + await getSliderValue(slider, thumbPosition), + ); + const endCoords = await getCoordsForValue(slider, value); + return await browser.actions() + .mouseMove(webElement, startCoords) + .mouseDown() + .mouseMove(webElement, endCoords) + .mouseUp() + .perform(); +} + +/** Returns the x and y coordinates for the given slider value. */ +async function getCoordsForValue(slider: ElementFinder, value: number): Promise { + const inputs = await slider.all(by.css('.mdc-slider__input')); + + const min = Number(await inputs[0].getAttribute('min')); + const max = Number(await inputs[inputs.length - 1].getAttribute('max')); + const percent = (value - min) / (max - min); + + const {width, height} = await slider.getSize(); + + // NOTE: We use Math.round here because protractor silently breaks if you pass in an imprecise + // floating point number with lots of decimals. This allows us to avoid the headache but it may + // cause some innaccuracies in places where these decimals mean the difference between values. + + const x = Math.round(width * percent); + const y = Math.round(height / 2); + + return {x, y}; +} From 38f03bc74cd0b6333bf82ff47ca1d283e653615e Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Tue, 13 Apr 2021 12:36:26 -0700 Subject: [PATCH 27/35] test(material-experimental/mdc-slider): add two-way binding unit tests (#22470) --- .../mdc-slider/slider.spec.ts | 134 +++++++++++++++--- .../mdc-slider/slider.ts | 8 ++ 2 files changed, 122 insertions(+), 20 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 4dae4dded28c..e34f95a626ac 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -12,13 +12,18 @@ import { dispatchPointerEvent, dispatchTouchEvent, } from '@angular/cdk/testing/private'; -import {Component, Type} from '@angular/core'; +import {Component, QueryList, Type, ViewChild, ViewChildren} from '@angular/core'; import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {Thumb} from '@material/slider'; import {MatSliderModule} from './module'; import {MatSlider, MatSliderThumb, MatSliderVisualThumb} from './slider'; +interface Point { + x: number; + y: number; +} + describe('MDC-based MatSlider' , () => { let platform: Platform; @@ -759,6 +764,71 @@ describe('MDC-based MatSlider' , () => { expect(endInputInstance.value).toBe(70); }); }); + + describe('slider with a two-way binding', () => { + let fixture: ComponentFixture; + let testComponent: SliderWithTwoWayBinding; + + beforeEach(() => { + fixture = createComponent(SliderWithTwoWayBinding); + fixture.detectChanges(); + testComponent = fixture.componentInstance; + }); + + it('should sync the value binding in both directions', () => { + expect(testComponent.value).toBe(0); + expect(testComponent.sliderInput.value).toBe(0); + + slideToValue(testComponent.slider, 10, Thumb.END, platform.IOS); + expect(testComponent.value).toBe(10); + expect(testComponent.sliderInput.value).toBe(10); + + testComponent.value = 20; + fixture.detectChanges(); + expect(testComponent.value).toBe(20); + expect(testComponent.sliderInput.value).toBe(20); + }); + }); + + describe('range slider with a two-way binding', () => { + let fixture: ComponentFixture; + let testComponent: RangeSliderWithTwoWayBinding; + + beforeEach(waitForAsync(() => { + fixture = createComponent(RangeSliderWithTwoWayBinding); + fixture.detectChanges(); + testComponent = fixture.componentInstance; + })); + + it('should sync the start value binding in both directions', () => { + expect(testComponent.startValue).toBe(0); + expect(testComponent.sliderInputs.get(0)!.value).toBe(0); + + slideToValue(testComponent.slider, 10, Thumb.START, platform.IOS); + + expect(testComponent.startValue).toBe(10); + expect(testComponent.sliderInputs.get(0)!.value).toBe(10); + + testComponent.startValue = 20; + fixture.detectChanges(); + expect(testComponent.startValue).toBe(20); + expect(testComponent.sliderInputs.get(0)!.value).toBe(20); + }); + + it('should sync the end value binding in both directions', () => { + expect(testComponent.endValue).toBe(100); + expect(testComponent.sliderInputs.get(1)!.value).toBe(100); + + slideToValue(testComponent.slider, 90, Thumb.END, platform.IOS); + expect(testComponent.endValue).toBe(90); + expect(testComponent.sliderInputs.get(1)!.value).toBe(90); + + testComponent.endValue = 80; + fixture.detectChanges(); + expect(testComponent.endValue).toBe(80); + expect(testComponent.sliderInputs.get(1)!.value).toBe(80); + }); + }); }); @@ -910,6 +980,34 @@ class RangeSliderWithOneWayBinding { endValue = 75; } +@Component({ + template: ` + + + + `, +}) +class SliderWithTwoWayBinding { + @ViewChild(MatSlider) slider: MatSlider; + @ViewChild(MatSliderThumb) sliderInput: MatSliderThumb; + value = 0; +} + +@Component({ + template: ` + + + + + `, +}) +class RangeSliderWithTwoWayBinding { + @ViewChild(MatSlider) slider: MatSlider; + @ViewChildren(MatSliderThumb) sliderInputs: QueryList; + startValue = 0; + endValue = 100; +} + /** The pointer event types used by the MDC Slider. */ const enum PointerEventType { POINTER_DOWN = 'pointerdown', @@ -926,13 +1024,8 @@ const enum TouchEventType { /** Clicks on the MatSlider at the coordinates corresponding to the given value. */ function setValueByClick(slider: MatSlider, value: number, isIOS: boolean) { - const {min, max} = slider; - const percent = (value - min) / (max - min); - const sliderElement = slider._elementRef.nativeElement; - const {top, left, width, height} = sliderElement.getBoundingClientRect(); - const x = left + (width * percent); - const y = top + (height / 2); + const {x, y} = getCoordsForValue(slider, value); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_DOWN, x, y, isIOS); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, x, y, isIOS); @@ -940,26 +1033,27 @@ function setValueByClick(slider: MatSlider, value: number, isIOS: boolean) { /** Slides the MatSlider's thumb to the given value. */ function slideToValue(slider: MatSlider, value: number, thumbPosition: Thumb, isIOS: boolean) { - const {min, max} = slider; - const percent = (value - min) / (max - min); - const sliderElement = slider._elementRef.nativeElement; - const thumbElement = slider._getThumbElement(thumbPosition); - - const sliderDimensions = sliderElement.getBoundingClientRect(); - const thumbDimensions = thumbElement.getBoundingClientRect(); - - const startX = thumbDimensions.left + (thumbDimensions.width / 2); - const startY = thumbDimensions.top + (thumbDimensions.height / 2); - - const endX = sliderDimensions.left + (sliderDimensions.width * percent); - const endY = sliderDimensions.top + (sliderDimensions.height / 2); + const {x: startX, y: startY} = getCoordsForValue(slider, slider._getInput(thumbPosition).value); + const {x: endX, y: endY} = getCoordsForValue(slider, value); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_DOWN, startX, startY, isIOS); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_MOVE, endX, endY, isIOS); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, endX, endY, isIOS); } +/** Returns the x and y coordinates for the given slider value. */ +function getCoordsForValue(slider: MatSlider, value: number): Point { + const {min, max} = slider; + const percent = (value - min) / (max - min); + + const {top, left, width, height} = slider._elementRef.nativeElement.getBoundingClientRect(); + const x = left + (width * percent); + const y = top + (height / 2); + + return {x, y}; +} + /** Dispatch a pointerdown or pointerup event if supported, otherwise dispatch the touch event. */ function dispatchPointerOrTouchEvent( node: Node, type: PointerEventType, x: number, y: number, isIOS: boolean) { diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 927ce8ba1dc8..4370d038bc0e 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -308,6 +308,13 @@ export class MatSliderThumb implements AfterViewInit, ControlValueAccessor, OnIn } } + /** + * Emits when the raw value of the slider changes. This is here primarily + * to facilitate the two-way binding for the `value` input. + * @docs-private + */ + @Output() readonly valueChange: EventEmitter = new EventEmitter(); + /** Event emitted when the slider thumb starts being dragged. */ @Output() readonly dragStart: EventEmitter = new EventEmitter(); @@ -954,6 +961,7 @@ class SliderAdapter implements MDCSliderAdapter { const input = this._delegate._getInput(thumbPosition); input._emitFakeEvent('change'); input._onChange(value); + input.valueChange.emit(value); } emitInputEvent = (value: number, thumbPosition: Thumb): void => { this._delegate._getInput(thumbPosition)._emitFakeEvent('input'); From c721c2d5c66c4cb65f0f4a48d7b04ae73f23d6e7 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Wed, 14 Apr 2021 09:23:01 -0700 Subject: [PATCH 28/35] test(material-experimental/mdc-slider): add ngModel unit tests (#22474) --- .../mdc-slider/slider.spec.ts | 154 +++++++++++++++++- 1 file changed, 152 insertions(+), 2 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index e34f95a626ac..97fd39fb8848 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -13,7 +13,15 @@ import { dispatchTouchEvent, } from '@angular/cdk/testing/private'; import {Component, QueryList, Type, ViewChild, ViewChildren} from '@angular/core'; -import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; import {Thumb} from '@material/slider'; import {MatSliderModule} from './module'; @@ -35,7 +43,7 @@ describe('MDC-based MatSlider' , () => { function createComponent(component: Type): ComponentFixture { TestBed.configureTestingModule({ - imports: [MatSliderModule], + imports: [FormsModule, MatSliderModule], declarations: [component], }).compileComponents(); return TestBed.createComponent(component); @@ -765,6 +773,122 @@ describe('MDC-based MatSlider' , () => { }); }); + describe('slider with ngModel', () => { + let fixture: ComponentFixture; + let testComponent: SliderWithNgModel; + let inputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + fixture = createComponent(SliderWithNgModel); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + const sliderInstance = sliderDebugElement.componentInstance; + inputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should update the model on mouseup', () => { + expect(testComponent.val).toBe(0); + setValueByClick(testComponent.slider, 76, platform.IOS); + fixture.detectChanges(); + expect(testComponent.val).toBe(76); + }); + + it('should update the model on slide', () => { + expect(testComponent.val).toBe(0); + slideToValue(testComponent.slider, 19, Thumb.END, platform.IOS); + fixture.detectChanges(); + expect(testComponent.val).toBe(19); + }); + + it('should be able to reset a slider by setting the model back to undefined', fakeAsync(() => { + expect(inputInstance.value).toBe(0); + testComponent.val = 5; + fixture.detectChanges(); + flush(); + expect(inputInstance.value).toBe(5); + + testComponent.val = undefined; + fixture.detectChanges(); + flush(); + expect(inputInstance.value).toBe(0); + })); + }); + + describe('slider with ngModel', () => { + let fixture: ComponentFixture; + let testComponent: RangeSliderWithNgModel; + + let startInputInstance: MatSliderThumb; + let endInputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + fixture = createComponent(RangeSliderWithNgModel); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + const sliderInstance = sliderDebugElement.componentInstance; + startInputInstance = sliderInstance._getInput(Thumb.START); + endInputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should update the start thumb model on mouseup', () => { + expect(testComponent.startVal).toBe(0); + setValueByClick(testComponent.slider, 25, platform.IOS); + fixture.detectChanges(); + expect(testComponent.startVal).toBe(25); + }); + + it('should update the end thumb model on mouseup', () => { + expect(testComponent.endVal).toBe(100); + setValueByClick(testComponent.slider, 75, platform.IOS); + fixture.detectChanges(); + expect(testComponent.endVal).toBe(75); + }); + + it('should update the start thumb model on slide', () => { + expect(testComponent.startVal).toBe(0); + slideToValue(testComponent.slider, 19, Thumb.START, platform.IOS); + fixture.detectChanges(); + expect(testComponent.startVal).toBe(19); + }); + + it('should update the end thumb model on slide', () => { + expect(testComponent.endVal).toBe(100); + slideToValue(testComponent.slider, 19, Thumb.END, platform.IOS); + fixture.detectChanges(); + expect(testComponent.endVal).toBe(19); + }); + + it('should be able to reset a slider by setting the start thumb model back to undefined', + fakeAsync(() => { + expect(startInputInstance.value).toBe(0); + testComponent.startVal = 5; + fixture.detectChanges(); + flush(); + expect(startInputInstance.value).toBe(5); + + testComponent.startVal = undefined; + fixture.detectChanges(); + flush(); + expect(startInputInstance.value).toBe(0); + })); + + it('should be able to reset a slider by setting the end thumb model back to undefined', + fakeAsync(() => { + expect(endInputInstance.value).toBe(100); + testComponent.endVal = 5; + fixture.detectChanges(); + flush(); + expect(endInputInstance.value).toBe(5); + + testComponent.endVal = undefined; + fixture.detectChanges(); + flush(); + expect(endInputInstance.value).toBe(0); + })); + }); + describe('slider with a two-way binding', () => { let fixture: ComponentFixture; let testComponent: SliderWithTwoWayBinding; @@ -980,6 +1104,32 @@ class RangeSliderWithOneWayBinding { endValue = 75; } +@Component({ + template: ` + + + + `, +}) +class SliderWithNgModel { + @ViewChild(MatSlider) slider: MatSlider; + val: number | undefined = 0; +} + +@Component({ + template: ` + + + + + `, +}) +class RangeSliderWithNgModel { + @ViewChild(MatSlider) slider: MatSlider; + startVal: number | undefined = 0; + endVal: number | undefined = 100; +} + @Component({ template: ` From 42a7717c6237b9b36f1d0e733b9b0f7ca39a4fa8 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Wed, 14 Apr 2021 11:30:24 -0700 Subject: [PATCH 29/35] test(material-experimental/mdc-slider): add change handler tests (#22478) --- .../mdc-slider/slider.spec.ts | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 97fd39fb8848..abed47f16a2a 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -773,6 +773,253 @@ describe('MDC-based MatSlider' , () => { }); }); + describe('slider with change handler', () => { + let sliderInstance: MatSlider; + let inputInstance: MatSliderThumb; + let sliderElement: HTMLElement; + let fixture: ComponentFixture; + let testComponent: SliderWithChangeHandler; + + beforeEach(waitForAsync(() => { + fixture = createComponent(SliderWithChangeHandler); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderElement = sliderDebugElement.nativeElement; + sliderInstance = sliderDebugElement.componentInstance; + inputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should emit change on mouseup', () => { + expect(testComponent.onChange).not.toHaveBeenCalled(); + setValueByClick(sliderInstance, 20, platform.IOS); + expect(testComponent.onChange).toHaveBeenCalledTimes(1); + }); + + it('should emit change on slide', () => { + expect(testComponent.onChange).not.toHaveBeenCalled(); + slideToValue(sliderInstance, 40, Thumb.END, platform.IOS); + expect(testComponent.onChange).toHaveBeenCalledTimes(1); + }); + + it('should not emit multiple changes for the same value', () => { + expect(testComponent.onChange).not.toHaveBeenCalled(); + + setValueByClick(sliderInstance, 60, platform.IOS); + slideToValue(sliderInstance, 60, Thumb.END, platform.IOS); + setValueByClick(sliderInstance, 60, platform.IOS); + slideToValue(sliderInstance, 60, Thumb.END, platform.IOS); + + expect(testComponent.onChange).toHaveBeenCalledTimes(1); + }); + + it('should dispatch events when changing back to previously emitted value after ' + + 'programmatically setting value', () => { + const dispatchSliderEvent = (type: PointerEventType, value: number) => { + const {x, y} = getCoordsForValue(sliderInstance, value); + dispatchPointerOrTouchEvent(sliderElement, type, x, y, platform.IOS); + }; + + expect(testComponent.onChange).not.toHaveBeenCalled(); + expect(testComponent.onInput).not.toHaveBeenCalled(); + + dispatchSliderEvent(PointerEventType.POINTER_DOWN, 20); + fixture.detectChanges(); + + expect(testComponent.onChange).not.toHaveBeenCalled(); + expect(testComponent.onInput).toHaveBeenCalledTimes(1); + + dispatchSliderEvent(PointerEventType.POINTER_UP, 20); + fixture.detectChanges(); + + expect(testComponent.onChange).toHaveBeenCalledTimes(1); + expect(testComponent.onInput).toHaveBeenCalledTimes(1); + + inputInstance.value = 0; + fixture.detectChanges(); + + expect(testComponent.onChange).toHaveBeenCalledTimes(1); + expect(testComponent.onInput).toHaveBeenCalledTimes(1); + + dispatchSliderEvent(PointerEventType.POINTER_DOWN, 20); + fixture.detectChanges(); + dispatchSliderEvent(PointerEventType.POINTER_UP, 20); + + expect(testComponent.onChange).toHaveBeenCalledTimes(2); + expect(testComponent.onInput).toHaveBeenCalledTimes(2); + }); + }); + + describe('range slider with change handlers', () => { + let sliderInstance: MatSlider; + let startInputInstance: MatSliderThumb; + let endInputInstance: MatSliderThumb; + let sliderElement: HTMLElement; + let fixture: ComponentFixture; + let testComponent: RangeSliderWithChangeHandler; + + beforeEach(waitForAsync(() => { + fixture = createComponent(RangeSliderWithChangeHandler); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderElement = sliderDebugElement.nativeElement; + sliderInstance = sliderDebugElement.componentInstance; + startInputInstance = sliderInstance._getInput(Thumb.START); + endInputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('should emit change on mouseup on the start thumb', () => { + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + setValueByClick(sliderInstance, 20, platform.IOS); + expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + }); + + it('should emit change on mouseup on the end thumb', () => { + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + setValueByClick(sliderInstance, 80, platform.IOS); + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1); + }); + + it('should emit change on start thumb slide', () => { + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + slideToValue(sliderInstance, 40, Thumb.START, platform.IOS); + expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + }); + + it('should emit change on end thumb slide', () => { + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + slideToValue(sliderInstance, 60, Thumb.END, platform.IOS); + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1); + }); + + it('should not emit multiple changes for the same start thumb value', () => { + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + + setValueByClick(sliderInstance, 30, platform.IOS); + slideToValue(sliderInstance, 30, Thumb.START, platform.IOS); + setValueByClick(sliderInstance, 30, platform.IOS); + slideToValue(sliderInstance, 30, Thumb.START, platform.IOS); + + expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + }); + + it('should not emit multiple changes for the same end thumb value', () => { + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + + setValueByClick(sliderInstance, 60, platform.IOS); + slideToValue(sliderInstance, 60, Thumb.END, platform.IOS); + setValueByClick(sliderInstance, 60, platform.IOS); + slideToValue(sliderInstance, 60, Thumb.END, platform.IOS); + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1); + }); + + it('should dispatch events when changing back to previously emitted value after ' + + 'programmatically setting the start value', () => { + const dispatchSliderEvent = (type: PointerEventType, value: number) => { + const {x, y} = getCoordsForValue(sliderInstance, value); + dispatchPointerOrTouchEvent(sliderElement, type, x, y, platform.IOS); + }; + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + + dispatchSliderEvent(PointerEventType.POINTER_DOWN, 20); + fixture.detectChanges(); + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(1); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + + dispatchSliderEvent(PointerEventType.POINTER_UP, 20); + fixture.detectChanges(); + + expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1); + expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(1); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + + startInputInstance.value = 0; + fixture.detectChanges(); + + expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1); + expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(1); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + + dispatchSliderEvent(PointerEventType.POINTER_DOWN, 20); + fixture.detectChanges(); + dispatchSliderEvent(PointerEventType.POINTER_UP, 20); + + expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(2); + expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(2); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + }); + + it('should dispatch events when changing back to previously emitted value after ' + + 'programmatically setting the end value', () => { + const dispatchSliderEvent = (type: PointerEventType, value: number) => { + const {x, y} = getCoordsForValue(sliderInstance, value); + dispatchPointerOrTouchEvent(sliderElement, type, x, y, platform.IOS); + }; + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + + dispatchSliderEvent(PointerEventType.POINTER_DOWN, 80); + fixture.detectChanges(); + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(1); + + dispatchSliderEvent(PointerEventType.POINTER_UP, 80); + fixture.detectChanges(); + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1); + expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(1); + + endInputInstance.value = 100; + fixture.detectChanges(); + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1); + expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(1); + + dispatchSliderEvent(PointerEventType.POINTER_DOWN, 80); + fixture.detectChanges(); + dispatchSliderEvent(PointerEventType.POINTER_UP, 80); + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(2); + expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(2); + }); + }); + describe('slider with ngModel', () => { let fixture: ComponentFixture; let testComponent: SliderWithNgModel; @@ -1104,6 +1351,41 @@ class RangeSliderWithOneWayBinding { endValue = 75; } +@Component({ + template: ` + + + + `, +}) +class SliderWithChangeHandler { + onChange = jasmine.createSpy('onChange'); + onInput = jasmine.createSpy('onChange'); + @ViewChild(MatSlider) slider: MatSlider; +} + +@Component({ + template: ` + + + + + `, +}) +class RangeSliderWithChangeHandler { + onStartThumbChange = jasmine.createSpy('onStartThumbChange'); + onStartThumbInput = jasmine.createSpy('onStartThumbInput'); + onEndThumbChange = jasmine.createSpy('onEndThumbChange'); + onEndThumbInput = jasmine.createSpy('onEndThumbInput'); + @ViewChild(MatSlider) slider: MatSlider; +} + @Component({ template: ` From 44ad910510fcd31336a0d18c22bb196bf1050048 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Thu, 15 Apr 2021 07:15:30 -0700 Subject: [PATCH 30/35] test(material-experimental/mdc-slider): add input handler tests (#22481) --- .../mdc-slider/slider.e2e.spec.ts | 2 - .../mdc-slider/slider.spec.ts | 147 ++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.e2e.spec.ts b/src/material-experimental/mdc-slider/slider.e2e.spec.ts index d6aca9fdd198..d0a2a25a0a49 100644 --- a/src/material-experimental/mdc-slider/slider.e2e.spec.ts +++ b/src/material-experimental/mdc-slider/slider.e2e.spec.ts @@ -57,9 +57,7 @@ describe('MDC-based MatSlider' , () => { }); it('should update the end thumb value on slide', async () => { - console.log('value:', await getSliderValue(slider, Thumb.END)); await slideToValue(slider, 55, Thumb.END); - console.log('value:', await getSliderValue(slider, Thumb.END)); expect(await getSliderValue(slider, Thumb.END)).toBe(55); }); diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index abed47f16a2a..05cba21cc314 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -1020,6 +1020,153 @@ describe('MDC-based MatSlider' , () => { }); }); + describe('slider with input event', () => { + let sliderInstance: MatSlider; + let sliderElement: HTMLElement; + let testComponent: SliderWithChangeHandler; + + beforeEach(waitForAsync(() => { + const fixture = createComponent(SliderWithChangeHandler); + fixture.detectChanges(); + + testComponent = fixture.debugElement.componentInstance; + + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + sliderElement = sliderInstance._elementRef.nativeElement; + })); + + it('should emit an input event while sliding', () => { + const dispatchSliderEvent = (type: PointerEventType, value: number) => { + const {x, y} = getCoordsForValue(sliderInstance, value); + dispatchPointerOrTouchEvent(sliderElement, type, x, y, platform.IOS); + }; + + expect(testComponent.onChange).not.toHaveBeenCalled(); + expect(testComponent.onInput).not.toHaveBeenCalled(); + + // pointer down on current value (should not trigger input event) + dispatchSliderEvent(PointerEventType.POINTER_DOWN, 0); + + // value changes (should trigger input) + dispatchSliderEvent(PointerEventType.POINTER_MOVE, 10); + dispatchSliderEvent(PointerEventType.POINTER_MOVE, 25); + + // a new value has been committed (should trigger change event) + dispatchSliderEvent(PointerEventType.POINTER_UP, 25); + + expect(testComponent.onInput).toHaveBeenCalledTimes(2); + expect(testComponent.onChange).toHaveBeenCalledTimes(1); + }); + + it('should emit an input event when clicking', () => { + expect(testComponent.onChange).not.toHaveBeenCalled(); + expect(testComponent.onInput).not.toHaveBeenCalled(); + setValueByClick(sliderInstance, 75, platform.IOS); + expect(testComponent.onInput).toHaveBeenCalledTimes(1); + expect(testComponent.onChange).toHaveBeenCalledTimes(1); + }); + }); + + describe('range slider with input event', () => { + let sliderInstance: MatSlider; + let sliderElement: HTMLElement; + let testComponent: RangeSliderWithChangeHandler; + + beforeEach(waitForAsync(() => { + const fixture = createComponent(RangeSliderWithChangeHandler); + fixture.detectChanges(); + + testComponent = fixture.debugElement.componentInstance; + + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + sliderElement = sliderInstance._elementRef.nativeElement; + })); + + it('should emit an input event while sliding the start thumb', () => { + const dispatchSliderEvent = (type: PointerEventType, value: number) => { + const {x, y} = getCoordsForValue(sliderInstance, value); + dispatchPointerOrTouchEvent(sliderElement, type, x, y, platform.IOS); + }; + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + + // pointer down on current start thumb value (should not trigger input event) + dispatchSliderEvent(PointerEventType.POINTER_DOWN, 0); + + // value changes (should trigger input) + dispatchSliderEvent(PointerEventType.POINTER_MOVE, 10); + dispatchSliderEvent(PointerEventType.POINTER_MOVE, 25); + + // a new value has been committed (should trigger change event) + dispatchSliderEvent(PointerEventType.POINTER_UP, 25); + + expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1); + expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(2); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + }); + + it('should emit an input event while sliding the end thumb', () => { + const dispatchSliderEvent = (type: PointerEventType, value: number) => { + const {x, y} = getCoordsForValue(sliderInstance, value); + dispatchPointerOrTouchEvent(sliderElement, type, x, y, platform.IOS); + }; + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + + // pointer down on current end thumb value (should not trigger input event) + dispatchSliderEvent(PointerEventType.POINTER_DOWN, 100); + + // value changes (should trigger input) + dispatchSliderEvent(PointerEventType.POINTER_MOVE, 90); + dispatchSliderEvent(PointerEventType.POINTER_MOVE, 55); + + // a new value has been committed (should trigger change event) + dispatchSliderEvent(PointerEventType.POINTER_UP, 55); + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1); + expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(2); + }); + + it('should emit an input event on the start thumb when clicking near it', () => { + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + + setValueByClick(sliderInstance, 30, platform.IOS); + + expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1); + expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(1); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + }); + + it('should emit an input event on the end thumb when clicking near it', () => { + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbInput).not.toHaveBeenCalled(); + + setValueByClick(sliderInstance, 55, platform.IOS); + + expect(testComponent.onStartThumbChange).not.toHaveBeenCalled(); + expect(testComponent.onStartThumbInput).not.toHaveBeenCalled(); + expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1); + expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(1); + }); + }); + describe('slider with ngModel', () => { let fixture: ComponentFixture; let testComponent: SliderWithNgModel; From 96ce2a9b4a1f4a00d1462c17aff717433da8ecf2 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Sat, 24 Apr 2021 11:32:45 -0700 Subject: [PATCH 31/35] test(material-experimental/mdc-slider): add custom form control tests (#22546) * 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 | 40 ++- 2 files changed, 274 insertions(+), 11 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..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. */ From d6923c1137a07fcaa568a752f87ccf23f1fb32e8 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Mon, 26 Apr 2021 10:16:03 -0700 Subject: [PATCH 32/35] fix(material-experimental/mdc-slider): avoid using whenStable (#22571) * whenStable was causing tests to pass even when they should have been failing --- .../mdc-slider/slider.spec.ts | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 90e8bb709e4e..6b48ed167397 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -657,18 +657,15 @@ describe('MDC-based MatSlider' , () => { }); it('should invoke the passed-in `displayWith` function with the value', () => { - spyOn(fixture.componentInstance, 'displayWith').and.callThrough(); + spyOn(sliderInstance, 'displayWith').and.callThrough(); sliderInstance._setValue(1337, Thumb.END); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.displayWith).toHaveBeenCalledWith(1337); - }); + expect(sliderInstance.displayWith).toHaveBeenCalledWith(1337); }); it('should format the thumb label based on the passed-in `displayWith` function', () => { sliderInstance._setValue(200000, Thumb.END); - fixture.whenStable().then(() => { - expect(valueIndicatorTextElement.textContent).toBe('200k'); - }); + fixture.detectChanges(); + expect(valueIndicatorTextElement.textContent).toBe('$200k'); }); }); @@ -693,33 +690,27 @@ describe('MDC-based MatSlider' , () => { }); it('should invoke the passed-in `displayWith` function with the start value', () => { - spyOn(fixture.componentInstance, 'displayWith').and.callThrough(); + spyOn(sliderInstance, 'displayWith').and.callThrough(); sliderInstance._setValue(1337, Thumb.START); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.displayWith).toHaveBeenCalledWith(1337); - }); + expect(sliderInstance.displayWith).toHaveBeenCalledWith(1337); }); it('should invoke the passed-in `displayWith` function with the end value', () => { - spyOn(fixture.componentInstance, 'displayWith').and.callThrough(); + spyOn(sliderInstance, 'displayWith').and.callThrough(); sliderInstance._setValue(5996, Thumb.END); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.displayWith).toHaveBeenCalledWith(5996); - }); + expect(sliderInstance.displayWith).toHaveBeenCalledWith(5996); }); it('should format the start thumb label based on the passed-in `displayWith` function', () => { sliderInstance._setValue(200000, Thumb.START); - fixture.whenStable().then(() => { - expect(startValueIndicatorTextElement.textContent).toBe('200k'); - }); + fixture.detectChanges(); + expect(startValueIndicatorTextElement.textContent).toBe('$200k'); }); it('should format the end thumb label based on the passed-in `displayWith` function', () => { sliderInstance._setValue(700000, Thumb.END); - fixture.whenStable().then(() => { - expect(endValueIndicatorTextElement.textContent).toBe('700k'); - }); + fixture.detectChanges(); + expect(endValueIndicatorTextElement.textContent).toBe('$700k'); }); }); From 05d4e1d6232da468fde6953408a91002218580ac Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Mon, 26 Apr 2021 12:54:26 -0700 Subject: [PATCH 33/35] test(material-experimental/mdc-slider): add directionality tests (#22572) --- .../mdc-slider/BUILD.bazel | 1 + .../mdc-slider/slider.spec.ts | 59 ++++++++++++++++++- .../mdc-slider/slider.ts | 12 +++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/material-experimental/mdc-slider/BUILD.bazel b/src/material-experimental/mdc-slider/BUILD.bazel index 2c3fa6c98228..55653eb3964f 100644 --- a/src/material-experimental/mdc-slider/BUILD.bazel +++ b/src/material-experimental/mdc-slider/BUILD.bazel @@ -79,6 +79,7 @@ ng_test_library( "@npm//@angular/forms", "@npm//@angular/platform-browser", "@npm//@material/slider", + "@npm//rxjs", ], ) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 6b48ed167397..625853d271c5 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ +import {BidiModule, Directionality} from '@angular/cdk/bidi'; import {Platform} from '@angular/cdk/platform'; import { dispatchMouseEvent, dispatchPointerEvent, dispatchTouchEvent, } from '@angular/cdk/testing/private'; -import {Component, QueryList, Type, ViewChild, ViewChildren} from '@angular/core'; +import {Component, Provider, QueryList, Type, ViewChild, ViewChildren} from '@angular/core'; import { ComponentFixture, fakeAsync, @@ -24,6 +25,7 @@ import { import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; import {Thumb} from '@material/slider'; +import {of} from 'rxjs'; import {MatSliderModule} from './module'; import {MatSlider, MatSliderThumb, MatSliderVisualThumb} from './slider'; @@ -41,10 +43,14 @@ describe('MDC-based MatSlider' , () => { spyOn(Element.prototype, 'setPointerCapture'); }); - function createComponent(component: Type): ComponentFixture { + function createComponent( + component: Type, + providers: Provider[] = [], + ): ComponentFixture { TestBed.configureTestingModule({ - imports: [FormsModule, MatSliderModule, ReactiveFormsModule], + imports: [FormsModule, MatSliderModule, ReactiveFormsModule, BidiModule], declarations: [component], + providers: [...providers], }).compileComponents(); return TestBed.createComponent(component); } @@ -1158,6 +1164,53 @@ describe('MDC-based MatSlider' , () => { }); }); + describe('slider with direction', () => { + let sliderInstance: MatSlider; + let inputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + const fixture = createComponent(StandardSlider, [{ + provide: Directionality, + useValue: ({value: 'rtl', change: of()}) + }]); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + inputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('works in RTL languages', () => { + setValueByClick(sliderInstance, 30, platform.IOS); + expect(inputInstance.value).toBe(70); + }); + }); + + describe('range slider with direction', () => { + let sliderInstance: MatSlider; + let startInputInstance: MatSliderThumb; + let endInputInstance: MatSliderThumb; + + beforeEach(waitForAsync(() => { + const fixture = createComponent(StandardRangeSlider, [{ + provide: Directionality, + useValue: ({value: 'rtl', change: of()}) + }]); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + sliderInstance = sliderDebugElement.componentInstance; + startInputInstance = sliderInstance._getInput(Thumb.START); + endInputInstance = sliderInstance._getInput(Thumb.END); + })); + + it('works in RTL languages', () => { + setValueByClick(sliderInstance, 90, platform.IOS); + expect(startInputInstance.value).toBe(10); + + setValueByClick(sliderInstance, 10, platform.IOS); + expect(endInputInstance.value).toBe(90); + }); + }); + describe('slider with ngModel', () => { let fixture: ComponentFixture; let testComponent: SliderWithNgModel; diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 2470a33053df..54459b694c54 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -630,6 +630,7 @@ export class MatSlider extends _MatSliderMixinBase private _dirChangeSubscription: Subscription; constructor( + readonly _ngZone: NgZone, readonly _cdr: ChangeDetectorRef, readonly _elementRef: ElementRef, private readonly _platform: Platform, @@ -641,7 +642,7 @@ export class MatSlider extends _MatSliderMixinBase super(_elementRef); this._document = document; this._window = this._document.defaultView || window; - this._dirChangeSubscription = this._dir.change.subscribe(() => this._reinitialize()); + this._dirChangeSubscription = this._dir.change.subscribe(() => this._onDirChange()); } ngAfterViewInit() { @@ -701,6 +702,15 @@ export class MatSlider extends _MatSliderMixinBase } } + /** Handles updating the slider foundation after a dir change. */ + private _onDirChange(): void { + this._ngZone.runOutsideAngular(() => { + // We need to call layout() a few milliseconds after the dir change callback + // because we need to wait until the bounding client rect of the slider has updated. + setTimeout(() => this._foundation.layout(), 10); + }); + } + /** Sets the value of a slider thumb. */ _setValue(value: number, thumbPosition: Thumb): void { thumbPosition === Thumb.START From 24c143a3514fbf4c42229047297d936a35db32f6 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Thu, 29 Apr 2021 12:05:28 -0700 Subject: [PATCH 34/35] fix(material-experimental/mdc-slider): keep slider ui in sync with foundation (#22579) * The MDC Foundation stores the bounding client rect when layout is first called. This means that if the position of the slider changes after the initial layout, the slider will break. To fix this broken behavior, we have to keep calling layout. * Added a unit test to ensure layout changes does not break the slider. --- .../mdc-slider/slider.spec.ts | 19 ++++++-- .../mdc-slider/slider.ts | 46 +++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 625853d271c5..608c3fe7f7d7 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -98,6 +98,13 @@ describe('MDC-based MatSlider' , () => { setValueByClick(sliderInstance, 0, platform.IOS); expect(document.activeElement).toBe(inputInstance._hostElement); }); + + it('should not break on when the page layout changes', () => { + sliderInstance._elementRef.nativeElement.style.marginLeft = '100px'; + setValueByClick(sliderInstance, 10, platform.IOS); + expect(inputInstance.value).toBe(10); + sliderInstance._elementRef.nativeElement.style.marginLeft = 'initial'; + }); }); describe('standard range slider', () => { @@ -1891,6 +1898,7 @@ function setValueByClick(slider: MatSlider, value: number, isIOS: boolean) { const sliderElement = slider._elementRef.nativeElement; const {x, y} = getCoordsForValue(slider, value); + dispatchPointerEvent(sliderElement, 'mouseenter', x, y); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_DOWN, x, y, isIOS); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, x, y, isIOS); } @@ -1901,6 +1909,7 @@ function slideToValue(slider: MatSlider, value: number, thumbPosition: Thumb, is const {x: startX, y: startY} = getCoordsForValue(slider, slider._getInput(thumbPosition).value); const {x: endX, y: endY} = getCoordsForValue(slider, value); + dispatchPointerEvent(sliderElement, 'mouseenter', startX, startY); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_DOWN, startX, startY, isIOS); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_MOVE, endX, endY, isIOS); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, endX, endY, isIOS); @@ -1921,11 +1930,11 @@ function getCoordsForValue(slider: MatSlider, value: number): Point { /** Dispatch a pointerdown or pointerup event if supported, otherwise dispatch the touch event. */ function dispatchPointerOrTouchEvent( node: Node, type: PointerEventType, x: number, y: number, isIOS: boolean) { - if (isIOS) { - dispatchTouchEvent(node, pointerEventTypeToTouchEventType(type), x, y, x, y); - } else { - dispatchPointerEvent(node, type, x, y); - } + if (isIOS) { + dispatchTouchEvent(node, pointerEventTypeToTouchEventType(type), x, y, x, y); + } else { + dispatchPointerEvent(node, type, x, y); + } } /** Returns the touch event equivalent of the given pointer event. */ diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 54459b694c54..44a70e413d88 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -626,6 +626,16 @@ export class MatSlider extends _MatSliderMixinBase /** The display value of the end thumb. */ _endValueIndicatorText: string; + /** + * Whether the browser supports pointer events. + * + * We exclude iOS to mirror the MDC Foundation. The MDC Foundation cannot use pointer events on + * iOS because of this open bug - https://bugs.webkit.org/show_bug.cgi?id=220196. + */ + private _SUPPORTS_POINTER_EVENTS = typeof PointerEvent !== 'undefined' + && !!PointerEvent + && !this._platform.IOS; + /** Subscription to changes to the directionality (LTR / RTL) context for the application. */ private _dirChangeSubscription: Subscription; @@ -643,6 +653,7 @@ export class MatSlider extends _MatSliderMixinBase this._document = document; this._window = this._document.defaultView || window; this._dirChangeSubscription = this._dir.change.subscribe(() => this._onDirChange()); + this._attachUISyncEventListener(); } ngAfterViewInit() { @@ -676,6 +687,7 @@ export class MatSlider extends _MatSliderMixinBase this._foundation.destroy(); } this._dirChangeSubscription.unsubscribe(); + this._removeUISyncEventListener(); } /** Returns true if the language direction for this slider element is right to left. */ @@ -683,6 +695,40 @@ export class MatSlider extends _MatSliderMixinBase return this._dir && this._dir.value === 'rtl'; } + /** + * Attaches an event listener that keeps sync the slider UI and the foundation in sync. + * + * Because the MDC Foundation stores the value of the bounding client rect when layout is called, + * we need to keep calling layout to avoid the position of the slider getting out of sync with + * what the foundation has stored. If we don't do this, the foundation will not be able to + * correctly calculate the slider value on click/slide. + */ + _attachUISyncEventListener(): void { + // Implementation detail: It may seem weird that we are using "mouseenter" instead of + // "mousedown" as the default for when a browser does not support pointer events. While we + // would prefer to use "mousedown" as the default, for some reason it does not work (the + // callback is never triggered). + if (this._SUPPORTS_POINTER_EVENTS) { + this._elementRef.nativeElement.addEventListener('pointerdown', this._layout); + } else { + this._elementRef.nativeElement.addEventListener('mouseenter', this._layout); + this._elementRef.nativeElement.addEventListener('touchstart', this._layout); + } + } + + /** Removes the event listener that keeps sync the slider UI and the foundation in sync. */ + _removeUISyncEventListener(): void { + if (this._SUPPORTS_POINTER_EVENTS) { + this._elementRef.nativeElement.removeEventListener('pointerdown', this._layout); + } else { + this._elementRef.nativeElement.removeEventListener('mouseenter', this._layout); + this._elementRef.nativeElement.removeEventListener('touchstart', this._layout); + } + } + + /** Wrapper function for calling layout (needed for adding & removing an event listener). */ + private _layout = () => this._foundation.layout(); + /** * Reinitializes the slider foundation and input state(s). * From 585ddd9aaff53063fffd4a3156d47f7ba0c9fbee Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Thu, 29 Apr 2021 13:30:25 -0700 Subject: [PATCH 35/35] fix(material-experimental/mdc-slider): fix unit tests after rebasing * define the width for the unit test component styles * fix decimal step tests that were failing because of the firefox decimal imprecision bug --- .../mdc-slider/slider.spec.ts | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 608c3fe7f7d7..215809988411 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -578,9 +578,9 @@ describe('MDC-based MatSlider' , () => { }); it('should truncate long decimal values when using a decimal step', () => { - sliderInstance.step = 0.1; - slideToValue(sliderInstance, 66.3333, Thumb.END, platform.IOS); - expect(inputInstance.value).toBe(66.3); + sliderInstance.step = 0.5; + slideToValue(sliderInstance, 55.555, Thumb.END, platform.IOS); + expect(inputInstance.value).toBe(55.5); }); }); @@ -635,14 +635,14 @@ describe('MDC-based MatSlider' , () => { it('should truncate long decimal start values when using a decimal step', () => { sliderInstance.step = 0.1; - slideToValue(sliderInstance, 66.3333, Thumb.START, platform.IOS); - expect(startInputInstance.value).toBe(66.3); + slideToValue(sliderInstance, 33.7, Thumb.START, platform.IOS); + expect(startInputInstance.value).toBe(33.7); }); it('should truncate long decimal end values when using a decimal step', () => { sliderInstance.step = 0.1; - slideToValue(sliderInstance, 66.3333, Thumb.END, platform.IOS); - expect(endInputInstance.value).toBe(66.3); + slideToValue(sliderInstance, 33.7, Thumb.END, platform.IOS); + expect(endInputInstance.value).toBe(33.7); // NOTE(wagnermaciel): Different browsers treat the clientX dispatched by us differently. // Below is an example of a case that should work but because Firefox rounds the clientX @@ -1619,6 +1619,7 @@ describe('MDC-based MatSlider' , () => { }); }); +const SLIDER_STYLES = ['.mat-mdc-slider { width: 300px; }']; @Component({ template: ` @@ -1626,6 +1627,7 @@ describe('MDC-based MatSlider' , () => { `, + styles: SLIDER_STYLES, }) class StandardSlider {} @@ -1636,6 +1638,7 @@ class StandardSlider {} `, + styles: SLIDER_STYLES, }) class StandardRangeSlider {} @@ -1645,6 +1648,7 @@ class StandardRangeSlider {} `, + styles: SLIDER_STYLES, }) class DisabledSlider {} @@ -1655,6 +1659,7 @@ class DisabledSlider {}
`, + styles: SLIDER_STYLES, }) class DisabledRangeSlider {} @@ -1664,6 +1669,7 @@ class DisabledRangeSlider {}
`, + styles: SLIDER_STYLES, }) class SliderWithMinAndMax {} @@ -1674,6 +1680,7 @@ class SliderWithMinAndMax {}
`, + styles: SLIDER_STYLES, }) class RangeSliderWithMinAndMax {} @@ -1683,6 +1690,7 @@ class RangeSliderWithMinAndMax {}
`, + styles: SLIDER_STYLES, }) class SliderWithValue {} @@ -1693,6 +1701,7 @@ class SliderWithValue {}
`, + styles: SLIDER_STYLES, }) class RangeSliderWithValue {} @@ -1702,6 +1711,7 @@ class RangeSliderWithValue {}
`, + styles: SLIDER_STYLES, }) class SliderWithStep {} @@ -1712,6 +1722,7 @@ class SliderWithStep {}
`, + styles: SLIDER_STYLES, }) class RangeSliderWithStep {} @@ -1721,6 +1732,7 @@ class RangeSliderWithStep {}
`, + styles: SLIDER_STYLES, }) class DiscreteSliderWithDisplayWith { displayWith(v: number) { @@ -1736,6 +1748,7 @@ class DiscreteSliderWithDisplayWith { `, + styles: SLIDER_STYLES, }) class DiscreteRangeSliderWithDisplayWith { displayWith(v: number) { @@ -1750,6 +1763,7 @@ class DiscreteRangeSliderWithDisplayWith { `, + styles: SLIDER_STYLES, }) class SliderWithOneWayBinding { value = 50; @@ -1762,6 +1776,7 @@ class SliderWithOneWayBinding { `, + styles: SLIDER_STYLES, }) class RangeSliderWithOneWayBinding { startValue = 25; @@ -1774,6 +1789,7 @@ class RangeSliderWithOneWayBinding { `, + styles: SLIDER_STYLES, }) class SliderWithChangeHandler { onChange = jasmine.createSpy('onChange'); @@ -1794,6 +1810,7 @@ class SliderWithChangeHandler { matSliderEndThumb> `, + styles: SLIDER_STYLES, }) class RangeSliderWithChangeHandler { onStartThumbChange = jasmine.createSpy('onStartThumbChange'); @@ -1809,6 +1826,7 @@ class RangeSliderWithChangeHandler { `, + styles: SLIDER_STYLES, }) class SliderWithNgModel { @ViewChild(MatSlider) slider: MatSlider; @@ -1822,6 +1840,7 @@ class SliderWithNgModel { `, + styles: SLIDER_STYLES, }) class RangeSliderWithNgModel { @ViewChild(MatSlider) slider: MatSlider; @@ -1834,6 +1853,7 @@ class RangeSliderWithNgModel { `, + styles: SLIDER_STYLES, }) class SliderWithFormControl { control = new FormControl(0); @@ -1845,6 +1865,7 @@ class SliderWithFormControl { `, + styles: SLIDER_STYLES, }) class RangeSliderWithFormControl { startInputControl = new FormControl(0); @@ -1857,6 +1878,7 @@ class RangeSliderWithFormControl { `, + styles: SLIDER_STYLES, }) class SliderWithTwoWayBinding { @ViewChild(MatSlider) slider: MatSlider; @@ -1871,6 +1893,7 @@ class SliderWithTwoWayBinding { `, + styles: SLIDER_STYLES, }) class RangeSliderWithTwoWayBinding { @ViewChild(MatSlider) slider: MatSlider;