Skip to content

Commit e3314e7

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

File tree

6 files changed

+218
-34
lines changed

6 files changed

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

113287
/** Indicates which slider thumb this input corresponds to. */
114-
private _thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb')
288+
_thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb')
115289
? Thumb.START
116290
: Thumb.END;
117291

@@ -235,14 +409,7 @@ const _MatSliderMixinBase:
235409
})
236410
export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnDestroy {
237411
/** 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>>;
412+
@ViewChildren(MatSliderVisualThumb) _thumbs: QueryList<MatSliderVisualThumb>;
246413

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

324491
/** The display value of the start thumb. */
325-
private _startValueIndicatorText: string;
492+
_startValueIndicatorText: string;
326493

327494
/** The display value of the end thumb. */
328-
private _endValueIndicatorText: string;
495+
_endValueIndicatorText: string;
329496

330497
constructor(
331498
readonly _cdr: ChangeDetectorRef,
@@ -383,30 +550,26 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD
383550

384551
/** Gets the slider thumb input of the given thumb position. */
385552
_getInput(thumbPosition: Thumb): MatSliderThumb {
386-
return thumbPosition === Thumb.END ? this._inputs.last! : this._inputs.first!;
553+
return thumbPosition === Thumb.END ? this._inputs.last : this._inputs.first;
387554
}
388555

389556
/** Gets the slider thumb HTML input element of the given thumb position. */
390557
_getInputElement(thumbPosition: Thumb): HTMLInputElement {
391558
return this._getInput(thumbPosition)._hostElement;
392559
}
393560

561+
private _getThumb(thumbPosition: Thumb): MatSliderVisualThumb {
562+
return thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first;
563+
}
564+
394565
/** Gets the slider thumb HTML element of the given thumb position. */
395566
_getThumbElement(thumbPosition: Thumb): HTMLElement {
396-
const thumbElementRef = thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first;
397-
return thumbElementRef.nativeElement;
567+
return this._getThumb(thumbPosition)._getHostElement();
398568
}
399569

400570
/** Gets the slider knob HTML element of the given thumb position. */
401571
_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;
572+
return this._getThumb(thumbPosition)._getKnob();
410573
}
411574

412575
/**

0 commit comments

Comments
 (0)