license import '@material/mwc-notched-outline' ; import { MDCFloatingLabelFoundation } from '@material/floating-label/foundation' ; import { MDCLineRippleFoundation } from '@material/line-ripple/foundation' ; import { addHasRemoveClass , FormElement } from '@material/mwc-base/form-element' ; import { observer } from '@material/mwc-base/observer' ; import { floatingLabel , FloatingLabel } from '@material/mwc-floating-label' ; import { lineRipple , LineRipple } from '@material/mwc-line-ripple' ; import { NotchedOutline } from '@material/mwc-notched-outline' ; import { MDCTextFieldAdapter , MDCTextFieldInputAdapter , MDCTextFieldLabelAdapter , MDCTextFieldLineRippleAdapter , MDCTextFieldOutlineAdapter , MDCTextFieldRootAdapter } from '@material/textfield/adapter' ; import MDCTextFieldFoundation from '@material/textfield/foundation' ; import { html , PropertyValues , TemplateResult } from 'lit' ; import { eventOptions , property , query , state } from 'lit/decorators.js' ; import { classMap } from 'lit/directives/class-map.js' ; import { ifDefined } from 'lit/directives/if-defined.js' ; import { live } from 'lit/directives/live.js' ; declare global { interface HTMLElement { floatingLabelFoundation ? : MDCFloatingLabelFoundation ; lineRippleFoundation ? : MDCLineRippleFoundation ; } interface Element { floatingLabelFoundation ? : MDCFloatingLabelFoundation ; lineRippleFoundation ? : MDCLineRippleFoundation ; } } type CustomValidityState = { - readonly [ P in keyof ValidityState ] : ValidityState [ P ] ; } ; const passiveEvents = [ 'touchstart' , 'touchmove' , 'scroll' , 'mousewheel' ] ; const createValidityObj = ( customValidity = { } ) : ValidityState => { const objectifiedCustomValidity = { } ; for ( const propName in customValidity ) { objectifiedCustomValidity [ propName as keyof CustomValidityState ] = customValidity [ propName as keyof ValidityState ] ; } return { badInput : false , customError : false , patternMismatch : false , rangeOverflow : false , rangeUnderflow : false , stepMismatch : false , tooLong : false , tooShort : false , typeMismatch : false , valid : true , valueMissing : false , ... objectifiedCustomValidity } ; } ; /**
 * This is the enumerated typeof HTMLInputElement.type as declared by
 * lit-analyzer.
 */ export type TextFieldType = 'text' | 'search' | 'tel' | 'url' | 'email' | 'password' | 'date' | 'month' | 'week' | 'time' | 'datetime-local' | 'number' | 'color' ; /**
 * This is the enumerated typeof HTMLInputElement.inputMode as declared by
 * lit-analyzer.
 */ export type TextFieldInputMode = 'verbatim' | 'latin' | 'latin-name' | 'latin-prose' | 'full-width-latin' | 'kana' | 'kana-name' | 'katakana' | 'numeric' | 'tel' | 'email' | 'url' ; export type TextAreaCharCounter = 'external' | 'internal' ; soyCompatible export abstract class TextFieldBase extends FormElement { protected mdcFoundation ! : MDCTextFieldFoundation ; protected readonly mdcFoundationClass = MDCTextFieldFoundation ; @ query ( '.mdc-text-field' ) protected mdcRoot ! : HTMLElement ; @ query ( 'input' ) protected formElement ! : HTMLInputElement ; @ query ( '.mdc-floating-label' ) protected labelElement ! : FloatingLabel | null ; @ query ( '.mdc-line-ripple' ) protected lineRippleElement ! : LineRipple | null ; @ query ( 'mwc-notched-outline' ) protected outlineElement ! : NotchedOutline | null ; @ query ( '.mdc-notched-outline__notch' ) protected notchElement ! : HTMLElement ; @ property ( { type : String } ) value = '' ; @ property ( { type : String } ) type : TextFieldType = 'text' ; @ property ( { type : String } ) placeholder = '' ; @ property ( { type : String } ) @ observer ( function ( this , _newVal , oldVal ) { if ( oldVal !== undefined && this . label !== oldVal ) { this . layout ( ) ; } } ) label = '' ; @ property ( { type : String } ) icon = '' ; @ property ( { type : String } ) iconTrailing = '' ; @ property ( { type : Boolean , reflect : true } ) override disabled = false ; @ property ( { type : Boolean } ) required = false ; @ property ( { type : Number } ) minLength = - 1 ; @ property ( { type : Number } ) maxLength = - 1 ; @ property ( { type : Boolean , reflect : true } ) @ observer ( function ( this , _newVal , oldVal ) { if ( oldVal !== undefined && this . outlined !== oldVal ) { this . layout ( ) ; } } ) outlined = false ; @ property ( { type : String } ) helper = '' ; @ property ( { type : Boolean } ) validateOnInitialRender = false ; @ property ( { type : String } ) validationMessage = '' ; @ property ( { type : Boolean } ) autoValidate = false ; @ property ( { type : String } ) pattern = '' ; @ property ( { type : String } ) min : number | string = '' ; @ property ( { type : String } ) max : number | string = '' ; /**
     * step can be a number or the keyword "any".
     *
     * Use `String` typing to pass down the value as a string and let the native
     * input cast internally as needed.
     */ @ property ( { type : String } ) step : number | 'any' | null = null ; @ property ( { type : Number } ) size : number | null = null ; @ property ( { type : Boolean } ) helperPersistent = false ; @ property ( { type : Boolean } ) charCounter : boolean | TextAreaCharCounter = false ; @ property ( { type : Boolean } ) endAligned = false ; @ property ( { type : String } ) override prefix = '' ; @ property ( { type : String } ) suffix = '' ; @ property ( { type : String } ) name = '' ; @ property ( { type : String } ) override inputMode ! : TextFieldInputMode ; @ property ( { type : Boolean } ) readOnly = false ; @ property ( { type : String } ) override autocapitalize = '' ; @ state ( ) protected outlineOpen = false ; @ state ( ) protected outlineWidth = 0 ; @ state ( ) protected isUiValid = true ; @ state ( ) protected focused = false ; protected _validity : ValidityState = createValidityObj ( ) ; get validity ( ) : ValidityState { this . _checkValidity ( this . value ) ; return this . _validity ; } get willValidate ( ) : boolean { return this . formElement . willValidate ; } get selectionStart ( ) : number | null { return this . formElement . selectionStart ; } get selectionEnd ( ) : number | null { return this . formElement . selectionEnd ; } validityTransform : ( ( value : string , nativeValidity : ValidityState ) => Partial < ValidityState > ) | null = null ; override focus ( ) { const focusEvt = new CustomEvent ( 'focus' ) ; this . formElement . dispatchEvent ( focusEvt ) ; this . formElement . focus ( ) ; } override blur ( ) { const blurEvt = new CustomEvent ( 'blur' ) ; this . formElement . dispatchEvent ( blurEvt ) ; this . formElement . blur ( ) ; } select ( ) { this . formElement . select ( ) ; } setSelectionRange ( selectionStart , selectionEnd , selectionDirection ? ) { this . formElement . setSelectionRange ( selectionStart , selectionEnd , selectionDirection ) ; } override update ( changedProperties ) { if ( changedProperties . has ( 'autoValidate' ) && this . mdcFoundation ) { this . mdcFoundation . setValidateOnValueChange ( this . autoValidate ) ; } if ( changedProperties . has ( 'value' ) && typeof this . value !== 'string' ) { this . value = `${ this . value }` ; } super . update ( changedProperties ) ; } protected setFormData ( formData ) { if ( this . name ) { formData . append ( this . name , this . value ) ; } } soyTemplate override render ( ) { const shouldRenderCharCounter = this . charCounter && this . maxLength !== - 1 ; const shouldRenderHelperText = ! ! this . helper || ! ! this . validationMessage || shouldRenderCharCounter ; classMap const classes = { 'mdc-text-field--disabled' : this . disabled , 'mdc-text-field--no-label' : ! this . label , 'mdc-text-field--filled' : ! this . outlined , 'mdc-text-field--outlined' : this . outlined , 'mdc-text-field--with-leading-icon' : this . icon , 'mdc-text-field--with-trailing-icon' : this . iconTrailing , 'mdc-text-field--end-aligned' : this . endAligned , } ; return html `
      <label class="mdc-text-field ${ classMap ( classes ) }">
        ${ this . renderRipple ( ) }
        ${ this . outlined ? this . renderOutline ( ) : this . renderLabel ( ) }
        ${ this . renderLeadingIcon ( ) }
        ${ this . renderPrefix ( ) }
        ${ this . renderInput ( shouldRenderHelperText ) }
        ${ this . renderSuffix ( ) }
        ${ this . renderTrailingIcon ( ) }
        ${ this . renderLineRipple ( ) }
      </label>
      ${ this . renderHelperText ( shouldRenderHelperText , shouldRenderCharCounter ) }
    ` ; } override updated ( changedProperties ) { if ( changedProperties . has ( 'value' ) && changedProperties . get ( 'value' ) !== undefined ) { this . mdcFoundation . setValue ( this . value ) ; if ( this . autoValidate ) { this . reportValidity ( ) ; } } } soyTemplate protected renderRipple ( ) { return this . outlined ? '' : html `
      <span class="mdc-text-field__ripple"></span>
    ` ; } soyTemplate protected renderOutline ( ) { return ! this . outlined ? '' : html `
      <mwc-notched-outline
          .width=${ this . outlineWidth }
          .open=${ this . outlineOpen }
          class="mdc-notched-outline">
        ${ this . renderLabel ( ) }
      </mwc-notched-outline>` ; } soyTemplate protected renderLabel ( ) { return ! this . label ? '' : html `
      <span
          .floatingLabelFoundation=${ floatingLabel ( this . label ) as unknown as MDCFloatingLabelFoundation }
          id="label">${ this . label }</span>
    ` ; } soyTemplate protected renderLeadingIcon ( ) { return this . icon ? this . renderIcon ( this . icon ) : '' ; } soyTemplate protected renderTrailingIcon ( ) { return this . iconTrailing ? this . renderIcon ( this . iconTrailing , true ) : '' ; } soyTemplate protected renderIcon ( icon , isTrailingIcon = false ) { classMap const classes = { 'mdc-text-field__icon--leading' : ! isTrailingIcon , 'mdc-text-field__icon--trailing' : isTrailingIcon } ; return html `<i class="material-icons mdc-text-field__icon ${ classMap ( classes ) }">${ icon }</i>` ; } soyTemplate protected renderPrefix ( ) { return this . prefix ? this . renderAffix ( this . prefix ) : '' ; } soyTemplate protected renderSuffix ( ) { return this . suffix ? this . renderAffix ( this . suffix , true ) : '' ; } soyTemplate protected renderAffix ( content , isSuffix = false ) { classMap const classes = { 'mdc-text-field__affix--prefix' : ! isSuffix , 'mdc-text-field__affix--suffix' : isSuffix } ; return html `<span class="mdc-text-field__affix ${ classMap ( classes ) }">
        ${ content }</span>` ; } soyTemplate protected renderInput ( shouldRenderHelperText ) { const minOrUndef = this . minLength === - 1 ? undefined : this . minLength ; const maxOrUndef = this . maxLength === - 1 ? undefined : this . maxLength ; const autocapitalizeOrUndef = this . autocapitalize ? this . autocapitalize as ( 'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters' ) : undefined ; const showValidationMessage = this . validationMessage && ! this . isUiValid ; const ariaLabelledbyOrUndef = ! ! this . label ? 'label' : undefined ; const ariaControlsOrUndef = shouldRenderHelperText ? 'helper-text' : undefined ; const ariaDescribedbyOrUndef = this . focused || this . helperPersistent || showValidationMessage ? 'helper-text' : undefined ; return html `
      <input
          aria-labelledby=${ ifDefined ( ariaLabelledbyOrUndef ) }
          aria-controls="${ ifDefined ( ariaControlsOrUndef ) }"
          aria-describedby="${ ifDefined ( ariaDescribedbyOrUndef ) }"
          class="mdc-text-field__input"
          type="${ this . type }"
          .value="${ live ( this . value ) as unknown as string }"
          ?disabled="${ this . disabled }"
          placeholder="${ this . placeholder }"
          ?required="${ this . required }"
          ?readonly="${ this . readOnly }"
          minlength="${ ifDefined ( minOrUndef ) }"
          maxlength="${ ifDefined ( maxOrUndef ) }"
          pattern="${ ifDefined ( this . pattern ? this . pattern : undefined ) }"
          min="${ ifDefined ( this . min === '' ? undefined : this . min as number ) }"
          max="${ ifDefined ( this . max === '' ? undefined : this . max as number ) }"
          step="${ ifDefined ( this . step === null ? undefined : this . step as number ) }"
          size="${ ifDefined ( this . size === null ? undefined : this . size ) }"
          name="${ ifDefined ( this . name === '' ? undefined : this . name ) }"
          inputmode="${ ifDefined ( this . inputMode ) }"
          autocapitalize="${ ifDefined ( autocapitalizeOrUndef ) }"
          @input="${ this . handleInputChange }"
          @focus="${ this . onInputFocus }"
          @blur="${ this . onInputBlur }">` ; } soyTemplate protected renderLineRipple ( ) { return this . outlined ? '' : html `
      <span .lineRippleFoundation=${ lineRipple ( ) as unknown as MDCLineRippleFoundation }></span>
    ` ; } soyTemplate protected renderHelperText ( shouldRenderHelperText , shouldRenderCharCounter ) { const showValidationMessage = this . validationMessage && ! this . isUiValid ; classMap const classes = { 'mdc-text-field-helper-text--persistent' : this . helperPersistent , 'mdc-text-field-helper-text--validation-msg' : showValidationMessage , } ; const ariaHiddenOrUndef = this . focused || this . helperPersistent || showValidationMessage ? undefined : 'true' ; const helperText = showValidationMessage ? this . validationMessage : this . helper ; return ! shouldRenderHelperText ? '' : html `
      <div class="mdc-text-field-helper-line">
        <div id="helper-text"
             aria-hidden="${ ifDefined ( ariaHiddenOrUndef ) }"
             class="mdc-text-field-helper-text ${ classMap ( classes ) }"
             >${ helperText }</div>
        ${ this . renderCharCounter ( shouldRenderCharCounter ) }
      </div>` ; } soyTemplate protected renderCharCounter ( shouldRenderCharCounter ) { const length = Math . min ( this . value . length , this . maxLength ) ; return ! shouldRenderCharCounter ? '' : html `
      <span class="mdc-text-field-character-counter"
            >${ length } / ${ this . maxLength }</span>` ; } protected onInputFocus ( ) { this . focused = true ; } protected onInputBlur ( ) { this . focused = false ; this . reportValidity ( ) ; } checkValidity ( ) { const isValid = this . _checkValidity ( this . value ) ; if ( ! isValid ) { const invalidEvent = new Event ( 'invalid' , { bubbles : false , cancelable : true } ) ; this . dispatchEvent ( invalidEvent ) ; } return isValid ; } reportValidity ( ) { const isValid = this . checkValidity ( ) ; this . mdcFoundation . setValid ( isValid ) ; this . isUiValid = isValid ; return isValid ; } protected _checkValidity ( value ) { const nativeValidity = this . formElement . validity ; let validity = createValidityObj ( nativeValidity ) ; if ( this . validityTransform ) { const customValidity = this . validityTransform ( value , validity ) ; validity = { ... validity , ... customValidity } ; this . mdcFoundation . setUseNativeValidation ( false ) ; } else { this . mdcFoundation . setUseNativeValidation ( true ) ; } this . _validity = validity ; return this . _validity . valid ; } setCustomValidity ( message ) { this . validationMessage = message ; this . formElement . setCustomValidity ( message ) ; } @ eventOptions ( { passive : true } ) protected handleInputChange ( ) { this . value = this . formElement . value ; } protected createAdapter ( ) { return { ... this . getRootAdapterMethods ( ) , ... this . getInputAdapterMethods ( ) , ... this . getLabelAdapterMethods ( ) , ... this . getLineRippleAdapterMethods ( ) , ... this . getOutlineAdapterMethods ( ) , } ; } protected getRootAdapterMethods ( ) { return { registerTextFieldInteractionHandler : ( evtType , handler ) => this . addEventListener ( evtType , handler ) , deregisterTextFieldInteractionHandler : ( evtType , handler ) => this . removeEventListener ( evtType , handler ) , registerValidationAttributeChangeHandler : ( handler ) => { const getAttributesList = ( mutationsList ) : string [ ] => { return mutationsList . map ( ( mutation ) => mutation . attributeName ) . filter ( ( attributeName ) => attributeName ) as string [ ] ; } ; const observer = new MutationObserver ( ( mutationsList ) => { handler ( getAttributesList ( mutationsList ) ) ; } ) ; const config = { attributes : true } ; observer . observe ( this . formElement , config ) ; return observer ; } , deregisterValidationAttributeChangeHandler : ( observer ) => observer . disconnect ( ) , ... addHasRemoveClass ( this . mdcRoot ) , } ; } protected getInputAdapterMethods ( ) { return { getNativeInput : ( ) => this . formElement , setInputAttr : ( ) => undefined , removeInputAttr : ( ) => undefined , isFocused : ( ) => this . shadowRoot ? this . shadowRoot . activeElement === this . formElement : false , registerInputInteractionHandler : ( evtType , handler ) => this . formElement . addEventListener ( evtType , handler , { passive : evtType in passiveEvents } ) , deregisterInputInteractionHandler : ( evtType , handler ) => this . formElement . removeEventListener ( evtType , handler ) , } ; } protected getLabelAdapterMethods ( ) { return { floatLabel : ( shouldFloat ) => this . labelElement && this . labelElement . floatingLabelFoundation . float ( shouldFloat ) , getLabelWidth : ( ) => { return this . labelElement ? this . labelElement . floatingLabelFoundation . getWidth ( ) : 0 ; } , hasLabel : ( ) => Boolean ( this . labelElement ) , shakeLabel : ( shouldShake ) => this . labelElement && this . labelElement . floatingLabelFoundation . shake ( shouldShake ) , setLabelRequired : ( isRequired ) => { if ( this . labelElement ) { this . labelElement . floatingLabelFoundation . setRequired ( isRequired ) ; } } , } ; } protected getLineRippleAdapterMethods ( ) { return { activateLineRipple : ( ) => { if ( this . lineRippleElement ) { this . lineRippleElement . lineRippleFoundation . activate ( ) ; } } , deactivateLineRipple : ( ) => { if ( this . lineRippleElement ) { this . lineRippleElement . lineRippleFoundation . deactivate ( ) ; } } , setLineRippleTransformOrigin : ( normalizedX ) => { if ( this . lineRippleElement ) { this . lineRippleElement . lineRippleFoundation . setRippleCenter ( normalizedX ) ; } } , } ; } protected override async getUpdateComplete ( ) { const result = await super . getUpdateComplete ( ) ; await this . outlineElement ?. updateComplete ; return result ; } override firstUpdated ( ) { super . firstUpdated ( ) ; this . mdcFoundation . setValidateOnValueChange ( this . autoValidate ) ; if ( this . validateOnInitialRender ) { this . reportValidity ( ) ; } this . outlineElement ?. updateComplete . then ( ( ) => { this . outlineWidth = this . labelElement ?. floatingLabelFoundation . getWidth ( ) || 0 ; } ) ; } protected getOutlineAdapterMethods ( ) { return { closeOutline : ( ) => this . outlineElement && ( this . outlineOpen = false ) , hasOutline : ( ) => Boolean ( this . outlineElement ) , notchOutline : ( labelWidth ) => { const outlineElement = this . outlineElement ; if ( outlineElement && ! this . outlineOpen ) { this . outlineWidth = labelWidth ; this . outlineOpen = true ; } } } ; } async layout ( ) { await this . updateComplete ; const labelElement = this . labelElement ; if ( ! labelElement ) { this . outlineOpen = false ; return ; } const shouldFloat = ! ! this . label && ! ! this . value ; labelElement . floatingLabelFoundation . float ( shouldFloat ) ; if ( ! this . outlined ) { return ; } this . outlineOpen = shouldFloat ; await this . updateComplete ; const labelWidth = labelElement . floatingLabelFoundation . getWidth ( ) ; if ( this . outlineOpen ) { this . outlineWidth = labelWidth ; await this . updateComplete ; } } }