vendredi 29 mai 2015

WAI ARIA screenreader not reading dynamically added rows

I've scenario where I am trying to add rows dynamically to a table using Ajax. I've following table structure:

<table class="user-table table" id="usersTable">
    <thead>
      <tr>
        <th class="user-col user-name">Name</th>
        <th class="user-col user-email">Email</th>
        <th class="user-col user-status">Admin</th>
        <th class="user-col user-i-col">Active</th>
        <th class="user-col user-i-col">Actions</th>
      </tr>
    </thead>
</table>

Now I am trying to dynamically add/remove rows to/from table using JavaScript/jQuery. There are basically 2 different ways I am doing this:

  1. looping through JSON data, creating tbody, rows and cell and appending the tbody to target table using JavaScript OR
  2. using $(element).append(HTML) to append the HTML returned by Ajax call.

if I use JavaScript to create tbody, rows and data cells, NVDA correctly reads the added rows Ref: updateTableAsync . So if I add 3 rows to table this way, NVDA will read table with 4 rows and 5 columns, which is correct.

However, if I try to append the returned html directory into table using $.append or $.html, it does not identify the rows that were added dynamically. Ref: updateTableContentAsync

Here is the JSON that is returned:

{"users":[
    {"name":"ABC","email":"ABC@mailcatch.com","admin":true, "active":true,"action":"Remove" },
    {"name":"XYZ","email":"XYZ@mailcatch.com","admin":false, "active":false,"action":"Remove"},
    {"name":"ASDF","email":"ASDF@mailcatch.com","admin":false, "active":true,"action":"Remove"}
    ]
}

(function(global, window, $) {
      'use strict';
      //Accessible Ajax Loading Test
      var AALoadingTest = AALoadingTest || {};
      AALoadingTest.AjaxUtil = (function() {          
          
          
          var makeDataCell = function(row, index, data ) {
            var cell = row.insertCell(index);
            var cellText  = document.createTextNode(data)
            cell.appendChild(cellText);
          };

          var setFocus = function(element, fallbackElement) {
            var focusElement = document.getElementById(element); 
            if(typeof focusElement == 'undefined') {
              focusElement = document.getElementById(fallbackElement);
            }                
            window.setTimeout(function(){
              window.console.log(focusElement);
              focusElement.focus();
            }, 100);
          };

          //Method : 1 Update table content by looping through json data using native Javascript   
          var updateTableAsync = function(url, targetTable, targetContainer, focusContainer, announce) {
            
            //TODO: remove timeout when in prod, need while testing because locally changes are loading too fast
            window.setTimeout( function() {
              $.get(url, function(data){              
                
                var tempTargetTBody = document.getElementById(targetContainer);
                var target = document.getElementById(targetTable);
                var targetTBody = document.createElement('tbody');
                targetTBody.innerHTML = '';
                targetTBody.setAttribute('aria-live', tempTargetTBody.getAttribute('aria-live'));
                // targetTBody.setAttribute('aria-atomic', tempTargetTBody.getAttribute('aria-live'));
                tempTargetTBody.remove();
                targetTBody.setAttribute('id',targetContainer);
                
                $.each(data.users, function(key, value){
                  var row = targetTBody.insertRow(targetTBody.rows.length);
                  makeDataCell(row, 0, value.name);
                  makeDataCell(row, 1, value.email);
                  makeDataCell(row, 2, value.admin ? 'Yes': 'No');
                  makeDataCell(row, 3, value.active ? 'Yes': 'No');
                  makeDataCell(row, 4, value.action);
                });
                target.appendChild(targetTBody);           
              })
            },5000);
          };

          //Method : 2 Update table content by inserting HTML element by looping through JSON returned into table  
          var updateTableHTMLAsync = function(url, targetTable,  targetContainer, focusContainer, announce) {                         
            

            //TODO: remove timeout when in prod, need while testing because locally changes are loading too fast
            window.setTimeout( function() {
              $.get(url, function(data){              
                
                var targetElement = $('#'+targetContainer);
                targetElement.remove();
                targetElement = $('<tbody></tbody>').attr({'aria-live': 'polite', 'id': targetContainer});
                
                $.each(data.users, function(key, value) {
                  window.setTimeout(function() {
                    var row = $("<tr> </tr>");
                      targetElement.append(row);
                      row.append($("<td></td>").text(value.name));
                      row.append($("<td></td>").text(value.email));
                      row.append($("<td></td>").text((value.admin ? 'Yes': 'No')));
                      row.append($("<td></td>").text((value.active ? 'Yes': 'No')));
                      row.append($("<td></td>").text(value.action));
                  },100);
                });      
                $("#"+targetTable).append(targetElement);         
                
              })
            },5000);
          };

          //Method : 3 Update table content by inserting HTML returned into table  
          var updateTableContentAsync = function(url, targetTable,  targetContainer, focusContainer, announce) {                         
            

            //TODO: remove timeout when in prod, need while testing because locally changes are loading too fast
            window.setTimeout( function() {
              $.get(url, function(data){              
                
                var targetElement = $('#'+targetContainer);
                targetElement.remove();
                targetElement = $('<tbody></tbody>').attr({/*'aria-live': 'polite',*/ 'id': targetContainer});
                $("#"+targetTable).append(targetElement);                
                
                targetElement.append($(data));                
              })
            },5000);
          };

          return {
            updateTableAsync: updateTableAsync,
            updateTableHTMLAsync: updateTableHTMLAsync,
            updateTableContentAsync: updateTableContentAsync,
            removeLoader: removeLoader,
            loader: loader
          };
      })();
      
      $("#asyncHTML").on("click", function(e){ 
        e.stopPropagation();
        AALoadingTest.AjaxUtil.updateTableHTMLAsync(
          $(this).attr('data-resource'),
          $(this).attr('data-target-table'),
          $(this).attr('data-target-element'),
          $(this).attr('data-focus-element'),
          true
        );       
        return false;
      });

      $("#asyncJSON").on("click", function(e){ 
        e.stopPropagation();
        AALoadingTest.AjaxUtil.updateTableAsync(
          $(this).attr('data-resource'),
          $(this).attr('data-target-table'),
          $(this).attr('data-target-element'),
          $(this).attr('data-focus-element'),
          true
        );       
        return false;
      });

      $("#asyncHTMLContent").on("click", function(e){ 
          e.stopPropagation();
          AALoadingTest.AjaxUtil.updateTableContentAsync(
            $(this).attr('href'),
            $(this).attr('data-target-table'),
            $(this).attr('data-target-element'),
            $(this).attr('data-focus-element'),
            true
          );       
          return false;
      });


      })(this, window,jQuery);
<link href="http://ift.tt/1mDOveJ" rel="stylesheet"/>
<script src="http://ift.tt/1qRgvOJ"></script>
<script src="http://ift.tt/1h4yM0u"></script>
<main>
      <div class="container">     
        <div class="container-fluid">         
          
          <h1>Dynamic Tables</h1>        
          <p> Load JSON using Ajax get, use JavaScript to create tbody, rows and data cell and append the tbody to table using appendChild</p>
          <br>
            <a class="btn btn-primary async" id="asyncJSON" data-target-table="usersTableJSON"
                role="button" data-target-element="userTableBodyJSON" data-focus-element="asyncContentJSON" 
                href="javascript:;" data-resource="data/users.json">
                Load Users <span class="sr-only">Load json Data</span>
            </a>
          <br>
          <br>
          <div class="panel" id="asyncContentJSON">
            <table class="user-table table" id="usersTableJSON">
            <thead>
              <tr>
                <th class="user-col user-name">Name</th>
                <th class="user-col user-email">Email</th>
                <th class="user-col user-status">Admin</th>
                <th class="user-col user-i-col">Active</th>
                <th class="user-col user-i-col">Actions</th>
              </tr>
            </thead>
            <tbody id="userTableBodyJSON" aria-live="polite"></tbody>            
          </table>
          </div>

          <br>
          <p> Load JSON using Ajax get, use jQuery to create tbody, rows and data cell and append the tbody to table using $.append</p>
          <br>
            <a class="btn btn-primary async2" id="asyncHTML"
                role="button" data-target-element="userTableBodyHTML" data-target-table="usersTableHTML" data-focus-element="asyncContentHTML" 
                href="data/rows.html" data-resource="data/users.json">
                Load Users<span class="sr-only">Load HTML Data</span>
            </a>
          <br>

          <br>
          <div class="panel" id="asyncContentHTML">
            <table class="user-table table" id="usersTableHTML">
            <thead>
              <tr>
                <th class="user-col user-name">Name</th>
                <th class="user-col user-email">Email</th>
                <th class="user-col user-status">Admin</th>
                <th class="user-col user-i-col">Active</th>
                <th class="user-col user-i-col">Actions</th>
              </tr>
            </thead>
            <tbody id="userTableBodyHTML" aria-live="polite"></tbody>
          </table>
          </div>

          <br>
          <p> Load JSON using Ajax get, use JavaScript to create tbody, rows and data cell and append the tbody to table using $.html</p>
          <br>
            <a class="btn btn-primary async2" id="asyncHTMLContent"
                role="button" data-target-element="userTableBodyHTMLContent" data-target-table="usersTableHTMLContent" data-focus-element="asyncContentHTMLContent" 
                href="data/rows.html" data-resource="">
                Load Users<span class="sr-only">Load HTML View</span>
            </a>
          <br>
          <br>
          <div class="panel" id="asyncContentHTMLContent">
            <table class="user-table table" id="usersTableHTMLContent">
            <thead>
              <tr>
                <th class="user-col user-name">Name</th>
                <th class="user-col user-email">Email</th>
                <th class="user-col user-status">Admin</th>
                <th class="user-col user-i-col">Active</th>
                <th class="user-col user-i-col">Actions</th>
              </tr>
            </thead>
            <tbody id="userTableBodyHTMLContent" aria-live="polite"></tbody>
          </table>
          </div>
        </div>        
      </div>
      <div id="overlay" class="hidden">          
        <span class="visuallyHidden" id="loadingMessage" role="status" aria-hidden="true"></span>
      </div>        
    </main>

One thing that I suspect here is: when I manually add rows one by one, NVDA is aware of individual DOM change, however, when I update the innerHTML all it know is "one update in DOM". Please let me know if someone has faced similar issue and how you fixed that.

Thanks

Aucun commentaire:

Enregistrer un commentaire