Building an accesible navigation bar: drop-down menu displaying a tab list

57 views Asked by At

I want to build a full accesible website and of course I can build it using all patterns provided in the follow guideline: https://www.w3.org/WAI/ARIA/apg/patterns

For the moment I'm trying to implement the main menu navigation in the header by combining the menu pattern with the tabs pattern. Here the two guideline: https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/ https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/

It seems that the common way to do build the menu is to follow the pattern with tags ul and li, adding extra attribute aria- and also some javascript code to enable the keyboard navigation. However, I want to show a tab list when the dropw-down menu is openend (see the screenshot).
At the end I did it (see the html code posted) but I don't understand if I've broken the accessibility or not. Basically I put the tabs pattern into the drop-down menu but I didn't find any example of it on the web and futhermore I tested the code above with WAVE Evaluation Tool chrome extension which dosn't give me any accessibility error. I'm still struggling if it is fine or not.

enter image description here

<html lang="en">
<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Bootstrap CSS -->
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">

  <!-- Bootstrap Icon library -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" integrity="sha384-He3RckdFB2wffiHOcESa3sf4Ida+ni/fw9SSzAcfY2EPnU1zkK/sLUzw2C5Tyuhj" crossorigin="anonymous">

  <style>
      [role="tabpanel"].is-hidden {
          display: none;
      }
  </style>

  <title>TITLE_GOES_HERE</title>
</head>
<body>
      <header>
          <nav aria-label="Mythical University" class="navbar navbar-expand-lg bg-light">
            <ul id="exTest" class="disclosure-nav navbar-nav me-auto mb-2 mb-lg-0">
              <li class="nav-item">
                <button type="button"
                        aria-expanded="true"
                        aria-controls="id_about_menu">
                  About
                </button>
                <ul id="id_about_menu">
                  <li>
                    <a href="#mythical-page-content">
                      Overview
                    </a>
                  </li>
                  <li>
                    <a href="#mythical-page-content">
                      Administration
                    </a>
                  </li>
                  <li>
                    <a href="#mythical-page-content">
                      Facts
                    </a>
                  </li>
                  <li>
                    <a href="#mythical-page-content">
                      Campus Tours
                    </a>
                  </li>
                </ul>
              </li>
              <li class="nav-item">
                <button type="button"
                        aria-expanded="true"
                        aria-controls="id_admissions_menu">
                  tabs
                </button>

              <div role="region" id="id_admissions_menu">
                <div class="tabs">
                  <h3 id="tablist-1">
                    Topics
                  </h3>                   
                  <div role="tablist" aria-labelledby="tablist-1" class="manual">
                    <button id="tab-1" type="button" role="tab" aria-selected="true" aria-controls="tabpanel-1">
                      <span class="focus">Maria Ahlefeldt</span>
                    </button>
                    <button id="tab-2" type="button" role="tab" aria-selected="false" aria-controls="tabpanel-2" tabindex="-1">
                      <span class="focus">Carl Andersen</span>
                    </button>
                    <button id="tab-3" type="button" role="tab" aria-selected="false" aria-controls="tabpanel-3" tabindex="-1">
                      <span class="focus">Ida da Fonseca</span>
                    </button>
                  </div>

                  <div id="tabpanel-1" role="tabpanel" aria-labelledby="tab-1">
                    <p>
                      <a href="https://en.wikipedia.org/wiki/Maria_Theresia_Ahlefeldt">Maria Theresia Ahlefeldt</a>
                      (16 January 1755 – 20 December 1810) was a Danish, (originally German), composer.
                      She is known as the first female composer in Denmark.
                      Maria Theresia composed music for several ballets, operas, and plays of the royal theatre.
                      She was given good critic as a composer and described as a “<span lang="da">virkelig Tonekunstnerinde</span>” ('a True Artist of Music').
                    </p>
                  </div>
                  <div id="tabpanel-2" role="tabpanel" aria-labelledby="tab-2" class="is-hidden">
                    <p>
                      <a href="https://en.wikipedia.org/wiki/Joachim_Andersen_(composer)">Carl Joachim Andersen</a>
                      (29 April 1847 – 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen.
                      Both as a virtuoso and as composer of flute music, he is considered one of the best of his time.
                      He was considered to be a tough leader and teacher and demanded as such a lot from his orchestras but through that style he reached a high level.
                    </p>
                  </div>
                  <div id="tabpanel-3" role="tabpanel" aria-labelledby="tab-3" class="is-hidden">
                    <p>
                      <a href="https://en.wikipedia.org/wiki/Ida_Henriette_da_Fonseca">Ida Henriette da Fonseca</a>
                      (July 27, 1802 – July 6, 1858) was a Danish opera singer and composer.
                      Ida Henriette da Fonseca was the daughter of Abraham da Fonseca (1776–1849) and Marie Sofie Kiærskou (1784–1863).
                      She and her sister Emilie da Fonseca were students of Giuseppe Siboni, choir master of the Opera in Copenhagen.
                      She was given a place at the royal Opera alongside her sister the same year she debuted in 1827.
                    </p>
                  </div>
                </div>
              </div>

              </li>
            </ul>
          </nav>
      </header>

  <!-- Bootstrap Bundle with Popper -->
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
  <script>
/*
*   This content is licensed according to the W3C Software License at
*   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
*   Supplemental JS for the disclosure menu keyboard behavior
*/

'use strict';

class DisclosureNav {
constructor(domNode) {
  this.rootNode = domNode;
  this.controlledNodes = [];
  this.openIndex = null;
  this.useArrowKeys = true;
  this.topLevelNodes = [
    ...this.rootNode.querySelectorAll(
      '.main-link, button[aria-expanded][aria-controls]'
    ),
  ];

  this.topLevelNodes.forEach((node) => {
    // handle button + menu
    if (
      node.tagName.toLowerCase() === 'button' &&
      node.hasAttribute('aria-controls')
    ) {
      const menu = node.parentNode.querySelector('ul');
      if (menu) {
        // save ref controlled menu
        this.controlledNodes.push(menu);

        // collapse menus
        node.setAttribute('aria-expanded', 'false');
        this.toggleMenu(menu, false);

        // attach event listeners
        menu.addEventListener('keydown', this.onMenuKeyDown.bind(this));
        node.addEventListener('click', this.onButtonClick.bind(this));
        node.addEventListener('keydown', this.onButtonKeyDown.bind(this));
      } else {
        const menu = node.parentNode.querySelector('div[role="region"]');
        if (menu) {
          // save ref controlled menu
          this.controlledNodes.push(menu);

          // collapse menus
          node.setAttribute('aria-expanded', 'false');
          this.toggleMenu(menu, false);

          // attach event listeners
          menu.addEventListener('keydown', this.onMenuKeyDown.bind(this));
          node.addEventListener('click', this.onButtonClick.bind(this));
          node.addEventListener('keydown', this.onButtonKeyDown.bind(this));
        }
      }
    }
    // handle links
    else {
      this.controlledNodes.push(null);
      node.addEventListener('keydown', this.onLinkKeyDown.bind(this));
    }
  });

  this.rootNode.addEventListener('focusout', this.onBlur.bind(this));
}

controlFocusByKey(keyboardEvent, nodeList, currentIndex) {
  switch (keyboardEvent.key) {
    case 'ArrowUp':
    case 'ArrowLeft':
      keyboardEvent.preventDefault();
      if (currentIndex > -1) {
        var prevIndex = Math.max(0, currentIndex - 1);
        nodeList[prevIndex].focus();
      }
      break;
    case 'ArrowDown':
    case 'ArrowRight':
      keyboardEvent.preventDefault();
      if (currentIndex > -1) {
        var nextIndex = Math.min(nodeList.length - 1, currentIndex + 1);
        nodeList[nextIndex].focus();
      }
      break;
    case 'Home':
      keyboardEvent.preventDefault();
      nodeList[0].focus();
      break;
    case 'End':
      keyboardEvent.preventDefault();
      nodeList[nodeList.length - 1].focus();
      break;
  }
}

// public function to close open menu
close() {
  this.toggleExpand(this.openIndex, false);
}

onBlur(event) {
  var menuContainsFocus = this.rootNode.contains(event.relatedTarget);
  if (!menuContainsFocus && this.openIndex !== null) {
    this.toggleExpand(this.openIndex, false);
  }
}

onButtonClick(event) {
  var button = event.target;
  var buttonIndex = this.topLevelNodes.indexOf(button);
  var buttonExpanded = button.getAttribute('aria-expanded') === 'true';
  this.toggleExpand(buttonIndex, !buttonExpanded);
}

onButtonKeyDown(event) {
  var targetButtonIndex = this.topLevelNodes.indexOf(document.activeElement);

  // close on escape
  if (event.key === 'Escape') {
    this.toggleExpand(this.openIndex, false);
  }

  // move focus into the open menu if the current menu is open
  else if (
    this.useArrowKeys &&
    this.openIndex === targetButtonIndex &&
    event.key === 'ArrowDown'
  ) {
    event.preventDefault();
    this.controlledNodes[this.openIndex].querySelector('a').focus();
  }

  // handle arrow key navigation between top-level buttons, if set
  else if (this.useArrowKeys) {
    this.controlFocusByKey(event, this.topLevelNodes, targetButtonIndex);
  }
}

onLinkKeyDown(event) {
  var targetLinkIndex = this.topLevelNodes.indexOf(document.activeElement);

  // handle arrow key navigation between top-level buttons, if set
  if (this.useArrowKeys) {
    this.controlFocusByKey(event, this.topLevelNodes, targetLinkIndex);
  }
}

onMenuKeyDown(event) {
  if (this.openIndex === null) {
    return;
  }

  var menuLinks = Array.prototype.slice.call(
    this.controlledNodes[this.openIndex].querySelectorAll('a')
  );
  var currentIndex = menuLinks.indexOf(document.activeElement);

  // close on escape
  if (event.key === 'Escape') {
    this.topLevelNodes[this.openIndex].focus();
    this.toggleExpand(this.openIndex, false);
  }

  // handle arrow key navigation within menu links, if set
  else if (this.useArrowKeys) {
    this.controlFocusByKey(event, menuLinks, currentIndex);
  }
}

toggleExpand(index, expanded) {
  // close open menu, if applicable
  if (this.openIndex !== index) {
    this.toggleExpand(this.openIndex, false);
  }

  // handle menu at called index
  if (this.topLevelNodes[index]) {
    this.openIndex = expanded ? index : null;
    this.topLevelNodes[index].setAttribute('aria-expanded', expanded);
    this.toggleMenu(this.controlledNodes[index], expanded);
  }
}

toggleMenu(domNode, show) {
  if (domNode) {
    domNode.style.display = show ? 'block' : 'none';
  }
}

updateKeyControls(useArrowKeys) {
  this.useArrowKeys = useArrowKeys;
}
}

/* Initialize Disclosure Menus */

window.addEventListener(
'load',
function () {
  var menus = document.querySelectorAll('.disclosure-nav');
  var disclosureMenus = [];

  for (var i = 0; i < menus.length; i++) {
    disclosureMenus[i] = new DisclosureNav(menus[i]);
  }

  // listen to arrow key checkbox
  var arrowKeySwitch = document.getElementById('arrow-behavior-switch');
  if (arrowKeySwitch) {
    arrowKeySwitch.addEventListener('change', function () {
      var checked = arrowKeySwitch.checked;
      for (var i = 0; i < disclosureMenus.length; i++) {
        disclosureMenus[i].updateKeyControls(checked);
      }
    });
  }

  // fake link behavior
  disclosureMenus.forEach((disclosureNav, i) => {
    var links = menus[i].querySelectorAll('[href="#mythical-page-content"]');
    var examplePageHeading = document.getElementById('mythical-page-heading');
    for (var k = 0; k < links.length; k++) {
      // The codepen export script updates the internal link href with a full URL
      // we're just manually fixing that behavior here
      links[k].href = '#mythical-page-content';

      links[k].addEventListener('click', (event) => {
        // change the heading text to fake a page change
        var pageTitle = event.target.innerText;
        examplePageHeading.innerText = pageTitle;

        // handle aria-current
        for (var n = 0; n < links.length; n++) {
          links[n].removeAttribute('aria-current');
        }
        event.target.setAttribute('aria-current', 'page');
      });
    }
  });
},
false
);
  </script>
  
  <script>
/*
*   This content is licensed according to the W3C Software License at
*   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
*   File:   tabs-manual.js
*
*   Desc:   Tablist widget that implements ARIA Authoring Practices
*/

'use strict';

class TabsManual {
constructor(groupNode) {
  this.tablistNode = groupNode;

  this.tabs = [];

  this.firstTab = null;
  this.lastTab = null;

  this.tabs = Array.from(this.tablistNode.querySelectorAll('[role=tab]'));
  this.tabpanels = [];

  for (var i = 0; i < this.tabs.length; i += 1) {
    var tab = this.tabs[i];
    var tabpanel = document.getElementById(tab.getAttribute('aria-controls'));

    tab.tabIndex = -1;
    tab.setAttribute('aria-selected', 'false');
    this.tabpanels.push(tabpanel);

    tab.addEventListener('keydown', this.onKeydown.bind(this));
    tab.addEventListener('click', this.onClick.bind(this));

    if (!this.firstTab) {
      this.firstTab = tab;
    }
    this.lastTab = tab;
  }

  this.setSelectedTab(this.firstTab);
}

setSelectedTab(currentTab) {
  for (var i = 0; i < this.tabs.length; i += 1) {
    var tab = this.tabs[i];
    if (currentTab === tab) {
      tab.setAttribute('aria-selected', 'true');
      tab.removeAttribute('tabindex');
      this.tabpanels[i].classList.remove('is-hidden');
    } else {
      tab.setAttribute('aria-selected', 'false');
      tab.tabIndex = -1;
      this.tabpanels[i].classList.add('is-hidden');
    }
  }
}

moveFocusToTab(currentTab) {
  currentTab.focus();
}

moveFocusToPreviousTab(currentTab) {
  var index;

  if (currentTab === this.firstTab) {
    this.moveFocusToTab(this.lastTab);
  } else {
    index = this.tabs.indexOf(currentTab);
    this.moveFocusToTab(this.tabs[index - 1]);
  }
}

moveFocusToNextTab(currentTab) {
  var index;

  if (currentTab === this.lastTab) {
    this.moveFocusToTab(this.firstTab);
  } else {
    index = this.tabs.indexOf(currentTab);
    this.moveFocusToTab(this.tabs[index + 1]);
  }
}

/* EVENT HANDLERS */

onKeydown(event) {
  var tgt = event.currentTarget,
    flag = false;

  switch (event.key) {
    case 'ArrowLeft':
      this.moveFocusToPreviousTab(tgt);
      flag = true;
      break;

    case 'ArrowRight':
      this.moveFocusToNextTab(tgt);
      flag = true;
      break;

    case 'Home':
      this.moveFocusToTab(this.firstTab);
      flag = true;
      break;

    case 'End':
      this.moveFocusToTab(this.lastTab);
      flag = true;
      break;

    default:
      break;
  }

  if (flag) {
    event.stopPropagation();
    event.preventDefault();
  }
}

// Since this example uses buttons for the tabs, the click onr also is activated
// with the space and enter keys
onClick(event) {
  this.setSelectedTab(event.currentTarget);
}
}

// Initialize tablist

window.addEventListener('load', function () {
var tablists = document.querySelectorAll('[role=tablist].manual');
for (var i = 0; i < tablists.length; i++) {
  new TabsManual(tablists[i]);
}
}); 
  </script>
</body>
</html>
0

There are 0 answers