Pour la bibliothèque jQuery-ui, on va créer des widgets qui vont étendre des composants déjà existants. Par exemple, pour créer un widget de boîte de dialogue qui va étendre le widget existant ui.dialog :
Une fois le widget créé, il faut éviter de ré-écrire tout le contenu des méthodes surchargées. Pour cela, on va utiliser la méthode this._super(); du même nom que le widget parent, avec les arguments spécifiés (s'il y en a). Voici la syntaxe à utiliser pour créer un widget JQuery-ui qui étend le composant ui.dialog, et qui surcharge la fonctione open: function() {…}
(function($){
$.widget( 'ui.dialog', $.ui.dialog, {
open: function(){
// surcharge avantthis._super(); // lance la fonction 'open' du widget ui.dialog parent// surcharge après
}
});
}(jQuery));
l’absence de titre du composant, aucun titre ne sera vocalisé lors de la prise de focus sur le composant ;
l’absence de valeur enrichie qui sera verbalisée à la place de la valeur courante de la barre de progression (par exemple, si la barre de progression est à 20%, les TA doivent pouvoir verbaliser “chargement en cours 20% - copie des fichiers”) et enfin si la barre de progression décrit la progression du chargement d’une zone particulière d’une page ;
la zone concernée doit pouvoir être informée de l’état de la progression, et l’internaute doit être informé que la zone concernée par la mise à jour est en train d’être modifiée.
Correctifs appliqués
Pour corriger les problèmes d’accessibilité, nous avons ajouté quatre nouveaux arguments lors de l’initialisation de la barre de progression :
ariaValuetextSuffix et ariaValuetextPrefix afin de pouvoir créer la valeur enrichie à partir de la valeur aria-value et ainsi la préfixer et la suffixer.
region : afin de définir si une zone particulière est mise à jour par le chargement de la barre de progression
labelledby : afin de définir une alternative au composant, ce paramètre peut être une chaîne de caractères ou alors une référence à un nœud du DOM.
Afin que le composant ait une description, on va étendre la fonction _create(){…} et tester si l’argument labelledby est défini, et si c’est le cas, on va de nouveau tester si c’est une chaîne de caractères :
si c'est une chaîne de caractères, on va ajouter un attribut title
sinon on ajoute l’attribut aria-labelledby
(function($){
$.widget('ui.progressbar', $.ui.progressbar, {
_create: function(event, index){
// Si un labelledby est définiif (typeof(this.options.labelledby !== typeofundefined)) {
if (jQuery.type(this.options.labelledby) === 'string') {
// si la région est définie en string, on ajoute un attr 'title'this.element.attr('title', this.options.labelledby);
} elseif (jQuery.type(this.options.labelledby) === 'object') {
// sinon, on ajout l'id du node associé dans le aria-labelledbythis.element.attr('aria-labelledby', this.options.labelledby[0].id);
}
}
// appel de la fonction _create du composant étenduthis._super(event, index);
},
});
}(jQuery));
Gestion d’une zone mise à jour par la barre de progression
Si l’attribut region est défini, on va ajouter un attribut aria-describedby sur cette zone afin de décrire la zone par le composant. À chaque fois qu’on mettera à jour la valeur de progression du composant, on va vérifier si la progression est en cours, on va ajouter l’attribut aria-busy et le définir à true.Quand la valeur de la barre de progression est à son maximum, on va de nouveau définir l’attribut aria-busy à false.
(function($){
$.widget('ui.progressbar', $.ui.progressbar, {
_create: function(event, index){
// Si une région est définieif (typeof(this.options.region !== typeofundefined)) {
// si la région est définie en string, on recherche le node avec l'id associéif (jQuery.type(this.options.region) === 'string') {
this.options.region = $('#' + this.options.region);
}
this.options.region.attr('aria-describedby', this.element[0].id);
}
// appel de la fonction _create du composant étenduthis._super(event, index);
},
_refreshValue: function(event, index){
// appel de la fonction _refreshValue du composant étenduthis._super(event, index);
if (typeof(this.options.region !== typeofundefined)) {
if (this.options.value === this.options.max) {
// Suppression de l'attribut aria-busy si on est arrivés au bout de la progressbarthis.options.region.attr('aria-busy', false);
} elseif (!this.indeterminate && this.options.value !== 0) {
this.options.region.attr('aria-busy', true);
}
}
}
});
}(jQuery));
Gestion de la description enrichie
Si les arguments ariaValuetextPrefix et ariaValuetextSuffix sont définis, on les utilise pour créer une description enrichie à partir de la valeur courante de la barre de progression en tant que préfixe et suffixe de cette valeur. Puis on met à jour cette valeur à chaque modification de la valeur du composant. On fait attention à bien supprimer cet attribut lorsque la barre de progression n’est pas ou plus active (c’est-à-dire que la progression est terminée ou pas encore lancée). On en profite aussi pour corriger une petite anomalie qui correspond à la non définition de l’attribut aria-valuemax s’il en existe un.
(function($){
$.widget('ui.progressbar', $.ui.progressbar, {
_create: function(event, index){
if (typeof(this.element.attr('aria-valuemax')) === typeofundefined) {
this.element.attr('aria-valuemax', this.options.max);
}
// appel de la fonction _create du composant étenduthis._super(event, index);
},
_destroy: function(event, index){
// Suppression de l'attribut aria-valuetext si on détruit la barre de progressionvar attr = this.element.attr('aria-valuetext');
if (typeof attr !== typeofundefined && attr !== false) {
this.element.removeAttr('aria-valuetext');
}
// appel de la fonction _destroy du composant étenduthis._super(event, index);
},
_refreshValue: function(event, index){
// appel de la fonction _refreshValue du composant étenduthis._super(event, index);
// Mise à jour de l'attribut aria-valuetextif (!this.indeterminate) {
var valuetext = this.options.value;
if (typeof(this.options.ariaValuetextPrefix !== typeofundefined)) {
valuetext = this.options.ariaValuetextPrefix + ' ' + valuetext;
}
if (typeof(this.options.ariaValuetextSuffix !== typeofundefined)) {
valuetext += ' ' + this.options.ariaValuetextSuffix;
}
this.element.attr({
'aria-valuetext': valuetext
});
}
}
});
}(jQuery));
aucun motif de conception n'est défini (role="slider"), ce qui implique que les TA ne savent pas comment implémenter leur assistance ;
l’absence de titre du composant : aucun titre ne sera vocalisé lors de la prise de focus sur le composant ;
l'absence d'indication de valeurs minimale et maximale du slider, ainsi que la valeur courante de la position du slider
l’absence de valeur enrichie qui doit être verbalisée à la place de la valeur courante du slider et qui peut être composée de la valeur courante, et d'un texte en suffixe ;
Correctifs appliqués
Pour corriger les problèmes d’accessibilité, nous avons ajouté deux nouveaux arguments lors de l’initialisation du slider :
ariaValuetext afin de pouvoir créer la valeur enrichie à partir de la valeur aria-value et ainsi la préfixer et la suffixer.
label : afin de définir une alternative au composant, ce paramètre peut être une chaîne de caractères ou alors une référence à un nœud du DOM. Cet attribut doit être un tableau afin de pouvoir définir deux valeurs dans le cas des sliders à intervalles. Même si ces derniers ne rentrent pas dans le cas de l'étude, on ne souhaite pas induire d'anomalies pour cette configuration de slider.
$( "#slider" ).slider({
ariaValuetext: '€',
label: ['curseur simple'] // chaine de caractères
});
$( "#slider2" ).slider({
ariaValuetext: '$',
label: [$('#slider_label')] // Référence à un noeud du DOM
});
On va étendre la fonction _createHandles(){…} et tester si l’argument labelledby est défini, et si c’est le cas, on va de nouveau tester si c’est une chaîne de caractères :
si c'est le cas, on ajoute un attribut title sur le composant
sinon on ajoute l’attribut aria-labelledby sur le composant
Attention, à la différence du composant progressbar, cet argument est un tableau JavaScript, par exemple : labelledby: ['value 1', 'value 2'] ; afin de rester compatible avec les sliders à intervalles (même s'i 'ils ne rentrent pas dans le cadre cette étude) et de ne pas attribuer deux fois le même titre sur les deux curseurs du composant.
Définition des valeurs minimale et maximale du composant et mise à jour de la valeur courante et de la valeur enrichie.
Lors de la création des curseurs, on va leur définir les attributs constants aria-valuemin et aria-valuemax.
Si le slider à une valeur définie par défaut, c'est aussi à ce moment qu'on va générer les attributs aria-valuenow et aria-valuetext (ce dernier n'est défini que si l'argument ariaValuetext est défini).
La mise à jour des attributs concernant la valeur courante et la valeur enriche se faisant à plusieurs endroits, cela a été externalisé dans une fonction _updateHandles{…} qui est appelée lors de la création des curseurs, et dans la fonction liée a leur déplacement (_slide{…}).
Lors de l'ouverture du composant, le focus n'est pas donné sur le premier élément focusable, cela pertube les utilisateurs du clavier ou d'autres TA. Car lors de l'ouverture d'un composant dialog, le contexte change, et la navigation au clavier est restreinte à l'intérieur du composant. Il faut donc qu'on place le focus sur le premier élément focusable comme si on naviguait dans une page à part entière.
Correctifs appliqués
Donc, pour corriger le problème d’accessibilité, il faut pouvoir identifier chaque composant de dialog, pour ce faire, nous allons utiliser un argument déjà existant "dialogClass" afin d'identifier la boite de dialog.
Attention, il faut être vigilant, car on utilise une class pour identifier la boîte de dialogue, ce qui n'est pas un élément unique dans la page. Mais l'argument pour attribuer une class est déjà présent dans la biliothèque du composant dialog, à la différence d'un identifiant.
Gestion du focus lors de l'ouverture de la boîte de dialogue
Lors de l'ouverture de la boîte de dialogue et après le test de la présence de l'attribut dialogClass, on redonne le focus sur le premier élément trouvé focusable.
Le titre possède la propriété aria-expanded="false" lorsque le panneau est masqué par défaut, mais si on ouvre puis on ferme un panneau alors la propriété aria-expanded reste à true alors qu'elle devrait être repassée à false. Cela signifie qu'en manipulant le composant, l'utilisateur peut avoir un décalage entre l'information qu'il perçoit et la réalité sur l'ouverture des panneaux.
Lors de la rédaction de ce tutoriel, la bibliothèque JQuery-ui à été mise a jour et est passée de la version 1.11.2 à la version 1.11.4, et dans le changelog de la version 1.11.3, il est indiqué une correction relative à l'attribut aria-expanded, nous n'avons pas encore testé la mise a jour, mais il se pourrait que cela corrige le seul soucis de ce composant. Voir le changelog de la version 1.11.3
Ajout d'un test typeof(data.newHeader[0]) === typeof undefined afin de savoir si aucun élément sélectionné après la mise a jour de l'attribut aria-expanded. Dans ce cas, on donne à l'attribut aria-expanded la valeur false. Ce test vérifie aussi si le panneau précédemment ouvert existe bien.
Le composant ne possède pas la propriété role="combobox"
Le composant ne possède pas la propriété aria-haspopup="false"
Le champ de saisie ne possède pas la propriété role="textbox" ?
Le champ de saisie ne possède pas la propriété aria-autocomplete="list" ?
Le champ de saisie ne possède pas la propriété aria-activedescendant="ID_option_selectionnée"
Le champ de saisie ne possède pas la propriété aria-owns="ID_liste"
Solution alternative
Pourquoi avoir choisi une solution alternative ?
Les nombre d'erreurs de ce composant, et la complexité de la mise en œuvre d'une correction ne nous permettent pas la correction de ce composant. L'alternative à un composant d'autocomplétion peut-être très variable en fonction de la nature de l'autocomplétion. Selon les cas il peut s'agir :
D'une liste déroulante (balise HTML select) la recherche par autocomplétion est alors remplacé par la recherche par caractères dans une liste déroulante.
D'une page regroupant tous les termes impactés par l'autocomplétion et traité sous la forme de lien de requête serveur. Là aussi le système d'autocomplétion est alors remplacé par la recherche plein texte dans la liste des liens.
Utiliser un élément datalist HTML 5, qui permet de monter une autocomplétion "accessible" mais, malheureusement son support est encore très limité par les navigateurs (voir le support de la balise datalist).
L’absence de description du composant, et de sémantique adaptée au composant, ce qui ne permet pas aux TA de savoir si une date est sélectionnée ou pas, ni de connaître le jour de la semaine et le mois lié à une date lorsqu'on parcrours le tableau :
Chaque groupe de titres de jours et de numéros ne possède pas d'attribut role="grid"
Chaque groupe de titres de jours et de numéros ne possède pas la propriété aria-labelledby="[ID_titre]" référençant le passage de texte contenant le mois et l'année du calendrier en cours. Donc lorsqu'une synthèse vocale passera sur les numéros de jour, aucune information sur le mois du jour qu'il est en train de parcourir ne sera donnée.
Aucun titre de jour ne possède un attribut role="columnheader"
Aucune ligne qui contient les numéros de jours ne possède un attribut role="row”
Aucun numéro de jour ne possède un attribut role="gridcell"
Pour caque jour sélectionné il ne possède pas la propriété aria-selected="true", et inversement.
Non conformité de l'ensemble des interactions clavier :
De l'extérieur du composant, l'utilisation de la touche Tabulation ne donne pas le focus sur le jour en cours.
Depuis le composant, l'utilisation de la touche Tabulation ne donne pas le focus sur l'élément focusable suivant à l'extérieur du composant.
Depuis un numéro de jour, l'utilisation de la touche de direction ↑ ne déplace pas le focus sur le même jour de la semaine précédente.
Depuis un numéro de jour, l'utilisation de la touche de direction ↓ ne déplace pas le focus sur le même jour de la semaine suivante
Depuis un numéro de jour, l'utilisation de la touche de direction → ne déplace pas le focus sur le jour suivant.
Depuis un numéro de jour, l'utilisation de la touche de direction ← ne déplace pas le focus sur le jour précedent.
Si le calendrier est une fenêtre, la touche Echap ne permet pas de fermer la fenêtre.
Lors de l'utilisation de la touche Echap, le focus n'est pas redonné, si nécessaire, à l'élément ayant ouvert le calendrier.
Si le calendrier est une fenêtre rattachée au champ, le focus n'est pas redonné au champ mis à jour lors de l'utilisation de la touche Entrée.
Solution alternative appliquée
Le datepicker doit être caché à la restitution d'écran, pour ne pas avoir à vocaliser l'intégralité du tableau de dates, grâce à l’attribut aria-hidden="true" ainsi que pour le bouton d'accès au composant ;
Interdire la navigation au clavier dans le composant avec l’attribut tabindex="-1" ainsi que pour le bouton d'accès au composant ;
Lors de la sélection d'une date, on replace le curseur dans le champ lié au composant.
Pourquoi avoir choisi une solution alternative ?
Étant donné le volume d'erreurs, et considérant que ce calendrier n'apporte pas d'informations (cf. il n'indique pas une sélection de jours réservables ou disponibles, toutes les dates sont sélectionnables), il est considéré comme un outil de confort ou d'assistance à la saisie. De ce fait, il est préférable de rendre invisible ce calendrier pour les personnes naviguant au clavier ou utilisant des TA plutôt que d'avoir a parcourir au clavier ou a vocaliser un tableau de dates relativemenet long et mal formaté.
Néanmoins, pour les personnes n'ayant pas la possibilité d'avoir l'aide de ce calendrier de saisie, il faut leur indiquer quel est le format de date attendu dans l'étiquette du champ de formulaire, et un exemple de date saisie avec l'attribut placeholder="…"
Mise en place de la solution alternative
Lors de l'appel du composant, l'attribut onClose est utilisé pour redonner le focus au champ de formulaire lorsque le composant est fermé : $( '#datepicker' ).focus();.
Dans notre exemple, le bouton d'ouverture du calendrier doit être rendu inaccessible au clavier : $('#datepicker').next().attr('tabindex', '-1');, et le calendrier lui-même ne doit pas être vocalisé : $('#ui-datepicker-div').attr('aria-hidden', 'true');, et les élements focusables ne doivent plus l'être : $('#ui-datepicker-div :focusable').attr('tabindex', '-1');.
(function($){
$( '#datepicker' ).datepicker({
showOn: 'button',
buttonText: 'Choisir une date',
onClose : function(){
$( '#datepicker' ).focus();
}
});
// Interdiction de la navigation au clavier dans le composant
$('#datepicker').next().attr('tabindex', '-1');
// Masquage du composant pour les technologies d'assitance
$('#ui-datepicker-div').attr('aria-hidden', 'true');
$('#ui-datepicker-div :focusable').attr('tabindex', '-1');
}(jQuery));
L'avis du développeur
Lors de l'écriture de ces correctifs, on peut voir qu'il est très simple de créer un widget, qui étend le fonctionnement d'un widget déjà existant afin d'en corriger le fonctionnement. Malheureusement, lorqu'il s'agit de modifier le DOM, cela peut s'avérer parfois très long, par exemple si on devait surclasser le fonction _generateHTML du widgetdatepicker, le temps a y passer serait très long. Donc parfois, pour certains composants dit "de confort", une solution alternative est préférable.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.