import { CSSResultArray , html , PropertyValues , SpectrumElement , TemplateResult , } from '@spectrum-web-components/base' ; import { property , query , } from '@spectrum-web-components/base/src/decorators.js' ; import type { OverlayDisplayQueryDetail , Placement , } from '@spectrum-web-components/overlay' ; import { openOverlay } from '@spectrum-web-components/overlay/src/loader.js' ; import tooltipStyles from './tooltip.css.js' ; export class TooltipProxy extends HTMLElement { disconnectedCallback ( ) { this . dispatchEvent ( new Event ( 'disconnected' ) ) ; } } customElements . define ( 'tooltip-proxy' , TooltipProxy ) ; element slot slot export class Tooltip extends SpectrumElement { public static get styles ( ) : CSSResultArray { return [ tooltipStyles ] ; } private static instanceCount = 0 ; private _tooltipId = `sp-tooltip-describedby-helper-${ Tooltip . instanceCount ++ }` ; @ property ( { type : Boolean , attribute : 'self-managed' } ) public selfManaged = false ; @ property ( { type : Number , reflect : true } ) public offset = 6 ; private hadTooltipId = false ; @ property ( { type : Boolean , reflect : true } ) public open = false ; type { "auto" | "auto-start" | "auto-end" | "top" | "bottom" | "right" | "left" | "top-start" | "top-end" | "bottom-start" | "bottom-end" | "right-start" | "right-end" | "left-start" | "left-end" | "none" } attr @ property ( { reflect : true } ) public placement : Placement = 'top' ; @ query ( '#tip' ) private tipElement ! : HTMLSpanElement ; private _variant = '' ; @ property ( { type : String } ) public get variant ( ) : string { return this . _variant ; } public set variant ( variant ) { if ( variant === this . variant ) { return ; } if ( [ 'info' , 'positive' , 'negative' ] . includes ( variant ) ) { this . setAttribute ( 'variant' , variant ) ; this . _variant = variant ; return ; } this . removeAttribute ( 'variant' ) ; this . _variant = '' ; } public constructor ( ) { super ( ) ; this . addEventListener ( 'sp-overlay-query' , this . onOverlayQuery ) ; } public onOverlayQuery ( event ) { if ( ! event . target ) return ; const target = event . target as Node ; if ( target !== this ) return ; event . detail . overlayContentTipElement = this . tipElement ; } private _proxy ! : HTMLElement ; private generateProxy ( ) { if ( this . _proxy ) { return ; } this . _proxy = document . createElement ( 'tooltip-proxy' ) ; this . _proxy . id = this . _tooltipId ; this . _proxy . hidden = true ; this . _proxy . slot = 'hidden-tooltip-content' ; this . _proxy . setAttribute ( 'role' , 'tooltip' ) ; this . _proxy . addEventListener ( 'disconnected' , this . closeOverlay ) ; } public overlayWillOpenCallback ( { trigger , } ) { this . setAttribute ( 'aria-hidden' , 'true' ) ; this . generateProxy ( ) ; this . _proxy . textContent = this . textContent ; const ariaDescribedby = trigger . getAttribute ( 'aria-describedby' ) || '' ; this . hadTooltipId = ariaDescribedby . search ( this . _tooltipId ) > - 1 ; this . insertAdjacentElement ( 'beforebegin' , this . _proxy ) ; if ( this . hadTooltipId ) return ; if ( ariaDescribedby ) { trigger . setAttribute ( 'aria-describedby' , `${ ariaDescribedby } ${ this . _tooltipId }` ) ; } else { trigger . setAttribute ( 'aria-describedby' , `${ this . _tooltipId }` ) ; } } public overlayOpenCancelledCallback ( { trigger , } ) { this . overlayCloseCallback ( { trigger } ) ; } public overlayCloseCallback ( { trigger } ) { const ariaDescribedby = trigger . getAttribute ( 'aria-describedby' ) || '' ; let descriptors = ariaDescribedby . split ( /\s+/ ) ; if ( ! this . hadTooltipId ) { descriptors = descriptors . filter ( ( descriptor ) => descriptor !== this . _tooltipId ) ; } if ( descriptors . length ) { trigger . setAttribute ( 'aria-describedby' , descriptors . join ( ' ' ) ) ; } else { trigger . removeAttribute ( 'aria-describedby' ) ; } this . removeAttribute ( 'aria-hidden' ) ; this . removeProxy ( ) ; } private removeProxy ( ) { this . _proxy . remove ( ) ; } private closeOverlayCallback ? : Promise < ( ) => void > ; private abortOverlay : ( cancelled : boolean ) => void = ( ) => { return ; } ; private openOverlay = ( ) : void => { const parentElement = this . parentElement as HTMLElement ; const abortPromise = new Promise ( ( res ) => { this . abortOverlay = res ; } ) ; this . closeOverlayCallback = openOverlay ( parentElement , 'hover' , this , { abortPromise , offset : this . offset , placement : this . placement , } ) ; } ; private closeOverlay = async ( ) : Promise < void > => { if ( this . abortOverlay ) this . abortOverlay ( true ) ; if ( ! this . closeOverlayCallback ) return ; ( await this . closeOverlayCallback ) ( ) ; delete this . closeOverlayCallback ; } ; private previousSlot ? : string ; private manageTooltip ( ) { const parentElement = this . parentElement as HTMLElement ; if ( this . selfManaged ) { if ( this . slot ) { this . previousSlot = this . slot ; } this . slot = 'self-managed-tooltip' ; parentElement . addEventListener ( 'pointerenter' , this . openOverlay ) ; parentElement . addEventListener ( 'focusin' , this . openOverlay ) ; parentElement . addEventListener ( 'pointerleave' , this . closeOverlay ) ; parentElement . addEventListener ( 'focusout' , this . closeOverlay ) ; } else { if ( this . previousSlot ) { this . slot = this . previousSlot ; } else if ( this . slot === 'self-managed-tooltip' ) { this . removeAttribute ( 'slot' ) ; } parentElement . removeEventListener ( 'pointerenter' , this . openOverlay ) ; parentElement . removeEventListener ( 'focusin' , this . openOverlay ) ; parentElement . removeEventListener ( 'pointerleave' , this . closeOverlay ) ; parentElement . removeEventListener ( 'focusout' , this . closeOverlay ) ; } } render ( ) { return html `
            <slot name="icon"></slot>
            <span id="label"><slot></slot></span>
            <span id="tip"></span>
        ` ; } protected async update ( changed ) { if ( changed . has ( 'open' ) && this . selfManaged ) { if ( this . open ) { this . openOverlay ( ) ; } else { this . closeOverlay ( ) ; } } this . generateProxy ( ) ; super . update ( changed ) ; } protected updated ( changed ) { super . updated ( changed ) ; if ( changed . has ( 'selfManaged' ) ) { this . manageTooltip ( ) ; } } }