Predictive Text

Predictive Text

The predictive text pattern allows users to type in values or to select values from a list of possible matches that appears after the user starts typing. Screen readers announce the availability of predictive text options, and users can select the options with keyboard, touch, or mouse.



Turn on a screen reader to experience this example in action.

Attribute/Option Description
mode This atrribute/option controls if the predictive text widget should be set in an auto mode(AUTO_HEIGHT_MODE) or a maximum height mode (MAX_HEIGHT_MODE). By default, it is set to auto mode.
max_height This atrribute/option specifies the maximum height at which the predictive text listbox should be set. This is a required option if the mode is set to MAX_HEIGHT_MODE.
highlight_color This atrribute/option specifies the color highlight to be used while selecting the option for the predictive text listbox should be set.
search_string_limit This atrribute/option specifies the minimum characters to be typed for activating the listbox within the predictive text.

HTML Source Code

<table class="data">
    <tr>
        <th width="150px" >Attribute/Option</th>
        <th>Description</th>
    </tr>
    <tr>
        <td>mode</td>
        <td>This atrribute/option controls if the predictive text widget should be set in an auto mode(AUTO_HEIGHT_MODE) or a maximum height mode (MAX_HEIGHT_MODE). 
            By default, it is set to auto mode.
        </td>
        </tr>
    <tr>
        <td>max_height</td>
        <td>This atrribute/option specifies the maximum height at which the predictive text listbox should be set. This is a required option if the mode is set to MAX_HEIGHT_MODE.
        </td>
    </tr>
    <tr>
        <td>highlight_color</td>
        <td>This atrribute/option specifies the color highlight to be used while selecting the option for the predictive text listbox should be set.
        </td>
    </tr>
    <tr>
        <td>search_string_limit</td>
        <td>This atrribute/option specifies the minimum characters to be typed for activating the listbox within the predictive text.
        </td>
    </tr>
    
</table>
<br/>
<div>
    <label for="stateInput" id="lblStateInput" style="width:100%;">Which state do you live in?</label>
    <div>
        <input  id="stateInput"  />
    </div>    
</div>

JavaScript Source Code

//Language object to be modified as per the language
var langText = {
    "errorNodeNotObject": "Node provided is not a DOM object.",
    "errorNodeNotInputObject": "Node provided is not an input HTML object.",
    "warningNoSearchItems":"Search items not provided."
}
var usStates = ['Alabama', 'Alaska', 'American Samoa',
'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut',
'Delaware', 'District of Columbia', 'Federated States of Micronesia',
'Florida', 'Georgia', 'Guam', 'Hawaii', 'Idaho', 'Illinois', 'Indiana',
'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Marshall Islands',
'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi',
'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey',
'New Mexico', 'New York', 'North Carolina', 'North Dakota',
'Northern Mariana Islands', 'Ohio', 'Oklahoma', 'Oregon', 'Palau',
'Pennsylvania', 'Puerto Rico', 'Rhode Island', 'South Carolina',
'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virgin Island',
'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'];


window.addEventListener('load', function () {
  
  var options = [];
  options["highlight_color"] = LIGHT_RED_1;
  options["mode"] = MAX_HEIGHT_MODE;
  options["max_height"] = 200;
  options["search_string_limit"] = 1;
  var input = document.getElementById("stateInput");
  statesPredictiveText = new PredictiveText(input, usStates, options);
});

const LIGHT_RED_1 = "#fce3e3";
const LIGHT_YELLOW_1 = "#ffeb3b69";
const LIGHT_GREEN_1 = "#34f40c52";

const AUTO_HEIGHT_MODE = 1;       
const MAX_HEIGHT_MODE = 2; 
class PredictiveText {
constructor(node, items, options) {
  // Check whether node is a DOM element
  if (typeof node !== 'object') {
      console.log(langText["errorNodeNotObject"]);
      return;
  }
  if( (typeof node.nodeName === "undefined") || ( node.nodeName.toLowerCase() !== "input" ))
  {
      console.log(langText["errorNodeNotInputObject"]);
      return;
  }
  this.node = node;
  this.parentNode = node.parentElement;
  this.searchItems = [];
  if(Array.isArray(items))
      this.searchItems = items;
  if (typeof options == "undefined") options = [];
  if (!Array.isArray(options))
  {
      options = [];
      console.log(langText["warningNoSearchItems"]);
  } 
  this.ulElement = null;
  if (this.searchItems.length > 0 )
  {
      var ulElement = document.createElement("ul");
      ulElement.style.minWidth = node.offsetWidth+"px";
      this.parentNode.appendChild(ulElement);
      ulElement.setAttribute("role","listbox");
      ulElement.setAttribute("aria-labelledby", "lblStateInput")
      ulElement.classList.add("option-list");
      ulElement.id =  crypto.randomUUID();
      this.ulElement = ulElement;
      this.hideList();
  }
  this.node.setAttribute("role", "combobox");
  this.node.setAttribute("type", "text");
  this.node.setAttribute("aria-expanded", "false");
  this.node.setAttribute("autocomplete", "off");
  this.node.setAttribute("aria-autocomplete", "list");
  this.node.setAttribute("aria-controls", ulElement.id );
  this.node.setAttribute("aria-owns", ulElement.id );

  var notifyElement = document.createElement("p");
  notifyElement.setAttribute("aria-live", "polite");
  notifyElement.classList.add("visually-hidden");
  this.parentNode.appendChild(notifyElement);
  this.notifyElement = notifyElement;
  this.highlightColor = "aliceblue";
  if(typeof options["highlight_color"] === "string")
      this.highlightColor = options["highlight_color"];
  this.mode = AUTO_HEIGHT_MODE;
  if(typeof options["mode"] === "number")
  {
      var val = options["mode"]; 
      if( val == MAX_HEIGHT_MODE)
      {
          if(typeof options["max_height"] === "number")
          {
              this.mode = val;
              this.maxHeight = options["max_height"];
              this.setMaxHeightMode(this.maxHeight);
          }
      }
      else
          this.setAutoMode();                    
  }
  this.searchStringLimit = 1;
  if( (typeof options["search_string_limit"] === "number") && (options["search_string_limit"] >= 1 ) )
      this.searchStringLimit = options["search_string_limit"];
  
  this.node.addEventListener('keyup', this.onKeyup.bind(this));
  this.ulElement.addEventListener('click', this.onClickOption.bind(this));
}

showList()
{
this.ulElement.style.display = "block";
this.node.setAttribute("aria-expanded", "true");
}
hideList()
{
this.ulElement.style.display = "none";
this.node.setAttribute("aria-expanded", "false");
}
setMaxHeightMode(maxHeight)
{
  this.ulElement.style.maxHeight = maxHeight+"px";
}
setAutoMode()
{
  this.ulElement.style.maxHeight = "";
}

optionMouseOver(event)
{
      this.selectOption(event.target.id);        
}
selectOption(idOption)
{
     var options = this.ulElement.getElementsByTagName("li");
     var selectedOption = null;
     for(var i=0; i < options.length; i++)
     {
          if(options[i].id == idOption )
          {
              options[i].setAttribute("aria-selected","true");
              options[i].style.backgroundColor = this.highlightColor;
              selectedOption = options[i];
              this.node.setAttribute("aria-activedescendant", selectedOption.id);
          }
          else
          {
              options[i].setAttribute("aria-selected","false");
              options[i].style.backgroundColor = "transparent";
          }
      }
      if( (this.mode == MAX_HEIGHT_MODE) && (selectedOption != null) )
          selectedOption.scrollIntoView({block: 'nearest'});

}
     
findOptionIndex(idOption)
{
     var options = this.ulElement.getElementsByTagName("li");
     for(var i=0; i < options.length; i++)
     {
          if(options[i].id == idOption )
              return i;
     }
     return -1; 
}
updateOptions()
{
      var searchText = this.node.value.trim();
      if( ( searchText == "") || (searchText.length < this.searchStringLimit ) )
      {
          this.hideList();
          return;
      }
      var list = [];
      for(let i=0; i < this.searchItems.length; i++)
          if(this.searchItems[i].toLowerCase().indexOf(searchText.toLowerCase()) != -1)
              list.push(this.searchItems[i]);
              
      this.ulElement.innerHTML = "";
      if(list.length == 0 )
      {
          this.hideList();
          return;        
      }
      for(var i=0; i < list.length; i++)
      {
          var item = document.createElement("li");
          item.innerHTML = list[i];
          item.id =  crypto.randomUUID();
          item.setAttribute("role","option");
          item.setAttribute("aria-selected","false");
          item.classList.add("option-item");
          var predictiveText = this;
          item.addEventListener("mouseover", function(event){ predictiveText.optionMouseOver(event);}) ;
          this.ulElement.appendChild(item);   
      }        
      this.showList();
      var isAre = list.length === 1 ? 'is' : 'are';
      var optionText = list.length === 1 ? 'option' : 'options';
      var message = 'There ' + isAre + ' currently ' + list.length + ' ' + optionText + ' containing text ' + searchText + '. Press down arrow to select an option';
      var self = this;
      this.announce(message, 600, 6000, "updateOptions");
      
    }

announce(message, initialDelay, msgTime, scope)
{
    var self=this;    
    this.message = message;
    if(typeof scope == "undefined") scope = "unknown";
    if(typeof this.scopeIds == "undefined" ) this.scopeIds = [];
    if( typeof this.scopeIds[scope] == "number")
    {
        clearTimeout(this.scopeIds[scope]);
        this.scopeIds[scope] = null;
    }
            
    if (initialDelay > 1)
    {
        this.scopeIds[scope] = setTimeout(function() {
            self.notifyElement.innerHTML = self.message;
        }, initialDelay);    
    }    
    else
        this.notifyElement.innerHTML = this.message;
    
    
    setTimeout(function() {
        self.notifyElement.innerHTML ="";
        this.message = "";
        self.scopeIds[scope] = null;
    }, msgTime);

}

onClickOption(event) 
{
      var selectedOption = this.ulElement.querySelector("li[aria-selected='true']");
      var self = this;
      if( selectedOption !== null )
      {
          this.node.value = selectedOption.innerHTML.trim();
          var itemPosition = this.findOptionIndex(selectedOption.id) + 1;
          if(itemPosition > 0)
          {
            var message = "Option "+itemPosition+" "+this.node.value+" selected";
            this.announce(message, 1, 3000, "onClickOption");
          }
          setTimeout(function(){ self.hideList(); }, 4000);  
          this.hideList();
      }   
}

onKeyup(event) 
{
      switch (event.key) 
      {                    
          case 'Enter':
            this.onClickOption(event);   
            break;
    
          case 'Escape':
              var selectedOption = this.ulElement.querySelector("li[aria-selected='true']");
              if( selectedOption !== null )
                  selectedOption.setAttribute("aria-selected","false");
              this.hideList();    
              break;
      
          case 'ArrowDown':
              var selectedOption = this.ulElement.querySelector("li[aria-selected='true']");
              var options = this.ulElement.getElementsByTagName("li");
              var nextOption = null;
              if( selectedOption == null )
                  this.selectOption(options[0].id);
              else
              {
                  var index = this.findOptionIndex(selectedOption.id);
                  if( (index != -1) && (index < (options.length-1) ) )
                      this.selectOption(options[index+1].id);
              }
              break;
      
          case 'ArrowUp':
              var selectedOption = this.ulElement.querySelector("li[aria-selected='true']");
              var options = this.ulElement.getElementsByTagName("li");
              var nextOption = null;
              if( selectedOption == null )
                  this.selectOption(options[0].id);
              else
              {
                  var index = this.findOptionIndex(selectedOption.id);
                  if( (index > 0 ) && (index < options.length ) )
                      this.selectOption(options[index-1].id);
              }
              break;
          
          case 'ArrowLeft':
              break;
          case 'ArrowRight':
              break;
              
          default:
              this.updateOptions();
              break;
      }
}


}  

CSS Source Code


#stateInput{
    padding: 5px;
    width: 200px;
}    

.option-list {
    width: max-content;
    list-style: none;
    padding: 3px;
    border: 1px solid gray;
    overflow: auto;
    margin-top: 0px;
}

.option-item { padding: 5px;  border-bottom: 1px solid lightgray; }
.visually-hidden{
    border: 0;
    clip: rect(0 0 0 0);
    height: 1px;
    margin: -1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

Copy and Paste Full Page Example