-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(material-experimental/mdc-slider): implement slider thumb ripples #21979
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
Changes from 3 commits
e3314e7
ebea665
15c0a31
4ea5993
a28d56d
4ac63aa
d811cca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<div class="mdc-slider__value-indicator-container" *ngIf="_discrete"> | ||
<div class="mdc-slider__value-indicator"> | ||
<span class="mdc-slider__value-indicator-text">{{_valueIndicatorText}}</span> | ||
</div> | ||
</div> | ||
<div class="mdc-slider__thumb-knob" #knob></div> | ||
<div matRipple></div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.mat-mdc-slider .mat-ripple { | ||
wagnermaciel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
height: 100%; | ||
width: 100%; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,11 +13,13 @@ | |
</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">{{_getValueIndicatorText(thumb)}}</span> | ||
</div> | ||
</div> | ||
<div class="mdc-slider__thumb-knob" #knob></div> | ||
</div> | ||
<mat-slider-visual-start-thumb | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should start a discussion on naming this thing- having a thumb directive and private thumb component is a bit confusing. |
||
[_discrete]="discrete" | ||
[_valueIndicatorText]="_startValueIndicatorText" | ||
*ngIf="_isRange()"> | ||
</mat-slider-visual-start-thumb> | ||
|
||
<mat-slider-visual-end-thumb | ||
[_discrete]="discrete" | ||
[_valueIndicatorText]="_endValueIndicatorText"> | ||
</mat-slider-visual-end-thumb> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,14 +25,22 @@ import { | |
EventEmitter, | ||
Inject, | ||
Input, | ||
NgZone, | ||
OnDestroy, | ||
Output, | ||
QueryList, | ||
ViewChild, | ||
ViewChildren, | ||
ViewEncapsulation, | ||
} from '@angular/core'; | ||
import {CanColorCtor, mixinColor} from '@angular/material/core'; | ||
import { | ||
CanColorCtor, | ||
MatRipple, | ||
mixinColor, | ||
RippleAnimationConfig, | ||
RippleRef, | ||
RippleState, | ||
} from '@angular/material/core'; | ||
import {SpecificEventListener, EventType} from '@material/base'; | ||
import {MDCSliderAdapter, MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; | ||
|
||
|
@@ -48,6 +56,194 @@ export interface MatSliderDragEvent { | |
value: number; | ||
} | ||
|
||
/** | ||
* The visual slider thumb. | ||
* | ||
* Handles the slider thumb ripple states (hover, focus, and active), | ||
* and displaying the value tooltip on discrete sliders. | ||
*/ | ||
@Component({ | ||
selector: 'mat-slider-visual-start-thumb, mat-slider-visual-end-thumb', | ||
wagnermaciel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
templateUrl: './slider-thumb.html', | ||
styleUrls: ['slider-thumb.css'], | ||
host: { | ||
'class': 'mdc-slider__thumb', | ||
}, | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
encapsulation: ViewEncapsulation.None, | ||
}) | ||
export class MatSliderVisualThumb implements AfterViewInit, OnDestroy { | ||
/** Whether the slider displays a numeric value label upon pressing the thumb. */ | ||
@Input() _discrete: boolean; | ||
wagnermaciel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** The display value of the slider thumb. */ | ||
@Input() _valueIndicatorText: string; | ||
|
||
/** The MatRipple for this slider thumb. */ | ||
@ViewChild(MatRipple) private readonly _ripple: MatRipple; | ||
|
||
/** The slider thumb knob */ | ||
@ViewChild('knob') _knob: ElementRef<HTMLElement>; | ||
|
||
/** Indicates which slider thumb this input corresponds to. */ | ||
_thumbPosition: Thumb = | ||
this._elementRef.nativeElement.tagName.toLowerCase() | ||
=== 'mat-slider-visual-start-thumb'.toLowerCase() | ||
? Thumb.START | ||
: Thumb.END; | ||
|
||
/** The slider input corresponding to this slider thumb. */ | ||
private _sliderInput: MatSliderThumb; | ||
|
||
/** The RippleRef for the slider thumbs hover state. */ | ||
private _hoverRippleRef: RippleRef; | ||
|
||
/** The RippleRef for the slider thumbs focus state. */ | ||
private _focusRippleRef: RippleRef; | ||
|
||
/** The RippleRef for the slider thumbs active state. */ | ||
private _activeRippleRef: RippleRef; | ||
|
||
/** 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, | ||
private readonly _slider: MatSlider, | ||
private readonly _elementRef: ElementRef<HTMLElement>) {} | ||
|
||
ngAfterViewInit() { | ||
this._ripple.radius = 24; | ||
wagnermaciel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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()); | ||
wagnermaciel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// 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); | ||
this._elementRef.nativeElement.addEventListener('mouseleave', this._onMouseLeave); | ||
}); | ||
} | ||
|
||
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); | ||
} | ||
|
||
// ** IMPORTANT NOTE ** | ||
// | ||
// _onMouseEnter() and _onMouseLeave() must be arrow functions because they are | ||
// called from _ngZone.runOutsideAngular() which will change what "this" refers to. | ||
wagnermaciel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private _onMouseEnter = () => { | ||
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 = () => { | ||
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('mdc-slider-hover-ripple'); | ||
wagnermaciel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
/** 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('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('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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't I also need to check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't fading out also visible? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fading out is still visible but it's on the way out so we don't want to return true for that because that would prevent a new ripple from launching in the place of the one about to fade out So we'd end up with the inverse of what is there now |
||
} | ||
|
||
/** Manually launches the slider thumb ripple using the specified ripple animation config. */ | ||
private _showRipple(animation: RippleAnimationConfig): RippleRef { | ||
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 `<mat-slider>`. | ||
* Up to two may be placed inside of a `<mat-slider>`. | ||
|
@@ -111,7 +307,7 @@ export class MatSliderThumb implements AfterViewInit { | |
@Output() readonly _focus: EventEmitter<void> = new EventEmitter<void>(); | ||
|
||
/** Indicates which slider thumb this input corresponds to. */ | ||
private _thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') | ||
_thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') | ||
? Thumb.START | ||
: Thumb.END; | ||
|
||
|
@@ -235,14 +431,7 @@ const _MatSliderMixinBase: | |
}) | ||
export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnDestroy { | ||
/** The slider thumb(s). */ | ||
@ViewChildren('thumb') _thumbs: QueryList<ElementRef<HTMLElement>>; | ||
|
||
/** The slider thumb knob(s) */ | ||
@ViewChildren('knob') _knobs: QueryList<ElementRef<HTMLElement>>; | ||
|
||
/** The span containing the slider thumb value indicator text */ | ||
@ViewChildren('valueIndicatorTextElement') | ||
_valueIndicatorTextElements: QueryList<ElementRef<HTMLElement>>; | ||
@ViewChildren(MatSliderVisualThumb) _thumbs: QueryList<MatSliderVisualThumb>; | ||
|
||
/** The active section of the slider track. */ | ||
@ViewChild('trackActive') _trackActive: ElementRef<HTMLElement>; | ||
|
@@ -322,10 +511,10 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD | |
_tickMarks: TickMark[]; | ||
|
||
/** The display value of the start thumb. */ | ||
private _startValueIndicatorText: string; | ||
_startValueIndicatorText: string; | ||
|
||
/** The display value of the end thumb. */ | ||
private _endValueIndicatorText: string; | ||
_endValueIndicatorText: string; | ||
|
||
constructor( | ||
readonly _cdr: ChangeDetectorRef, | ||
|
@@ -383,30 +572,26 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, OnD | |
|
||
/** Gets the slider thumb input of the given thumb position. */ | ||
_getInput(thumbPosition: Thumb): MatSliderThumb { | ||
return thumbPosition === Thumb.END ? this._inputs.last! : this._inputs.first!; | ||
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)._hostElement; | ||
} | ||
|
||
private _getThumb(thumbPosition: Thumb): MatSliderVisualThumb { | ||
return thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first; | ||
} | ||
|
||
/** 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; | ||
return this._getThumb(thumbPosition)._getHostElement(); | ||
} | ||
|
||
/** 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; | ||
} | ||
|
||
_getValueIndicatorText(thumbPosition: Thumb) { | ||
return thumbPosition === Thumb.START | ||
? this._startValueIndicatorText | ||
: this._endValueIndicatorText; | ||
return this._getThumb(thumbPosition)._getKnob(); | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we document where we get these opacity values? Is it possible to grab em from MDC?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I'm aware, no. They don't seem to have a specific opacity for their ripples. I was just visually following the MDC Slider Spec
I did see that mdc does define a ripple opacity var for the mdc-radio, which we use in our implementation. Should I create an issue for them to do this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, if they could expose the opacity then that would be helpful for us and one less thing we need to maintain
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created Issue #6876 requesting they fix their current ripple implementation (they are missing the "pressed" state) and then expose the 'hover', 'focused', and 'pressed' ripple opacities