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

2

7

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.

Flews answered 7/7, 2016 at 22:42 Comment(3)
Do you have a working example at a URL we can review? My first reaction is that I see two aria-live regions and that may throw things off, but without a page in full context this is not easy to debug.Diver
Are two live areas an anti-pattern? I'll work on adding a live example. I'll need to create some fake Ajax calls.Flews
It can be problematic, but must see in context: "When more than one area of a page updates, screen readers will not be able to determine the type of update that is occurring, the priority that one area may have over another in announcing alerts to the user, and whether the entire area or a just a portion of it should be announced. " SourceDiver
F
0

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.

Flotage answered 17/7, 2016 at 10:45 Comment(0)
L
1

You describe a situation, not a problem.

As long as your screenreader can read the content when the visual focus moves on it (using your AT shortcuts), then it's the expected result.

The screenreader won't move by itself to the checkbox list after you select a city. It's due to the way the visual focus of the screenreader is uncorrelated with the standard focus.

aria-live is something to make an announcement but won't let you interact with any inner control inside the same div and it would not move the visual focus to this element.

In your case you should describe the action of the form:

Chose a city in the list to select your favorite building

Lobation answered 13/7, 2016 at 8:12 Comment(0)
F
0

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.

Flotage answered 17/7, 2016 at 10:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.