Registernavigationen mit ARIA veröffentlicht in 2015

Klassischer Tabpanel mit einer Linkliste

Registernavigationen müssen für Hilfsmittel wie Screenreader zugänglich gestaltet werden. Hierzu zählt:

  1. Nur der aktive Reiter steht in der Fokus-Reihenfolge. Innerhalb der Registerleiste wird der Fokus u.a. per Pfeiltasten bewegt.
  2. Die Reiterleiste und die Reiter benötigen eine Rolle (tablist bzw. tab), die Browser in den Accessibility-Tree ablegen können. Darüber hinaus benötigt der aktive Reiter eine semantische Kennzeichnung (aria-selected).
  3. Ein Reiter wird aktiviert (und der Inhalt der Registerseite ausgetauscht), wenn die Leer- oder Eingabetaste gedrückt wird. Die Aktivierung darf nicht durch bloßes Fokussieren stattfinden.
  4. Die Registerseiten benötigen eine Rolle (tabpanel) und eine Beschriftung. Wenn die Beschriftung durch den Reiter erfolgt, dann muss der Text des Reiters mit der Registerseite verknüpft werden.

Registernavigationen können im HTML unterschiedlich aufgebaut werden. Diese Variante setzt folgenden HTML-Aufbau und -Attribute voraus:

<div class="register">
  <ul class="registerleiste">
    <li id="beschriftung-id1" class="reiter"><a href="#id1" class="komponente">Beschriftung 1</a></li>
    <li id="beschriftung-id2" class="reiter"><a href="#id2" class="komponente">Beschriftung 2</a></li>
    <li id="beschriftung-id3" class="reiter"><a href="#id3" class="komponente">Beschriftung 3</a></li>
    ...
  </ul>
  <div id="id1" class="registerseite">
    <p>Inhalt für Registerseite 1.</p>
  </div>
  <div id="id2" class="registerseite">
    <p>Inhalt für Registerseite 2.</p>
  </div>
  <div id="id3" class="registerseite">
    <p>Inhalt für Registerseite 3.</p>
  </div>
  ...
</div>

Durch die Zuweisung von Rollen wird die Bedeutung der ursprünglichen HTML-Elemente vollständig verändert. Nachdem die HTML-Struktur mit JavaScript verarbeitet wird, muss die Registernavigation folgende Attribute aufweisen:

<div class="register">
  <element role="tablist">
    <element role="tab" aria-selected="true" aria-controls="id1" tabindex="0" id="beschriftung-id1">Beschriftung 1</element>
    <element role="tab" aria-controls="id2" tabindex="-1" id="beschriftung-id2">Beschriftung 2</element>
    <element role="tab" aria-controls="id3" tabindex="-1" id="beschriftung-id3">Beschriftung 3</element>
    ...
  </element>
  <element role="tabpanel" aria-labelledby="beschriftung-id1" id="id1">
    <p>Inhalt für Registerseite 1.</p>
  </element>
  <element role="tabpanel" aria-hidden="true" aria-labelledby="beschriftung-id2" id="id2">
    <p>Inhalt für Registerseite 2.</p>
  </element>
  <element role="tabpanel" aria-hidden="true" aria-labelledby="beschriftung-id3" id="id3">
    <p>Inhalt für Registerseite 3.</p>
  </element>
  ...
</div>

Die ARIA-Attribute haben folgende Bedeutung:

  • Die role-Attribute definieren die Semantik der Elemente neu. Damit können die Reiter und die Registerseiten semantisch identifiziert werden.
  • Mit aria-selected="true" wird das Element als aktives Element im Accessibility-Tree gekennzeichnet.
  • Das Attribut aria-controls mit der ID der zugehörigen Reiterseite als Wert erlaubt es Screenreadern, mit einem Tastenbefehl zur zugehörigen Registerseite zu springen.
  • Hat das tabindex-Attribut einen Wert von 0, so steht das Element in der Fokus-Reihenfolge. Ist hingegen der Wert -1, so kann das Element nur per JavaScript fokussiert werden. Damit kann gewährleistet werden, dass in der Reiterleiste nur das aktive Element in der Fokus-Reihenfolge steht. die Veränderung des Fokus erfolgt beispielsweise durch Pfeiltasten.
  • Mit dem aria-labelledby-Attribut wird ein Text als Beschriftung für die Registerseite herangezogen.
  • Mit aria-hidden="true" wird ein Element und alle seiner Kindknoten nicht an den Intern: Accessibility-Tree des Betriebssystems übertragen, d.h. die Inhalte sind zwar sichtbar, können aber nicht von Screenreadern erfasst werden (siehe dazu die Hinweise unten.

Fokus-Reihenfolge der Reiterleiste:

  • Wenn der Fokus in die Reiterleiste per Tab-Taste gesetzt wird, wird das aktive Element fokussiert. Ein erneutes Drücken der Tab-Taste führt zum nächsten interaktiven Element hinter der Reiterleiste.
  • Gleiches gilt, wenn mit Umschalt+Tab der Fokus in die Reiterleiste gesetzt wird, nur wird beim erneuten Drücken der Tastenkombination das nächste interaktive Element vor der Reiterleiste fokussiert.

Tastaturbedienung, wenn der Fokus in der Reiterleiste gesetzt ist:

  • Die Leertaste oder Eingabetaste aktiviert den fokussierten Reiter und ersetzt die Registerseite mit der zum Reiter zugehörigen Registerseite, ohne dabei den Fokus zu verändern.
  • Pfeiltaste nach rechts oder nach unten setzt den Fokus auf den nächsten Reiter. Ist der Fokus bereits auf den letzten Reiter, wird der Fokus auf den ersten Reiter gesetzt
  • Pfeiltaste nach links oder nach oben setzt den Fokus auf den vorherigen Reiter. Ist der Fokus bereits auf den ersten Reiter, wird der Fokus auf den letzten Reiter gesetzt
  • Die Pos1-Taste setzt den Fokus auf den ersten Reiter.
  • Die Ende-Taste setzt den Fokus auf den letzten Reiter.

Tastaturbedienung, wenn der Fokus in der Registerseite gesetzt ist:

  • Strg+Pfeil nach oben oder Strg+Pfeil nach links setzt den Fokus auf den zur Registerseite zugehörigen Reiter.
  • Strg+SeiteAuf setzt den Fokus auf den vorherigen Reiter und aktiviert sie. Wird die erste Registerseite bereits angezeigt, wird der letzte Reiter fokussiert und aktiviert.
  • Strg+SeiteAb setzt den Fokus auf den nächsten Reiter und aktiviert sie. Wird die letzte Registerseite bereits angezeigt, wird der erste Reiter fokussiert und aktiviert.

Hinweis: Die Tastenkombinationen für die Registerseite funktionieren nicht in Screenreadern (siehe dazu den einleitenden Beitrag Intern: Registernavigation für das Web).

Hinweise

Das CSS für dieses Widget muss für andere Webseiten angepasst werden. Zwei Aspekte dürfen dabei nicht vernachlässigt werden:

  1. Der Tastaturfokus muss sichtbar sein, was auch für den Kontrastmodus gilt. Für den Kontrastmodus wurde ein border-bottom für die Visualisierung in der Reiterleiste gewählt.
  2. Es gibt eine CSS-Eigenschaft, die in jedem Fall übernommen werden sollte: .registerseite[aria-hidden=true] {display:none;}.

Einzelne Reiter sollten mit ihren zugehörigen Registerseiten verknüpft werden. Die ARIA-Spezifikation gibt 2 alternative Möglichkeiten vor, die beide berücksichtigt wurden:

Dieses Beispiel verwendet eine Linkliste gefolgt von den Inhalten der Registerseiten. Andere Beispiele stehen zur Verfügung mit einem anderen Aufbau im HTML:

HTML

<div class="register">
  <ul class="registerleiste">
    <li id="beschriftung-id1" class="reiter"><a href="#id1" class="komponente">Beschriftung 1</a></li>
    <li id="beschriftung-id2" class="reiter"><a href="#id2" class="komponente">Beschriftung 2</a></li>
    <li id="beschriftung-id3" class="reiter"><a href="#id3" class="komponente">Beschriftung 3</a></li>
    ...
  </ul>
  <div id="id1" class="registerseite">
    <p>Inhalt für Registerseite 1.</p>
  </div>
  <div id="id2" class="registerseite">
    <p>Inhalt für Registerseite 2.</p>
  </div>
  <div id="id3" class="registerseite">
    <p>Inhalt für Registerseite 3.</p>
  </div>
  ...
</div>

JavaScript (jQuery)

      function tabpanelInitialisieren( containerClass, tablistClass, tabClass, linkClass, tabpanelClass, tabpanelLabelPrefix ) {
        tabNavigationErstellen( containerClass, tablistClass, tabClass, linkClass, tabpanelClass, tabpanelLabelPrefix );
        $( '.' + tablistClass ) .children( '[role="tab"]' ) .each( function() {
          var reiterID = $( this ) .attr( 'id' );
          reiterEvents( reiterID );
          registerseiteEvents( reiterID, tabpanelLabelPrefix );
        });
        reiterAktualisierung( $( '.' + tablistClass ) .children( '[role="tab"]' ) .first() .attr( 'id' ) );
      }
      function tabNavigationErstellen( containerClass, tablistClass, tabClass, linkClass, tabpanelClass, tabpanelLabelPrefix ) {
        $( '.' + containerClass ) .each( function() {
          var $registerleiste = $( this ) .children( '.' + tablistClass ), $registerseiten = $( this ) .children( '.' + tabpanelClass );
          $registerleiste .attr( 'role', 'tablist' )
          .children( '.' + tabClass ) .each( function() {
            var $reiter = $( this ), reiterID = $reiter .attr( 'id' ), $sprunglink = $reiter .find( '.' + linkClass ), seitenID = $sprunglink .attr( 'href' ) .replace( '#', '' );
            $reiter .attr({
              'role': 'tab',
              'aria-controls': seitenID
            })
            .append( $sprunglink .contents() );
            $sprunglink .remove();
          });
          $registerseiten .each( function() {
            var $seite = $( this ), reiterID = tabpanelLabelPrefix + $seite .attr( 'id' );
            $seite .attr({
              'role': 'tabpanel',
              'tabindex': -1,
              'aria-labelledby': reiterID
            });

          });
        })
      }
      function reiterAktualisierung( reiterID ) {
        var $reiter = $( '#' + reiterID ), $alleReiter = $reiter .closest( '[role="tablist"]' ) .children( '[role="tab"]' );
        $alleReiter .attr({
          'aria-selected': 'false',
          'tabindex': -1
        })
        .removeAttr( 'accesskey' );
        $reiter .attr({
  'accesskey': 5,
  'aria-selected': 'true',
  'tabindex': 0
        });
        $alleReiter .each(function() {
  var seitenID = $( this ) .attr( 'aria-controls' );
          if ( $( this ) .attr( 'aria-selected' ) == 'true' ) {
            $( '#' + seitenID ) .attr( 'aria-hidden', 'false' );
          }
          else {
            $( '#' + seitenID ) .attr( 'aria-hidden', 'true' );
          }
        })
      }
      function reiterEvents( reiterID ) {
        $( '#' + reiterID ) .click( function( event ) {
          reiterAktualisierung( reiterID );
        })
        .keydown( function( event ) {
          var $aktuellerReiter = $( '#' + reiterID ), $alleReiter = $aktuellerReiter .closest( '[role="tablist"]' ) .children( '[role="tab"]' );
          if (event.keyCode == 13 || event.keyCode == 32 ) {
            $aktuellerReiter .click() .focus();
            event .preventDefault();
          }
          else if (event.keyCode == 9  && !event.shiftKey) {
            var seitenID = $( '#' + reiterID) .closest( '[role="tablist"]' ) .find( '[aria-selected="true"]' ) .attr( 'aria-controls' );
            $( '#' + seitenID ) .focus();
            event .preventDefault();
          }
          else if ( event.keyCode == 35 ) {
            $alleReiter .last() .focus();
            event .preventDefault();
          }
          else if ( event.keyCode == 36 ) {
            $alleReiter .first() .focus();
            event .preventDefault();
          }
          else if (event.keyCode == 37 || event.keyCode == 38 ) {
            if ( $aktuellerReiter .is ( $alleReiter .first() ) ) {
            $alleReiter .last() .focus();
            event .preventDefault();
            }
            else {
              $aktuellerReiter .prev() .focus();
              event .preventDefault();
            }
          }
          else if (event.keyCode == 39 || event.keyCode == 40 ) {
            if ( $aktuellerReiter .is ( $alleReiter .last() ) ) {
            $alleReiter .first() .focus();
            }
            else {
              $aktuellerReiter .next() .focus();
            }
          }
        })
      }
      function registerseiteEvents( reiterID, tabpanelLabelPrefix ) {
        var registerseiteID = reiterID .replace( tabpanelLabelPrefix, '' ), $registerseite = $( '#' + registerseiteID ), $aktuellerReiter = $( '#' + reiterID ), $alleReiter = $aktuellerReiter .closest( '[role="tablist"]' ) .children( '[role="tab"]' );
        $registerseite .keydown( function( event ) {
          if ((event.ctrlKey) && ((event.keyCode == 37 || event.keyCode == 38))) {
            $aktuellerReiter .focus();
            event.preventDefault();
          }
          else if (event.ctrlKey && event.keyCode == 33) {
            event .preventDefault();
            if ( $aktuellerReiter .is( $alleReiter .first() ) ) {
              $alleReiter .last() .click() .focus();
            }
            else {
              $aktuellerReiter .prev() .click() .focus();
            }
          }
          else if (event.ctrlKey && event.keyCode == 34) {
            event .preventDefault();
            if ( $aktuellerReiter .is( $alleReiter .last() ) ) {
              $alleReiter .first() .click() .focus();
            }
            else {
              $aktuellerReiter .next() .click() .focus();
            }
          }
        })
      }

      tabpanelInitialisieren(         'register', 'registerleiste', 'reiter', 'komponente', 'registerseite', 'beschriftung-' );

CSS

.register {
  width: 100%;
  background: #fff;
}
.register [role=tablist] {
  display: block;
  width: 100%;
}
.register [role=tab] {
  border-color: #6fbed6;
  border-style: solid;
  border-width: 1px 1px 0;
  border-top-right-radius: 3px;
  border-top-left-radius: 3px;
  margin-right: .4em;
  display:block;
  float:left;
  width:15%;
  color: #fff;
  background: #005f87;
  text-align:center;
  cursor: pointer;
  padding: 10px 2px;
  font-weight: bold;
  font-size: .9em;
}
.register [role=tab]:focus {
  outline: 2px dotted #000;
}
.register [role=tab]:hover,
.register [role=tab]:focus {
  padding-bottom: 8px;
  border-bottom: 2px dotted #005f87;
}
.register [role=tab][aria-selected=true] {
  background: #ddf0fa;
  color:#000;
  cursor: default;
  padding-bottom: 7px;
  border-bottom: 3px solid #ddf0fa;
  font-size: 1em;
}
.register [role=tab][aria-selected=true]:hover {
  text-decoration:none;
}
.register [role=tabpanel] {
  clear:left;
  padding: 0;
  border-top-right-radius: 3px;
  border-width: 0 1px 1px;
  border-color: #005f87;
  background: #ddf0fa;
  color: #000;
}
.register [role=tabpanel]:focus {
  border-width: 0 1px 1px;
  border-style:dashed;
  border-color:#000;
}
.register [role=tabpanel][aria-hidden=true] {
  display: none;
  }