diff --git a/src/material-experimental/mdc-slider/slider.scss b/src/material-experimental/mdc-slider/slider.scss
index e69de29bb2d1..edbdb0402de2 100644
--- a/src/material-experimental/mdc-slider/slider.scss
+++ b/src/material-experimental/mdc-slider/slider.scss
@@ -0,0 +1,6 @@
+@import '@material/slider/slider';
+@include core-styles;
+
+.mdc-slider {
+ display: block;
+}
diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts
index 2c06e7b736a8..5283903f42c2 100644
--- a/src/material-experimental/mdc-slider/slider.ts
+++ b/src/material-experimental/mdc-slider/slider.ts
@@ -20,18 +20,181 @@ import {
ChangeDetectorRef,
Component,
ContentChildren,
+ Directive,
ElementRef,
+ EventEmitter,
Inject,
Input,
OnDestroy,
+ Output,
QueryList,
ViewChild,
ViewChildren,
ViewEncapsulation,
} from '@angular/core';
-import {MDCSliderFoundation, Thumb, TickMark} from '@material/slider';
-import {SliderAdapter} from './slider-adapter';
-import {MatSliderThumb} from './slider-thumb';
+import {SpecificEventListener, EventType} from '@material/base';
+import {MDCSliderAdapter, MDCSliderFoundation, Thumb, TickMark} from '@material/slider';
+
+/** Represents a drag event emitted by the MatSlider component. */
+export interface MatSliderDragEvent {
+ /** The MatSliderThumb that was interacted with. */
+ source: MatSliderThumb;
+
+ /** The MatSlider that was interacted with. */
+ parent: MatSlider;
+
+ /** The current value of the slider. */
+ value: number;
+}
+
+/**
+ * Directive that adds slider-specific behaviors to an input element inside ``.
+ * Up to two may be placed inside of a ``.
+ *
+ * If one is used, the selector `matSliderThumb` must be used, and the outcome will be a normal
+ * slider. If two are used, the selectors `matSliderStartThumb` and `matSliderEndThumb` must be
+ * used, and the outcome will be a range slider with two slider thumbs.
+ */
+@Directive({
+ selector: 'input[matSliderThumb], input[matSliderStartThumb], input[matSliderEndThumb]',
+ host: {
+ 'class': 'mdc-slider__input',
+ 'type': 'range',
+ '(blur)': '_blur.emit()',
+ '(focus)': '_focus.emit()',
+ },
+})
+export class MatSliderThumb implements AfterViewInit {
+
+ // ** IMPORTANT NOTE **
+ //
+ // The way `value` is implemented for MatSliderThumb doesn't follow typical Angular conventions.
+ // Normally we would define a private variable `_value` as the source of truth for the value of
+ // the slider thumb input. The source of truth for the value of the slider inputs has already
+ // been decided for us by MDC to be the value attribute on the slider thumb inputs. This is
+ // because the MDC foundation and adapter expect that the value attribute is the source of truth
+ // for the slider inputs.
+ //
+ // Also, note that the value attribute is completely disconnected from the value property.
+
+ /** The current value of this slider input. */
+ @Input()
+ get value(): number {
+ return coerceNumberProperty(this._elementRef.nativeElement.getAttribute('value'));
+ }
+ set value(v: number) {
+ const value = coerceNumberProperty(v);
+
+ // If the foundation has already been initialized, we need to
+ // relay any value updates to it so that it can update the UI.
+ if (this._slider._initialized) {
+ this._slider._setValue(value, this._thumbPosition);
+ } else {
+ // Setup for the MDC foundation.
+ this._elementRef.nativeElement.setAttribute('value', `${value}`);
+ }
+ }
+
+ /** Event emitted when the slider thumb starts being dragged. */
+ @Output() readonly dragStart: EventEmitter
+ = new EventEmitter();
+
+ /** Event emitted when the slider thumb stops being dragged. */
+ @Output() readonly dragEnd: EventEmitter
+ = new EventEmitter();
+
+ /** Event emitted every time the MatSliderThumb is blurred. */
+ @Output() readonly _blur: EventEmitter = new EventEmitter();
+
+ /** Event emitted every time the MatSliderThumb is focused. */
+ @Output() readonly _focus: EventEmitter = new EventEmitter();
+
+ /** Indicates which slider thumb this input corresponds to. */
+ private _thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb')
+ ? Thumb.START
+ : Thumb.END;
+
+ private _document: Document;
+
+ constructor(
+ @Inject(DOCUMENT) document: any,
+ private readonly _slider: MatSlider,
+ readonly _elementRef: ElementRef,
+ ) {
+ this._document = document;
+ // By calling this in the constructor we guarantee that the sibling sliders initial value by
+ // has already been set by the time we reach ngAfterViewInit().
+ this._initializeInputValueAttribute();
+ }
+
+ ngAfterViewInit() {
+ this._initializeInputMinMax();
+ this._initializeInputValueProperty();
+
+ // Setup for the MDC foundation.
+ if (this._slider.disabled) {
+ this._elementRef.nativeElement.disabled = true;
+ }
+ }
+
+ /** Returns true if this slider input currently has focus. */
+ _isFocused(): boolean {
+ return this._document.activeElement === this._elementRef.nativeElement;
+ }
+
+ /**
+ * Sets the min and max properties on the slider thumb input.
+ *
+ * Must be called AFTER the sibling slider thumb input is guaranteed to have had its value
+ * attribute value set. For a range slider, the min and max of the slider thumb input depends on
+ * the value of its sibling slider thumb inputs value.
+ *
+ * Must be called BEFORE the value property is set. In the case where the min and max have not
+ * yet been set and we are setting the input value property to a value outside of the native
+ * inputs default min or max. The value property would not be set to our desired value, but
+ * instead be capped at either the default min or max.
+ *
+ */
+ private _initializeInputMinMax(): void {
+ const min = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb')
+ ? this._slider._getInput(Thumb.START).value
+ : this._slider.min;
+ const max = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb')
+ ? this._slider._getInput(Thumb.END).value
+ : this._slider.max;
+ this._elementRef.nativeElement.min = `${min}`;
+ this._elementRef.nativeElement.max = `${max}`;
+ }
+
+ /**
+ * Sets the value property on the slider thumb input.
+ *
+ * Must be called AFTER the min and max have been set. In the case where the min and max have not
+ * yet been set and we are setting the input value property to a value outside of the native
+ * inputs default min or max. The value property would not be set to our desired value, but
+ * instead be capped at either the default min or max.
+ */
+ private _initializeInputValueProperty(): void {
+ this._elementRef.nativeElement.value = `${this.value}`;
+ }
+
+ /**
+ * Ensures the value attribute is initialized.
+ *
+ * Must be called BEFORE the min and max are set. For a range slider, the min and max of the
+ * slider thumb input depends on the value of its sibling slider thumb inputs value.
+ */
+ private _initializeInputValueAttribute(): void {
+ // Only set the default value if an initial value has not already been provided.
+ if (!this._elementRef.nativeElement.hasAttribute('value')) {
+ this.value = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb')
+ ? this._slider.max
+ : this._slider.min;
+ }
+ }
+
+ static ngAcceptInputType_value: NumberInput;
+}
/**
* Allows users to select from a range of values by moving the slider thumb. It is similar in
@@ -59,11 +222,16 @@ export class MatSlider implements AfterViewInit, OnDestroy {
/** The slider thumb knob(s) */
@ViewChildren('knob') _knobs: QueryList>;
+ /** The span containing the slider thumb value indicator text */
+ @ViewChildren('valueIndicatorTextElement')
+ _valueIndicatorTextElements: QueryList>;
+
/** The active section of the slider track. */
@ViewChild('trackActive') _trackActive: ElementRef;
/** The sliders hidden range input(s). */
- @ContentChildren(MatSliderThumb, {descendants: false}) _inputs: QueryList;
+ @ContentChildren(MatSliderThumb, {descendants: false})
+ _inputs: QueryList;
/** Whether the slider is disabled. */
@Input()
@@ -89,22 +257,20 @@ export class MatSlider implements AfterViewInit, OnDestroy {
/** Whether the slider displays tick marks along the slider track. */
@Input()
get showTickMarks(): boolean { return this._showTickMarks; }
- set showTickMarks(v: boolean) {
- this._showTickMarks = coerceBooleanProperty(v);
- }
+ set showTickMarks(v: boolean) { this._showTickMarks = coerceBooleanProperty(v); }
private _showTickMarks: boolean = false;
/** The minimum value that the slider can have. */
@Input()
get min(): number { return this._min; }
set min(v: number) { this._min = coerceNumberProperty(v, this._min); }
- private _min = 0;
+ private _min: number = 0;
/** The maximum value that the slider can have. */
@Input()
get max(): number { return this._max; }
set max(v: number) { this._max = coerceNumberProperty(v, this._max); }
- private _max = 100;
+ private _max: number = 100;
/** The values at which the thumb will snap. */
@Input()
@@ -120,17 +286,11 @@ export class MatSlider implements AfterViewInit, OnDestroy {
@Input() displayWith: ((value: number) => string) | null;
/** Instance of the MDC slider foundation for this slider. */
- private _foundation = new MDCSliderFoundation(new SliderAdapter());
+ private _foundation = new MDCSliderFoundation(new SliderAdapter(this));
/** Whether the foundation has been initialized. */
_initialized: boolean = false;
- /** The string representation of the start thumbs value. */
- _startValueIndicatorText: string;
-
- /** The string representation of the end thumbs value. */
- _endValueIndicatorText: string;
-
/** The injected document if available or fallback to the global document reference. */
_document: Document;
@@ -140,28 +300,48 @@ export class MatSlider implements AfterViewInit, OnDestroy {
*/
_window: Window;
- /** The hosts native HTML element. */
- _hostElement: HTMLElement;
-
/** Used to keep track of & render the active & inactive tick marks on the slider track. */
_tickMarks: TickMark[];
+ /** The display value of the start thumb. */
+ private _startValueIndicatorText: string;
+
+ /** The display value of the end thumb. */
+ private _endValueIndicatorText: string;
+
constructor(
readonly _cdr: ChangeDetectorRef,
- private readonly _elementRef: ElementRef,
+ readonly _elementRef: ElementRef,
private readonly _platform: Platform,
@Inject(DOCUMENT) document: any) {
this._document = document;
this._window = this._document.defaultView || window;
- this._hostElement = this._elementRef.nativeElement;
}
ngAfterViewInit() {
- this._foundation.init();
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
+ _validateInputs(
+ this._isRange(),
+ this._getInputElement(Thumb.START),
+ this._getInputElement(Thumb.END),
+ );
+ }
if (this._platform.isBrowser) {
+ this._foundation.init();
this._foundation.layout();
+ this._initialized = true;
}
- this._initialized = true;
+ // The MDC foundation requires access to the view and content children of the MatSlider. In
+ // order to access the view and content children of MatSlider we need to wait until change
+ // detection runs and materializes them. That is why we call init() and layout() in
+ // ngAfterViewInit().
+ //
+ // The MDC foundation then uses the information it gathers from the DOM to compute an initial
+ // value for the tickMarks array. It then tries to update the component data, but because it is
+ // updating the component data AFTER change detection already ran, we will get a changed after
+ // checked error. Because of this, we need to force change detection to update the UI with the
+ // new state.
+ this._cdr.detectChanges();
}
ngOnDestroy() {
@@ -170,16 +350,9 @@ export class MatSlider implements AfterViewInit, OnDestroy {
}
}
- /** Gets the current value of given slider thumb. */
- _getValue(thumb: Thumb): number {
- return thumb === Thumb.START
- ? this._foundation.getValueStart()
- : this._foundation.getValue();
- }
-
/** Sets the value of a slider thumb. */
- _setValue(value: number, thumb: Thumb): void {
- thumb === Thumb.START
+ _setValue(value: number, thumbPosition: Thumb): void {
+ thumbPosition === Thumb.START
? this._foundation.setValueStart(value)
: this._foundation.setValue(value);
}
@@ -189,41 +362,45 @@ export class MatSlider implements AfterViewInit, OnDestroy {
return this._inputs.length === 2;
}
- /** Gets the slider thumb input of the given thumb. */
- _getInput(thumb: Thumb): MatSliderThumb {
- return thumb === Thumb.END ? this._inputs.get(this._inputs.length - 1)! : this._inputs.get(0)!;
+ /** Gets the slider thumb input of the given thumb position. */
+ _getInput(thumbPosition: Thumb): MatSliderThumb {
+ return thumbPosition === Thumb.END ? this._inputs.last! : this._inputs.first!;
+ }
+
+ /** Gets the slider thumb HTML input element of the given thumb position. */
+ _getInputElement(thumbPosition: Thumb): HTMLInputElement {
+ return this._getInput(thumbPosition)._elementRef.nativeElement;
}
- /** Gets the slider thumb HTML input element of the given thumb. */
- _getInputElement(thumb: Thumb): HTMLInputElement {
- return this._getInput(thumb)._elementRef.nativeElement;
+ /** Gets the slider thumb HTML element of the given thumb position. */
+ _getThumbElement(thumbPosition: Thumb): HTMLElement {
+ const thumbElementRef = thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first;
+ return thumbElementRef.nativeElement;
}
- /** Gets the slider thumb HTML element of the given thumb. */
- _getThumbElement(thumb: Thumb): HTMLElement {
- const thumbs = this._thumbs.toArray().map(e => e.nativeElement);
- return thumb === Thumb.END ? thumbs[thumbs.length - 1] : thumbs[0];
+ /** Gets the slider knob HTML element of the given thumb position. */
+ _getKnobElement(thumbPosition: Thumb): HTMLElement {
+ const knobElementRef = thumbPosition === Thumb.END ? this._knobs.last : this._knobs.first;
+ return knobElementRef.nativeElement;
}
- /** Gets the slider knob HTML element of the given thumb. */
- _getKnobElement(thumb: Thumb): HTMLElement {
- const knobs = this._knobs.toArray().map(e => e.nativeElement);
- return thumb === Thumb.END ? knobs[knobs.length - 1] : knobs[0];
+ _getValueIndicatorText(thumbPosition: Thumb) {
+ return thumbPosition === Thumb.START
+ ? this._startValueIndicatorText
+ : this._endValueIndicatorText;
}
/**
- * Gets the text representation of the given value.
+ * Sets the value indicator text of the given thumb position using the given value.
*
- * Uses the `displayWith` function if one has been provided. Otherwise, it just returns the
- * current numeric value as a string.
+ * Uses the `displayWith` function if one has been provided. Otherwise, it just uses the
+ * numeric value as a string.
*/
- _getValueIndicatorText(value: number): string {
- return this.displayWith ? this.displayWith(value) : value.toString();
- }
-
- /** Gets the text representation of the current value of the given thumb. */
- _getValueIndicatorTextByThumb(thumb: Thumb): string {
- return this._getValueIndicatorText(this._getValue(thumb));
+ _setValueIndicatorText(value: number, thumbPosition: Thumb) {
+ const valueText = this.displayWith ? this.displayWith(value) : `${value}`;
+ thumbPosition === Thumb.START
+ ? this._startValueIndicatorText = valueText
+ : this._endValueIndicatorText = valueText;
}
/** Determines the class name for a HTML element. */
@@ -245,3 +422,183 @@ export class MatSlider implements AfterViewInit, OnDestroy {
static ngAcceptInputType_max: NumberInput;
static ngAcceptInputType_step: NumberInput;
}
+
+/** The MDCSliderAdapter implementation. */
+class SliderAdapter implements MDCSliderAdapter {
+ constructor(private readonly _delegate: MatSlider) {}
+
+ // We manually assign functions instead of using prototype methods because
+ // MDC clobbers the values otherwise.
+ // See https://github.com/material-components/material-components-web/pull/6256
+
+ hasClass = (className: string): boolean => {
+ return this._delegate._elementRef.nativeElement.classList.contains(className);
+ }
+ addClass = (className: string): void => {
+ this._delegate._elementRef.nativeElement.classList.add(className);
+ }
+ removeClass = (className: string): void => {
+ this._delegate._elementRef.nativeElement.classList.remove(className);
+ }
+ getAttribute = (attribute: string): string | null => {
+ return this._delegate._elementRef.nativeElement.getAttribute(attribute);
+ }
+ addThumbClass = (className: string, thumbPosition: Thumb): void => {
+ this._delegate._getThumbElement(thumbPosition).classList.add(className);
+ }
+ removeThumbClass = (className: string, thumbPosition: Thumb): void => {
+ this._delegate._getThumbElement(thumbPosition).classList.remove(className);
+ }
+ getInputValue = (thumbPosition: Thumb): string => {
+ return this._delegate._getInputElement(thumbPosition).value;
+ }
+ setInputValue = (value: string, thumbPosition: Thumb): void => {
+ this._delegate._getInputElement(thumbPosition).value = value;
+ }
+ getInputAttribute = (attribute: string, thumbPosition: Thumb): string | null => {
+ return this._delegate._getInputElement(thumbPosition).getAttribute(attribute);
+ }
+ setInputAttribute = (attribute: string, value: string, thumbPosition: Thumb): void => {
+ this._delegate._getInputElement(thumbPosition).setAttribute(attribute, value);
+ }
+ removeInputAttribute = (attribute: string, thumbPosition: Thumb): void => {
+ this._delegate._getInputElement(thumbPosition).removeAttribute(attribute);
+ }
+ focusInput = (thumbPosition: Thumb): void => {
+ this._delegate._getInputElement(thumbPosition).focus();
+ }
+ isInputFocused = (thumbPosition: Thumb): boolean => {
+ return this._delegate._getInput(thumbPosition)._isFocused();
+ }
+ getThumbKnobWidth = (thumbPosition: Thumb): number => {
+ // TODO(wagnermaciel): Check if this causes issues for SSR
+ // once the mdc-slider is added back to the kitchen sink SSR app.
+ return this._delegate._getKnobElement(thumbPosition).getBoundingClientRect().width;
+ }
+ getThumbBoundingClientRect = (thumbPosition: Thumb): ClientRect => {
+ return this._delegate._getThumbElement(thumbPosition).getBoundingClientRect();
+ }
+ getBoundingClientRect = (): ClientRect => {
+ return this._delegate._elementRef.nativeElement.getBoundingClientRect();
+ }
+ isRTL = (): boolean => {
+ // TODO(wagnermaciel): Actually implementing this.
+ return false;
+ }
+ setThumbStyleProperty = (propertyName: string, value: string, thumbPosition: Thumb): void => {
+ this._delegate._getThumbElement(thumbPosition).style.setProperty(propertyName, value);
+ }
+ removeThumbStyleProperty = (propertyName: string, thumbPosition: Thumb): void => {
+ this._delegate._getThumbElement(thumbPosition).style.removeProperty(propertyName);
+ }
+ setTrackActiveStyleProperty = (propertyName: string, value: string): void => {
+ this._delegate._trackActive.nativeElement.style.setProperty(propertyName, value);
+ }
+ removeTrackActiveStyleProperty = (propertyName: string): void => {
+ this._delegate._trackActive.nativeElement.style.removeProperty(propertyName);
+ }
+ setValueIndicatorText = (value: number, thumbPosition: Thumb): void => {
+ this._delegate._setValueIndicatorText(value, thumbPosition);
+ }
+ getValueToAriaValueTextFn = (): ((value: number) => string) | null => {
+ return this._delegate.displayWith;
+ }
+ updateTickMarks = (tickMarks: TickMark[]): void => {
+ this._delegate._tickMarks = tickMarks;
+ this._delegate._cdr.markForCheck();
+ }
+ setPointerCapture = (pointerId: number): void => {
+ this._delegate._elementRef.nativeElement.setPointerCapture(pointerId);
+ }
+ // We ignore emitChangeEvent and emitInputEvent because the slider inputs
+ // are already exposed so users can just listen for those events directly themselves.
+ emitChangeEvent = (value: number, thumbPosition: Thumb): void => {};
+ emitInputEvent = (value: number, thumbPosition: Thumb): void => {};
+ emitDragStartEvent = (value: number, thumbPosition: Thumb): void => {
+ const input = this._delegate._getInput(thumbPosition);
+ input.dragStart.emit({ source: input, parent: this._delegate, value });
+ }
+ emitDragEndEvent = (value: number, thumbPosition: Thumb): void => {
+ const input = this._delegate._getInput(thumbPosition);
+ input.dragEnd.emit({ source: input, parent: this._delegate, value });
+ }
+ registerEventHandler =
+ (evtType: K, handler: SpecificEventListener): void => {
+ this._delegate._elementRef.nativeElement.addEventListener(evtType, handler);
+ }
+ deregisterEventHandler =
+ (evtType: K, handler: SpecificEventListener): void => {
+ this._delegate._elementRef.nativeElement.removeEventListener(evtType, handler);
+ }
+ registerThumbEventHandler =
+ (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => {
+ this._delegate._getThumbElement(thumbPosition).addEventListener(evtType, handler);
+ }
+ deregisterThumbEventHandler =
+ (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => {
+ this._delegate._getThumbElement(thumbPosition).removeEventListener(evtType, handler);
+ }
+ registerInputEventHandler =
+ (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => {
+ this._delegate._getInputElement(thumbPosition).addEventListener(evtType, handler);
+ }
+ deregisterInputEventHandler =
+ (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => {
+ this._delegate._getInputElement(thumbPosition).removeEventListener(evtType, handler);
+ }
+ registerBodyEventHandler =
+ (evtType: K, handler: SpecificEventListener): void => {
+ this._delegate._document.body.addEventListener(evtType, handler);
+ }
+ deregisterBodyEventHandler =
+ (evtType: K, handler: SpecificEventListener): void => {
+ this._delegate._document.body.removeEventListener(evtType, handler);
+ }
+ registerWindowEventHandler =
+ (evtType: K, handler: SpecificEventListener): void => {
+ this._delegate._window.addEventListener(evtType, handler);
+ }
+ deregisterWindowEventHandler =
+ (evtType: K, handler: SpecificEventListener): void => {
+ this._delegate._window.removeEventListener(evtType, handler);
+ }
+}
+
+/**
+ * Ensures that there is not an invalid configuration for the slider thumb inputs.
+ */
+function _validateInputs(
+ isRange: boolean,
+ startInputElement: HTMLInputElement,
+ endInputElement: HTMLInputElement): void {
+ if (isRange) {
+ if (!startInputElement.hasAttribute('matSliderStartThumb')) {
+ _throwInvalidInputConfigurationError();
+ }
+ if (!endInputElement.hasAttribute('matSliderEndThumb')) {
+ _throwInvalidInputConfigurationError();
+ }
+ } else {
+ if (!endInputElement.hasAttribute('matSliderThumb')) {
+ _throwInvalidInputConfigurationError();
+ }
+ }
+}
+
+function _throwInvalidInputConfigurationError(): void {
+ throw Error(`Invalid slider thumb input configuration!
+
+ Valid configurations are as follows:
+
+
+
+
+
+ or
+
+
+
+
+
+ `);
+}