import { CSSResultArray , html , TemplateResult , } from '@spectrum-web-components/base' ; import { property , query , } from '@spectrum-web-components/base/src/decorators.js' ; import { classMap , ifDefined , repeat , styleMap , } from '@spectrum-web-components/base/src/directives.js' ; import sliderStyles from './slider.css.js' ; import { ObserveSlotText } from '@spectrum-web-components/shared/src/observe-slot-text.js' ; import { StyleInfo } from 'lit-html/directives/style-map' ; import '@spectrum-web-components/field-label/sp-field-label.js' ; import type { NumberField } from '@spectrum-web-components/number-field' ; import { HandleController , HandleValueDictionary } from './HandleController.js' ; import { SliderHandle } from './SliderHandle.js' ; import { streamingListener } from '@spectrum-web-components/base/src/streaming-listener.js' ; export const variants = [ 'filled' , 'ramp' , 'range' , 'tick' ] ; element slot export class Slider extends ObserveSlotText ( SliderHandle , '' ) { public static get styles ( ) : CSSResultArray { return [ sliderStyles ] ; } public handleController : HandleController = new HandleController ( this ) ; /**
     * Whether to display a Number Field along side the slider UI
     */ @ property ( { type : Boolean , reflect : true } ) public get editable ( ) : boolean { return this . _editable ; } public set editable ( editable ) { if ( editable === this . editable ) return ; const oldValue = this . editable ; this . _editable = this . handleController . size < 2 ? editable : false ; if ( this . editable ) { this . _numberFieldInput = import ( '@spectrum-web-components/number-field/sp-number-field.js' ) ; } if ( oldValue !== this . editable ) { this . requestUpdate ( 'editable' , oldValue ) ; } } private _editable = false ; /**
     * Whether the stepper UI of the Number Field is hidden or not
     */ @ property ( { type : Boolean , reflect : true , attribute : 'hide-stepper' } ) public hideStepper = false ; @ property ( ) public type = '' ; @ property ( { type : String } ) public set variant ( variant ) { const oldVariant = this . variant ; if ( variant === this . variant ) { return ; } if ( variants . includes ( variant ) ) { this . setAttribute ( 'variant' , variant ) ; this . _variant = variant ; } else { this . removeAttribute ( 'variant' ) ; this . _variant = '' ; } this . requestUpdate ( 'variant' , oldVariant ) ; } public get variant ( ) : string { return this . _variant ; } public get values ( ) : HandleValueDictionary { return this . handleController . values ; } public get handleName ( ) : string { return 'value' ; } private _variant = '' ; @ property ( { attribute : false } ) public getAriaValueText : ( values : Map < string , string > ) => string = ( values ) => { const valueArray = [ ... values . values ( ) ] ; if ( valueArray . length === 2 ) return `${ valueArray [ 0 ] }${ this . _forcedUnit } - ${ valueArray [ 1 ] }${ this . _forcedUnit }` ; return valueArray . join ( `${ this . _forcedUnit }, ` ) + this . _forcedUnit ; } ; public get ariaValueText ( ) : string { if ( ! this . getAriaValueText ) { return `${ this . value }${ this . _forcedUnit }` ; } return this . getAriaValueText ( this . handleController . formattedValues ) ; } @ property ( { type : String , reflect : true , attribute : 'label-visibility' } ) public labelVisibility ? : 'text' | 'value' | 'none' ; @ property ( { type : Number , reflect : true } ) public min = 0 ; @ property ( { type : Number , reflect : true } ) public max = 100 ; @ property ( { type : Number } ) public step = 1 ; @ property ( { type : Number , attribute : 'tick-step' } ) public tickStep = 0 ; @ property ( { type : Boolean , attribute : 'tick-labels' } ) public tickLabels = false ; @ property ( { type : Boolean , reflect : true } ) public disabled = false ; @ query ( '#label' ) public labelEl ! : HTMLLabelElement ; @ query ( '#number-field' ) public numberField ! : NumberField ; @ query ( '#track' ) public track ! : HTMLDivElement ; public get numberFormat ( ) : Intl . NumberFormat { return this . getNumberFormat ( ) ; } public get focusElement ( ) : HTMLElement { return this . handleController . focusElement ; } protected handleLabelClick ( event ) { if ( this . editable ) { event . preventDefault ( ) ; this . focus ( ) ; } } protected render ( ) { return html `
            ${ this . renderLabel ( ) } ${ this . renderTrack ( ) }
            ${ this . editable ? html `
                      <sp-number-field
                          .formatOptions=${ this . formatOptions || { } }
                          id="number-field"
                          min=${ this . min }
                          max=${ this . max }
                          step=${ this . step }
                          value=${ this . value }
                          ?hide-stepper=${ this . hideStepper }
                          ?disabled=${ this . disabled }
                          @input=${ this . handleNumberInput }
                          @change=${ this . handleNumberChange }
                      ></sp-number-field>
                  ` : html `` }
        ` ; } public connectedCallback ( ) { super . connectedCallback ( ) ; this . handleController . hostConnected ( ) ; } public disconnectedCallback ( ) { super . disconnectedCallback ( ) ; this . handleController . hostDisconnected ( ) ; } public update ( changedProperties ) { this . handleController . hostUpdate ( ) ; super . update ( changedProperties ) ; } private renderLabel ( ) { const textLabelVisible = this . labelVisibility === 'none' || this . labelVisibility === 'value' ; const valueLabelVisible = this . labelVisibility === 'none' || this . labelVisibility === 'text' ; return html `
            <div id="label-container">
                <sp-field-label
                    class=${ classMap ( { 'visually-hidden' : textLabelVisible , } ) }
                    ?disabled=${ this . disabled }
                    id="label"
                    for=${ this . editable ? 'number-field' : this . handleController . activeHandleInputId }
                    @click=${ this . handleLabelClick }
                >
                    ${ this . slotHasContent ? html `` : this . label }
                    <slot>${ this . label }</slot>
                </sp-field-label>
                <output
                    class=${ classMap ( { 'visually-hidden' : valueLabelVisible , } ) }
                    id="value"
                    aria-live="off"
                    for="input"
                >
                    ${ this . ariaValueText }
                </output>
            </div>
        ` ; } private renderRamp ( ) { if ( this . variant !== 'ramp' ) { return html `` ; } return html `
            <div id="ramp">
                <svg
                    viewBox="0 0 240 16"
                    preserveAspectRatio="none"
                    aria-hidden="true"
                    focusable="false"
                >
                    <path
                        d="M240,4v8c0,2.3-1.9,4.1-4.2,4L1,9C0.4,9,0,8.5,0,8c0-0.5,0.4-1,1-1l234.8-7C238.1-0.1,240,1.7,240,4z"
                    ></path>
                </svg>
            </div>
        ` ; } private renderTicks ( ) { if ( this . variant !== 'tick' ) { return html `` ; } const tickStep = this . tickStep || this . step ; const tickCount = ( ( this . max as number ) - ( this . min as number ) ) / tickStep ; const partialFit = tickCount % 1 !== 0 ; const ticks = new Array ( Math . floor ( tickCount + 1 ) ) ; ticks . fill ( 0 , 0 , tickCount + 1 ) ; return html `
            <div
                class="${ partialFit ? 'not-exact ' : '' }ticks"
                style=${ ifDefined ( partialFit ? `--sp-slider-tick-offset: calc(100% / ${ this . max } * ${ this . tickStep }` : undefined ) }
            >
                ${ ticks . map ( ( _tick , i ) => html `
                        <div class="tick">
                            ${ this . tickLabels ? html `
                                      <div class="tickLabel">
                                          ${ i * tickStep }
                                      </div>
                                  ` : html `` }
                        </div>
                    ` ) }
            </div>
        ` ; } private renderTrackSegment ( start , end ) { if ( this . variant === 'ramp' ) { return html `` ; } return html `
            <div
                class="track"
                style=${ styleMap ( this . trackSegmentStyles ( start , end ) ) }
                role="presentation"
            ></div>
        ` ; } private renderTrack ( ) { const segments = this . handleController . trackSegments ( ) ; const trackItems = [ { id : 'track0' , html : this . renderTrackSegment ( ... segments [ 0 ] ) } , { id : 'ramp' , html : this . renderRamp ( ) } , { id : 'ticks' , html : this . renderTicks ( ) } , { id : 'handles' , html : this . handleController . render ( ) } , ... segments . slice ( 1 ) . map ( ( [ start , end ] , index ) => ( { id : `track${ index + 1 }` , html : this . renderTrackSegment ( start , end ) , } ) ) , ] ; return html `
            <div
                id="track"
                ${ streamingListener ( { start : [ 'pointerdown' , this . handlePointerdown ] , streamInside : [ 'pointermove' , this . handlePointermove ] , end : [ [ 'pointerup' , 'pointercancel' ] , this . handlePointerup ] , } ) }
            >
                <div id="controls">
                    ${ repeat ( trackItems , ( item ) => item . id , ( item ) => item . html ) }
                </div>
            </div>
        ` ; } protected handlePointerdown ( event ) { this . handleController . handlePointerdown ( event ) ; } protected handlePointermove ( event ) { this . handleController . handlePointermove ( event ) ; } protected handlePointerup ( event ) { this . handleController . handlePointerup ( event ) ; } private handleNumberInput ( event ) { const { value } = event . target ; if ( event . target ?. stepperActive && ! isNaN ( value ) ) { this . value = value ; return ; } event . stopPropagation ( ) ; } private handleNumberChange ( event ) { const { value } = event . target ; if ( isNaN ( value ) ) { event . target . value = this . value ; event . stopPropagation ( ) ; } else { this . value = value ; if ( ! event . target ?. stepperActive ) { this . dispatchInputEvent ( ) ; } } } private trackSegmentStyles ( start , end ) { const size = end - start ; const styles = { width : `${ size * 100 }%` , '--spectrum-slider-track-background-size' : `${ ( 1 / size ) * 100 }%` , '--spectrum-slider-track-segment-position' : `${ start * 100 }%` , } ; return styles ; } private _numberFieldInput : Promise < unknown > = Promise . resolve ( ) ; protected async getUpdateComplete ( ) { const complete = ( await super . getUpdateComplete ( ) ) as boolean ; if ( this . editable ) { await this . _numberFieldInput ; await this . numberField . updateComplete ; } await this . handleController . handleUpdatesComplete ( ) ; return complete ; } }