@@ -81,6 +81,13 @@ export class Textarea implements ComponentInterface {
8181 */
8282 @State ( ) hasFocus = false ;
8383
84+ /**
85+ * Track validation state for proper aria-live announcements
86+ */
87+ @State ( ) isInvalid = false ;
88+
89+ private validationObserver ?: MutationObserver ;
90+
8491 /**
8592 * The color to use from your application's color palette.
8693 * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -328,6 +335,13 @@ export class Textarea implements ComponentInterface {
328335 }
329336 }
330337
338+ /**
339+ * Checks if the textarea is in an invalid state based on validation classes
340+ */
341+ private checkValidationState ( ) : boolean {
342+ return this . el . classList . contains ( 'ion-touched' ) && this . el . classList . contains ( 'ion-invalid' ) ;
343+ }
344+
331345 connectedCallback ( ) {
332346 const { el } = this ;
333347 this . slotMutationController = createSlotMutationController ( el , [ 'label' , 'start' , 'end' ] , ( ) => forceUpdate ( this ) ) ;
@@ -336,6 +350,25 @@ export class Textarea implements ComponentInterface {
336350 ( ) => this . notchSpacerEl ,
337351 ( ) => this . labelSlot
338352 ) ;
353+
354+ // Watch for class changes to update validation state
355+ if ( Build . isBrowser ) {
356+ this . validationObserver = new MutationObserver ( ( ) => {
357+ const newIsInvalid = this . checkValidationState ( ) ;
358+ if ( this . isInvalid !== newIsInvalid ) {
359+ this . isInvalid = newIsInvalid ;
360+ }
361+ } ) ;
362+
363+ this . validationObserver . observe ( el , {
364+ attributes : true ,
365+ attributeFilter : [ 'class' ] ,
366+ } ) ;
367+
368+ // Set initial state
369+ this . isInvalid = this . checkValidationState ( ) ;
370+ }
371+
339372 this . debounceChanged ( ) ;
340373 if ( Build . isBrowser ) {
341374 document . dispatchEvent (
@@ -364,6 +397,12 @@ export class Textarea implements ComponentInterface {
364397 this . notchController . destroy ( ) ;
365398 this . notchController = undefined ;
366399 }
400+
401+ // Clean up validation observer to prevent memory leaks
402+ if ( this . validationObserver ) {
403+ this . validationObserver . disconnect ( ) ;
404+ this . validationObserver = undefined ;
405+ }
367406 }
368407
369408 componentWillLoad ( ) {
@@ -628,22 +667,28 @@ export class Textarea implements ComponentInterface {
628667 * Renders the helper text or error text values
629668 */
630669 private renderHintText ( ) {
631- const { helperText, errorText, helperTextId, errorTextId } = this ;
670+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this ;
632671
633672 return [
634673 < div id = { helperTextId } class = "helper-text" >
635674 { helperText }
636675 </ div > ,
637- < div id = { errorTextId } class = "error-text" >
638- { errorText }
676+ < div
677+ id = { errorTextId }
678+ class = "error-text"
679+ role = { isInvalid && errorText ? 'alert' : undefined }
680+ aria-live = { isInvalid && errorText ? 'polite' : 'off' }
681+ aria-atomic = "true"
682+ >
683+ { isInvalid && errorText ? errorText : '' }
639684 </ div > ,
640685 ] ;
641686 }
642687
643688 private getHintTextID ( ) : string | undefined {
644- const { el , helperText, errorText, helperTextId, errorTextId } = this ;
689+ const { isInvalid , helperText, errorText, helperTextId, errorTextId } = this ;
645690
646- if ( el . classList . contains ( 'ion-touched' ) && el . classList . contains ( 'ion-invalid' ) && errorText ) {
691+ if ( isInvalid && errorText ) {
647692 return errorTextId ;
648693 }
649694
0 commit comments