Search code examples
javascripthtmlaccessibilitywai-ariascreen-readers

How to create screen reader accessible AJAX regions and DOM insert/remove updates?


My user interaction process is as follows:

  1. The user is presented with a drop-down list containing a list of cities
  2. After selecting a city, an AJAX request gets all buildings in the city, and inserts them into a div (this AJAX request returns a list of checkboxes)
  3. The user then can check/uncheck a checkbox to add the city to a table that is on the same page. (This dynamically inserts/removes table rows)

Here is my select dropdown:

<label for="city-selector">Choose your favorite city?</label>
  <select name="select" size="1" id="city-selector" aria-controls="city-info">
  <option value="1">Amsterdam</option>
  <option value="2">Buenos Aires</option>
  <option value="3">Delhi</option>
  <option value="4">Hong Kong</option>
  <option value="5">London</option>
  <option value="6">Los Angeles</option>
  <option value="7">Moscow</option>
  <option value="8">Mumbai</option>
  <option value="9">New York</option>
  <option value="10">Sao Paulo</option>
  <option value="11">Tokyo</option>
</select>

Here is the ajax div that gets empties/populated:

<div role="region" id="city-info" aria-live="polite">
<!-- AJAX CONTENT LOADED HERE -->
</div>

Here is the checkbox list that gets placed inside the ajax div:

<fieldset id="building-selector" aria-controls="building-table">
  <legend>Select your favorite building:</legend>  
  <input id="fox-plaza" type="checkbox" name="buildings" value="fox-plaza">
  <label for="fox-plaza">Fox Plaza</label><br>
  <input id="chrysler-building" type="checkbox" name="buildings" value="chrysler-building">
  <label for="chrysler-building">Chrysler Building</label><br>
  <input id="empire-state-building" type="checkbox" name="buildings" value="empire-state-building">
  <label for="empire-state-building">Empire State Building</label><br>
</fieldset>

And finally the table that holds the cities that he user adds/removes

<table id="building-table" aria-live="polite">
  <caption>List of buildings you have selected</caption>

  <tr>
    <th scope="col">Building name</th>
    <th scope="col">Delete Building</th>
  </tr>

  <tr>
    <td>Empire State Building</td>
    <td><button>Delete</button> /td>
  </tr>

</table>

I thought I was on the right path by using aria-controls="" and aria-live="", but that doesn't seem to be enough for the screen reader to detect the changes. In fact, I don't know if I'm missing something in my markup, or if I need to trigger any alert events or anything like that, to make this work.


Solution

  • Please consider to use vue.js (light framework) or angular2 (heavy framework) to make this task. It will very simplify that kind of dom-js interaction that you need. Is use it with aria-live="polite" aria-atomic='true' and it works. Here is solution for your case in vue.js:

    https://jsfiddle.net/nuLjb4uc/8/

    HTML:

    In head section put

    <script type="text/javascript" src="https://vuejs.org/js/vue.min.js"></script>
    

    And in body

    <script type="x-template" id="my-template">
    
    <label for="city-selector">Choose your favorite city?</label>
      <select name="select" size="1" id="city-selector" aria-controls="city-info" @change="onCityChange($event)">
      <option disabled selected value>-- choose city --</option>
      <option v-for=" (index, city) of cities" value="{{index+1}}">{{ city }}</option>
    </select>
    
    <fieldset v-if="currentCityBuildings" id="building-selector" aria-controls="building-table">
      <legend>Select your favorite building:</legend>  
    
      <div v-for="building of currentCityBuildings" aria-live="polite" aria-atomic='true'>
        <input v-model="building.checked" id="{{ building.name }}" @click='selectBuilding(building)' value="{{ building.name }}"  type="checkbox" name="buildings">
        <label for="{{ building.name }}">{{ building.name }}</label><br>
      </div>
    </fieldset>
    
    <table v-if='checkedCityBuildings' id="building-table" aria-live="polite">
      <caption>List of buildings you have selected</caption>
    
      <tr>
        <th scope="col">Building name</th>
        <th scope="col">Delete Building</th>
      </tr>
    
      <tr v-for="building of checkedCityBuildings" aria-live="polite" aria-atomic='true'>
        <td>{{ building.name }}</td>
        <td><button @click='deleteBuilding(building)'>Delete</button></td>
      </tr>
    
    </table>
    
    </script>
    
    <div id="app"></div>
    

    JS:

    new Vue({
      el: '#app',
      data: {
        cities: ['Amsterdam','Buenos Aires', 'Delhi', 'Hong Kong', 'London', 'Los Angeles', 'Moscow', 'Mumbai', 'New York', 'Sao Paulo', 'Tokyo' ],
        currentCityBuildings: null,
        checkedCityBuildings: null,
      },
      template: '#my-template',
      methods: {
        onCityChange: function(e) {
            var index = e.target.value-1;
            // instead of below line, you should call proper AJAX function which, when it get data, put them into this.checkedCityBuildings like 'simulateAJAX function do.
            setTimeout(this.simulateAJAX(this.cities[index]), 1000); 
        },
    
        simulateAJAX: function(city) {
          this.checkedCityBuildings = null;
          this.currentCityBuildings = [];
          this.currentCityBuildings.push({name:'fox-plaza-'+city, checked:false});
          this.currentCityBuildings.push({name:'chrysler-building-'+city, checked:false});
          this.currentCityBuildings.push({name:'empire-state-building-'+city, checked:false});
        },
    
        selectBuilding: function(building) {
          building.checked = !building.checked;
          this.checkedCityBuildings = [];
          for(var b of this.currentCityBuildings) {
            if(b.checked) this.checkedCityBuildings.push(b);
          }
        },
    
        deleteBuilding: function(building) {
            this.selectBuilding(building)
        },
    
    
      },
    
    })
    

    I tested it on Chrome + ChromeVox plugin.