Corrections
Plusieurs améliorations sont nécessaires pour rendre ce composant accessible :
-
L'accordéon doit posséder un attribut
role="tablist"
afin d'indiquer sa fonction
- Chaque titre doit posséder un attribut
role="tab"
-
Chaque titre d'un panneau ouvert doit comporter un attribut
aria-selected="true"
(ou "false"
si le panneau est fermé) pour préciser son état.
- Chaque panneau doit posséder un attribut
role="tabpanel"
-
Chaque panneau doit posséder un attribut
aria-labelledby="id-du-titre"
pour le lier à son titre.
-
Chaque panneau ouvert doit posséder un attribut
aria-hidden="false"
(ou "true"
s'il est fermé) pour préciser son état.
-
Depuis un titre, les touches ↑ et ←
doivent permettre d'atteindre le titre précédent.
-
Depuis un titre, les touches ↓ et →
doivent permettre d'atteindre le titre suivant.
-
Depuis un titre, la touche Espace doit permettre d'ouvrir
ou de fermer le titre correspondant.
La méthode render()
retourne un Accordion
configuré avec la clé du panneau actif et un gestionnaire d'événement qui
sera appelé à chaque sélection de panneau.
var AccessibleAccordion = React.createClass({
render: function() {
return (
<Accordion
activeKey={this.state.activeKey}
onSelect={this.handleSelect}
>
{this.props.children}
</Accordion>
);
}
});
Il faut donc initialiser la variable d'état activeKey
.
Comme dans le composant original, sa valeur est donnée par la propriété
defaultActiveKey
.
getInitialState: function() {
return {
activeKey: this.props.defaultActiveKey || null
};
}
À l'initialisation du composant, on récupère les onglets et les panneaux
composant l'accordéon. On ajoute aussi des gestionnaires d'événements pour
gérer la navigation au clavier et le focus. On appelle finalement différentes
méthodes d'initialisation pour ne pas surcharger la méthode
componentDidMount()
.
componentDidMount: function() {
this.node = React.findDOMNode(this);
this.tabs = this.node.querySelectorAll('.panel-heading a');
this.panes = this.node.getElementsByClassName('panel-collapse');
this.node.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('focus', this.handleFocus, true);
this.setupAttributes();
this.setupPanesAttributes();
this.updatePanesAttributes();
}
Au démontage du composant, on détache les gestionnaires d'événements.
componentWillUnmount: function() {
this.node.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('focus', this.handleFocus, true);
}
La méthode setupAttributes()
initialise les attributs
permettant de préciser la fonction du composant.
setupAttributes: function() {
this.node.setAttribute('role', 'tablist');
this.node.setAttribute('aria-multiselectable', 'false');
}
La méthode setupPanesAttributes()
initialise les
attributs permettant de lier les titres et les panneaux, et de préciser
leur état.
setupPanesAttributes: function() {
for (var i = 0, l = this.tabs.length; i < l; i++) {
var tab = this.tabs[i];
var pane = this.panes[i];
var id = tab.getAttribute('id');
// si l'onglet n'a pas d'id, on lui en assigne un
// suivant son index
if (!id) {
id = 'tab-' + i;
tab.setAttribute('id', id);
}
tab.setAttribute('role', 'tab');
pane.setAttribute('role', 'tabpanel');
pane.setAttribute('aria-labelledby', id);
}
}
La méthode updatePanesAttributes()
met à jour les
attributs précisant l'état des titres et des panneaux. On se base ici sur
l'attribut aria-expanded
, déjà défini sur les
panneaux par la bibliothèque originale, pour en connaître l'état.
updatePanesAttributes: function() {
for (var i = 0, l = this.panes.length; i < l; i++) {
var tab = this.tabs[i];
var pane = this.panes[i];
var isActive = (pane.getAttribute('aria-expanded') === 'true');
pane.setAttribute('aria-hidden', isActive ? 'false' : 'true');
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
}
}
La méthode handleSelect()
permet d'ouvrir ou de
refermer un panneau de l'accordéon. Une fois l'état modifié, le focus est
donné au panneau ouvert. Finalement, on transmet l'événement si nécessaire.
handleSelect: function(key) {
// si le panneau demandé est déjà ouvert, on le referme
if (key === this.state.activeKey) {
key = null;
}
this.setState({
activeKey: key
}, function() {
this.updatePanesAttributes();
if (this.state.activeKey !== null) {
this.focusActiveTab();
}
});
if (this.props.onSelect) {
this.props.onSelect(key);
}
}
Dans la méthode précédente, on trouve un appel à focusActiveTab()
,
qui n'est pas encore définie. Cette méthode permet simplement de transporter le
focus sur le panneau actuellement ouvert. Elle utilise activeTabIndex()
,
qui retourne l'index du panneau ouvert en cherchant celui qui possède un attribut
aria-selected="true"
.
focusActiveTab: function() {
var index = this.activeTabIndex();
this.tabs[index].focus();
}
activeTabIndex: function() {
for (var i = 0, l = this.tabs.length; i < l; i++) {
if (this.tabs[i].getAttribute('aria-selected') === 'true') {
return i;
}
}
return 0;
}
La méthode handleFocus()
est appelée lorsqu'un événement
focus
se produit dans la page. Si le focus vient de
l'extérieur du composant vers l'intérieur, on le transporte sur le titre
actif :
handleFocus: function(event) {
if (!this.node.contains(this.focused) && this.node.contains(event.target)) {
this.focusActiveTab();
}
this.focused = event.target;
}
Il reste à traiter la navigation au clavier dans la méthode handleKeyDown()
.
Les flèches ↑ et ← transportent
le focus sur le titre précédent, les flèches ↓ et →
sur le titre suivant. La touche Espace ouvre ou ferme
le panneau courant.
handleKeyDown: function(event) {
// on ne traite l'événement que si on est sur un titre
if (event.target.getAttribute('role') !== 'tab') {
return;
}
switch (event.keyCode) {
case 37: // gauche
case 38: // haut
this.focusSiblingTab(-1);
break;
case 39: // droite
case 40: // bas
this.focusSiblingTab(1);
break;
case 32: // espace
event.target.click();
break;
default:
return;
}
event.preventDefault();
}
On utilise au-dessus la méthode focusSiblingTab()
qui
permet de donner le focus à un titre voisin du titre courant. Elle prend en
paramètre une direction : 1 pour sélectionner le suivant, -1 pour le précédent.
Cette méthode utilise focusedTabIndex()
, qui retourne
le titre ayant actuellement le focus.
focusSiblingTab: function(direction) {
var index = this.focusedTabIndex() + direction;
if (index < 0) {
index = this.tabs.length - 1;
}
if (index > this.tabs.length - 1) {
index = 0;
}
this.tabs[index].focus();
}
focusedTabIndex: function() {
for (var i = 0, l = this.tabs.length; i < l; i++) {
if (this.tabs[i] === document.activeElement) {
return i;
}
}
return 0;
}