import { CSSResultArray , html , nothing , PropertyValues , TemplateResult , } from '@spectrum-web-components/base' ; import { ifDefined , live , } from '@spectrum-web-components/base/src/directives.js' ; import { property , query , state , } from '@spectrum-web-components/base/src/decorators.js' ; import { ManageHelpText } from '@spectrum-web-components/help-text/src/manage-help-text.js' ; import { Focusable } from '@spectrum-web-components/shared/src/focusable.js' ; import '@spectrum-web-components/icons-ui/icons/sp-icon-checkmark100.js' ; import '@spectrum-web-components/icons-workflow/icons/sp-icon-alert.js' ; import textfieldStyles from './textfield.css.js' ; import checkmarkStyles from '@spectrum-web-components/icon/src/spectrum-icon-checkmark.css.js' ; const textfieldTypes = [ 'text' , 'url' , 'tel' , 'email' , 'password' ] as const ; export type TextfieldType = typeof textfieldTypes [ number ] ; fires fires export class TextfieldBase extends ManageHelpText ( Focusable ) { public static get styles ( ) : CSSResultArray { return [ textfieldStyles , checkmarkStyles ] ; } @ property ( { attribute : 'allowed-keys' } ) allowedKeys = '' ; @ property ( { type : Boolean , reflect : true } ) public focused = false ; @ query ( '.input' ) protected inputElement ! : HTMLInputElement | HTMLTextAreaElement ; @ property ( { type : Boolean , reflect : true } ) public invalid = false ; @ property ( ) public label = '' ; @ property ( ) public placeholder = '' ; @ property ( { attribute : 'type' , reflect : true } ) private _type : TextfieldType = 'text' ; @ state ( ) get type ( ) : TextfieldType { return textfieldTypes . find ( ( t ) => t === this . _type ) ?? 'text' ; } set type ( val ) { const prev = this . _type ; this . _type = val ; this . requestUpdate ( 'type' , prev ) ; } @ property ( ) public pattern ? : string ; @ property ( { type : Boolean , reflect : true } ) public grows = false ; @ property ( { type : Number } ) public maxlength = - 1 ; @ property ( { type : Number } ) public minlength = - 1 ; @ property ( { type : Boolean , reflect : true } ) public multiline = false ; @ property ( { type : Boolean , reflect : true } ) public readonly = false ; @ property ( { type : Boolean , reflect : true } ) public valid = false ; @ property ( { type : String } ) public set value ( value ) { if ( value === this . value ) { return ; } const oldValue = this . _value ; this . _value = value ; this . requestUpdate ( 'value' , oldValue ) ; } public get value ( ) : string | number { return this . _value ; } protected _value : string | number = '' ; @ property ( { type : Boolean , reflect : true } ) public quiet = false ; @ property ( { type : Boolean , reflect : true } ) public required = false ; @ property ( { type : String , reflect : true } ) public autocomplete ? : HTMLInputElement [ 'autocomplete' ] | HTMLTextAreaElement [ 'autocomplete' ] ; public get focusElement ( ) : HTMLInputElement | HTMLTextAreaElement { return this . inputElement ; } protected onInput ( ) { if ( this . allowedKeys && this . inputElement . value ) { const regExp = new RegExp ( `^[${ this . allowedKeys }]*$` , 'u' ) ; if ( ! regExp . test ( this . inputElement . value ) ) { const selectionStart = this . inputElement . selectionStart as number ; const nextSelectStart = selectionStart - 1 ; this . inputElement . value = this . value . toString ( ) ; this . inputElement . setSelectionRange ( nextSelectStart , nextSelectStart ) ; return ; } } this . value = this . inputElement . value ; } protected onChange ( ) { this . dispatchEvent ( new Event ( 'change' , { bubbles : true , composed : true , } ) ) ; } protected onFocus ( ) { this . focused = true ; } protected onBlur ( ) { this . focused = false ; } protected renderStateIcons ( ) { if ( this . invalid ) { return html `
                <sp-icon-alert id="invalid" class="icon"></sp-icon-alert>
            ` ; } else if ( this . valid ) { return html `
                <sp-icon-checkmark100
                    id="valid"
                    class="icon spectrum-UIIcon-Checkmark100"
                ></sp-icon-checkmark100>
            ` ; } return nothing ; } protected get displayValue ( ) : string { return this . value . toString ( ) ; } public select ( ) { this . inputElement . select ( ) ; } private get renderMultiline ( ) : TemplateResult { return html `
            ${ this . grows && ! this . quiet ? html `
                      <div id="sizer">${ this . value }</div>
                  ` : nothing }
            <!-- @ts-ignore -->
            <textarea
                aria-describedby=${ this . helpTextId }
                aria-label=${ this . label || this . placeholder }
                aria-invalid=${ ifDefined ( this . invalid || undefined ) }
                class="input"
                maxlength=${ ifDefined ( this . maxlength > - 1 ? this . maxlength : undefined ) }
                minlength=${ ifDefined ( this . minlength > - 1 ? this . minlength : undefined ) }
                pattern=${ ifDefined ( this . pattern ) }
                placeholder=${ this . placeholder }
                .value=${ this . displayValue }
                @change=${ this . onChange }
                @input=${ this . onInput }
                @focus=${ this . onFocus }
                @blur=${ this . onBlur }
                ?disabled=${ this . disabled }
                ?required=${ this . required }
                ?readonly=${ this . readonly }
                autocomplete=${ ifDefined ( this . autocomplete ) }
            ></textarea>
        ` ; } private get renderInput ( ) : TemplateResult { return html `
            <!-- @ts-ignore -->
            <input
                type=${ this . type }
                aria-describedby=${ this . helpTextId }
                aria-label=${ this . label || this . placeholder }
                aria-invalid=${ ifDefined ( this . invalid || undefined ) }
                class="input"
                maxlength=${ ifDefined ( this . maxlength > - 1 ? this . maxlength : undefined ) }
                minlength=${ ifDefined ( this . minlength > - 1 ? this . minlength : undefined ) }
                pattern=${ ifDefined ( this . pattern ) }
                placeholder=${ this . placeholder }
                .value=${ live ( this . displayValue ) }
                @change=${ this . onChange }
                @input=${ this . onInput }
                @focus=${ this . onFocus }
                @blur=${ this . onBlur }
                ?disabled=${ this . disabled }
                ?required=${ this . required }
                ?readonly=${ this . readonly }
                autocomplete=${ ifDefined ( this . autocomplete ) }
            />
        ` ; } protected renderField ( ) { return html `
            ${ this . renderStateIcons ( ) }
            ${ this . multiline ? this . renderMultiline : this . renderInput }
        ` ; } protected render ( ) { return html `
            <div id="textfield">${ this . renderField ( ) }</div>
            ${ this . renderHelpText ( this . invalid ) }
        ` ; } protected updated ( changedProperties ) { if ( changedProperties . has ( 'value' ) || ( changedProperties . has ( 'required' ) && this . required ) ) { this . checkValidity ( ) ; } } public checkValidity ( ) { let validity = this . inputElement . checkValidity ( ) ; if ( this . required || ( this . value && this . pattern ) ) { if ( ( this . disabled || this . multiline ) && this . pattern ) { const regex = new RegExp ( `^${ this . pattern }$` , 'u' ) ; validity = regex . test ( this . value . toString ( ) ) ; } if ( typeof this . minlength !== 'undefined' ) { validity = validity && this . value . toString ( ) . length > this . minlength ; } this . valid = validity ; this . invalid = ! validity ; } return validity ; } } element slot slot export class Textfield extends TextfieldBase { @ property ( { type : String } ) public set value ( value ) { if ( value === this . value ) { return ; } const oldValue = this . _value ; this . _value = value ; this . requestUpdate ( 'value' , oldValue ) ; } public get value ( ) : string { return this . _value ; } protected _value = '' ; }