license import { AnimationEvent } from '@angular/animations' ; import { AriaDescriber , FocusMonitor } from '@angular/cdk/a11y' ; import { Directionality } from '@angular/cdk/bidi' ; import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import { ESCAPE } from '@angular/cdk/keycodes' ; import { BreakpointObserver , Breakpoints , BreakpointState } from '@angular/cdk/layout' ; import { HammerLoader , HAMMER_LOADER } from '@angular/platform-browser' ; import { FlexibleConnectedPositionStrategy , HorizontalConnectionPos , OriginConnectionPosition , Overlay , OverlayConnectionPosition , OverlayRef , VerticalConnectionPos , ScrollStrategy , } from '@angular/cdk/overlay' ; import { ScrollDispatcher } from '@angular/cdk/scrolling' ; import { Platform } from '@angular/cdk/platform' ; import { ComponentPortal } from '@angular/cdk/portal' ; import { take , takeUntil } from 'rxjs/operators' ; import { ChangeDetectionStrategy , ChangeDetectorRef , Component , Directive , ElementRef , Inject , InjectionToken , Input , NgZone , OnDestroy , Optional , ViewContainerRef , ViewEncapsulation , } from '@angular/core' ; import { Subject , Observable } from 'rxjs' ; import { matTooltipAnimations } from './tooltip-animations' ; export type TooltipPosition = 'left' | 'right' | 'above' | 'below' | 'before' | 'after' ; /** Time in ms to throttle repositioning after scroll events. */ export const SCROLL_THROTTLE_MS = 20 ; /** CSS class that will be attached to the overlay panel. */ export const TOOLTIP_PANEL_CLASS = 'mat-tooltip-panel' ; docs-private export function getMatTooltipInvalidPositionError ( position ) { return Error ( `Tooltip position "${ position }" is invalid.` ) ; } /** Injection token that determines the scroll handling while a tooltip is visible. */ export const MAT_TOOLTIP_SCROLL_STRATEGY = new InjectionToken < ( ) => ScrollStrategy > ( 'mat-tooltip-scroll-strategy' ) ; docs-private export function MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY ( overlay ) { return ( ) => overlay . scrollStrategies . reposition ( { scrollThrottle : SCROLL_THROTTLE_MS } ) ; } docs-private export const MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER = { provide : MAT_TOOLTIP_SCROLL_STRATEGY , deps : [ Overlay ] , useFactory : MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY , } ; /** Default `matTooltip` options that can be overridden. */ export interface MatTooltipDefaultOptions { showDelay : number ; hideDelay : number ; touchendHideDelay : number ; } /** Injection token to be used to override the default options for `matTooltip`. */ export const MAT_TOOLTIP_DEFAULT_OPTIONS = new InjectionToken < MatTooltipDefaultOptions > ( 'mat-tooltip-default-options' , { providedIn : 'root' , factory : MAT_TOOLTIP_DEFAULT_OPTIONS_FACTORY } ) ; docs-private export function MAT_TOOLTIP_DEFAULT_OPTIONS_FACTORY ( ) { return { showDelay : 0 , hideDelay : 0 , touchendHideDelay : 1500 , } ; } /**
 * Directive that attaches a material design tooltip to the host element. Animates the showing and
 * hiding of a tooltip provided position (defaults to below the element).
 *
 * https://material.io/design/components/tooltips.html
 */ @ Directive ( { selector : '[matTooltip]' , exportAs : 'matTooltip' , host : { '(longpress)' : 'show()' , '(keydown)' : '_handleKeydown($event)' , '(touchend)' : '_handleTouchend()' , } , } ) export class MatTooltip implements OnDestroy { _overlayRef : OverlayRef | null ; _tooltipInstance : TooltipComponent | null ; private _portal : ComponentPortal < TooltipComponent > ; private _position : TooltipPosition = 'below' ; private _disabled : boolean = false ; private _tooltipClass : string | string [ ] | Set < string > | { [ key ] : any ; } ; private _scrollStrategy : ( ) => ScrollStrategy ; /** Allows the user to define the position of the tooltip relative to the parent element */ @ Input ( 'matTooltipPosition' ) get position ( ) : TooltipPosition { return this . _position ; } set position ( value ) { if ( value !== this . _position ) { this . _position = value ; if ( this . _overlayRef ) { this . _updatePosition ( ) ; if ( this . _tooltipInstance ) { this . _tooltipInstance ! . show ( 0 ) ; } this . _overlayRef . updatePosition ( ) ; } } } /** Disables the display of the tooltip. */ @ Input ( 'matTooltipDisabled' ) get disabled ( ) : boolean { return this . _disabled ; } set disabled ( value ) { this . _disabled = coerceBooleanProperty ( value ) ; if ( this . _disabled ) { this . hide ( 0 ) ; } } /** The default delay in ms before showing the tooltip after show is called */ @ Input ( 'matTooltipShowDelay' ) showDelay = this . _defaultOptions . showDelay ; /** The default delay in ms before hiding the tooltip after hide is called */ @ Input ( 'matTooltipHideDelay' ) hideDelay = this . _defaultOptions . hideDelay ; private _message = '' ; /** The message to be displayed in the tooltip */ @ Input ( 'matTooltip' ) get message ( ) { return this . _message ; } set message ( value ) { this . _ariaDescriber . removeDescription ( this . _elementRef . nativeElement , this . _message ) ; this . _message = value != null ? `${ value }` . trim ( ) : '' ; if ( ! this . _message && this . _isTooltipVisible ( ) ) { this . hide ( 0 ) ; } else { this . _updateTooltipMessage ( ) ; this . _ariaDescriber . describe ( this . _elementRef . nativeElement , this . message ) ; } } /** Classes to be passed to the tooltip. Supports the same syntax as `ngClass`. */ @ Input ( 'matTooltipClass' ) get tooltipClass ( ) { return this . _tooltipClass ; } set tooltipClass ( value ) { this . _tooltipClass = value ; if ( this . _tooltipInstance ) { this . _setTooltipClass ( this . _tooltipClass ) ; } } private _manualListeners = new Map < string , EventListenerOrEventListenerObject > ( ) ; /** Emits when the component is destroyed. */ private readonly _destroyed = new Subject < void > ( ) ; constructor ( private _overlay , private _elementRef , private _scrollDispatcher , private _viewContainerRef , private _ngZone , platform , private _ariaDescriber , private _focusMonitor , @ Inject ( MAT_TOOLTIP_SCROLL_STRATEGY ) scrollStrategy , @ Optional ( ) private _dir , @ Optional ( ) @ Inject ( MAT_TOOLTIP_DEFAULT_OPTIONS ) private _defaultOptions , @ Optional ( ) @ Inject ( HAMMER_LOADER ) hammerLoader ? ) { this . _scrollStrategy = scrollStrategy ; const element = _elementRef . nativeElement ; const elementStyle = element . style as CSSStyleDeclaration & { webkitUserDrag : string ; } ; const hasGestures = typeof window === 'undefined' || ( window as any ) . Hammer || hammerLoader ; if ( ! platform . IOS && ! platform . ANDROID ) { this . _manualListeners . set ( 'mouseenter' , ( ) => this . show ( ) ) . set ( 'mouseleave' , ( ) => this . hide ( ) ) ; } else if ( ! hasGestures ) { this . _manualListeners . set ( 'touchstart' , ( ) => this . show ( ) ) ; } this . _manualListeners . forEach ( ( listener , event ) => element . addEventListener ( event , listener ) ) ; if ( element . nodeName === 'INPUT' || element . nodeName === 'TEXTAREA' ) { elementStyle . webkitUserSelect = elementStyle . userSelect = elementStyle . msUserSelect = '' ; } if ( element . draggable && elementStyle . webkitUserDrag === 'none' ) { elementStyle . webkitUserDrag = '' ; } _focusMonitor . monitor ( _elementRef ) . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( origin => { if ( ! origin ) { _ngZone . run ( ( ) => this . hide ( 0 ) ) ; } else if ( origin === 'keyboard' ) { _ngZone . run ( ( ) => this . show ( ) ) ; } } ) ; } /**
     * Dispose the tooltip when destroyed.
     */ ngOnDestroy ( ) { if ( this . _overlayRef ) { this . _overlayRef . dispose ( ) ; this . _tooltipInstance = null ; } this . _manualListeners . forEach ( ( listener , event ) => { this . _elementRef . nativeElement . removeEventListener ( event , listener ) ; } ) ; this . _manualListeners . clear ( ) ; this . _destroyed . next ( ) ; this . _destroyed . complete ( ) ; this . _ariaDescriber . removeDescription ( this . _elementRef . nativeElement , this . message ) ; this . _focusMonitor . stopMonitoring ( this . _elementRef ) ; } /** Shows the tooltip after the delay in ms, defaults to tooltip-delay-show or 0ms if no input */ show ( delay = this . showDelay ) { if ( this . disabled || ! this . message || ( this . _isTooltipVisible ( ) && ! this . _tooltipInstance ! . _showTimeoutId && ! this . _tooltipInstance ! . _hideTimeoutId ) ) { return ; } const overlayRef = this . _createOverlay ( ) ; this . _detach ( ) ; this . _portal = this . _portal || new ComponentPortal ( TooltipComponent , this . _viewContainerRef ) ; this . _tooltipInstance = overlayRef . attach ( this . _portal ) . instance ; this . _tooltipInstance . afterHidden ( ) . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => this . _detach ( ) ) ; this . _setTooltipClass ( this . _tooltipClass ) ; this . _updateTooltipMessage ( ) ; this . _tooltipInstance ! . show ( delay ) ; } /** Hides the tooltip after the delay in ms, defaults to tooltip-delay-hide or 0ms if no input */ hide ( delay = this . hideDelay ) { if ( this . _tooltipInstance ) { this . _tooltipInstance . hide ( delay ) ; } } /** Shows/hides the tooltip */ toggle ( ) { this . _isTooltipVisible ( ) ? this . hide ( ) : this . show ( ) ; } /** Returns true if the tooltip is currently visible to the user */ _isTooltipVisible ( ) { return ! ! this . _tooltipInstance && this . _tooltipInstance . isVisible ( ) ; } /** Handles the keydown events on the host element. */ _handleKeydown ( e ) { if ( this . _isTooltipVisible ( ) && e . keyCode === ESCAPE ) { e . stopPropagation ( ) ; this . hide ( 0 ) ; } } /** Handles the touchend events on the host element. */ _handleTouchend ( ) { this . hide ( this . _defaultOptions . touchendHideDelay ) ; } /** Create the overlay config and position strategy */ private _createOverlay ( ) { if ( this . _overlayRef ) { return this . _overlayRef ; } const strategy = this . _overlay . position ( ) . flexibleConnectedTo ( this . _elementRef ) . withTransformOriginOn ( '.mat-tooltip' ) . withFlexibleDimensions ( false ) . withViewportMargin ( 8 ) ; const scrollableAncestors = this . _scrollDispatcher . getAncestorScrollContainers ( this . _elementRef ) ; strategy . withScrollableContainers ( scrollableAncestors ) ; strategy . positionChanges . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( change => { if ( this . _tooltipInstance ) { if ( change . scrollableViewProperties . isOverlayClipped && this . _tooltipInstance . isVisible ( ) ) { this . _ngZone . run ( ( ) => this . hide ( 0 ) ) ; } } } ) ; this . _overlayRef = this . _overlay . create ( { direction : this . _dir , positionStrategy : strategy , panelClass : TOOLTIP_PANEL_CLASS , scrollStrategy : this . _scrollStrategy ( ) } ) ; this . _updatePosition ( ) ; this . _overlayRef . detachments ( ) . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => this . _detach ( ) ) ; return this . _overlayRef ; } /** Detaches the currently-attached tooltip. */ private _detach ( ) { if ( this . _overlayRef && this . _overlayRef . hasAttached ( ) ) { this . _overlayRef . detach ( ) ; } this . _tooltipInstance = null ; } /** Updates the position of the current tooltip. */ private _updatePosition ( ) { const position = this . _overlayRef ! . getConfig ( ) . positionStrategy as FlexibleConnectedPositionStrategy ; const origin = this . _getOrigin ( ) ; const overlay = this . _getOverlayPosition ( ) ; position . withPositions ( [ { ... origin . main , ... overlay . main } , { ... origin . fallback , ... overlay . fallback } ] ) ; } /**
     * Returns the origin position and a fallback position based on the user's position preference.
     * The fallback position is the inverse of the origin (e.g. `'below' -> 'above'`).
     */ _getOrigin ( ) { const isLtr = ! this . _dir || this . _dir . value == 'ltr' ; const position = this . position ; let originPosition ; if ( position == 'above' || position == 'below' ) { originPosition = { originX : 'center' , originY : position == 'above' ? 'top' : 'bottom' } ; } else if ( position == 'before' || ( position == 'left' && isLtr ) || ( position == 'right' && ! isLtr ) ) { originPosition = { originX : 'start' , originY : 'center' } ; } else if ( position == 'after' || ( position == 'right' && isLtr ) || ( position == 'left' && ! isLtr ) ) { originPosition = { originX : 'end' , originY : 'center' } ; } else { throw getMatTooltipInvalidPositionError ( position ) ; } const { x , y } = this . _invertPosition ( originPosition . originX , originPosition . originY ) ; return { main : originPosition , fallback : { originX : x , originY : y } } ; } /** Returns the overlay position and a fallback position based on the user's preference */ _getOverlayPosition ( ) { const isLtr = ! this . _dir || this . _dir . value == 'ltr' ; const position = this . position ; let overlayPosition ; if ( position == 'above' ) { overlayPosition = { overlayX : 'center' , overlayY : 'bottom' } ; } else if ( position == 'below' ) { overlayPosition = { overlayX : 'center' , overlayY : 'top' } ; } else if ( position == 'before' || ( position == 'left' && isLtr ) || ( position == 'right' && ! isLtr ) ) { overlayPosition = { overlayX : 'end' , overlayY : 'center' } ; } else if ( position == 'after' || ( position == 'right' && isLtr ) || ( position == 'left' && ! isLtr ) ) { overlayPosition = { overlayX : 'start' , overlayY : 'center' } ; } else { throw getMatTooltipInvalidPositionError ( position ) ; } const { x , y } = this . _invertPosition ( overlayPosition . overlayX , overlayPosition . overlayY ) ; return { main : overlayPosition , fallback : { overlayX : x , overlayY : y } } ; } /** Updates the tooltip message and repositions the overlay according to the new message length */ private _updateTooltipMessage ( ) { if ( this . _tooltipInstance ) { this . _tooltipInstance . message = this . message ; this . _tooltipInstance . _markForCheck ( ) ; this . _ngZone . onMicrotaskEmpty . asObservable ( ) . pipe ( take ( 1 ) , takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => { if ( this . _tooltipInstance ) { this . _overlayRef ! . updatePosition ( ) ; } } ) ; } } /** Updates the tooltip class */ private _setTooltipClass ( tooltipClass ) { if ( this . _tooltipInstance ) { this . _tooltipInstance . tooltipClass = tooltipClass ; this . _tooltipInstance . _markForCheck ( ) ; } } /** Inverts an overlay position. */ private _invertPosition ( x , y ) { if ( this . position === 'above' || this . position === 'below' ) { if ( y === 'top' ) { y = 'bottom' ; } else if ( y === 'bottom' ) { y = 'top' ; } } else { if ( x === 'end' ) { x = 'start' ; } else if ( x === 'start' ) { x = 'end' ; } } return { x , y } ; } } export type TooltipVisibility = 'initial' | 'visible' | 'hidden' ; docs-private @ Component ( { moduleId : module . id , selector : 'mat-tooltip-component' , templateUrl : 'tooltip.html' , styleUrls : [ 'tooltip.css' ] , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , animations : [ matTooltipAnimations . tooltipState ] , host : { '[style.zoom]' : '_visibility === "visible" ? 1 : null' , '(body:click)' : 'this._handleBodyInteraction()' , 'aria-hidden' : 'true' , } } ) export class TooltipComponent { /** Message to display in the tooltip */ message : string ; /** Classes to be added to the tooltip. Supports the same syntax as `ngClass`. */ tooltipClass : string | string [ ] | Set < string > | { [ key ] : any ; } ; /** The timeout ID of any current timer set to show the tooltip */ _showTimeoutId : number | null ; /** The timeout ID of any current timer set to hide the tooltip */ _hideTimeoutId : number | null ; /** Property watched by the animation framework to show or hide the tooltip */ _visibility : TooltipVisibility = 'initial' ; /** Whether interactions on the page should close the tooltip */ private _closeOnInteraction : boolean = false ; /** Subject for notifying that the tooltip has been hidden from the view */ private readonly _onHide : Subject < any > = new Subject ( ) ; /** Stream that emits whether the user has a handset-sized display.  */ _isHandset : Observable < BreakpointState > = this . _breakpointObserver . observe ( Breakpoints . Handset ) ; constructor ( private _changeDetectorRef , private _breakpointObserver ) { } param delay show ( delay ) { if ( this . _hideTimeoutId ) { clearTimeout ( this . _hideTimeoutId ) ; this . _hideTimeoutId = null ; } this . _closeOnInteraction = true ; this . _showTimeoutId = setTimeout ( ( ) => { this . _visibility = 'visible' ; this . _showTimeoutId = null ; this . _markForCheck ( ) ; } , delay ) ; } param delay hide ( delay ) { if ( this . _showTimeoutId ) { clearTimeout ( this . _showTimeoutId ) ; this . _showTimeoutId = null ; } this . _hideTimeoutId = setTimeout ( ( ) => { this . _visibility = 'hidden' ; this . _hideTimeoutId = null ; this . _markForCheck ( ) ; } , delay ) ; } /** Returns an observable that notifies when the tooltip has been hidden from view. */ afterHidden ( ) { return this . _onHide . asObservable ( ) ; } /** Whether the tooltip is being displayed. */ isVisible ( ) { return this . _visibility === 'visible' ; } _animationStart ( ) { this . _closeOnInteraction = false ; } _animationDone ( event ) { const toState = event . toState as TooltipVisibility ; if ( toState === 'hidden' && ! this . isVisible ( ) ) { this . _onHide . next ( ) ; } if ( toState === 'visible' || toState === 'hidden' ) { this . _closeOnInteraction = true ; } } /**
     * Interactions on the HTML body should close the tooltip immediately as defined in the
     * material design spec.
     * https://material.io/design/components/tooltips.html#behavior
     */ _handleBodyInteraction ( ) { if ( this . _closeOnInteraction ) { this . hide ( 0 ) ; } } /**
     * Marks that the tooltip needs to be checked in the next change detection run.
     * Mainly used for rendering the initial text before positioning a tooltip, which
     * can be problematic in components with OnPush change detection.
     */ _markForCheck ( ) { this . _changeDetectorRef . markForCheck ( ) ; } }