Search code examples
accessibilitynvda

Why does my screen reader not announce the selected tab when activated


I'm working on making my existing tab component accessible, I'm basing my design off of the W3C's Example of Tabs with Manual Activation.

You can access my demo here

HTML

<div class="tab-container" lang="en">
  <div class="tabs" role="tablist">
    <button class="tab" aria-selected="true" href="#" role="tab" data-tab-name="tab1" tabindex="0">
      <!-- tab name -->
    </button>
    <!-- more tabs -->
  </div>
  <div class="tab-content" data-name="tab1" role="tabpanel" tabindex="0">
    <!-- tab panel content -->
  </div>
  <!-- more tab panels -->
</div>

JQuery

function getTabContent($tabContents, tabName) {
  return $tabContents.filter('[data-name="' + tabName + '"]');
}

function setSelectedTab($tab) {
  var tabName = $tab.data('tab-name'),
      $tabSet = $tab.closest('.tabs'),
      $tabContents = $tab.closest('.tab-container').find('.tab-content');

  // update the tab indices and aria attributes
  $tabSet.find('.tab').attr('aria-selected', 'false').attr('tabindex', '-1');
  $tab.attr('aria-selected', 'true').removeAttr('tabindex');

  $tabContents.addClass('hidden');
  getTabContent($tabContents, tabName).removeClass('hidden');
}

function handleTabSelection(event) {
  var $tab = $(event.target);

  if ($tab.data('tab-name')) {
    event.preventDefault();
    setSelectedTab($tab);
    $tab.focus();
  }
}

// Our tab control needs to be used in many places on our site, we cannot guarantee that all devs will use unique IDs
//   so we need to generate them here
function initTabs($tabContainer) {
  var $tabList = $tabContainer.find('.tabs'),
      $tabContents = $tabContainer.find('.tab-content'),
      tabSetName = $tabList.data.name,
      tabIdPrefix = 'tab-',
      contentIdPrefix = 'tab-content-';

  // add unique ids and labels
  $tabList.children().each(function() {
    var $tab = $(this),
        tabName = $tab.data('tab-name'),
        $tabContent = getTabContent($tabContents, tabName),
        tabId = getUniqueId(tabIdPrefix + tabName),
        contentId = getUniqueId(contentIdPrefix + tabName);
    // add the unique id and associate the link with the content
    $tab.attr('id', tabId).attr('aria-controls', contentId);
    // add the unique id and use the link as the label for the content
    $tabContent.attr('id', contentId).attr('aria-labelledby', tabId);
  });
}

function getUniqueId(id, index) {
  var newId = id;
  if (index) {
    newId += '--' + index;
    index++;
  } else {
    index = 1;
  }
  if (document.getElementById(newId)) {
    return getUniqueId(id, index);
  }
  return newId;
}

function handleKeyPress(event) {
  var $tab = $(event.target);
  if ($tab.is('.tab')) {
    var keyCode = event.which,
        $tab = $(event.target);
    if (keyCode === 13 || keyCode === 32) {
      // user pressed enter, or space
      setSelectedTab($tab);
      event.preventDefault();
    } else if (keyCode === 37 || keyCode === 39) {
      // the user pressed left or right
      var $newTab = $tab[keyCode === 39 ? 'next' : 'prev']();

      // move the focus
      if ($newTab.length > 0) {
        $newTab.focus();
      }
      event.preventDefault();
    }
  }
}

$('.tabs').click(handleTabSelection);
$('.tabs').keyup(handleKeyPress);

$('.tab-container').each(function() {
  initTabs($(this));
});

A user can use the left and right keys to move focus within the tab list, and enter or space to select a tab.

When a user selects a tab however, the screen reader simply announces "selected" where on the W3C's example, it announces the tab name followed by "selected".

I'm testing using NVDA in Firefox and here are my steps to reproduce:

  1. Set the focus on the "Nils Frahm" tab
  2. Press TAB
  3. You should hear "Agnes Obel tab two of three"
  4. Press ENTER
  5. You should hear "Agnes Obel tab selected tab two of three"

This is exactly what happens in the W3C's example, but in mine, the final step only reads "selected".

I've tried to match their example as closely as possible but I have yet to figure out how to get my example to announce the tab name when activated.

What could cause NVDA to skip reading the tab name once it is activated?


Solution

  • I discovered how to solve the problem, but as of yet, not why the problem exists.

    When I add an after CSS rule on my selected tab, the screen reader starts reading the content when selected.

    .tab[aria-selected="true"]::after {
      content: '';
    }
    

    If I add the after tag to all tabs, the problem persists; it needs to only be on the selected element.

    My guess is that this is fooling the screen reader into thinking that the content has changed, so it reads the new tab name.

    Here is the working demo