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.
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/dev-app/mdc-slider/mdc-slider-demo.html b/src/dev-app/mdc-slider/mdc-slider-demo.html
index bb8e3de17870..68db534874f7 100644
--- a/src/dev-app/mdc-slider/mdc-slider-demo.html
+++ b/src/dev-app/mdc-slider/mdc-slider-demo.html
@@ -1,53 +1,88 @@
Default Slider
-Label
+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
-
+
+
+
-
+
+
+
+
+
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 fab41d01fc41..2ccd963c7744 100644
--- a/src/dev-app/mdc-slider/mdc-slider-demo.ts
+++ b/src/dev-app/mdc-slider/mdc-slider-demo.ts
@@ -12,6 +12,7 @@ import {Component} from '@angular/core';
@Component({
selector: 'mdc-slider-demo',
templateUrl: 'mdc-slider-demo.html',
+ styles: ['.mat-mdc-slider { width: 300px; }'],
})
export class MdcSliderDemo {
demo: number;
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/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-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",
diff --git a/src/material-experimental/mdc-slider/BUILD.bazel b/src/material-experimental/mdc-slider/BUILD.bazel
index 5d1fe42b70c2..55653eb3964f 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
###########
@@ -66,8 +75,11 @@ 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",
+ "@npm//rxjs",
],
)
@@ -84,7 +96,9 @@ ng_e2e_test_library(
name = "e2e_test_sources",
srcs = glob(["**/*.e2e.spec.ts"]),
deps = [
+ ":mdc-slider",
"//src/cdk/testing/private/e2e",
+ "@npm//@material/slider",
],
)
diff --git a/src/material-experimental/mdc-slider/_slider-theme.scss b/src/material-experimental/mdc-slider/_slider-theme.scss
index bf9f432751da..d465538fa955 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,52 @@
}
}
+@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
+ ),
+ ));
+ .mat-mdc-slider-hover-ripple {
+ background-color: rgba($ripple-color, 0.05);
+ }
+ .mat-mdc-slider-focus-ripple, .mat-mdc-slider-active-ripple {
+ background-color: rgba($ripple-color, 0.2);
+ }
+}
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..7f56b7cc1741
--- /dev/null
+++ b/src/material-experimental/mdc-slider/global-change-and-input-listener.ts
@@ -0,0 +1,67 @@
+/**
+ * @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, OnDestroy} from '@angular/core';
+import {SpecificEventListener} from '@material/base';
+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 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 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 _observables = new Map>();
+
+ /** 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;
+ }
+
+ 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 {
+ // 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));
+ }
+
+ return this._ngZone.runOutsideAngular(() =>
+ this._observables.get(type)!.subscribe((event: Event) =>
+ this._ngZone.run(() => callback(event))
+ )
+ );
+ }
+
+ /** 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/module.ts b/src/material-experimental/mdc-slider/module.ts
index b77814532ae8..1f06dffadb88 100644
--- a/src/material-experimental/mdc-slider/module.ts
+++ b/src/material-experimental/mdc-slider/module.ts
@@ -8,13 +8,17 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
-import {MatCommonModule} from '@angular/material-experimental/mdc-core';
-import {MatSlider} from './slider';
+import {MatCommonModule, MatRippleModule} from '@angular/material-experimental/mdc-core';
+import {MatSlider, MatSliderThumb, MatSliderVisualThumb} from './slider';
@NgModule({
- imports: [MatCommonModule, CommonModule],
- exports: [MatSlider, MatCommonModule],
- declarations: [MatSlider],
+ imports: [MatCommonModule, CommonModule, MatRippleModule],
+ exports: [MatSlider, MatSliderThumb],
+ declarations: [
+ MatSlider,
+ MatSliderThumb,
+ MatSliderVisualThumb,
+ ],
})
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..294dbb8a1be7 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, MatSliderThumb, MatSliderDragEvent} from './slider';
+export {MatSliderModule} from './module';
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..595c373fe1f7
--- /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.e2e.spec.ts b/src/material-experimental/mdc-slider/slider.e2e.spec.ts
index e82f8ebde4b4..d0a2a25a0a49 100644
--- a/src/material-experimental/mdc-slider/slider.e2e.spec.ts
+++ b/src/material-experimental/mdc-slider/slider.e2e.spec.ts
@@ -1,14 +1,128 @@
-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('', () => {}));
+import {clickElementAtPoint, getElement, Point} from '@angular/cdk/testing/private/e2e';
+import {Thumb} from '@material/slider';
+import {browser, by, element, ElementFinder} from 'protractor';
-// tslint:disable-next-line:ban
-xdescribe('mat-slider', () => {
- beforeEach(async () => await browser.get('/mdc-slider'));
+describe('MDC-based MatSlider' , () => {
+ const getStandardSlider = () => element(by.id('standard-slider'));
+ const getDisabledSlider = () => element(by.id('disabled-slider'));
+ const getRangeSlider = () => element(by.id('range-slider'));
- it('should show a slider', async () => {
- expect(await element(by.tagName('mat-slider')).isPresent()).toBe(true);
+ 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 () => {
+ await slideToValue(slider, 55, 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};
+}
diff --git a/src/material-experimental/mdc-slider/slider.html b/src/material-experimental/mdc-slider/slider.html
index f925043d4f5c..caa46f348407 100644
--- a/src/material-experimental/mdc-slider/slider.html
+++ b/src/material-experimental/mdc-slider/slider.html
@@ -1 +1,22 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/material-experimental/mdc-slider/slider.scss b/src/material-experimental/mdc-slider/slider.scss
index dc38ea9472a2..e8d05b58bda9 100644
--- a/src/material-experimental/mdc-slider/slider.scss
+++ b/src/material-experimental/mdc-slider/slider.scss
@@ -1,6 +1,8 @@
-// TODO: disabled until we implement the new MDC slider.
-// @use '@material/slider' as mdc-slider;
+@use '@material/slider/slider' as mdc-slider;
@use '../../cdk/a11y';
+@use '../mdc-helpers/mdc-helpers';
+
+@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.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts
index 0d0398232c4a..215809988411 100644
--- a/src/material-experimental/mdc-slider/slider.spec.ts
+++ b/src/material-experimental/mdc-slider/slider.spec.ts
@@ -1,1176 +1,1388 @@
-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';
+/**
+ * @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 {BidiModule, Directionality} from '@angular/cdk/bidi';
import {Platform} from '@angular/cdk/platform';
import {
- createKeyboardEvent,
- createMouseEvent,
- createPointerEvent,
- dispatchEvent,
- dispatchFakeEvent,
- dispatchKeyboardEvent,
dispatchMouseEvent,
+ dispatchPointerEvent,
+ dispatchTouchEvent,
} 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 {Component, Provider, QueryList, Type, ViewChild, ViewChildren} from '@angular/core';
+import {
+ ComponentFixture,
+ fakeAsync,
+ flush,
+ TestBed,
+ tick,
+ waitForAsync,
+} 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';
+import {Thumb} from '@material/slider';
+import {of} from 'rxjs';
+import {MatSliderModule} from './module';
+import {MatSlider, MatSliderThumb, MatSliderVisualThumb} from './slider';
+
+interface Point {
+ x: number;
+ y: number;
+}
-// 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('', () => {}));
+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');
+ });
-// tslint:disable-next-line:ban
-xdescribe('MDC-based MatSlider', () => {
- function createComponent(component: Type): ComponentFixture {
+ function createComponent(
+ component: Type,
+ providers: Provider[] = [],
+ ): ComponentFixture {
TestBed.configureTestingModule({
- imports: [
- MatSliderModule,
- ReactiveFormsModule,
- FormsModule,
- BidiModule,
- NoopAnimationsModule,
- ],
+ imports: [FormsModule, MatSliderModule, ReactiveFormsModule, BidiModule],
declarations: [component],
+ providers: [...providers],
}).compileComponents();
-
return TestBed.createComponent(component);
}
describe('standard slider', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
let sliderInstance: MatSlider;
+ let inputInstance: MatSliderThumb;
- beforeEach(() => {
- fixture = createComponent(StandardSlider);
+ beforeEach(waitForAsync(() => {
+ const fixture = createComponent(StandardSlider);
fixture.detectChanges();
-
- sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
- sliderNativeElement = sliderDebugElement.nativeElement;
+ const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
sliderInstance = sliderDebugElement.componentInstance;
- });
+ inputInstance = sliderInstance._getInput(Thumb.END);
+ }));
it('should set the default values', () => {
- expect(sliderInstance.value).toBe(0);
+ expect(inputInstance.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);
+ setValueByClick(sliderInstance, 19, platform.IOS);
+ expect(inputInstance.value).toBe(19);
});
it('should update the value on a slide', () => {
- expect(sliderInstance.value).toBe(0);
-
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.89);
-
- expect(sliderInstance.value).toBe(89);
+ slideToValue(sliderInstance, 77, Thumb.END, platform.IOS);
+ expect(inputInstance.value).toBe(77);
});
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);
+ slideToValue(sliderInstance, -1, Thumb.END, platform.IOS);
+ expect(inputInstance.value).toBe(0);
});
it('should set the value as max when sliding past the track', () => {
- expect(sliderInstance.value).toBe(0);
+ slideToValue(sliderInstance, 101, Thumb.END, platform.IOS);
+ expect(inputInstance.value).toBe(100);
+ });
- dispatchSlideEventSequence(sliderNativeElement, 0, 1.75);
+ 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);
+ });
- expect(sliderInstance.value).toBe(100);
+ 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';
});
+ });
- it('should not change value without emitting a change event', () => {
- const onChangeSpy = jasmine.createSpy('slider onChange');
+ describe('standard range slider', () => {
+ let sliderInstance: MatSlider;
+ let startInputInstance: MatSliderThumb;
+ let endInputInstance: MatSliderThumb;
- sliderInstance.change.subscribe(onChangeSpy);
- sliderInstance.value = 50;
+ beforeEach(waitForAsync(() => {
+ const fixture = createComponent(StandardRangeSlider);
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', () => {
+ expect(startInputInstance.value).toBe(0);
+ expect(endInputInstance.value).toBe(100);
+ expect(sliderInstance.min).toBe(0);
+ expect(sliderInstance.max).toBe(100);
+ });
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.1);
+ it('should update the start value on a slide', () => {
+ slideToValue(sliderInstance, 19, Thumb.START, platform.IOS);
+ expect(startInputInstance.value).toBe(19);
+ });
- expect(onChangeSpy).toHaveBeenCalledTimes(1);
+ it('should update the end value on a slide', () => {
+ slideToValue(sliderInstance, 27, Thumb.END, platform.IOS);
+ expect(endInputInstance.value).toBe(27);
});
- it('should have aria-orientation horizontal', () => {
- expect(sliderNativeElement.getAttribute('aria-orientation')).toEqual('horizontal');
+ 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 slide to the max value when the steps do not divide evenly into it', () => {
- sliderInstance.min = 5;
- sliderInstance.max = 100;
- sliderInstance.step = 15;
+ 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);
+ });
- dispatchSlideEventSequence(sliderNativeElement, 0, 1);
- fixture.detectChanges();
+ 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);
+ });
- expect(sliderInstance.value).toBe(100);
+ 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 have a focus indicator', () => {
- expect(sliderNativeElement.classList.contains('mat-mdc-focus-indicator')).toBe(true);
+ 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('disabled slider', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
let sliderInstance: MatSlider;
+ let inputInstance: MatSliderThumb;
- beforeEach(() => {
- fixture = createComponent(DisabledSlider);
+ beforeEach(waitForAsync(() => {
+ const fixture = createComponent(DisabledSlider);
fixture.detectChanges();
-
- sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
- sliderNativeElement = sliderDebugElement.nativeElement;
+ const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
sliderInstance = sliderDebugElement.componentInstance;
- });
+ inputInstance = sliderInstance._getInput(Thumb.END);
+ }));
it('should be disabled', () => {
- expect(sliderInstance.disabled).toBeTruthy();
+ 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 not change the value on mousedown when disabled', () => {
- expect(sliderInstance.value).toBe(0);
+ it('should set the disabled attribute on the input element', () => {
+ expect(inputInstance._hostElement.disabled).toBeTrue();
+ });
- dispatchMousedownEventSequence(sliderNativeElement, 0.63);
+ it('should not update the value on mousedown', () => {
+ setValueByClick(sliderInstance, 19, platform.IOS);
+ expect(inputInstance.value).toBe(0);
+ });
- expect(sliderInstance.value).toBe(0);
+ it('should not update the value on a slide', () => {
+ slideToValue(sliderInstance, 77, Thumb.END, platform.IOS);
+ expect(inputInstance.value).toBe(0);
});
+ });
- it('should not change the value on slide when disabled', () => {
- expect(sliderInstance.value).toBe(0);
+ describe('disabled range slider', () => {
+ let sliderInstance: MatSlider;
+ let startInputInstance: MatSliderThumb;
+ let endInputInstance: MatSliderThumb;
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.5);
+ 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);
+ }));
- expect(sliderInstance.value).toBe(0);
+ it('should be disabled', () => {
+ expect(sliderInstance.disabled).toBeTrue();
});
- it('should not emit change when disabled', () => {
- const onChangeSpy = jasmine.createSpy('slider onChange');
- sliderInstance.change.subscribe(onChangeSpy);
-
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.5);
+ it('should have the disabled class on the root element', () => {
+ expect(sliderInstance._elementRef.nativeElement.classList).toContain('mdc-slider--disabled');
+ });
- expect(onChangeSpy).toHaveBeenCalledTimes(0);
+ it('should set the disabled attribute on the input elements', () => {
+ expect(startInputInstance._hostElement.disabled).toBeTrue();
+ expect(endInputInstance._hostElement.disabled).toBeTrue();
});
- it('should not add the mat-slider-active class on mousedown when disabled', () => {
- expect(sliderNativeElement.classList).not.toContain('mat-slider-active');
+ it('should not update the start value on a slide', () => {
+ slideToValue(sliderInstance, 19, Thumb.START, platform.IOS);
+ expect(startInputInstance.value).toBe(0);
+ });
- dispatchMousedownEventSequence(sliderNativeElement, 0.43);
- fixture.detectChanges();
+ it('should not update the end value on a slide', () => {
+ slideToValue(sliderInstance, 27, Thumb.END, platform.IOS);
+ expect(endInputInstance.value).toBe(100);
+ });
- expect(sliderNativeElement.classList).not.toContain('mat-slider-active');
+ 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 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);
- }
- }));
+ 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('slider with set min and max', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
- let sliderInstance: MatSlider;
- let thumbContainerEl: HTMLElement;
+ describe('ripple states', () => {
+ let inputInstance: MatSliderThumb;
+ let thumbInstance: MatSliderVisualThumb;
+ let thumbElement: HTMLElement;
+ let thumbX: number;
+ let thumbY: number;
- beforeEach(fakeAsync(() => {
- fixture = createComponent(SliderWithMinAndMax);
+ beforeEach(waitForAsync(() => {
+ const 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
+ );
+ }
- sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
- sliderNativeElement = sliderDebugElement.nativeElement;
- sliderInstance = sliderDebugElement.injector.get(MatSlider);
- thumbContainerEl = sliderNativeElement
- .querySelector('.mdc-slider__thumb-container');
+ function pointerup() {
+ dispatchPointerOrTouchEvent(
+ thumbElement, PointerEventType.POINTER_UP, thumbX, thumbY, platform.IOS
+ );
+ }
- // Flush the "requestAnimationFrame" timer that performs the initial
- // rendering of the MDC slider.
- flushRequestAnimationFrame();
+ it('should show the hover ripple on mouseenter', fakeAsync(() => {
+ expect(isRippleVisible('hover')).toBeFalse();
+ mouseenter();
+ expect(isRippleVisible('hover')).toBeTrue();
}));
- 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 hide the hover ripple on mouseleave', fakeAsync(() => {
+ mouseenter();
+ mouseleave();
+ expect(isRippleVisible('hover')).toBeFalse();
+ }));
- it('should set the correct value on mousedown', () => {
- dispatchMousedownEventSequence(sliderNativeElement, 0.09);
- fixture.detectChanges();
+ it('should show the focus ripple on pointerdown', fakeAsync(() => {
+ expect(isRippleVisible('focus')).toBeFalse();
+ pointerdown();
+ expect(isRippleVisible('focus')).toBeTrue();
+ }));
- // 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 continue to show the focus ripple on pointerup', fakeAsync(() => {
+ pointerdown();
+ pointerup();
+ expect(isRippleVisible('focus')).toBeTrue();
+ }));
- it('should set the correct value on slide', () => {
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.62);
- fixture.detectChanges();
+ it('should hide the focus ripple on blur', fakeAsync(() => {
+ pointerdown();
+ pointerup();
+ blur();
+ expect(isRippleVisible('focus')).toBeFalse();
+ }));
- // 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 show the active ripple on pointerdown', fakeAsync(() => {
+ expect(isRippleVisible('active')).toBeFalse();
+ pointerdown();
+ expect(isRippleVisible('active')).toBeTrue();
+ }));
- it('should snap the fill to the nearest value on mousedown', fakeAsync(() => {
- dispatchMousedownEventSequence(sliderNativeElement, 0.68);
- fixture.detectChanges();
- flushRequestAnimationFrame();
+ it('should hide the active ripple on pointerup', fakeAsync(() => {
+ pointerdown();
+ pointerup();
+ expect(isRippleVisible('active')).toBeFalse();
+ }));
- // The closest snap is halfway on the slider.
- expect(thumbContainerEl.style.transform).toContain('translateX(50px)');
+ // Edge cases.
+
+ it('should not show the hover ripple if the thumb is already focused', fakeAsync(() => {
+ pointerdown();
+ mouseenter();
+ expect(isRippleVisible('hover')).toBeFalse();
}));
- it('should snap the fill to the nearest value on slide', fakeAsync(() => {
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.74);
- fixture.detectChanges();
- flushRequestAnimationFrame();
+ it('should hide the hover ripple if the thumb is focused', fakeAsync(() => {
+ mouseenter();
+ pointerdown();
+ expect(isRippleVisible('hover')).toBeFalse();
+ }));
+
+ it('should not hide the focus ripple if the thumb is pressed', fakeAsync(() => {
+ pointerdown();
+ blur();
+ expect(isRippleVisible('focus')).toBeTrue();
+ }));
+
+ it('should not hide the hover ripple on blur if the thumb is hovered', fakeAsync(() => {
+ mouseenter();
+ pointerdown();
+ pointerup();
+ blur();
+ expect(isRippleVisible('hover')).toBeTrue();
+ }));
- // The closest snap is at the halfway point on the slider.
- expect(thumbContainerEl.style.transform).toContain('translateX(50px)');
+ it('should hide the focus ripple on drag end if the thumb already lost focus', fakeAsync(() => {
+ pointerdown();
+ blur();
+ pointerup();
+ expect(isRippleVisible('focus')).toBeFalse();
}));
});
- describe('slider with set value', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
+ describe('slider with set min and max', () => {
+ let fixture: ComponentFixture;
let sliderInstance: MatSlider;
+ let inputInstance: MatSliderThumb;
- beforeEach(() => {
- fixture = createComponent(SliderWithValue);
+ beforeEach(waitForAsync(() => {
+ fixture = createComponent(SliderWithMinAndMax);
fixture.detectChanges();
+ const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
+ sliderInstance = sliderDebugElement.componentInstance;
+ inputInstance = sliderInstance._getInput(Thumb.END);
+ }));
- 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 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', () => {
- 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);
+ setValueByClick(sliderInstance, 33, platform.IOS);
+ expect(inputInstance.value).toBe(33);
});
it('should set the correct value on slide', () => {
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.32);
- fixture.detectChanges();
+ slideToValue(sliderInstance, 55, Thumb.END, platform.IOS);
+ expect(inputInstance.value).toBe(55);
+ });
- expect(sliderInstance.value).toBe(32);
+ it('should be able to set the min and max values when they are more precise ' +
+ 'than the step', () => {
+ sliderInstance.step = 10;
+ 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('slider with set step', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
+ describe('range slider with set min and max', () => {
+ let fixture: ComponentFixture;
let sliderInstance: MatSlider;
- let thumbContainerEl: HTMLElement;
+ let startInputInstance: MatSliderThumb;
+ let endInputInstance: MatSliderThumb;
- beforeEach(fakeAsync(() => {
- fixture = createComponent(SliderWithStep);
+ beforeEach(waitForAsync(() => {
+ fixture = createComponent(RangeSliderWithMinAndMax);
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();
+ 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', () => {
- expect(sliderInstance.value).toBe(0);
+ 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);
+ });
- dispatchMousedownEventSequence(sliderNativeElement, 0.13);
- fixture.detectChanges();
+ 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);
+ });
- expect(sliderInstance.value).toBe(25);
+ 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 step value on keydown', () => {
- expect(sliderInstance.value).toBe(0);
+ it('should set the correct start value on slide', () => {
+ slideToValue(sliderInstance, 40, Thumb.START, platform.IOS);
+ expect(startInputInstance.value).toBe(40);
+ });
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', UP_ARROW);
- fixture.detectChanges();
+ it('should set the correct end value on slide', () => {
+ slideToValue(sliderInstance, 60, Thumb.END, platform.IOS);
+ expect(endInputInstance.value).toBe(60);
+ });
- expect(sliderInstance.value).toBe(25);
+ 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);
});
+ });
- it('should snap the fill to a step on mousedown', fakeAsync(() => {
- dispatchMousedownEventSequence(sliderNativeElement, 0.66);
- fixture.detectChanges();
- flushRequestAnimationFrame();
+ describe('slider with set value', () => {
+ let sliderInstance: MatSlider;
+ let inputInstance: MatSliderThumb;
- // The closest step is at 75% of the slider.
- expect(thumbContainerEl.style.transform).toContain('translateX(75px)');
+ 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 correct step value on slide', () => {
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.07);
- fixture.detectChanges();
+ it('should set the default value from the attribute', () => {
+ expect(inputInstance.value).toBe(50);
+ });
- expect(sliderInstance.value).toBe(0);
+ it('should set the correct value on mousedown', () => {
+ setValueByClick(sliderInstance, 19, platform.IOS);
+ expect(inputInstance.value).toBe(19);
});
- it('should snap the thumb and fill to a step on slide', fakeAsync(() => {
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.88);
- fixture.detectChanges();
- flushRequestAnimationFrame();
+ it('should set the correct value on slide', () => {
+ slideToValue(sliderInstance, 77, Thumb.END, platform.IOS);
+ expect(inputInstance.value).toBe(77);
+ });
+ });
- // The closest snap is at the end of the slider.
- expect(thumbContainerEl.style.transform).toContain('translateX(100px)');
- }));
+ describe('range slider with set value', () => {
+ let sliderInstance: MatSlider;
+ let startInputInstance: MatSliderThumb;
+ let endInputInstance: MatSliderThumb;
- it('should not add decimals to the value if it is a whole number', () => {
- fixture.componentInstance.step = 0.1;
+ 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);
+ }));
- dispatchSlideEventSequence(sliderNativeElement, 0, 1);
+ it('should set the default value from the attribute', () => {
+ expect(startInputInstance.value).toBe(25);
+ expect(endInputInstance.value).toBe(75);
+ });
- expect(sliderDebugElement.componentInstance.displayValue).toBe('100');
+ it('should set the correct start value on mousedown behind the start thumb', () => {
+ setValueByClick(sliderInstance, 19, platform.IOS);
+ expect(startInputInstance.value).toBe(19);
});
- // 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();
+ 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);
+ });
- for (let i = 0; i < 3; i++) {
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', UP_ARROW);
- }
+ it('should set the correct start value on slide', () => {
+ slideToValue(sliderInstance, 73, Thumb.START, platform.IOS);
+ expect(startInputInstance.value).toBe(73);
+ });
- expect(sliderInstance.value).toBe(0.3);
+ it('should set the correct end value on slide', () => {
+ slideToValue(sliderInstance, 99, Thumb.END, platform.IOS);
+ expect(endInputInstance.value).toBe(99);
});
});
- describe('slider with set tick interval', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
- let ticksContainerElement: HTMLElement;
+ describe('slider with set step', () => {
+ let fixture: ComponentFixture;
+ let sliderInstance: MatSlider;
+ let inputInstance: MatSliderThumb;
- beforeEach(() => {
- fixture = createComponent(SliderWithSetTickInterval);
+ beforeEach(waitForAsync(() => {
+ fixture = createComponent(SliderWithStep);
fixture.detectChanges();
+ const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
+ sliderInstance = sliderDebugElement.componentInstance;
+ inputInstance = sliderInstance._getInput(Thumb.END);
+ }));
- 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();
+ 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);
+ });
- expect(sliderNativeElement.classList)
- .not.toContain('mat-slider-has-ticks', 'Expected element not to have ticks after reset.');
+ 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', () => {
+ sliderInstance.step = 0.5;
+ slideToValue(sliderInstance, 55.555, Thumb.END, platform.IOS);
+ expect(inputInstance.value).toBe(55.5);
});
});
- describe('slider with thumb label', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
+ describe('range slider with set step', () => {
+ let fixture: ComponentFixture;
let sliderInstance: MatSlider;
- let thumbLabelTextElement: Element;
+ let startInputInstance: MatSliderThumb;
+ let endInputInstance: MatSliderThumb;
- beforeEach(() => {
- fixture = createComponent(SliderWithThumbLabel);
+ beforeEach(waitForAsync(() => {
+ fixture = createComponent(RangeSliderWithStep);
fixture.detectChanges();
-
- sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
- sliderNativeElement = sliderDebugElement.nativeElement;
+ const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
sliderInstance = sliderDebugElement.componentInstance;
- thumbLabelTextElement = sliderNativeElement.querySelector('.mdc-slider__pin-value-marker')!;
+ 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 add the thumb label class to the slider container', () => {
- expect(sliderNativeElement.classList).toContain('mat-slider-thumb-label-showing');
+ 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 update the thumb label text on mousedown', () => {
- expect(thumbLabelTextElement.textContent).toBe('0');
+ it('should set the correct start thumb step value on slide', () => {
+ slideToValue(sliderInstance, 26, Thumb.START, platform.IOS);
+ expect(startInputInstance.value).toBe(25);
+ });
- dispatchMousedownEventSequence(sliderNativeElement, 0.13);
- fixture.detectChanges();
+ 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);
+ });
- // The thumb label text is set to the slider's value. These should always be the same.
- expect(thumbLabelTextElement.textContent).toBe('13');
+ 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 update the thumb label text on slide', () => {
- expect(thumbLabelTextElement.textContent).toBe('0');
+ it('should truncate long decimal start values when using a decimal step', () => {
+ sliderInstance.step = 0.1;
+ slideToValue(sliderInstance, 33.7, Thumb.START, platform.IOS);
+ expect(startInputInstance.value).toBe(33.7);
+ });
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.56);
- fixture.detectChanges();
+ it('should truncate long decimal end values when using a decimal step', () => {
+ sliderInstance.step = 0.1;
+ slideToValue(sliderInstance, 33.7, Thumb.END, platform.IOS);
+ expect(endInputInstance.value).toBe(33.7);
- // The thumb label text is set to the slider's value. These should always be the same.
- expect(thumbLabelTextElement.textContent).toBe(`${sliderInstance.value}`);
+ // 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);
});
});
describe('slider with custom thumb label formatting', () => {
- let fixture: ComponentFixture;
- let sliderNativeElement: HTMLElement;
- let thumbLabelTextElement: Element;
+ let fixture: ComponentFixture;
+ let sliderInstance: MatSlider;
+ let valueIndicatorTextElement: Element;
beforeEach(() => {
- fixture = createComponent(SliderWithCustomThumbLabelFormatting);
+ fixture = createComponent(DiscreteSliderWithDisplayWith);
fixture.detectChanges();
-
- const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
- sliderNativeElement = sliderDebugElement.nativeElement;
- thumbLabelTextElement = sliderNativeElement.querySelector('.mdc-slider__pin-value-marker')!;
+ 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();
-
- 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');
+ spyOn(sliderInstance, 'displayWith').and.callThrough();
+ sliderInstance._setValue(1337, Thumb.END);
+ expect(sliderInstance.displayWith).toHaveBeenCalledWith(1337);
});
it('should format the thumb label based on the passed-in `displayWith` function', () => {
- dispatchMousedownEventSequence(sliderNativeElement, 1);
+ sliderInstance._setValue(200000, Thumb.END);
fixture.detectChanges();
-
- expect(thumbLabelTextElement.textContent).toBe('100k');
+ expect(valueIndicatorTextElement.textContent).toBe('$200k');
});
});
- describe('slider with value property binding', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
+ describe('range slider with custom thumb label formatting', () => {
+ let fixture: ComponentFixture;
let sliderInstance: MatSlider;
- let testComponent: SliderWithOneWayBinding;
- let thumbContainerEl: HTMLElement;
+ let startValueIndicatorTextElement: Element;
+ let endValueIndicatorTextElement: Element;
- beforeEach(fakeAsync(() => {
- fixture = createComponent(SliderWithOneWayBinding);
+ beforeEach(() => {
+ fixture = createComponent(DiscreteRangeSliderWithDisplayWith);
fixture.detectChanges();
+ const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider))!;
+ sliderInstance = sliderDebugElement.componentInstance;
- 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;
+ 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')!;
+ });
- // Flush the "requestAnimationFrame" timer that performs the initial
- // rendering of the MDC slider.
- flushRequestAnimationFrame();
- }));
+ it('should invoke the passed-in `displayWith` function with the start value', () => {
+ spyOn(sliderInstance, 'displayWith').and.callThrough();
+ sliderInstance._setValue(1337, Thumb.START);
+ expect(sliderInstance.displayWith).toHaveBeenCalledWith(1337);
+ });
- it('should initialize based on bound value', () => {
- expect(sliderInstance.value).toBe(50);
- expect(thumbContainerEl.style.transform).toContain('translateX(50px)');
+ it('should invoke the passed-in `displayWith` function with the end value', () => {
+ spyOn(sliderInstance, 'displayWith').and.callThrough();
+ sliderInstance._setValue(5996, Thumb.END);
+ expect(sliderInstance.displayWith).toHaveBeenCalledWith(5996);
});
- it('should update when bound value changes', fakeAsync(() => {
- testComponent.val = 75;
+ it('should format the start thumb label based on the passed-in `displayWith` function', () => {
+ sliderInstance._setValue(200000, Thumb.START);
fixture.detectChanges();
- flushRequestAnimationFrame();
+ expect(startValueIndicatorTextElement.textContent).toBe('$200k');
+ });
- expect(sliderInstance.value).toBe(75);
- expect(thumbContainerEl.style.transform).toContain('translateX(75px)');
- }));
+ it('should format the end thumb label based on the passed-in `displayWith` function', () => {
+ sliderInstance._setValue(700000, Thumb.END);
+ fixture.detectChanges();
+ expect(endValueIndicatorTextElement.textContent).toBe('$700k');
+ });
});
- 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;
+ describe('slider with value property binding', () => {
+ let fixture: ComponentFixture;
+ let testComponent: SliderWithOneWayBinding;
+ let inputInstance: MatSliderThumb;
- beforeEach(fakeAsync(() => {
- fixture = createComponent(SliderWithValueSmallerThanMin);
+ beforeEach(waitForAsync(() => {
+ fixture = createComponent(SliderWithOneWayBinding);
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();
+ testComponent = fixture.debugElement.componentInstance;
+ const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
+ const sliderInstance = sliderDebugElement.componentInstance;
+ inputInstance = sliderInstance._getInput(Thumb.END);
}));
- 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)');
+ it('should update when bound value changes', () => {
+ testComponent.value = 75;
+ fixture.detectChanges();
+ expect(inputInstance.value).toBe(75);
});
});
- 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;
+ describe('range slider with value property binding', () => {
+ let fixture: ComponentFixture;
+ let testComponent: RangeSliderWithOneWayBinding;
+ let startInputInstance: MatSliderThumb;
+ let endInputInstance: MatSliderThumb;
- beforeEach(fakeAsync(() => {
- fixture = createComponent(SliderWithValueGreaterThanMax);
+ beforeEach(waitForAsync(() => {
+ fixture = createComponent(RangeSliderWithOneWayBinding);
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();
+ 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 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 update when bound start value changes', () => {
+ testComponent.startValue = 30;
+ fixture.detectChanges();
+ expect(startInputInstance.value).toBe(30);
});
- it('should set the fill to the max value', () => {
- expect(thumbContainerEl.style.transform).toContain('translateX(100px)');
+ it('should update when bound end value changes', () => {
+ testComponent.endValue = 70;
+ fixture.detectChanges();
+ expect(endInputInstance.value).toBe(70);
});
});
describe('slider with change handler', () => {
+ let sliderInstance: MatSlider;
+ let inputInstance: MatSliderThumb;
+ let sliderElement: HTMLElement;
let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
let testComponent: SliderWithChangeHandler;
- beforeEach(() => {
+ beforeEach(waitForAsync(() => {
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;
- });
+ const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
+ sliderElement = sliderDebugElement.nativeElement;
+ sliderInstance = sliderDebugElement.componentInstance;
+ inputInstance = sliderInstance._getInput(Thumb.END);
+ }));
- it('should emit change on mousedown', () => {
+ it('should emit change on mouseup', () => {
expect(testComponent.onChange).not.toHaveBeenCalled();
-
- dispatchMousedownEventSequence(sliderNativeElement, 0.2);
- fixture.detectChanges();
-
+ setValueByClick(sliderInstance, 20, platform.IOS);
expect(testComponent.onChange).toHaveBeenCalledTimes(1);
});
it('should emit change on slide', () => {
expect(testComponent.onChange).not.toHaveBeenCalled();
-
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.4);
- fixture.detectChanges();
-
+ slideToValue(sliderInstance, 40, Thumb.END, platform.IOS);
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', () => {
+ it('should not emit multiple changes for the same value', () => {
expect(testComponent.onChange).not.toHaveBeenCalled();
- dispatchMousedownEventSequence(sliderNativeElement, 0.6);
- dispatchSlideEventSequence(sliderNativeElement, 0.6, 0.6);
- fixture.detectChanges();
+ 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', () => {
- expect(testComponent.onChange).not.toHaveBeenCalled();
- expect(testComponent.onInput).not.toHaveBeenCalled();
+ const dispatchSliderEvent = (type: PointerEventType, value: number) => {
+ const {x, y} = getCoordsForValue(sliderInstance, value);
+ dispatchPointerOrTouchEvent(sliderElement, type, x, y, platform.IOS);
+ };
- dispatchMousedownEventSequence(sliderNativeElement, 0.2);
- fixture.detectChanges();
+ expect(testComponent.onChange).not.toHaveBeenCalled();
+ expect(testComponent.onInput).not.toHaveBeenCalled();
- expect(testComponent.onChange).toHaveBeenCalledTimes(1);
- expect(testComponent.onInput).toHaveBeenCalledTimes(1);
+ dispatchSliderEvent(PointerEventType.POINTER_DOWN, 20);
+ fixture.detectChanges();
- testComponent.value = 0;
- fixture.detectChanges();
+ expect(testComponent.onChange).not.toHaveBeenCalled();
+ expect(testComponent.onInput).toHaveBeenCalledTimes(1);
- expect(testComponent.onChange).toHaveBeenCalledTimes(1);
- expect(testComponent.onInput).toHaveBeenCalledTimes(1);
-
- dispatchMousedownEventSequence(sliderNativeElement, 0.2);
- fixture.detectChanges();
+ dispatchSliderEvent(PointerEventType.POINTER_UP, 20);
+ fixture.detectChanges();
- expect(testComponent.onChange).toHaveBeenCalledTimes(2);
- expect(testComponent.onInput).toHaveBeenCalledTimes(2);
- });
- });
+ expect(testComponent.onChange).toHaveBeenCalledTimes(1);
+ expect(testComponent.onInput).toHaveBeenCalledTimes(1);
- describe('slider with input event', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
- let testComponent: SliderWithChangeHandler;
+ inputInstance.value = 0;
+ fixture.detectChanges();
- beforeEach(() => {
- fixture = createComponent(SliderWithChangeHandler);
- fixture.detectChanges();
+ expect(testComponent.onChange).toHaveBeenCalledTimes(1);
+ expect(testComponent.onInput).toHaveBeenCalledTimes(1);
- testComponent = fixture.debugElement.componentInstance;
- spyOn(testComponent, 'onInput');
- spyOn(testComponent, 'onChange');
+ dispatchSliderEvent(PointerEventType.POINTER_DOWN, 20);
+ fixture.detectChanges();
+ dispatchSliderEvent(PointerEventType.POINTER_UP, 20);
- sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
- sliderNativeElement = sliderDebugElement.nativeElement;
+ expect(testComponent.onChange).toHaveBeenCalledTimes(2);
+ expect(testComponent.onInput).toHaveBeenCalledTimes(2);
});
+ });
- 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);
+ 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);
+ }));
- // 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 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 an input event when clicking', () => {
- expect(testComponent.onChange).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);
+ });
- dispatchMousedownEventSequence(sliderNativeElement, 0.75);
- fixture.detectChanges();
+ 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();
+ });
- // The `onInput` event should be emitted once due to a single click.
- expect(testComponent.onInput).toHaveBeenCalledTimes(1);
- expect(testComponent.onChange).toHaveBeenCalledTimes(1);
+ 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();
- describe('slider with auto ticks', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
- let ticksContainerElement: HTMLElement;
+ setValueByClick(sliderInstance, 30, platform.IOS);
+ slideToValue(sliderInstance, 30, Thumb.START, platform.IOS);
+ setValueByClick(sliderInstance, 30, platform.IOS);
+ slideToValue(sliderInstance, 30, Thumb.START, platform.IOS);
- beforeEach(fakeAsync(() => {
- fixture = createComponent(SliderWithAutoTickInterval);
- fixture.detectChanges();
+ expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1);
+ expect(testComponent.onEndThumbChange).not.toHaveBeenCalled();
+ });
- sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
- sliderNativeElement = sliderDebugElement.nativeElement;
- ticksContainerElement =
- sliderNativeElement.querySelector('.mdc-slider__track-marker-container');
+ it('should not emit multiple changes for the same end thumb value', () => {
+ expect(testComponent.onStartThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbChange).not.toHaveBeenCalled();
- flushRequestAnimationFrame();
- }));
+ setValueByClick(sliderInstance, 60, platform.IOS);
+ slideToValue(sliderInstance, 60, Thumb.END, platform.IOS);
+ setValueByClick(sliderInstance, 60, platform.IOS);
+ slideToValue(sliderInstance, 60, Thumb.END, platform.IOS);
- it('should set the correct tick separation', () => {
- expect(ticksContainerElement.style.background).toContain('30px');
+ expect(testComponent.onStartThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1);
});
- });
- describe('keyboard support', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
- let testComponent: SliderWithChangeHandler;
- let sliderInstance: MatSlider;
-
- beforeEach(() => {
- fixture = createComponent(SliderWithChangeHandler);
- fixture.detectChanges();
+ 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();
- testComponent = fixture.debugElement.componentInstance;
- spyOn(testComponent, 'onInput');
- spyOn(testComponent, 'onChange');
+ expect(testComponent.onStartThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(1);
+ expect(testComponent.onEndThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbInput).not.toHaveBeenCalled();
- sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
- sliderNativeElement = sliderDebugElement.nativeElement;
- sliderInstance = sliderDebugElement.injector.get(MatSlider);
- });
+ dispatchSliderEvent(PointerEventType.POINTER_UP, 20);
+ fixture.detectChanges();
- it('should increment slider by 1 on up arrow pressed', () => {
- expect(testComponent.onChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1);
+ expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(1);
+ expect(testComponent.onEndThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbInput).not.toHaveBeenCalled();
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', UP_ARROW);
- fixture.detectChanges();
+ startInputInstance.value = 0;
+ 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);
- });
+ expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1);
+ expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(1);
+ expect(testComponent.onEndThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbInput).not.toHaveBeenCalled();
- it('should increment slider by 1 on right arrow pressed', () => {
- expect(testComponent.onChange).not.toHaveBeenCalled();
-
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', RIGHT_ARROW);
- fixture.detectChanges();
+ dispatchSliderEvent(PointerEventType.POINTER_DOWN, 20);
+ fixture.detectChanges();
+ dispatchSliderEvent(PointerEventType.POINTER_UP, 20);
- // 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);
+ expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(2);
+ expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(2);
+ expect(testComponent.onEndThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbInput).not.toHaveBeenCalled();
});
- it('should decrement slider by 1 on down arrow pressed', () => {
- fixture.componentInstance.value = 100;
- fixture.detectChanges();
+ 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.onChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbInput).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(1);
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', DOWN_ARROW);
- fixture.detectChanges();
+ dispatchSliderEvent(PointerEventType.POINTER_UP, 80);
+ 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);
- });
+ expect(testComponent.onStartThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbInput).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1);
+ expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(1);
- it('should decrement slider by 1 on left arrow pressed', () => {
- fixture.componentInstance.value = 100;
- fixture.detectChanges();
+ endInputInstance.value = 100;
+ fixture.detectChanges();
- expect(testComponent.onChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbInput).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1);
+ expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(1);
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', LEFT_ARROW);
- fixture.detectChanges();
+ dispatchSliderEvent(PointerEventType.POINTER_DOWN, 80);
+ fixture.detectChanges();
+ dispatchSliderEvent(PointerEventType.POINTER_UP, 80);
- // 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);
+ expect(testComponent.onStartThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbInput).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(2);
+ expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(2);
});
+ });
- // 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();
+ describe('slider with input event', () => {
+ let sliderInstance: MatSlider;
+ let sliderElement: HTMLElement;
+ let testComponent: SliderWithChangeHandler;
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', PAGE_UP);
+ beforeEach(waitForAsync(() => {
+ const fixture = createComponent(SliderWithChangeHandler);
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);
- });
+ testComponent = fixture.debugElement.componentInstance;
- // 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();
+ const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
+ sliderInstance = sliderDebugElement.componentInstance;
+ sliderElement = sliderInstance._elementRef.nativeElement;
+ }));
- expect(testComponent.onChange).not.toHaveBeenCalled();
+ 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);
+ };
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', PAGE_DOWN);
- fixture.detectChanges();
+ expect(testComponent.onChange).not.toHaveBeenCalled();
+ expect(testComponent.onInput).not.toHaveBeenCalled();
- // 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);
- });
+ // pointer down on current value (should not trigger input event)
+ dispatchSliderEvent(PointerEventType.POINTER_DOWN, 0);
- it('should set slider to max on end pressed', () => {
- expect(testComponent.onChange).not.toHaveBeenCalled();
+ // value changes (should trigger input)
+ dispatchSliderEvent(PointerEventType.POINTER_MOVE, 10);
+ dispatchSliderEvent(PointerEventType.POINTER_MOVE, 25);
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', END);
- fixture.detectChanges();
+ // a new value has been committed (should trigger change event)
+ dispatchSliderEvent(PointerEventType.POINTER_UP, 25);
- // The `onInput` event should be emitted once due to a single keyboard press.
- expect(testComponent.onInput).toHaveBeenCalledTimes(1);
+ expect(testComponent.onInput).toHaveBeenCalledTimes(2);
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();
-
+ it('should emit an input event when clicking', () => {
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).not.toHaveBeenCalled();
+ setValueByClick(sliderInstance, 75, platform.IOS);
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();
+ describe('range slider with input event', () => {
+ let sliderInstance: MatSlider;
+ let sliderElement: HTMLElement;
+ let testComponent: RangeSliderWithChangeHandler;
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', BACKSPACE);
+ beforeEach(waitForAsync(() => {
+ const fixture = createComponent(RangeSliderWithChangeHandler);
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;
+ testComponent = fixture.debugElement.componentInstance;
- [
- 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);
- });
+ const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
+ sliderInstance = sliderDebugElement.componentInstance;
+ sliderElement = sliderInstance._elementRef.nativeElement;
+ }));
- expect(testComponent.onInput).not.toHaveBeenCalled();
- expect(testComponent.onChange).not.toHaveBeenCalled();
- expect(sliderInstance.value).toBe(0);
- });
- });
+ 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);
+ };
- describe('slider with direction', () => {
- let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
- let sliderInstance: MatSlider;
- let testComponent: SliderWithDir;
+ expect(testComponent.onStartThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbInput).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbInput).not.toHaveBeenCalled();
- beforeEach(() => {
- fixture = createComponent(SliderWithDir);
- fixture.detectChanges();
+ // pointer down on current start thumb value (should not trigger input event)
+ dispatchSliderEvent(PointerEventType.POINTER_DOWN, 0);
- testComponent = fixture.debugElement.componentInstance;
- sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
- sliderInstance = sliderDebugElement.injector.get(MatSlider);
- sliderNativeElement = sliderDebugElement.nativeElement;
- });
+ // value changes (should trigger input)
+ dispatchSliderEvent(PointerEventType.POINTER_MOVE, 10);
+ dispatchSliderEvent(PointerEventType.POINTER_MOVE, 25);
- it('works in RTL languages', fakeAsync(() => {
- testComponent.dir = 'rtl';
- fixture.detectChanges();
- flushRequestAnimationFrame();
+ // a new value has been committed (should trigger change event)
+ dispatchSliderEvent(PointerEventType.POINTER_UP, 25);
- dispatchMousedownEventSequence(sliderNativeElement, 0.3);
- fixture.detectChanges();
+ expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1);
+ expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(2);
+ expect(testComponent.onEndThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbInput).not.toHaveBeenCalled();
+ });
- expect(sliderInstance.value).toBe(70);
- }));
+ 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);
+ };
- it('should re-render slider with updated style upon directionality change', fakeAsync(() => {
- testComponent.dir = 'rtl';
- fixture.detectChanges();
- flushRequestAnimationFrame();
+ expect(testComponent.onStartThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbInput).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbInput).not.toHaveBeenCalled();
- const thumbContainerEl = sliderNativeElement
- .querySelector('.mdc-slider__thumb-container');
+ // pointer down on current end thumb value (should not trigger input event)
+ dispatchSliderEvent(PointerEventType.POINTER_DOWN, 100);
- expect(thumbContainerEl.style.transform).toContain('translateX(100px)');
+ // value changes (should trigger input)
+ dispatchSliderEvent(PointerEventType.POINTER_MOVE, 90);
+ dispatchSliderEvent(PointerEventType.POINTER_MOVE, 55);
- testComponent.dir = 'ltr';
- fixture.detectChanges();
- flushRequestAnimationFrame();
+ // a new value has been committed (should trigger change event)
+ dispatchSliderEvent(PointerEventType.POINTER_UP, 55);
- expect(thumbContainerEl.style.transform).toContain('translateX(0px)');
- }));
+ expect(testComponent.onStartThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbInput).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1);
+ expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(2);
+ });
- it('should decrement RTL slider by 1 on right arrow pressed', () => {
- testComponent.dir = 'rtl';
- testComponent.value = 100;
- fixture.detectChanges();
+ 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();
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', RIGHT_ARROW);
- fixture.detectChanges();
+ setValueByClick(sliderInstance, 30, platform.IOS);
- expect(sliderInstance.value).toBe(99);
+ expect(testComponent.onStartThumbChange).toHaveBeenCalledTimes(1);
+ expect(testComponent.onStartThumbInput).toHaveBeenCalledTimes(1);
+ expect(testComponent.onEndThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbInput).not.toHaveBeenCalled();
});
- it('should increment RTL slider by 1 on left arrow pressed', () => {
- testComponent.dir = 'rtl';
- fixture.detectChanges();
+ 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();
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', LEFT_ARROW);
- fixture.detectChanges();
+ setValueByClick(sliderInstance, 55, platform.IOS);
- expect(sliderInstance.value).toBe(1);
+ expect(testComponent.onStartThumbChange).not.toHaveBeenCalled();
+ expect(testComponent.onStartThumbInput).not.toHaveBeenCalled();
+ expect(testComponent.onEndThumbChange).toHaveBeenCalledTimes(1);
+ expect(testComponent.onEndThumbInput).toHaveBeenCalledTimes(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.');
+ describe('slider with direction', () => {
+ let sliderInstance: MatSlider;
+ let inputInstance: MatSliderThumb;
- fixture.componentInstance.tabIndex = 3;
+ 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);
+ }));
- expect(sliderNativeEl.tabIndex).toBe(3, 'Expected the tabIndex to have been changed.');
+ 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;
- it('should detect the native tabindex attribute', () => {
- const fixture = createComponent(SliderWithNativeTabindexAttr);
+ 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);
+ }));
- const slider = fixture.debugElement.query(By.directive(MatSlider)).componentInstance;
+ it('works in RTL languages', () => {
+ setValueByClick(sliderInstance, 90, platform.IOS);
+ expect(startInputInstance.value).toBe(10);
- expect(slider.tabIndex)
- .toBe(5, 'Expected the tabIndex to be set to the value of the native attribute.');
+ setValueByClick(sliderInstance, 10, platform.IOS);
+ expect(endInputInstance.value).toBe(90);
});
});
describe('slider with ngModel', () => {
let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
let testComponent: SliderWithNgModel;
+ let inputInstance: MatSliderThumb;
- beforeEach(() => {
+ 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);
+ }));
- sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
- sliderNativeElement = sliderDebugElement.nativeElement;
- });
-
- it('should update the model on mousedown', () => {
+ it('should update the model on mouseup', () => {
expect(testComponent.val).toBe(0);
-
- dispatchMousedownEventSequence(sliderNativeElement, 0.76);
+ 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);
-
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.19);
+ slideToValue(testComponent.slider, 19, Thumb.END, platform.IOS);
fixture.detectChanges();
-
expect(testComponent.val).toBe(19);
});
- it('should update the model on keydown', () => {
- expect(testComponent.val).toBe(0);
+ 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);
- dispatchKeyboardEvent(sliderNativeElement, 'keydown', UP_ARROW);
+ testComponent.val = undefined;
fixture.detectChanges();
+ flush();
+ expect(inputInstance.value).toBe(0);
+ }));
+ });
- expect(testComponent.val).toBe(1);
- });
+ describe('slider with ngModel', () => {
+ let fixture: ComponentFixture;
+ let testComponent: RangeSliderWithNgModel;
- it('should be able to reset a slider by setting the model back to undefined', fakeAsync(() => {
- expect(testComponent.slider.value).toBe(0);
+ let startInputInstance: MatSliderThumb;
+ let endInputInstance: MatSliderThumb;
- testComponent.val = 5;
+ beforeEach(waitForAsync(() => {
+ fixture = createComponent(RangeSliderWithNgModel);
fixture.detectChanges();
- flush();
+ 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);
+ });
- expect(testComponent.slider.value).toBe(5);
+ 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);
+ });
- testComponent.val = undefined;
+ it('should update the start thumb model on slide', () => {
+ expect(testComponent.startVal).toBe(0);
+ slideToValue(testComponent.slider, 19, Thumb.START, platform.IOS);
fixture.detectChanges();
- flush();
+ 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);
+ });
- expect(testComponent.slider.value).toBe(0);
+ 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 as a custom form control', () => {
let fixture: ComponentFixture;
- let sliderDebugElement: DebugElement;
- let sliderNativeElement: HTMLElement;
- let sliderInstance: MatSlider;
let testComponent: SliderWithFormControl;
+ let sliderInstance: MatSlider;
+ let inputInstance: MatSliderThumb;
- beforeEach(() => {
+ beforeEach(waitForAsync(() => {
fixture = createComponent(SliderWithFormControl);
fixture.detectChanges();
-
testComponent = fixture.debugElement.componentInstance;
-
- sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider));
- sliderNativeElement = sliderDebugElement.nativeElement;
- sliderInstance = sliderDebugElement.injector.get(MatSlider);
- });
+ 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);
-
- sliderInstance.value = 11;
+ inputInstance.value = 11;
fixture.detectChanges();
-
expect(testComponent.control.value).toBe(0);
});
- it('should update the control on mousedown', () => {
+ it('should update the control on mouseup', () => {
expect(testComponent.control.value).toBe(0);
-
- dispatchMousedownEventSequence(sliderNativeElement, 0.76);
- fixture.detectChanges();
-
+ setValueByClick(sliderInstance, 76, platform.IOS);
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();
-
+ slideToValue(sliderInstance, 19, Thumb.END, platform.IOS);
expect(testComponent.control.value).toBe(19);
});
it('should update the value when the control is set', () => {
- expect(sliderInstance.value).toBe(0);
-
+ expect(inputInstance.value).toBe(0);
testComponent.control.setValue(7);
- fixture.detectChanges();
-
- expect(sliderInstance.value).toBe(7);
+ expect(inputInstance.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);
});
@@ -1184,16 +1396,155 @@ xdescribe('MDC-based MatSlider', () => {
// After changing the value, the control should become dirty (not pristine),
// but remain untouched.
- dispatchMousedownEventSequence(sliderNativeElement, 0.5);
+ 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.
- dispatchFakeEvent(sliderNativeElement, 'blur');
+ 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);
@@ -1205,264 +1556,418 @@ xdescribe('MDC-based MatSlider', () => {
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.sliderInput.value).toBe(0);
+ slideToValue(testComponent.slider, 10, Thumb.END, platform.IOS);
expect(testComponent.value).toBe(10);
- expect(testComponent.slider.value).toBe(10);
+ expect(testComponent.sliderInput.value).toBe(10);
testComponent.value = 20;
fixture.detectChanges();
-
expect(testComponent.value).toBe(20);
- expect(testComponent.slider.value).toBe(20);
+ expect(testComponent.sliderInput.value).toBe(20);
});
});
-});
+ describe('range slider with a two-way binding', () => {
+ let fixture: ComponentFixture;
+ let testComponent: RangeSliderWithTwoWayBinding;
-function flushRequestAnimationFrame() {
- // Flush the "requestAnimationFrame" timer that performs the rendering of
- // the MDC slider. Zone uses 16ms for "requestAnimationFrame".
- tick(16);
-}
+ beforeEach(waitForAsync(() => {
+ fixture = createComponent(RangeSliderWithTwoWayBinding);
+ fixture.detectChanges();
+ testComponent = fixture.componentInstance;
+ }));
-// 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;
- }
-`;
+ 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);
+ });
+ });
+});
+
+const SLIDER_STYLES = ['.mat-mdc-slider { width: 300px; }'];
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class StandardSlider { }
+class StandardSlider {}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class DisabledSlider { }
+class StandardRangeSlider {}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithMinAndMax {
- min = 4;
- max = 6;
-}
+class DisabledSlider {}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithValue { }
+class DisabledRangeSlider {}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithStep {
- step = 25;
- max = 100;
-}
+class SliderWithMinAndMax {}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithAutoTickInterval { }
+class RangeSliderWithMinAndMax {}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithSetTickInterval {
- tickInterval = 6;
-}
+class SliderWithValue {}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithThumbLabel { }
-
+class RangeSliderWithValue {}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithCustomThumbLabelFormatting {
- value = 0;
-
- displayWith(value: number | null) {
- if (!value) {
- return 0;
- }
+class SliderWithStep {}
- if (value >= 1000) {
- return (value / 1000) + 'k';
- }
+@Component({
+ template: `
+
+
+
+
+ `,
+ styles: SLIDER_STYLES,
+})
+class RangeSliderWithStep {}
- return value;
+@Component({
+ template: `
+
+
+
+ `,
+ styles: SLIDER_STYLES,
+})
+class DiscreteSliderWithDisplayWith {
+ displayWith(v: number) {
+ if (v >= 1000) { return `$${v / 1000}k`; }
+ return `$${v}`;
}
}
+@Component({
+ template: `
+
+
+
+
+ `,
+ styles: SLIDER_STYLES,
+})
+class DiscreteRangeSliderWithDisplayWith {
+ displayWith(v: number) {
+ if (v >= 1000) { return `$${v / 1000}k`; }
+ return `$${v}`;
+ }
+}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
class SliderWithOneWayBinding {
- val = 50;
+ value = 50;
}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithFormControl {
- control = new FormControl(0);
+class RangeSliderWithOneWayBinding {
+ startValue = 25;
+ endValue = 75;
}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithNgModel {
+class SliderWithChangeHandler {
+ onChange = jasmine.createSpy('onChange');
+ onInput = jasmine.createSpy('onChange');
@ViewChild(MatSlider) slider: MatSlider;
- val: number | undefined = 0;
}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithValueSmallerThanMin { }
+class RangeSliderWithChangeHandler {
+ onStartThumbChange = jasmine.createSpy('onStartThumbChange');
+ onStartThumbInput = jasmine.createSpy('onStartThumbInput');
+ onEndThumbChange = jasmine.createSpy('onEndThumbChange');
+ onEndThumbInput = jasmine.createSpy('onEndThumbInput');
+ @ViewChild(MatSlider) slider: MatSlider;
+}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithValueGreaterThanMax { }
+class SliderWithNgModel {
+ @ViewChild(MatSlider) slider: MatSlider;
+ val: number | undefined = 0;
+}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithChangeHandler {
- value = 0;
- onChange() { }
- onInput() { }
-
+class RangeSliderWithNgModel {
@ViewChild(MatSlider) slider: MatSlider;
+ startVal: number | undefined = 0;
+ endVal: number | undefined = 100;
}
@Component({
- template: `
`,
- styles: [styles],
+ template: `
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithDir {
- value = 0;
- dir = 'ltr';
+class SliderWithFormControl {
+ control = new FormControl(0);
}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithTabIndexBinding {
- tabIndex: number;
+class RangeSliderWithFormControl {
+ startInputControl = new FormControl(0);
+ endInputControl = new FormControl(100);
}
@Component({
- template: ``,
- styles: [styles],
+ template: `
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithNativeTabindexAttr {
- tabIndex: number;
+class SliderWithTwoWayBinding {
+ @ViewChild(MatSlider) slider: MatSlider;
+ @ViewChild(MatSliderThumb) sliderInput: MatSliderThumb;
+ value = 0;
}
@Component({
- template: '',
- styles: [styles],
+ template: `
+
+
+
+
+ `,
+ styles: SLIDER_STYLES,
})
-class SliderWithTwoWayBinding {
+class RangeSliderWithTwoWayBinding {
@ViewChild(MatSlider) slider: MatSlider;
- value = 0;
+ @ViewChildren(MatSliderThumb) sliderInputs: QueryList;
+ startValue = 0;
+ endValue = 100;
}
-/**
- * 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);
+/** The pointer event types used by the MDC Slider. */
+const enum PointerEventType {
+ POINTER_DOWN = 'pointerdown',
+ POINTER_UP = 'pointerup',
+ POINTER_MOVE = 'pointermove',
}
-/**
- * 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);
+/** The touch event types used by the MDC Slider. */
+const enum TouchEventType {
+ TOUCH_START = 'touchstart',
+ TOUCH_END = 'touchend',
+ TOUCH_MOVE = 'touchmove',
}
-/**
- * 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));
+/** Clicks on the MatSlider at the coordinates corresponding to the given value. */
+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);
+}
+
+/** Slides the MatSlider's thumb to the given value. */
+function slideToValue(slider: MatSlider, value: number, thumbPosition: Thumb, isIOS: boolean) {
+ const sliderElement = slider._elementRef.nativeElement;
+ 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);
+}
+
+/** 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) {
+ 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;
}
- 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..44a70e413d88 100644
--- a/src/material-experimental/mdc-slider/slider.ts
+++ b/src/material-experimental/mdc-slider/slider.ts
@@ -13,516 +13,1130 @@ import {
coerceNumberProperty,
NumberInput
} from '@angular/cdk/coercion';
-import {normalizePassiveListenerOptions, Platform} from '@angular/cdk/platform';
+import {Platform} from '@angular/cdk/platform';
+import {DOCUMENT} from '@angular/common';
import {
AfterViewInit,
- Attribute,
ChangeDetectionStrategy,
+ ChangeDetectorRef,
Component,
+ ContentChildren,
+ Directive,
ElementRef,
EventEmitter,
forwardRef,
Inject,
Input,
NgZone,
- OnChanges,
OnDestroy,
+ OnInit,
Optional,
Output,
- SimpleChanges,
+ QueryList,
ViewChild,
- ViewEncapsulation
+ ViewChildren,
+ 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 {
+ CanColorCtor,
+ CanDisableRipple,
+ CanDisableRippleCtor,
+ MatRipple,
+ MAT_RIPPLE_GLOBAL_OPTIONS,
+ mixinColor,
+ mixinDisableRipple,
+ RippleAnimationConfig,
+ RippleGlobalOptions,
+ RippleRef,
+ RippleState,
+} from '@angular/material/core';
+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';
-/**
- * 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;
+/** Represents a drag event emitted by the MatSlider component. */
+export interface MatSliderDragEvent {
+ /** The MatSliderThumb that was interacted with. */
+ source: MatSliderThumb;
-// 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});
+ /** The MatSlider that was interacted with. */
+ parent: MatSlider;
-// 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});
+ /** The current value of the slider. */
+ value: number;
+}
/**
- * Provider Expression that allows mat-slider to register as a ControlValueAccessor.
- * This allows it to support [(ngModel)] and [formControl].
+ * The visual slider thumb.
+ *
+ * Handles the slider thumb ripple states (hover, focus, and active),
+ * and displaying the value tooltip on discrete sliders.
* @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;
-}
-
@Component({
- selector: 'mat-slider',
- templateUrl: 'slider.html',
- styleUrls: ['slider.css'],
+ selector: 'mat-slider-visual-thumb',
+ templateUrl: './slider-thumb.html',
+ styleUrls: ['slider-thumb.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()',
+ 'class': 'mdc-slider__thumb mat-mdc-slider-visual-thumb',
},
- exportAs: 'matSlider',
- encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
- providers: [MAT_SLIDER_VALUE_ACCESSOR],
+ 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;
+
+ /** 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;
+
+ /** 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 | undefined;
+
+ /** The RippleRef for the slider thumbs focus state. */
+ private _focusRippleRef: RippleRef | undefined;
+
+ /** The RippleRef for the slider thumbs active state. */
+ private _activeRippleRef: RippleRef | undefined;
+
+ /** 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,
+ @Inject(forwardRef(() => MatSlider)) 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 | undefined {
+ if (this.disableRipple) {
+ return;
+ }
+ 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 ``.
+ *
+ * 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]',
+ exportAs: 'matSliderThumb',
+ host: {
+ 'class': 'mdc-slider__input',
+ 'type': 'range',
+ '(blur)': '_onBlur()',
+ '(focus)': '_focus.emit()',
+ },
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: MatSliderThumb,
+ multi: true
+ }],
})
-export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlValueAccessor {
- /** Event emitted when the slider value has changed. */
- @Output() readonly change: EventEmitter = new EventEmitter();
+export class MatSliderThumb implements AfterViewInit, ControlValueAccessor, OnInit {
- /** Event emitted when the slider thumb moves. */
- @Output() readonly input: EventEmitter = new EventEmitter();
+ // ** 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._hostElement.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._hostElement.setAttribute('value', `${value}`);
+ }
+ }
/**
* 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();
+ @Output() readonly valueChange: EventEmitter = new EventEmitter();
+
+ /** 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();
- /** Tabindex for the slider. */
- @Input() tabIndex: number = 0;
+ /** Event emitted every time the MatSliderThumb is focused. */
+ @Output() readonly _focus: EventEmitter = new EventEmitter();
- /** The color palette for this slider. */
- @Input() color: ThemePalette = 'accent';
+ /** 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();
/**
- * 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.
+ * 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.
*/
- @Input() displayWith: (value: number) => string | number;
+ _disabled: boolean = false;
- /** The minimum value that the slider can have. */
- @Input()
- get min(): number {
- return this._min;
+ /**
+ * 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
+ : 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,
+ @Inject(forwardRef(() => MatSlider)) private readonly _slider: MatSlider,
+ private readonly _elementRef: ElementRef) {
+ this._document = document;
+ this._hostElement = _elementRef.nativeElement;
+ }
+
+ 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();
}
- set min(value: number) {
- this._min = coerceNumberProperty(value);
+
+ ngAfterViewInit() {
+ this._initializeInputState();
+ this._initializeInputValueProperty();
+
+ // Setup for the MDC foundation.
+ if (this._slider.disabled) {
+ this._hostElement.disabled = true;
+ }
}
- private _min = 0;
- /** The maximum value that the slider can have. */
- @Input()
- get max(): number {
- return this._max;
+ _onBlur(): void {
+ this._onTouched();
+ this._blur.emit();
}
- set max(value: number) {
- this._max = coerceNumberProperty(value);
+
+ _emitFakeEvent(type: 'change'|'input') {
+ const event = new Event(type) as any;
+ event._matIsHandled = true;
+ this._hostElement.dispatchEvent(event);
}
- 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;
+ /**
+ * Sets the model value. Implemented as part of ControlValueAccessor.
+ * @param value
+ */
+ writeValue(value: any): void {
+ this.value = value;
}
- set value(value: number|null) {
- this._value = coerceNumberProperty(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;
}
- private _value: number|null = null;
- /** The values at which the thumb will snap. */
- @Input()
- get step(): number {
- return this._step;
+ /**
+ * 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;
}
- set step(v: number) {
- this._step = coerceNumberProperty(v, this._step);
+
+ /**
+ * Sets whether the component should be disabled.
+ * Implemented as part of ControlValueAccessor.
+ * @param isDisabled
+ */
+ setDisabledState(isDisabled: boolean): void {
+ this._disabled = isDisabled;
+ 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;
}
- 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).
+ * 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
+ * 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.
+ *
*/
- @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;
+ _initializeInputState(): void {
+ const min = this._hostElement.hasAttribute('matSliderEndThumb')
+ ? this._slider._getInput(Thumb.START).value
+ : this._slider.min;
+ const max = this._hostElement.hasAttribute('matSliderStartThumb')
+ ? this._slider._getInput(Thumb.END).value
+ : this._slider.max;
+ this._hostElement.min = `${min}`;
+ this._hostElement.max = `${max}`;
+ this._hostElement.step = `${this._slider.step}`;
+ }
+
+ /**
+ * 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._hostElement.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._hostElement.hasAttribute('value')) {
+ this.value = this._hostElement.hasAttribute('matSliderEndThumb')
+ ? this._slider.max
+ : this._slider.min;
}
}
- private _tickInterval: number|'auto' = 0;
- /** Whether or not to show the thumb label. */
+ static ngAcceptInputType_value: NumberInput;
+}
+
+// Boilerplate for applying mixins to MatSlider.
+/** @docs-private */
+class MatSliderBase {
+ constructor(public _elementRef: ElementRef) {}
+}
+const _MatSliderMixinBase:
+ CanColorCtor &
+ CanDisableRippleCtor &
+ typeof MatSliderBase =
+ mixinColor(mixinDisableRipple(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.
+ */
+@Component({
+ 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,
+ inputs: ['color', 'disableRipple'],
+})
+export class MatSlider extends _MatSliderMixinBase
+ implements AfterViewInit, CanDisableRipple, OnDestroy {
+ /** The slider thumb(s). */
+ @ViewChildren(MatSliderVisualThumb) _thumbs: 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._setDisabled(coerceBooleanProperty(v));
+ this._updateInputsDisabledState();
+ }
+ 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 thumbLabel(): boolean {
- return this._thumbLabel;
+ get min(): number { return this._min; }
+ set min(v: number) {
+ this._min = coerceNumberProperty(v, this._min);
+ this._reinitialize();
}
- set thumbLabel(value: boolean) {
- this._thumbLabel = coerceBooleanProperty(value);
+ 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);
+ this._reinitialize();
}
- private _thumbLabel: boolean = false;
+ private _max: number = 100;
- /** Whether the slider is disabled. */
+ /** The values at which the thumb will snap. */
@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) => {},
- };
+ get step(): number { return this._step; }
+ set step(v: number) {
+ this._step = coerceNumberProperty(v, this._step);
+ this._reinitialize();
+ }
+ 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) = (value: number) => `${value}`;
/** Instance of the MDC slider foundation for this slider. */
- private _foundation = new MDCSliderFoundation(this._sliderAdapter);
+ private _foundation = new MDCSliderFoundation(new SliderAdapter(this));
+
+ /** Whether the foundation has been initialized. */
+ _initialized: boolean = false;
- /** Whether the MDC foundation has been initialized. */
- private _isInitialized = false;
+ /** The injected document if available or fallback to the global document reference. */
+ _document: Document;
- /** Function that notifies the control value accessor about a value change. */
- private _controlValueAccessorChangeFn: (value: number) => void = () => {};
+ /**
+ * The defaultView of the injected document if
+ * available or fallback to global window reference.
+ */
+ _window: Window;
+
+ /** Used to keep track of & render the active & inactive tick marks on the slider track. */
+ _tickMarks: TickMark[];
- /** Subscription to the Directionality change EventEmitter. */
- private _dirChangeSubscription = Subscription.EMPTY;
+ /** The display value of the start thumb. */
+ _startValueIndicatorText: string;
- /** Function that marks the slider as touched. Registered via "registerOnTouch". */
- _markAsTouched: () => any = () => {};
+ /** 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;
- @ViewChild('thumbContainer') _thumbContainer: ElementRef;
- @ViewChild('track') _track: ElementRef;
- @ViewChild('pinValueMarker') _pinValueMarker: ElementRef;
- @ViewChild('trackMarker') _trackMarker: ElementRef;
+ /** Subscription to changes to the directionality (LTR / RTL) context for the application. */
+ private _dirChangeSubscription: Subscription;
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()));
- });
+ readonly _ngZone: NgZone,
+ 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) {
+ super(_elementRef);
+ this._document = document;
+ this._window = this._document.defaultView || window;
+ this._dirChangeSubscription = this._dir.change.subscribe(() => this._onDirChange());
+ this._attachUISyncEventListener();
}
- }
ngAfterViewInit() {
- this._isInitialized = true;
+ 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;
+ }
+ // 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() {
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._foundation.destroy();
}
+ this._dirChangeSubscription.unsubscribe();
+ this._removeUISyncEventListener();
+ }
- this._syncDisabled();
+ /** Returns true if the language direction for this slider element is right to left. */
+ _isRTL() {
+ return this._dir && this._dir.value === 'rtl';
}
- ngOnChanges(changes: SimpleChanges) {
- if (!this._isInitialized) {
- return;
+ /**
+ * 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);
}
+ }
- 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();
+ /** 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);
}
}
- 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) {
+ /** 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).
+ *
+ * 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();
}
}
- /** Focuses the slider. */
- focus(options?: FocusOptions) {
- this._elementRef.nativeElement.focus(options);
+ /** 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);
+ });
}
- /** Blurs the slider. */
- blur() {
- this._elementRef.nativeElement.blur();
+ /** Sets the value of a slider thumb. */
+ _setValue(value: number, thumbPosition: Thumb): void {
+ thumbPosition === Thumb.START
+ ? this._foundation.setValueStart(value)
+ : this._foundation.setValue(value);
}
- /** Gets the display text of the current value. */
- get displayValue() {
- if (this.displayWith) {
- return this.displayWith(this.value!).toString();
+ /** 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);
}
- 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;
+ /** 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;
+ }
+ }
}
- // 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));
+ /** Whether this is a ranged slider. */
+ _isRange(): boolean {
+ return this._inputs.length === 2;
}
- // 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)`;
+ /** Sets the disabled state based on the disabled state of the inputs (ControlValueAccessor). */
+ _updateDisabled(): void {
+ const disabled = this._inputs.some(input => input._disabled);
+ this._setDisabled(disabled);
+ }
- 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`;
- }
+ /** Gets the slider thumb input of the given thumb position. */
+ _getInput(thumbPosition: Thumb): MatSliderThumb {
+ return thumbPosition === Thumb.END ? this._inputs.last : this._inputs.first;
+ }
- // 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}`;
+ /** Gets the slider thumb HTML input element of the given thumb position. */
+ _getInputElement(thumbPosition: Thumb): HTMLInputElement {
+ return this._getInput(thumbPosition)._hostElement;
}
- /** 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;
+ _getThumb(thumbPosition: Thumb): MatSliderVisualThumb {
+ return thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first;
+ }
- // TODO: disabled until we implement the new MDC slider.
- // this._foundation.setupTrackMarker();
+ /** Gets the slider thumb HTML element of the given thumb position. */
+ _getThumbElement(thumbPosition: Thumb): HTMLElement {
+ return this._getThumb(thumbPosition)._getHostElement();
}
- /** 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);
+ /** Gets the slider knob HTML element of the given thumb position. */
+ _getKnobElement(thumbPosition: Thumb): HTMLElement {
+ return this._getThumb(thumbPosition)._getKnob();
}
- /** 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);
+ /**
+ * 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 uses the
+ * numeric value as a string.
+ */
+ _setValueIndicatorText(value: number, thumbPosition: Thumb) {
+ thumbPosition === Thumb.START
+ ? this._startValueIndicatorText = this.displayWith(value)
+ : this._endValueIndicatorText = this.displayWith(value);
+ this._cdr.markForCheck();
}
- /** 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);
+ /** Gets the value indicator text for the given thumb position. */
+ _getValueIndicatorText(thumbPosition: Thumb): string {
+ return thumbPosition === Thumb.START
+ ? this._startValueIndicatorText
+ : this._endValueIndicatorText;
}
- /** 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!);
+ /** 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';
}
- /** 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 thumb ripples should be disabled. */
+ _isRippleDisabled(): boolean {
+ return this.disabled || this.disableRipple || !!this._globalRippleOptions?.disabled;
}
- /** Whether the slider is displayed in RTL-mode. */
- _isRtl(): boolean {
- return this._dir && this._dir.value === 'rtl';
+ 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. */
+class SliderAdapter implements MDCSliderAdapter {
+
+ /** The global event listener subscription used to handle events on the slider inputs. */
+ private _globalEventSubscriptions = new Subscription();
+
+ /** 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'));
}
/**
- * Registers a callback to be triggered when the value has changed.
- * Implemented as part of ControlValueAccessor.
- * @param fn Callback to be registered.
+ * 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.
*/
- registerOnChange(fn: any) {
- this._controlValueAccessorChangeFn = fn;
+ 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);
+ }
+ });
}
- /**
- * 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;
+ /** 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);
+ }
}
- /**
- * Sets whether the component should be disabled.
- * Implemented as part of ControlValueAccessor.
- * @param isDisabled
- */
- setDisabledState(isDisabled: boolean) {
- this.disabled = isDisabled;
- this._syncDisabled();
+ /** 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;
+ }
}
/**
- * Sets the model value.
- * Implemented as part of ControlValueAccessor.
- * @param value
+ * Returns the thumb position of the given event target.
+ * Returns null if the given event target does not correspond to a slider thumb input.
*/
- writeValue(value: any) {
- this.value = value;
- this._syncValue();
+ 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;
}
- 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;
+ // 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 => {
+ return this._delegate._isRTL();
+ }
+ 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);
+ }
+ emitChangeEvent = (value: number, thumbPosition: Thumb): void => {
+ // 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);
+ input.valueChange.emit(value);
+ }
+ emitInputEvent = (value: number, thumbPosition: Thumb): void => {
+ this._delegate._getInput(thumbPosition)._emitFakeEvent('input');
+ }
+ 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 => {
+ if (evtType === 'change') {
+ this._saveChangeEventHandler(thumbPosition, handler as SpecificEventListener);
+ } else {
+ this._delegate._getInputElement(thumbPosition).addEventListener(evtType, handler);
+ }
+ }
+ deregisterInputEventHandler =
+ (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => {
+ if (evtType === 'change') {
+ this._globalEventSubscriptions.unsubscribe();
+ } else {
+ 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
+
+
+
+
+
+ `);
}
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
}
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);
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 @@