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 843410f23827..83730bffb575 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';
@@ -42,6 +43,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);