[SalesForce] Adding aria-describedby to ui:inputText in Lightning Components

I'm trying to add an aria-describedby attribute to ui:inputText to programmatically associate some inline help text with the input field for assistive technology.

enter image description here

The resulting markup should look something like this:

<input type="text" aria-describedby="expnameHelp" />
<div id="expnameHelp">Some help text</div>

The relevant component markup currently looks like this:

<div class="slds-form-element slds-is-required">
  <div class="slds-form-element__control">
    <ui:inputText aura:id="expname" label="Expense Name"
      class="slds-input"
      labelClass="slds-form-element__label"
      value="{!v.newExpense.Name}"
      required="true" />
 </div>
 <div aura:id="expnameHelp" class="help-block">
   <p>Please use the format: Date - last name - purpose</p>
 </div>

However, there is no way I can see to set aria-describedby on the ui:inputText component, and according to this thread, there is no way to set a pass-through attribute. I would expect something like
ariaDescribedBy="expnameHelp" as an attribute on the ui:inputText tag, but this doesn't seem possible.

I thought maybe I could set the attribute dynamically using JavaScript, so I added the following to the controller:

doInit : function(component, event, helper) {
  // Programmatically associate help text with form element
  helper.updateHelpText(component);
}

And the following to the helper:

updateHelpText: function(component) {
  // Set the aria-describedby attribute on the expense name component
  var expnameField = component.find('expname');
  var expnameHelpDiv = component.find('expnameHelp');
  expnameHelpDiv.set('v.id', expnameHelpDiv.getGlobalId());
  expnameField.set('v.aria-describedby', expnameHelpDiv.getGlobalId());
}

I can now access the values of those attributes using JavaScript, but when I inspect the DOM the changes aren't visible.

What am I missing? Also, this seems like a convoluted approach — am I missing a simpler solution?

UPDATE #1: As per Peter's suggestion, I moved the updateHelpText() helper function call to afterrender (and also tried render, and rerrender), but the changes still don't show up in the DOM.

afterrender: function(cmp, helper) {
  var ret = this.superAfterRender();
  helper.updateHelpText(cmp);
}

UPDATE #2: Thanks to Peter's additional comments, I have the following now working. However, I'm wondering if there is a better way to reference the actual input HTML element in the component?

Renderer JS:

// Case is important!
afterRender: function(cmp, helper) {
  this.superAfterRender();
  helper.updateHelpText(cmp);
}

Helper JS:

updateHelpText: function(component) {
  // Set the aria-describedby attribute on the expense name component -
  var expnameElm = component.find('expname').getElement();

  // Turns out component.find('expname').getElement() returns the
  // div that contains the input field, not the actual input field
  // so getting that via getElementsByTagName - better way?
  expnameElm.getElementsByTagName('input')[0].setAttribute('aria-describedby', 'expnameHelp');
}

Best Answer

As a heads up this direct reaching inside of a component's internals that you do not own is not supported and with the upcoming security model enhancement called LockerService this code will no longer even be possible.

This line will fail:

var expnameElm = component.find('expname').getElement();

and even if you tried to go directly to the DOM you are going to be blocked by the security infrastructure that applies fine grained object capability locking to the DOM (and all Lightning APIs too).

because only the ui:inputText should depend on its internal private parts otherwise there is no way for the component's author to maintain API compatibility release over release. This is the tradeoff for having push upgrades in an enterprise grade environment - everything needs to be formal API contracts or there is no way that Salesforce can guarantee backward compatibility.

I have brought this to the attention of the folks that own the ui:visible interface that defines v.ariaDescribedBy to see if they can add the access="GLOBAL" to this to make it visible so your original approach of just setting ariaDescribedBy= would work.

Related Topic