@@ -48,6 +48,7 @@ export class Input implements ComponentInterface {
4848 private inputId = `ion-input-${ inputIds ++ } ` ;
4949 private helperTextId = `${ this . inputId } -helper-text` ;
5050 private errorTextId = `${ this . inputId } -error-text` ;
51+ private labelTextId = `${ this . inputId } -label` ;
5152 private inheritedAttributes : Attributes = { } ;
5253 private isComposing = false ;
5354 private slotMutationController ?: SlotMutationController ;
@@ -406,7 +407,12 @@ export class Input implements ComponentInterface {
406407 connectedCallback ( ) {
407408 const { el } = this ;
408409
409- this . slotMutationController = createSlotMutationController ( el , [ 'label' , 'start' , 'end' ] , ( ) => forceUpdate ( this ) ) ;
410+ this . slotMutationController = createSlotMutationController ( el , [ 'label' , 'start' , 'end' ] , ( ) => {
411+ this . setSlottedLabelId ( ) ;
412+ forceUpdate ( this ) ;
413+ } ) ;
414+
415+ this . setSlottedLabelId ( ) ;
410416 this . notchController = createNotchController (
411417 el ,
412418 ( ) => this . notchSpacerEl ,
@@ -721,16 +727,25 @@ export class Input implements ComponentInterface {
721727 }
722728
723729 private renderLabel ( ) {
724- const { label } = this ;
730+ const { label, labelTextId } = this ;
725731
726732 return (
727733 < div
728734 class = { {
729735 'label-text-wrapper' : true ,
730736 'label-text-wrapper-hidden' : ! this . hasLabel ,
731737 } }
738+ // Prevents Android TalkBack from focusing the label separately.
739+ // The input remains labelled via aria-labelledby.
740+ aria-hidden = { this . hasLabel ? 'true' : null }
732741 >
733- { label === undefined ? < slot name = "label" > </ slot > : < div class = "label-text" > { label } </ div > }
742+ { label === undefined ? (
743+ < slot name = "label" > </ slot >
744+ ) : (
745+ < div class = "label-text" id = { labelTextId } >
746+ { label }
747+ </ div >
748+ ) }
734749 </ div >
735750 ) ;
736751 }
@@ -743,6 +758,33 @@ export class Input implements ComponentInterface {
743758 return this . el . querySelector ( '[slot="label"]' ) ;
744759 }
745760
761+ /**
762+ * Ensures the slotted label element has an ID for aria-labelledby.
763+ * If no ID exists, we assign one using our generated labelTextId.
764+ */
765+ private setSlottedLabelId ( ) {
766+ const slottedLabel = this . labelSlot ;
767+ if ( slottedLabel && ! slottedLabel . id ) {
768+ slottedLabel . id = this . labelTextId ;
769+ }
770+ }
771+
772+ /**
773+ * Returns the ID to use for aria-labelledby on the native input,
774+ * or undefined if aria-label is explicitly set (to avoid conflicts).
775+ */
776+ private getLabelledById ( ) : string | undefined {
777+ if ( this . inheritedAttributes [ 'aria-label' ] ) {
778+ return undefined ;
779+ }
780+
781+ if ( this . label !== undefined ) {
782+ return this . labelTextId ;
783+ }
784+
785+ return this . labelSlot ?. id || undefined ;
786+ }
787+
746788 /**
747789 * Returns `true` if label content is provided
748790 * either by a prop or a content. If you want
@@ -898,6 +940,7 @@ export class Input implements ComponentInterface {
898940 onCompositionend = { this . onCompositionEnd }
899941 aria-describedby = { this . getHintTextID ( ) }
900942 aria-invalid = { this . isInvalid ? 'true' : undefined }
943+ aria-labelledby = { this . getLabelledById ( ) }
901944 { ...this . inheritedAttributes }
902945 />
903946 { this . clearInput && ! readonly && ! disabled && (
0 commit comments