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:
- Nur der aktive Reiter steht in der Fokus-Reihenfolge. Innerhalb der Registerleiste wird der Fokus u.a. per Pfeiltasten bewegt.
- 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
). - 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.
- 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 denAccessibility-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 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:
- 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. - 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:
- Wenn ein Reiter ein
aria-controls
-Attribut mit dem ID-Wert der zugehörigen Registerseite erhält, sollten Screenreadernutzer erfahren, mit welcher Tastenkombination sie direkt zur zugehörigen Registerseite wechseln können. Eine nennenswerte Unterstützung dieses Attributs kann derzeit allerdings nicht festgestellt werden. Das JavaScript für dieses Widget verwendet jedoch dieses Attribut; deswegen darf es nicht entfernt werden. - Die Registerseite sollte mit dem
aria-labelledby
-Attribut beschriftet werden. Die Beschriftung referenziert dabei den zugehörigen Reiter.
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;
}