Skip to content

feat(material-experimental/mdc-slider): implement MatSlider #21680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
2 changes: 1 addition & 1 deletion src/material-experimental/mdc-slider/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {MatSliderThumb} from './slider-thumb';

@NgModule({
imports: [MatCommonModule, CommonModule],
exports: [MatSlider],
exports: [MatSlider, MatSliderThumb],
declarations: [
MatSlider,
MatSliderThumb,
Expand Down
35 changes: 28 additions & 7 deletions src/material-experimental/mdc-slider/slider-thumb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ import {NumberInput} from '@angular/cdk/coercion';
import {DOCUMENT} from '@angular/common';
import {Directive, ElementRef, EventEmitter, Inject, Input, Output} from '@angular/core';
import {Thumb} from '@material/slider';
import {MatSlider} from './slider';

/**
* Represents a drag event emitted by the MatSlider component.
*/
export interface MatSliderDragEvent {
/** The MatSliderThumb that was interacted with. */
source: MatSliderThumb;

/** The parent MatSlider that was interacted with. */
parent: MatSlider;

/** The current value of the slider. */
value: number;

/** The thumb that was interacted with. */
thumb: Thumb;
}

/**
* The native input used by the MatSlider.
Expand Down Expand Up @@ -52,6 +70,14 @@ import {Thumb} from '@material/slider';
@Input()
set disabled(v: boolean) { throw Error('Invalid attribute "disabled" on MatSliderThumb.'); }

/** Event emitted when the slider thumb starts being dragged. */
@Output() readonly dragStart: EventEmitter<MatSliderDragEvent>
= new EventEmitter<MatSliderDragEvent>();

/** Event emitted when the slider thumb stops being dragged. */
@Output() readonly dragEnd: EventEmitter<MatSliderDragEvent>
= new EventEmitter<MatSliderDragEvent>();

/** Event emitted every time the MatSliderThumb is blurred. */
@Output() readonly _blur: EventEmitter<void> = new EventEmitter<void>();

Expand All @@ -63,17 +89,12 @@ import {Thumb} from '@material/slider';

constructor(
@Inject(DOCUMENT) private readonly _document: Document,
private readonly _elementRef: ElementRef<HTMLInputElement>,
readonly _elementRef: ElementRef<HTMLInputElement>,
) {}

/** Returns the hosts native HTML element. */
_getHostElement(): HTMLInputElement {
return this._elementRef.nativeElement;
}

/** Returns true if this slider input currently has focus. */
_isFocused(): boolean {
return this._document.activeElement === this._getHostElement();
return this._document.activeElement === this._elementRef.nativeElement;
}

static ngAcceptInputType_value: NumberInput;
Expand Down
25 changes: 25 additions & 0 deletions src/material-experimental/mdc-slider/slider.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!-- Inputs -->
<ng-content></ng-content>

<!-- Track -->
<div class="mdc-slider__track">
<div class="mdc-slider__track--inactive"></div>
<div class="mdc-slider__track--active">
<div class="mdc-slider__track--active_fill" #trackActive></div>
</div>
<div *ngIf="showTickMarks" class="mdc-slider__tick-marks" #tickMarkContainer>
<div *ngFor="let tickMark of _tickMarks" [class]="_getTickMarkClass(tickMark)"></div>
</div>
</div>

<!-- Thumbs -->
<div class="mdc-slider__thumb" *ngFor="let thumb of _getThumbTypes()" #thumb>
<div class="mdc-slider__value-indicator-container" *ngIf="discrete">
<div class="mdc-slider__value-indicator">
<span class="mdc-slider__value-indicator-text">
{{_getValueIndicatorTextByThumb(thumb)}}
</span>
</div>
</div>
<div class="mdc-slider__thumb-knob" #knob></div>
</div>
223 changes: 219 additions & 4 deletions src/material-experimental/mdc-slider/slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,33 @@
*/

import {
BooleanInput,
coerceBooleanProperty,
coerceNumberProperty,
NumberInput
} from '@angular/cdk/coercion';
import {Platform} from '@angular/cdk/platform';
import {DOCUMENT} from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChildren,
ElementRef,
EventEmitter,
Inject,
Input,
OnDestroy,
Output,
QueryList,
ViewChild,
ViewChildren,
ViewEncapsulation,
} from '@angular/core';
import {MDCSliderFoundation} from '@material/slider';
import {MDCSliderFoundation, Thumb, TickMark} from '@material/slider';
import {SliderAdapter} from './slider-adapter';
import {MatSliderThumb} from './slider-thumb';

/**
* Allows users to select from a range of values by moving the slider thumb. It is similar in
Expand All @@ -22,13 +43,207 @@ import {SliderAdapter} from './slider-adapter';
selector: 'mat-slider',
templateUrl: 'slider.html',
styleUrls: ['slider.css'],
host: {
'class': 'mat-mdc-slider mdc-slider',
'[class.mdc-slider--range]': '_isRange()',
'[class.mdc-slider--disabled]': 'disabled',
'[class.mdc-slider--discrete]': 'discrete',
'[class.mdc-slider--tick-marks]': 'showTickMarks',
},
exportAs: 'matSlider',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class MatSlider {
export class MatSlider implements AfterViewInit, OnDestroy {
/** The slider thumb(s). */
@ViewChildren('thumb') _thumbs: QueryList<ElementRef<HTMLElement>>;

/** Instance of the MDC slider foundation for this slider. */
/** The slider thumb knob(s) */
@ViewChildren('knob') _knobs: QueryList<ElementRef<HTMLElement>>;

/** The active section of the slider track. */
@ViewChild('trackActive') _trackActive: ElementRef<HTMLElement>;

/** The sliders hidden range input(s). */
@ContentChildren(MatSliderThumb, {descendants: false}) _inputs: QueryList<MatSliderThumb>;

/** Whether the slider is disabled. */
@Input()
get disabled(): boolean { return this._disabled; }
set disabled(v: boolean) {
this._disabled = coerceBooleanProperty(v);

// If we want to disable the slider after the foundation has been initialized,
// we need to inform the foundation by calling `setDisabled`. Also, we can't call
// this before initializing the foundation because it will throw errors.
if (this._initialized) {
this._foundation.setDisabled(v);
}
}
private _disabled: boolean = false;

/** Whether the slider displays a numeric value label upon pressing the thumb. */
@Input()
get discrete(): boolean { return this._discrete; }
set discrete(v: boolean) { this._discrete = coerceBooleanProperty(v); }
private _discrete: boolean = false;

/** Whether the slider displays tick marks along the slider track. */
@Input()
get showTickMarks(): boolean { return this._showTickMarks; }
set showTickMarks(v: boolean) {
this._showTickMarks = coerceBooleanProperty(v);
}
private _showTickMarks: boolean = false;

/** The minimum value that the slider can have. */
@Input()
get min(): number { return this._min; }
set min(v: number) { this._min = coerceNumberProperty(v, this._min); }
private _min = 0;

// tslint:disable-next-line:no-unused-variable
/** The maximum value that the slider can have. */
@Input()
get max(): number { return this._max; }
set max(v: number) { this._max = coerceNumberProperty(v, this._max); }
private _max = 100;

/** The values at which the thumb will snap. */
@Input()
get step(): number { return this._step; }
set step(v: number) { this._step = coerceNumberProperty(v, this._step); }
private _step: number = 1;

/**
* Function that will be used to format the value before it is displayed
* in the thumb label. Can be used to format very large number in order
* for them to fit into the slider thumb.
*/
@Input() displayWith: ((value: number) => string) | null;

/** Instance of the MDC slider foundation for this slider. */
private _foundation = new MDCSliderFoundation(new SliderAdapter());

/** Whether the foundation has been initialized. */
_initialized: boolean = false;

/** The string representation of the start thumbs value. */
_startValueIndicatorText: string;

/** The string representation of the end thumbs value. */
_endValueIndicatorText: string;

/** The injected document if available or fallback to the global document reference. */
_document: Document;

/**
* The defaultView of the injected document if
* available or fallback to global window reference.
*/
_window: Window;

/** The hosts native HTML element. */
_hostElement: HTMLElement;

/** Used to keep track of & render the active & inactive tick marks on the slider track. */
_tickMarks: TickMark[];

constructor(
readonly _cdr: ChangeDetectorRef,
private readonly _elementRef: ElementRef<HTMLElement>,
private readonly _platform: Platform,
@Inject(DOCUMENT) document: any) {
this._document = document;
this._window = this._document.defaultView || window;
this._hostElement = this._elementRef.nativeElement;
}

ngAfterViewInit() {
this._foundation.init();
if (this._platform.isBrowser) {
this._foundation.layout();
}
this._initialized = true;
}

ngOnDestroy() {
if (this._platform.isBrowser) {
this._foundation.destroy();
}
}

/** Gets the current value of given slider thumb. */
_getValue(thumb: Thumb): number {
return thumb === Thumb.START
? this._foundation.getValueStart()
: this._foundation.getValue();
}

/** Sets the value of a slider thumb. */
_setValue(value: number, thumb: Thumb): void {
thumb === Thumb.START
? this._foundation.setValueStart(value)
: this._foundation.setValue(value);
}

/** Whether this is a ranged slider. */
_isRange(): boolean {
return this._inputs.length === 2;
}

/** Gets the slider thumb input of the given thumb. */
_getInput(thumb: Thumb): MatSliderThumb {
return thumb === Thumb.END ? this._inputs.get(this._inputs.length - 1)! : this._inputs.get(0)!;
}

/** Gets the slider thumb HTML input element of the given thumb. */
_getInputElement(thumb: Thumb): HTMLInputElement {
return this._getInput(thumb)._elementRef.nativeElement;
}

/** Gets the slider thumb HTML element of the given thumb. */
_getThumbElement(thumb: Thumb): HTMLElement {
const thumbs = this._thumbs.toArray().map(e => e.nativeElement);
return thumb === Thumb.END ? thumbs[thumbs.length - 1] : thumbs[0];
}

/** Gets the slider knob HTML element of the given thumb. */
_getKnobElement(thumb: Thumb): HTMLElement {
const knobs = this._knobs.toArray().map(e => e.nativeElement);
return thumb === Thumb.END ? knobs[knobs.length - 1] : knobs[0];
}

/**
* Gets the text representation of the given value.
*
* Uses the `displayWith` function if one has been provided. Otherwise, it just returns the
* current numeric value as a string.
*/
_getValueIndicatorText(value: number): string {
return this.displayWith ? this.displayWith(value) : value.toString();
}

/** Gets the text representation of the current value of the given thumb. */
_getValueIndicatorTextByThumb(thumb: Thumb): string {
return this._getValueIndicatorText(this._getValue(thumb));
}

/** Determines the class name for a HTML element. */
_getTickMarkClass(tickMark: TickMark): string {
return tickMark === TickMark.ACTIVE
? 'mdc-slider__tick-mark--active'
: 'mdc-slider__tick-mark--inactive';
}

/** Returns an array of the thumb types that exist on the current slider instance. */
_getThumbTypes(): Thumb[] {
return this._isRange() ? [Thumb.START, Thumb.END] : [Thumb.END];
}

static ngAcceptInputType_disabled: BooleanInput;
static ngAcceptInputType_discrete: BooleanInput;
static ngAcceptInputType_showTickMarks: BooleanInput;
static ngAcceptInputType_min: NumberInput;
static ngAcceptInputType_max: NumberInput;
static ngAcceptInputType_step: NumberInput;
}
4 changes: 0 additions & 4 deletions src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,6 @@ <h2>MDC slide-toggle</h2>
<mat-slide-toggle>with a label</mat-slide-toggle>

<h2>MDC Slider</h2>
<mat-slider></mat-slider>
<mat-slider value="50"></mat-slider>
<mat-slider tickInterval="1" min="1" max="10" value="5" thumbLabel></mat-slider>
<mat-slider disabled></mat-slider>

<h2>MDC Tabs</h2>

Expand Down