sass-references/angular-material/material/slider/slider-thumb.ts

322 lines
10 KiB
TypeScript
Raw Normal View History

2024-12-06 10:42:08 +08:00
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Input,
NgZone,
OnDestroy,
ViewChild,
ViewEncapsulation,
inject,
} from '@angular/core';
import {MatRipple, RippleAnimationConfig, RippleRef, RippleState} from '@angular/material/core';
import {
_MatThumb,
_MatSlider,
_MatSliderThumb,
_MatSliderVisualThumb,
MAT_SLIDER,
MAT_SLIDER_VISUAL_THUMB,
} from './slider-interface';
import {Platform} from '@angular/cdk/platform';
/**
* The visual slider thumb.
*
* Handles the slider thumb ripple states (hover, focus, and active),
* and displaying the value tooltip on discrete sliders.
* @docs-private
*/
@Component({
selector: 'mat-slider-visual-thumb',
templateUrl: './slider-thumb.html',
styleUrl: 'slider-thumb.css',
host: {
'class': 'mdc-slider__thumb mat-mdc-slider-visual-thumb',
},
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
providers: [{provide: MAT_SLIDER_VISUAL_THUMB, useExisting: MatSliderVisualThumb}],
imports: [MatRipple],
})
export class MatSliderVisualThumb implements _MatSliderVisualThumb, AfterViewInit, OnDestroy {
readonly _cdr = inject(ChangeDetectorRef);
private readonly _ngZone = inject(NgZone);
private _slider = inject<_MatSlider>(MAT_SLIDER);
/** Whether the slider displays a numeric value label upon pressing the thumb. */
@Input() discrete: boolean;
/** Indicates which slider thumb this input corresponds to. */
@Input() thumbPosition: _MatThumb;
/** The display value of the slider thumb. */
@Input() valueIndicatorText: string;
/** The MatRipple for this slider thumb. */
@ViewChild(MatRipple) readonly _ripple: MatRipple;
/** The slider thumb knob. */
@ViewChild('knob') _knob: ElementRef<HTMLElement>;
/** The slider thumb value indicator container. */
@ViewChild('valueIndicatorContainer')
_valueIndicatorContainer: ElementRef<HTMLElement>;
/** The slider input corresponding to this slider thumb. */
private _sliderInput: _MatSliderThumb;
/** The native html element of the slider input corresponding to this thumb. */
private _sliderInputEl: HTMLInputElement | undefined;
/** The RippleRef for the slider thumbs hover state. */
private _hoverRippleRef: RippleRef | undefined;
/** The RippleRef for the slider thumbs focus state. */
private _focusRippleRef: RippleRef | undefined;
/** The RippleRef for the slider thumbs active state. */
private _activeRippleRef: RippleRef | undefined;
/** Whether the slider thumb is currently being hovered. */
private _isHovered: boolean = false;
/** Whether the slider thumb is currently being pressed. */
_isActive = false;
/** Whether the value indicator tooltip is visible. */
_isValueIndicatorVisible: boolean = false;
/** The host native HTML input element. */
_hostElement = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
private _platform = inject(Platform);
constructor(...args: unknown[]);
constructor() {}
ngAfterViewInit() {
const sliderInput = this._slider._getInput(this.thumbPosition);
// No-op if the slider isn't configured properly. `MatSlider` will
// throw an error instructing the user how to set up the slider.
if (!sliderInput) {
return;
}
this._ripple.radius = 24;
this._sliderInput = sliderInput;
this._sliderInputEl = this._sliderInput._hostElement;
// These listeners don't update any data bindings so we bind them outside
// of the NgZone to prevent Angular from needlessly running change detection.
this._ngZone.runOutsideAngular(() => {
const input = this._sliderInputEl!;
input.addEventListener('pointermove', this._onPointerMove);
input.addEventListener('pointerdown', this._onDragStart);
input.addEventListener('pointerup', this._onDragEnd);
input.addEventListener('pointerleave', this._onMouseLeave);
input.addEventListener('focus', this._onFocus);
input.addEventListener('blur', this._onBlur);
});
}
ngOnDestroy() {
const input = this._sliderInputEl;
if (input) {
input.removeEventListener('pointermove', this._onPointerMove);
input.removeEventListener('pointerdown', this._onDragStart);
input.removeEventListener('pointerup', this._onDragEnd);
input.removeEventListener('pointerleave', this._onMouseLeave);
input.removeEventListener('focus', this._onFocus);
input.removeEventListener('blur', this._onBlur);
}
}
private _onPointerMove = (event: PointerEvent): void => {
if (this._sliderInput._isFocused) {
return;
}
const rect = this._hostElement.getBoundingClientRect();
const isHovered = this._slider._isCursorOnSliderThumb(event, rect);
this._isHovered = isHovered;
if (isHovered) {
this._showHoverRipple();
} else {
this._hideRipple(this._hoverRippleRef);
}
};
private _onMouseLeave = (): void => {
this._isHovered = false;
this._hideRipple(this._hoverRippleRef);
};
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._hideRipple(this._hoverRippleRef);
this._showFocusRipple();
this._hostElement.classList.add('mdc-slider__thumb--focused');
};
private _onBlur = (): void => {
// Happens when the user tabs away while still dragging a thumb.
if (!this._isActive) {
this._hideRipple(this._focusRippleRef);
}
// Happens when the user tabs away from a thumb but their cursor is still over it.
if (this._isHovered) {
this._showHoverRipple();
}
this._hostElement.classList.remove('mdc-slider__thumb--focused');
};
private _onDragStart = (event: PointerEvent): void => {
if (event.button !== 0) {
return;
}
this._isActive = true;
this._showActiveRipple();
};
private _onDragEnd = (): void => {
this._isActive = false;
this._hideRipple(this._activeRippleRef);
// Happens when the user starts dragging a thumb, tabs away, and then stops dragging.
if (!this._sliderInput._isFocused) {
this._hideRipple(this._focusRippleRef);
}
// On Safari we need to immediately re-show the hover ripple because
// sliders do not retain focus from pointer events on that platform.
if (this._platform.SAFARI) {
this._showHoverRipple();
}
};
/** 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('mat-mdc-slider-hover-ripple');
}
}
/** Handles displaying the focus ripple. */
private _showFocusRipple(): void {
// Show the focus ripple event if noop animations are enabled.
if (!this._isShowingRipple(this._focusRippleRef)) {
this._focusRippleRef = this._showRipple({enterDuration: 0, exitDuration: 0}, true);
this._focusRippleRef?.element.classList.add('mat-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('mat-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;
}
/** Manually launches the slider thumb ripple using the specified ripple animation config. */
private _showRipple(
animation: RippleAnimationConfig,
ignoreGlobalRippleConfig?: boolean,
): RippleRef | undefined {
if (this._slider.disabled) {
return;
}
this._showValueIndicator();
if (this._slider._isRange) {
const sibling = this._slider._getThumb(
this.thumbPosition === _MatThumb.START ? _MatThumb.END : _MatThumb.START,
);
sibling._showValueIndicator();
}
if (this._slider._globalRippleOptions?.disabled && !ignoreGlobalRippleConfig) {
return;
}
return this._ripple.launch({
animation: this._slider._noopAnimations ? {enterDuration: 0, exitDuration: 0} : animation,
centered: true,
persistent: true,
});
}
/**
* Fades out the given ripple.
* Also hides the value indicator if no ripple is showing.
*/
private _hideRipple(rippleRef?: RippleRef): void {
rippleRef?.fadeOut();
if (this._isShowingAnyRipple()) {
return;
}
if (!this._slider._isRange) {
this._hideValueIndicator();
}
const sibling = this._getSibling();
if (!sibling._isShowingAnyRipple()) {
this._hideValueIndicator();
sibling._hideValueIndicator();
}
}
/** Shows the value indicator ui. */
_showValueIndicator(): void {
this._hostElement.classList.add('mdc-slider__thumb--with-indicator');
}
/** Hides the value indicator ui. */
_hideValueIndicator(): void {
this._hostElement.classList.remove('mdc-slider__thumb--with-indicator');
}
_getSibling(): _MatSliderVisualThumb {
return this._slider._getThumb(
this.thumbPosition === _MatThumb.START ? _MatThumb.END : _MatThumb.START,
);
}
/** Gets the value indicator container's native HTML element. */
_getValueIndicatorContainer(): HTMLElement | undefined {
return this._valueIndicatorContainer?.nativeElement;
}
/** Gets the native HTML element of the slider thumb knob. */
_getKnob(): HTMLElement {
return this._knob.nativeElement;
}
_isShowingAnyRipple(): boolean {
return (
this._isShowingRipple(this._hoverRippleRef) ||
this._isShowingRipple(this._focusRippleRef) ||
this._isShowingRipple(this._activeRippleRef)
);
}
}