@@ -105,6 +105,10 @@ export abstract class TextField extends LitElement {
105
105
* LTR notation for fractions.
106
106
*/
107
107
@property ( { type : String } ) textDirection = '' ;
108
+ /**
109
+ * The ID on the character counter element, used for SSR.
110
+ */
111
+ @property ( { type : String } ) counterId = 'counter' ;
108
112
109
113
// ARIA
110
114
// TODO(b/210730484): replace with @soyParam annotation
@@ -480,10 +484,10 @@ export abstract class TextField extends LitElement {
480
484
/** @soyTemplate */
481
485
override render ( ) : TemplateResult {
482
486
return html `
483
- < span class ="md3-text-field ${ classMap ( this . getRenderClasses ( ) ) } ">
484
- ${ this . renderField ( ) }
485
- </ span >
486
- ` ;
487
+ < span class ="md3-text-field ${ classMap ( this . getRenderClasses ( ) ) } ">
488
+ ${ this . renderField ( ) }
489
+ </ span >
490
+ ` ;
487
491
}
488
492
489
493
/** @soyTemplate */
@@ -502,21 +506,22 @@ export abstract class TextField extends LitElement {
502
506
const inputValue = this . getInputValue ( ) ;
503
507
504
508
return staticHtml `< ${ this . fieldTag }
505
- class ="md3-text-field__field"
506
- ?disabled=${ this . disabled }
507
- ?error=${ this . error }
508
- ?focused=${ this . focused }
509
- ?hasEnd=${ this . hasTrailingIcon }
510
- ?hasStart=${ this . hasLeadingIcon }
511
- .label=${ this . label }
512
- ?populated=${ ! ! inputValue }
513
- ?required=${ this . required }
514
- >
515
- ${ this . renderLeadingIcon ( ) }
516
- ${ prefix } ${ input } ${ suffix }
517
- ${ this . renderTrailingIcon ( ) }
518
- ${ this . renderSupportingText ( ) }
519
- </ ${ this . fieldTag } > ` ;
509
+ class ="md3-text-field__field"
510
+ ?disabled=${ this . disabled }
511
+ ?error=${ this . error }
512
+ ?focused=${ this . focused }
513
+ ?hasEnd=${ this . hasTrailingIcon }
514
+ ?hasStart=${ this . hasLeadingIcon }
515
+ .label=${ this . label }
516
+ ?populated=${ ! ! inputValue }
517
+ ?required=${ this . required }
518
+ >
519
+ ${ this . renderLeadingIcon ( ) }
520
+ ${ prefix } ${ input } ${ suffix }
521
+ ${ this . renderTrailingIcon ( ) }
522
+ ${ this . renderSupportingText ( ) }
523
+ ${ this . renderCounter ( ) }
524
+ </ ${ this . fieldTag } > ` ;
520
525
}
521
526
522
527
/**
@@ -525,11 +530,11 @@ export abstract class TextField extends LitElement {
525
530
*/
526
531
protected renderLeadingIcon ( ) : TemplateResult {
527
532
return html `
528
- < span class ="md3-text-field__icon md3-text-field__icon--leading "
529
- slot ="start ">
530
- < slot name ="leadingicon " @slotchange =${ this . handleIconChange } > </ slot >
531
- </ span >
532
- ` ;
533
+ < span class ="md3-text-field__icon md3-text-field__icon--leading "
534
+ slot ="start ">
535
+ < slot name ="leadingicon " @slotchange =${ this . handleIconChange } > </ slot >
536
+ </ span >
537
+ ` ;
533
538
}
534
539
535
540
/**
@@ -538,11 +543,11 @@ export abstract class TextField extends LitElement {
538
543
*/
539
544
protected renderTrailingIcon ( ) : TemplateResult {
540
545
return html `
541
- < span class ="md3-text-field__icon md3-text-field__icon--trailing "
542
- slot ="end ">
543
- < slot name ="trailingicon " @slotchange =${ this . handleIconChange } > </ slot >
544
- </ span >
545
- ` ;
546
+ < span class ="md3-text-field__icon md3-text-field__icon--trailing "
547
+ slot ="end ">
548
+ < slot name ="trailingicon " @slotchange =${ this . handleIconChange } > </ slot >
549
+ </ span >
550
+ ` ;
546
551
}
547
552
548
553
/** @soyTemplate */
@@ -552,8 +557,7 @@ export abstract class TextField extends LitElement {
552
557
const ariaActiveDescendantValue = this . ariaActiveDescendant || undefined ;
553
558
const ariaAutoCompleteValue = this . ariaAutoComplete || undefined ;
554
559
const ariaControlsValue = this . ariaControls || undefined ;
555
- const ariaDescribedByValue =
556
- this . getSupportingText ( ) ? this . supportingTextId : undefined ;
560
+ const ariaDescribedByValue = this . getAriaDescribedBy ( ) || undefined ;
557
561
const ariaExpandedValue = this . ariaExpanded || undefined ;
558
562
const ariaLabelValue = this . ariaLabel || this . label || undefined ;
559
563
const ariaLabelledByValue = this . ariaLabelledBy || undefined ;
@@ -571,40 +575,51 @@ export abstract class TextField extends LitElement {
571
575
// TODO(b/243805848): remove `as unknown as number` once lit analyzer is
572
576
// fixed
573
577
return html `< input
574
- class ="md3-text-field__input "
575
- style =${ styleMap ( style ) }
576
- aria-activedescendant =${ ifDefined ( ariaActiveDescendantValue ) }
577
- aria-autocomplete=${ ifDefined ( ariaAutoCompleteValue ) }
578
- aria-controls=${ ifDefined ( ariaControlsValue ) }
579
- aria-describedby=${ ifDefined ( ariaDescribedByValue ) }
580
- aria-expanded=${ ifDefined ( ariaExpandedValue ) }
581
- aria-invalid=${ this . error }
582
- aria-label=${ ifDefined ( ariaLabelValue ) }
583
- aria-labelledby=${ ifDefined ( ariaLabelledByValue ) }
584
- ?disabled=${ this . disabled }
585
- max=${ ifDefined ( maxValue as unknown as number ) }
586
- maxlength=${ ifDefined ( maxLengthValue ) }
587
- min=${ ifDefined ( minValue as unknown as number ) }
588
- minlength=${ ifDefined ( minLengthValue ) }
589
- pattern=${ ifDefined ( patternValue ) }
590
- placeholder=${ ifDefined ( placeholderValue ) }
591
- role=${ ifDefined ( roleValue ) }
592
- ?readonly=${ this . readOnly }
593
- ?required=${ this . required }
594
- step=${ ifDefined ( stepValue as unknown as number ) }
595
- type=${ this . type }
596
- .value=${ live ( this . getInputValue ( ) ) }
597
- @change=${ this . redispatchEvent }
598
- @input=${ this . handleInput }
599
- @select=${ this . redispatchEvent }
600
- > ` ;
578
+ class ="md3-text-field__input "
579
+ style =${ styleMap ( style ) }
580
+ aria-activedescendant =${ ifDefined ( ariaActiveDescendantValue ) }
581
+ aria-autocomplete=${ ifDefined ( ariaAutoCompleteValue ) }
582
+ aria-controls=${ ifDefined ( ariaControlsValue ) }
583
+ aria-describedby=${ ifDefined ( ariaDescribedByValue ) }
584
+ aria-expanded=${ ifDefined ( ariaExpandedValue ) }
585
+ aria-invalid=${ this . error }
586
+ aria-label=${ ifDefined ( ariaLabelValue ) }
587
+ aria-labelledby=${ ifDefined ( ariaLabelledByValue ) }
588
+ ?disabled=${ this . disabled }
589
+ max=${ ifDefined ( maxValue as unknown as number ) }
590
+ maxlength=${ ifDefined ( maxLengthValue ) }
591
+ min=${ ifDefined ( minValue as unknown as number ) }
592
+ minlength=${ ifDefined ( minLengthValue ) }
593
+ pattern=${ ifDefined ( patternValue ) }
594
+ placeholder=${ ifDefined ( placeholderValue ) }
595
+ role=${ ifDefined ( roleValue ) }
596
+ ?readonly=${ this . readOnly }
597
+ ?required=${ this . required }
598
+ step=${ ifDefined ( stepValue as unknown as number ) }
599
+ type=${ this . type }
600
+ .value=${ live ( this . getInputValue ( ) ) }
601
+ @change=${ this . redispatchEvent }
602
+ @input=${ this . handleInput }
603
+ @select=${ this . redispatchEvent }
604
+ > ` ;
601
605
}
602
606
603
607
/** @soyTemplate */
604
608
protected getInputValue ( ) : string {
605
609
return this . dirty ? this . value : this . value || this . defaultValue ;
606
610
}
607
611
612
+ /** @soyTemplate */
613
+ protected getAriaDescribedBy ( ) : string {
614
+ const hasSupport = ! ! this . getSupportingText ( ) ;
615
+ const hasCounter = this . hasCounter ( ) ;
616
+ // TODO(b/244609052): remove parens
617
+ return ( hasSupport || hasCounter ) ?
618
+ `${ hasSupport ? this . supportingTextId : '' } ${
619
+ hasCounter ? this . counterId : '' } ` :
620
+ '' ;
621
+ }
622
+
608
623
/** @soyTemplate */
609
624
protected renderPrefix ( ) : TemplateResult {
610
625
return this . prefixText ?
@@ -642,6 +657,31 @@ export abstract class TextField extends LitElement {
642
657
return this . error && this . errorText ? this . errorText : this . supportingText ;
643
658
}
644
659
660
+ /**
661
+ * @soyTemplate
662
+ * @slotName supporting-text-end
663
+ */
664
+ protected renderCounter ( ) : TemplateResult {
665
+ const counter = html `< span id =${ this . counterId }
666
+ class ="md3-text-field__counter"
667
+ slot="supporting-text-end"> ${ this . getCounterText ( ) } </ span > ` ;
668
+ // TODO(b/244473435): add aria-label and announcements
669
+ return this . hasCounter ( ) ? counter : html `` ;
670
+ }
671
+
672
+ // TODO(b/244197198): replace with !!this.getCounterText()
673
+ /** @soyTemplate */
674
+ protected hasCounter ( ) : boolean {
675
+ return this . maxLength > - 1 ;
676
+ }
677
+
678
+ /** @soyTemplate */
679
+ protected getCounterText ( ) : TemplateResult {
680
+ // TODO(b/244197198): replace with string return
681
+ const length = this . value . length ;
682
+ return this . hasCounter ( ) ? html `${ length } / ${ this . maxLength } ` : html `` ;
683
+ }
684
+
645
685
protected override updated ( ) {
646
686
// If a property such as `type` changes and causes the internal <input>
647
687
// value to change without dispatching an event, re-sync it.
0 commit comments