1- import { html , LitElement , nothing , type TemplateResult } from 'lit' ;
21import {
3- property ,
4- query ,
5- queryAssignedElements ,
6- queryAssignedNodes ,
7- } from 'lit/decorators.js' ;
2+ html ,
3+ LitElement ,
4+ nothing ,
5+ type PropertyValues ,
6+ type TemplateResult ,
7+ } from 'lit' ;
8+ import { property , query } from 'lit/decorators.js' ;
9+ import { cache } from 'lit/directives/cache.js' ;
810import { ifDefined } from 'lit/directives/if-defined.js' ;
911import { live } from 'lit/directives/live.js' ;
10- import { type StyleInfo , styleMap } from 'lit/directives/style-map.js' ;
11-
12+ import { styleMap } from 'lit/directives/style-map.js' ;
1213import { addThemingController } from '../../theming/theming-controller.js' ;
1314import { createResizeObserverController } from '../common/controllers/resize-observer.js' ;
15+ import {
16+ addSlotController ,
17+ type InferSlotNames ,
18+ type SlotChangeCallbackParameters ,
19+ setSlots ,
20+ } from '../common/controllers/slot.js' ;
1421import { shadowOptions } from '../common/decorators/shadow-options.js' ;
15- import { watch } from '../common/decorators/watch.js' ;
1622import { registerComponent } from '../common/definitions/register.js' ;
1723import type { Constructor } from '../common/mixins/constructor.js' ;
1824import { EventEmitterMixin } from '../common/mixins/event-emitter.js' ;
@@ -22,12 +28,7 @@ import {
2228 type FormValueOf ,
2329} from '../common/mixins/forms/form-value.js' ;
2430import { partMap } from '../common/part-map.js' ;
25- import {
26- addSafeEventListener ,
27- asNumber ,
28- createCounter ,
29- isEmpty ,
30- } from '../common/util.js' ;
31+ import { addSafeEventListener , asNumber } from '../common/util.js' ;
3132import type {
3233 RangeTextSelectMode ,
3334 SelectionRangeDirection ,
@@ -49,6 +50,18 @@ export interface IgcTextareaComponentEventMap {
4950 blur : FocusEvent ;
5051}
5152
53+ let nextId = 1 ;
54+ const Slots = setSlots (
55+ 'prefix' ,
56+ 'suffix' ,
57+ 'helper-text' ,
58+ 'value-missing' ,
59+ 'too-long' ,
60+ 'too-short' ,
61+ 'custom-error' ,
62+ 'invalid'
63+ ) ;
64+
5265/**
5366 * This element represents a multi-line plain-text editing control,
5467 * useful when you want to allow users to enter a sizeable amount of free-form text,
@@ -92,43 +105,25 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
92105
93106 //#region Private properties and state
94107
95- private static readonly increment = createCounter ( ) ;
108+ private readonly _inputId = `textarea- ${ nextId ++ } ` ;
96109
97110 private readonly _themes = addThemingController ( this , all ) ;
98111
112+ private readonly _slots = addSlotController ( this , {
113+ slots : Slots ,
114+ onChange : this . _handleSlotChange ,
115+ } ) ;
116+
117+ @query ( 'textarea' , true )
118+ private readonly _input ! : HTMLTextAreaElement ;
119+
99120 protected override get __validators ( ) {
100121 return textAreaValidators ;
101122 }
102123
103124 protected override readonly _formValue : FormValueOf < string > =
104125 createFormValueState ( this , { initialValue : '' } ) ;
105126
106- protected readonly _inputId = `textarea-${ IgcTextareaComponent . increment ( ) } ` ;
107-
108- @queryAssignedNodes ( { flatten : true } )
109- private readonly _projected ! : Node [ ] ;
110-
111- @queryAssignedElements ( {
112- slot : 'prefix' ,
113- selector : '[slot="prefix"]:not([hidden])' ,
114- } )
115- protected readonly _prefixes ! : HTMLElement [ ] ;
116-
117- @queryAssignedElements ( {
118- slot : 'suffix' ,
119- selector : '[slot="suffix"]:not([hidden])' ,
120- } )
121- protected readonly _suffixes ! : HTMLElement [ ] ;
122-
123- @query ( 'textarea' , true )
124- private readonly _input ! : HTMLTextAreaElement ;
125-
126- private get _resizeStyles ( ) : StyleInfo {
127- return {
128- resize : this . resize === 'auto' ? 'none' : this . resize ,
129- } ;
130- }
131-
132127 //#endregion
133128
134129 //#region Public properties and attributes
@@ -281,27 +276,6 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
281276
282277 //#endregion
283278
284- //#region Watchers
285-
286- @watch ( 'value' )
287- protected async _valueChanged ( ) : Promise < void > {
288- await this . updateComplete ;
289- this . _setAreaHeight ( ) ;
290- }
291-
292- @watch ( 'rows' , { waitUntilFirstUpdate : true } )
293- @watch ( 'resize' , { waitUntilFirstUpdate : true } )
294- protected _setAreaHeight ( ) : void {
295- if ( this . resize === 'auto' ) {
296- this . _input . style . height = 'auto' ;
297- this . _input . style . height = `${ this . _setAutoHeight ( ) } px` ;
298- } else {
299- Object . assign ( this . _input . style , { height : undefined } ) ;
300- }
301- }
302-
303- //#endregion
304-
305279 //#region Life-cycle hooks
306280
307281 constructor ( ) {
@@ -315,27 +289,16 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
315289 addSafeEventListener ( this , 'blur' , this . _handleBlur ) ;
316290 }
317291
318- protected override createRenderRoot ( ) : HTMLElement | DocumentFragment {
319- const root = super . createRenderRoot ( ) ;
320- root . addEventListener ( 'slotchange' , ( event ) =>
321- this . _handleSlotChange ( event )
322- ) ;
323- return root ;
292+ protected override updated ( props : PropertyValues < this> ) : void {
293+ if ( props . has ( 'rows' ) || props . has ( 'resize' ) || props . has ( 'value' ) ) {
294+ this . _setAreaHeight ( ) ;
295+ }
324296 }
325297
326298 //#endregion
327299
328300 //#region Internal methods
329301
330- protected _resolvePartNames ( ) {
331- return {
332- container : true ,
333- prefixed : this . _prefixes . length > 0 ,
334- suffixed : this . _suffixes . length > 0 ,
335- filled : ! ! this . value ,
336- } ;
337- }
338-
339302 private _setAutoHeight ( ) : number {
340303 const { borderTopWidth, borderBottomWidth } = getComputedStyle ( this . _input ) ;
341304 return (
@@ -345,16 +308,38 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
345308 ) ;
346309 }
347310
311+ protected _setAreaHeight ( ) : void {
312+ if ( this . resize === 'auto' ) {
313+ this . _input . style . height = 'auto' ;
314+ this . _input . style . height = `${ this . _setAutoHeight ( ) } px` ;
315+ } else {
316+ Object . assign ( this . _input . style , { height : undefined } ) ;
317+ }
318+ }
319+
320+ protected _resolvePartNames ( ) {
321+ return {
322+ container : true ,
323+ prefixed : this . _slots . hasAssignedElements ( 'prefix' , {
324+ selector : ':not([hidden])' ,
325+ } ) ,
326+ suffixed : this . _slots . hasAssignedElements ( 'suffix' , {
327+ selector : ':not([hidden])' ,
328+ } ) ,
329+ filled : ! ! this . value ,
330+ } ;
331+ }
332+
348333 //#endregion
349334
350335 //#region Event handlers
351336
352- protected _handleSlotChange ( { target } : Event ) : void {
353- const slot = target as HTMLSlotElement ;
354-
355- // Default slot used for declarative value projection
356- if ( ! slot . name ) {
357- const value = this . _projected
337+ private _handleSlotChange ( {
338+ isDefault ,
339+ } : SlotChangeCallbackParameters < InferSlotNames < typeof Slots > > ) : void {
340+ if ( isDefault ) {
341+ const value = this . _slots
342+ . getAssignedNodes ( '[default]' , true )
358343 . map ( ( node ) => node . textContent ?. trim ( ) )
359344 . filter ( ( node ) => Boolean ( node ) )
360345 . join ( '\r\n' ) ;
@@ -363,8 +348,6 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
363348 this . value = value ;
364349 }
365350 }
366-
367- this . requestUpdate ( ) ;
368351 }
369352
370353 protected _handleInput ( ) : void {
@@ -428,18 +411,14 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
428411
429412 //#region Renderers
430413
431- protected _renderPrefix ( ) {
432- return html `
433- < div part ="prefix " .hidden =${ isEmpty ( this . _prefixes ) } >
434- < slot name ="prefix "> </ slot >
435- </ div >
436- ` ;
437- }
414+ protected _renderSlot ( name : InferSlotNames < typeof Slots > ) {
415+ const isHidden = ! this . _slots . hasAssignedElements ( name , {
416+ selector : ':not([hidden])' ,
417+ } ) ;
438418
439- protected _renderSuffix ( ) {
440419 return html `
441- < div part =" suffix " . hidden =${ isEmpty ( this . _suffixes ) } >
442- < slot name =" suffix " > </ slot >
420+ < div part =${ name } ? hidden =${ isHidden } >
421+ < slot name =${ name } > </ slot >
443422 </ div >
444423 ` ;
445424 }
@@ -458,7 +437,8 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
458437 return html `
459438 ${ this . _renderLabel ( ) }
460439 < div part =${ partMap ( this . _resolvePartNames ( ) ) } >
461- ${ this . _renderPrefix ( ) } ${ this . _renderInput ( ) } ${ this . _renderSuffix ( ) }
440+ ${ this . _renderSlot ( 'prefix' ) } ${ this . _renderInput ( ) }
441+ ${ this . _renderSlot ( 'suffix' ) }
462442 </ div >
463443 ${ this . _renderValidationContainer ( ) }
464444 ` ;
@@ -473,23 +453,29 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
473453 placeholder : ! ! this . placeholder ,
474454 } ) }
475455 >
476- < div part ="start "> ${ this . _renderPrefix ( ) } </ div >
456+ < div part ="start "> ${ this . _renderSlot ( 'prefix' ) } </ div >
477457 ${ this . _renderInput ( ) }
478458 < div part ="notch "> ${ this . _renderLabel ( ) } </ div >
479459 < div part ="filler "> </ div >
480- < div part ="end "> ${ this . _renderSuffix ( ) } </ div >
460+ < div part ="end "> ${ this . _renderSlot ( 'suffix' ) } </ div >
481461 </ div >
482462 ${ this . _renderValidationContainer ( ) }
483463 ` ;
484464 }
485465
486466 protected _renderInput ( ) {
467+ const describedBy = this . _slots . hasAssignedElements ( 'helper-text' )
468+ ? 'helper-text'
469+ : nothing ;
470+
487471 return html `
488472 < slot style ="display: none "> </ slot >
489473 < textarea
490474 id =${ this . id || this . _inputId }
491475 part ="input"
492- style=${ styleMap ( this . _resizeStyles ) }
476+ style=${ styleMap ( {
477+ resize : this . resize === 'auto' ? 'none' : this . resize ,
478+ } ) }
493479 @input=${ this . _handleInput }
494480 @change=${ this . _handleChange }
495481 placeholder=${ ifDefined ( this . placeholder ) }
@@ -505,7 +491,8 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
505491 ?disabled=${ this . disabled }
506492 ?required=${ this . required }
507493 ?readonly=${ this . readOnly }
508- aria-invalid=${ this . invalid ? 'true' : 'false' }
494+ aria-invalid=${ this . invalid }
495+ aria-describedby=${ describedBy }
509496 > </ textarea >
510497 ` ;
511498 }
@@ -515,9 +502,11 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
515502 }
516503
517504 protected override render ( ) {
518- return this . _themes . theme === 'material'
519- ? this . _renderMaterial ( )
520- : this . _renderStandard ( ) ;
505+ return cache (
506+ this . _themes . theme === 'material'
507+ ? this . _renderMaterial ( )
508+ : this . _renderStandard ( )
509+ ) ;
521510 }
522511
523512 //#endregion
0 commit comments