Skip to content

Commit 23e9348

Browse files
committed
feat(material-experimental/mdc-slider): implement slider thumb ripples
* create MatSliderVisualThumb
1 parent e5cbc8c commit 23e9348

File tree

6 files changed

+216
-34
lines changed

6 files changed

+216
-34
lines changed

src/material-experimental/mdc-slider/_slider-theme.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,10 @@
110110
base: mdc-theme-prop-value($color)
111111
),
112112
));
113+
.mdc-slider-hover-ripple {
114+
background-color: rgba(mdc-theme-prop-value($color), 0.05);
115+
}
116+
.mdc-slider-focus-ripple, .mdc-slider-active-ripple {
117+
background-color: rgba(mdc-theme-prop-value($color), 0.2);
118+
}
113119
}

src/material-experimental/mdc-slider/module.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88

99
import {CommonModule} from '@angular/common';
1010
import {NgModule} from '@angular/core';
11-
import {MatCommonModule} from '@angular/material-experimental/mdc-core';
12-
import {MatSlider, MatSliderThumb} from './slider';
11+
import {MatCommonModule, MatRippleModule} from '@angular/material-experimental/mdc-core';
12+
import {MatSlider, MatSliderThumb, MatSliderVisualThumb} from './slider';
1313

1414
@NgModule({
15-
imports: [MatCommonModule, CommonModule],
15+
imports: [MatCommonModule, CommonModule, MatRippleModule],
1616
exports: [MatSlider, MatSliderThumb],
1717
declarations: [
1818
MatSlider,
1919
MatSliderThumb,
20+
MatSliderVisualThumb,
2021
],
2122
})
2223
export class MatSliderModule {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div class="mdc-slider__value-indicator-container" *ngIf="_discrete">
2+
<div class="mdc-slider__value-indicator">
3+
<span class="mdc-slider__value-indicator-text">{{_valueIndicatorText}}</span>
4+
</div>
5+
</div>
6+
<div class="mdc-slider__thumb-knob" #knob></div>
7+
<div matRipple></div>

src/material-experimental/mdc-slider/slider.html

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
</div>
1414

1515
<!-- Thumbs -->
16-
<div class="mdc-slider__thumb" *ngFor="let thumb of _getThumbTypes()" #thumb>
17-
<div class="mdc-slider__value-indicator-container" *ngIf="discrete">
18-
<div class="mdc-slider__value-indicator">
19-
<span class="mdc-slider__value-indicator-text">{{_getValueIndicatorText(thumb)}}</span>
20-
</div>
21-
</div>
22-
<div class="mdc-slider__thumb-knob" #knob></div>
23-
</div>
16+
<mat-slider-visual-start-thumb
17+
[_discrete]="discrete"
18+
[_valueIndicatorText]="_startValueIndicatorText"
19+
*ngIf="_isRange()">
20+
</mat-slider-visual-start-thumb>
21+
22+
<mat-slider-visual-end-thumb
23+
[_discrete]="discrete"
24+
[_valueIndicatorText]="_endValueIndicatorText">
25+
</mat-slider-visual-end-thumb>

src/material-experimental/mdc-slider/slider.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@
66
.mdc-slider {
77
display: block;
88
}
9+
10+
.mat-ripple {
11+
height: 100%;
12+
width: 100%;
13+
}

src/material-experimental/mdc-slider/slider.ts

Lines changed: 184 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ import {
3232
ViewChildren,
3333
ViewEncapsulation,
3434
} from '@angular/core';
35-
import {CanColorCtor, mixinColor} from '@angular/material/core';
35+
import {
36+
CanColorCtor,
37+
MatRipple,
38+
mixinColor,
39+
RippleAnimationConfig,
40+
RippleRef,
41+
RippleState,
42+
} from '@angular/material/core';
3643
import {SpecificEventListener, EventType} from '@material/base';
3744
import {MDCSliderAdapter, MDCSliderFoundation, Thumb, TickMark} from '@material/slider';
3845

@@ -48,6 +55,171 @@ export interface MatSliderDragEvent {
4855
value: number;
4956
}
5057

58+
/**
59+
* The visual slider thumb.
60+
*
61+
* Handles the slider thumb ripple states (hover, focus, and active),
62+
* and displaying the value tooltip on discrete sliders.
63+
*/
64+
@Component({
65+
selector: 'mat-slider-visual-start-thumb, mat-slider-visual-end-thumb',
66+
templateUrl: './slider-thumb.html',
67+
host: {
68+
'class': 'mdc-slider__thumb',
69+
'(mouseenter)': '_onMouseEnter()',
70+
'(mouseleave)': '_onMouseLeave()',
71+
}
72+
})
73+
export class MatSliderVisualThumb {
74+
/** Whether the slider displays a numeric value label upon pressing the thumb. */
75+
@Input() _discrete: boolean;
76+
77+
/** The display value of the slider thumb. */
78+
@Input() _valueIndicatorText: string;
79+
80+
/** The MatRipple for this slider thumb. */
81+
@ViewChild(MatRipple) private readonly _ripple: MatRipple;
82+
83+
/** The slider thumb knob */
84+
@ViewChild('knob') _knob: ElementRef<HTMLElement>;
85+
86+
/** Indicates which slider thumb this input corresponds to. */
87+
_thumbPosition: Thumb =
88+
this._elementRef.nativeElement.tagName.toLowerCase()
89+
=== 'mat-slider-visual-start-thumb'.toLowerCase()
90+
? Thumb.START
91+
: Thumb.END;
92+
93+
/** The slider input corresponding to this slider thumb. */
94+
private _sliderInput: MatSliderThumb;
95+
96+
/** The RippleRef for the slider thumbs hover state. */
97+
private _hoverRippleRef: RippleRef;
98+
99+
/** The RippleRef for the slider thumbs focus state. */
100+
private _focusRippleRef: RippleRef;
101+
102+
/** The RippleRef for the slider thumbs active state. */
103+
private _activeRippleRef: RippleRef;
104+
105+
/** Whether the slider thumb is currently being pressed. */
106+
private _isActive: boolean = false;
107+
108+
/** Whether the slider thumb is currently being hovered. */
109+
private _isHovered: boolean = false;
110+
111+
constructor(
112+
private readonly _slider: MatSlider,
113+
private readonly _elementRef: ElementRef<HTMLElement>) {}
114+
115+
ngAfterViewInit() {
116+
this._ripple.radius = 24;
117+
this._sliderInput = this._slider._getInput(this._thumbPosition);
118+
119+
this._sliderInput.dragStart.subscribe((e: MatSliderDragEvent) => this._onDragStart(e));
120+
this._sliderInput.dragEnd.subscribe((e: MatSliderDragEvent) => this._onDragEnd(e));
121+
122+
this._sliderInput._focus.subscribe(() => this._onFocus());
123+
this._sliderInput._blur.subscribe(() => this._onBlur());
124+
}
125+
126+
_onMouseEnter(): void {
127+
this._isHovered = true;
128+
// We don't want to show the hover ripple on top of the focus ripple.
129+
// This can happen if the user tabs to a thumb and then the user moves their cursor over it.
130+
if (!this._isShowingRipple(this._focusRippleRef)) {
131+
this._showHoverRipple();
132+
}
133+
}
134+
135+
_onMouseLeave(): void {
136+
this._isHovered = false;
137+
this._hoverRippleRef?.fadeOut();
138+
}
139+
140+
private _onFocus(): void {
141+
// We don't want to show the hover ripple on top of the focus ripple.
142+
// Happen when the users cursor is over a thumb and then the user tabs to it.
143+
this._hoverRippleRef?.fadeOut();
144+
this._showFocusRipple();
145+
}
146+
147+
private _onBlur(): void {
148+
// Happens when the user tabs away while still dragging a thumb.
149+
if (!this._isActive) {
150+
this._focusRippleRef?.fadeOut();
151+
}
152+
// Happens when the user tabs away from a thumb but their cursor is still over it.
153+
if (this._isHovered) {
154+
this._showHoverRipple();
155+
}
156+
}
157+
158+
private _onDragStart(event: MatSliderDragEvent): void {
159+
if (event.source._thumbPosition === this._thumbPosition) {
160+
this._isActive = true;
161+
this._showActiveRipple();
162+
}
163+
}
164+
165+
private _onDragEnd(event: MatSliderDragEvent): void {
166+
if (event.source._thumbPosition === this._thumbPosition) {
167+
this._isActive = false;
168+
this._activeRippleRef?.fadeOut();
169+
// Happens when the user starts dragging a thumb, tabs away, and then stops dragging.
170+
if (!this._sliderInput._isFocused()) {
171+
this._focusRippleRef?.fadeOut();
172+
}
173+
}
174+
}
175+
176+
/** Handles displaying the hover ripple. */
177+
private _showHoverRipple(): void {
178+
if (!this._isShowingRipple(this._hoverRippleRef)) {
179+
this._hoverRippleRef = this._showRipple({ enterDuration: 0, exitDuration: 0 });
180+
this._hoverRippleRef.element.classList.add('mdc-slider-hover-ripple');
181+
}
182+
}
183+
184+
/** Handles displaying the focus ripple. */
185+
private _showFocusRipple(): void {
186+
if (!this._isShowingRipple(this._focusRippleRef)) {
187+
this._focusRippleRef = this._showRipple({ enterDuration: 0, exitDuration: 0 });
188+
this._focusRippleRef.element.classList.add('mdc-slider-focus-ripple');
189+
}
190+
}
191+
192+
/** Handles displaying the active ripple. */
193+
private _showActiveRipple(): void {
194+
if (!this._isShowingRipple(this._activeRippleRef)) {
195+
this._activeRippleRef = this._showRipple({ enterDuration: 225, exitDuration: 400 });
196+
this._activeRippleRef.element.classList.add('mdc-slider-active-ripple');
197+
}
198+
}
199+
200+
/** Whether the given rippleRef is currently fading in or visible. */
201+
private _isShowingRipple(rippleRef?: RippleRef): boolean {
202+
return rippleRef?.state === RippleState.FADING_IN || rippleRef?.state === RippleState.VISIBLE;
203+
}
204+
205+
/** Manually launches the slider thumb ripple using the specified ripple animation config. */
206+
private _showRipple(animation: RippleAnimationConfig): RippleRef {
207+
return this._ripple.launch(
208+
{animation, centered: true, persistent: true},
209+
);
210+
}
211+
212+
/** Gets the hosts native HTML element. */
213+
_getHostElement(): HTMLElement {
214+
return this._elementRef.nativeElement;
215+
}
216+
217+
/** Gets the native HTML element of the slider thumb knob. */
218+
_getKnob(): HTMLElement {
219+
return this._knob.nativeElement;
220+
}
221+
}
222+
51223
/**
52224
* Directive that adds slider-specific behaviors to an input element inside `<mat-slider>`.
53225
* Up to two may be placed inside of a `<mat-slider>`.
@@ -111,7 +283,7 @@ export class MatSliderThumb implements AfterViewInit {
111283
@Output() readonly _focus: EventEmitter<void> = new EventEmitter<void>();
112284

113285
/** Indicates which slider thumb this input corresponds to. */
114-
private _thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb')
286+
_thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb')
115287
? Thumb.START
116288
: Thumb.END;
117289

@@ -235,14 +407,7 @@ const _MatSliderMixinBase:
235407
})
236408
export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnDestroy {
237409
/** The slider thumb(s). */
238-
@ViewChildren('thumb') _thumbs: QueryList<ElementRef<HTMLElement>>;
239-
240-
/** The slider thumb knob(s) */
241-
@ViewChildren('knob') _knobs: QueryList<ElementRef<HTMLElement>>;
242-
243-
/** The span containing the slider thumb value indicator text */
244-
@ViewChildren('valueIndicatorTextElement')
245-
_valueIndicatorTextElements: QueryList<ElementRef<HTMLElement>>;
410+
@ViewChildren(MatSliderVisualThumb) _thumbs: QueryList<MatSliderVisualThumb>;
246411

247412
/** The active section of the slider track. */
248413
@ViewChild('trackActive') _trackActive: ElementRef<HTMLElement>;
@@ -322,10 +487,10 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD
322487
_tickMarks: TickMark[];
323488

324489
/** The display value of the start thumb. */
325-
private _startValueIndicatorText: string;
490+
_startValueIndicatorText: string;
326491

327492
/** The display value of the end thumb. */
328-
private _endValueIndicatorText: string;
493+
_endValueIndicatorText: string;
329494

330495
constructor(
331496
readonly _cdr: ChangeDetectorRef,
@@ -383,30 +548,26 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD
383548

384549
/** Gets the slider thumb input of the given thumb position. */
385550
_getInput(thumbPosition: Thumb): MatSliderThumb {
386-
return thumbPosition === Thumb.END ? this._inputs.last! : this._inputs.first!;
551+
return thumbPosition === Thumb.END ? this._inputs.last : this._inputs.first;
387552
}
388553

389554
/** Gets the slider thumb HTML input element of the given thumb position. */
390555
_getInputElement(thumbPosition: Thumb): HTMLInputElement {
391556
return this._getInput(thumbPosition)._hostElement;
392557
}
393558

559+
private _getThumb(thumbPosition: Thumb): MatSliderVisualThumb {
560+
return thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first;
561+
}
562+
394563
/** Gets the slider thumb HTML element of the given thumb position. */
395564
_getThumbElement(thumbPosition: Thumb): HTMLElement {
396-
const thumbElementRef = thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first;
397-
return thumbElementRef.nativeElement;
565+
return this._getThumb(thumbPosition)._getHostElement();
398566
}
399567

400568
/** Gets the slider knob HTML element of the given thumb position. */
401569
_getKnobElement(thumbPosition: Thumb): HTMLElement {
402-
const knobElementRef = thumbPosition === Thumb.END ? this._knobs.last : this._knobs.first;
403-
return knobElementRef.nativeElement;
404-
}
405-
406-
_getValueIndicatorText(thumbPosition: Thumb) {
407-
return thumbPosition === Thumb.START
408-
? this._startValueIndicatorText
409-
: this._endValueIndicatorText;
570+
return this._getThumb(thumbPosition)._getKnob();
410571
}
411572

412573
/**

0 commit comments

Comments
 (0)