Nous allons étudier les composants d'interface d'Ionic en version 1.0.1 "vanadium-vaquita" (2015-06-30).
Il n’y a pas de documentation ou de composants dédiés à l’accessibilité dans Ionic, et l’une des seules références est une anomalie pour rendre Ionic accessible mais annulée car trop vague. On remarque que l'accessibilité n'est pas une priorité de l'équipe d'Ionic pour la version 1.0.1.
Les directives d'Ionic ne sont pas très flexibles car les templates ne sont pas customisables. Ce qui veut dire que toute correction devra se faire dans une nouvelle directive en recopiant la fautive. Dans le module et les directives, Ionic a renommé les fonctions angular pour simplifier l'écriture. En recopiant une directive il faudra donc migrer certaines fonctions. Voici la liste :
- extend = angular.extend
- forEach = angular.forEach
- isDefined = angular.isDefined
- isNumber = angular.isNumber
- isString = angular.isString
- jqLite = angular.element
- noop = angular.noop
Si nous copions une directive contenant forEach
, il faudra rennommer la fonction en angular.forEach
(Exemple avec ion-toggle).
En recopiant une directive, il faudra la renommer pour ne pas interférer avec l'ancienne.
Dans notre exemple, toutes les directives recopiées ont été renommées en ajoutant -ally
(exemple: ion-toggle
devient ion-toggle-ally
).
Le module CSS corrigera les règles erronées dans Ionic. Elles peuvent être de plusieurs natures :
- Les évémements sont bloqués par la propriété
pointer-events:none
et les clics ne sont plus transmis à l'application JavaScript ; - Un élément HTML n'est pas focusable et il n'est donc pas utilisable par le lecteur d'écran ;
- Les contrastes ne sont pas suffisants et il est nécessaire de les corriger.
Lors de l'utilisation du framework, les erreurs relevées sont :
- Les événements du lecteur d'écran sont interceptés par Ionic sous Android 4.3 et 4.4 empêchant toute intéraction par utilisateur.
- Il est impossible d'utiliser le scroll avec le lecteur d'écran iOS.
Lors des tests sous Android 4.3 et Android 4.4, il est impossible de cliquer ou de changer l’état d'une case à cocher avec talkback. Les événements sont interceptés par Ionic pour réduire le délai de latence des 300ms. http://blog.ionic.io/hybrid-apps-and-the-curse-of-the-300ms-delay/
Il faut donc désactiver cette interception en rajoutant l’attribut data-tap-disabled="true"
sur la balise body
.
<body ng-app="starter" data-tap-disabled="true">
…
</body>
La désactivation du délai de tap rend l'application beaucoup plus lente lorsque le lecteur d'écran est inactif. Il faut donc corriger cette impression de latence.
L'une des forces de l'implémentation hybride est de pouvoir accéder à une partie de l'API native depuis notre application écrite en JavaScript. On peut par exemple connaître l'état du lecteur d'écran pour corriger une erreur inhérente au framework Ionic.
Le but est de modifier l'attribut data-tap-disabled
en fonction du lecteur d'écran.
angular.module('a11y-ionic', ['$mobileAccessibility'])
.run(function($rootScope, $mobileA11yScreenReaderStatus) {
function isScreenReaderRunningCallback(event, boolean) {
var element = document.body;
if (boolean) {
console.log("Screen reader: ON");
element.setAttribute("data-tap-disabled", "true");
} else {
console.log("Screen reader: OFF");
element.setAttribute("data-tap-disabled", "false");
}
}
$rootScope.$on('$mobileAccessibilityScreenReaderStatus:status', isScreenReaderRunningCallback);
})
De cette façon, si le lecteur d'écran est allumé, les événements click
ne seront pas bloqués par Ionic et on pourra changer l'état des checkbox. Et lorsque le lecteur d'écran sera éteint, le délai de 300 ms sera supprimé pour donner une impression d'application native.
Ionic fut créé lorsque les événements scroll
natifs web n'étaient pas implémentés. Ils ont dû pour cela réimplémenter les événements scroll
en JavaScript. Depuis, Android 4.1 a implémenté les événements scroll
natifs, mais les WebViews iOS ne supportent pas cette implémentation. Donc les scrolls VoiceOver dans iOS sont interceptés par Ionic. Cela empêche de défiler correctement vers le bas avec un lecteur d'écran.
Lorsque l'on désactive le CSS, le défilement fonctionne correctement. Il faut donc trouver la classe puis la propriété en erreur pour corriger cette anomalie.
Cette recherche peut se révéler très fastidieuse voire impossible sans l'aide du déboguage distant. Avec des essais successifs, on peut se rendre compte que les classes .scroll-content
et .pane
sont en cause, il faut revenir en position statique. De plus en ajoutant les classes .platform-ios
et .sr-on
, nous pouvons faire cette correction uniquement pour la plateforme iOS ayant un lecteur d'écran actif.
.platform-ios.sr-on .pane,
.platform-ios.sr-on .scroll-content{
position: static;
}
En ajoutant cette correction, il est nécessaire de faire un ajustement de style pour le header.
.platform-ios.sr-on .scroll-content{
overflow-y: auto;
}
.scroll-content.has-header > .scroll {
margin-top: 44px;
}
De cette façon, le défilement dans iOS fonctionne correctement. Néanmoins il est possible que d'autres régressions de style soient présentes dans des cas particuliers d'utilisation de Ionic. Le mieux serait une correction faite par l'équipe d'Ionic qui a une meilleure vue d'ensemble du projet.
Documentation des champs de formulaire Ionic : http://ionicframework.com/docs/components/#forms
Ionic fournit plusieurs styles d'input :
- Placeholder Labels
- Inline Labels
- Stacked Labels
- Floating Labels
- Inset Forms
- Inset Inputs
- Input Icons
- Header Inputs
Pour les composants Placeholder Labels, Inset Forms, Inset Inputs, Input Icons, Header Inputs
, les erreurs relevées sont :
- L'utlisation de placeholder n'est pas une alternative au label, en effet le placeholder n'est plus visible dès que l'on commence à remplir le champ ;
- Le label est vide.
Pour le composant Floating Labels
, l'erreur relevée est :
- L'étiquette de champ et son champ associé ne sont pas accolés. L'étiquette n'est pas visible.
Pour les composants Inline Labels
et Stacked Labels
, l'erreur relevée est :
- Absence de l'attribut
for
sur le label et l'id
correspondant sur l'input.
Les composants Placeholder Labels, Inset Forms, Inset Inputs, Input Icons, Header Inputs, Floating Labels
ne peuvent pas être corrigés sans changer complètement l'aspect voulu par Ionic.
Pour corriger les problèmes d'accessibilité sur les composants Inline Labels
et Stacked Labels
, nous avons ajouté l'attribut for
sur le label et l'id.
<div class="list">
<label for="username" class="item item-input">
<span class="input-label">Username</span>
<input id="username" type="text">
</label>
</div>
<div class="list">
<label for="first-name" class="item item-input item-stacked-label">
<span class="input-label">First Name</span>
<input id="first-name" type="text" placeholder="John">
</label>
</div>
Documentation de ion-checkbox dans Ionic: http://ionicframework.com/docs/1.0.1/api/directive/ionCheckbox/
Pour le composant ion-checkbox l'erreur relevée est :
- La checkbox n’est pas focusable avec le lecteur d’écran.
On va forcer la checkbox en display:block
puis la rendre visible uniquement au lecteur d’écran.
body .checkbox.checkbox-input-hidden input {
display: block !important;
}
.checkbox.checkbox-input-hidden input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
On peut effectuer les tests de restitution sous iOS et Android. La prise de focus fonctionne bien, le label est énoncé par le lecteur d’écran, son type (“case à cocher”) et son état (“non coché”).
Documentation de ion-toggle dans Ionic: http://ionicframework.com/docs/1.0.1/api/directive/ionToggle/
Pour le composant ion-toggle les erreurs relevées sont :
- L'input checkbox n’est pas focusable avec le lecteur d’écran.
- Le label est vide.
- Les événements
click
ne sont pas envoyé sous Android 5.0.
On va forcer la checkbox en display:block
puis la rendre visible uniquement au lecteur d’écran.
body .toggle input{
display: block !important;
}
.toggle input{
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
Nous voyons aussi que la structure HTML n’est pas correcte : le label
est vide. Malheureusement le template de la directive n’est pas modifiable, nous devons recopier la directive sous un autre nom pour la modifier :
.directive('ionToggleAlly', [
'$timeout',
'$ionicConfig',
function($timeout, $ionicConfig) {
return {
restrict: 'E',
replace: true,
require: '?ngModel',
transclude: true,
template:
'<label class="item item-toggle">' +
'<div ng-transclude></div>' +
'<div class="toggle">' +
'<input type="checkbox">' +
'<div class="track">' +
'<div class="handle"></div>' +
'</div>' +
'</div>' +
'</label>',
compile: function(element, attr) {
var input = element.find('input');
angular.forEach({
'name': attr.name,
'ng-value': attr.ngValue,
'ng-model': attr.ngModel,
'ng-checked': attr.ngChecked,
'ng-disabled': attr.ngDisabled,
'ng-true-value': attr.ngTrueValue,
'ng-false-value': attr.ngFalseValue,
'ng-change': attr.ngChange,
'ng-required': attr.ngRequired,
'required': attr.required
}, function(value, name) {
if (angular.isDefined(value)) {
input.attr(name, value);
}
});
if (attr.toggleClass) {
element[0].getElementsByTagName('div')[1].classList.add(attr.toggleClass);
}
element.addClass('toggle-' + $ionicConfig.form.toggle());
return function($scope, $element) {
var el = $element[0].getElementsByTagName('div')[1];
var checkbox = el.children[0];
var track = el.children[1];
var handle = track.children[0];
var ngModelController = angular.element(checkbox).controller('ngModel');
$scope.toggle = new ionic.views.Toggle({
el: el,
track: track,
checkbox: checkbox,
handle: handle,
onChange: function() {
if (ngModelController) {
ngModelController.$setViewValue(checkbox.checked);
$scope.$apply();
}
}
});
$scope.$on('$destroy', function() {
$scope.toggle.destroy();
});
};
}
};
}])
Avec ce nouveau template, le label n'est plus vide. Mais en effectuant les tests sous Android 5.0 la checkbox
ne change pas d'état.
En effet les événements sont bloqués par la propriété CSS pointer-events:none;
sur l'item toggle. Il faut donc remettre à la valeur par défaut.
.item-toggle{
pointer-events: auto;
}
De cette manière les événements click
sont correctement interceptés.
Documentation de ion-radio dans Ionic: http://ionicframework.com/docs/1.0.1/api/directive/ionRadio/
Pour le composant ion-toggle les erreurs relevées sont :
- L'input radio n'est pas focusable sous Android 5.0.
- L'icône est focusable sous Android 5.0
Cette fois-ci, une erreur apparaît lors de l'activation de l'input radio
sous Android 5.0, on ne peut pas changer l'état. Pour diagnostiquer l'erreur, il faut partir d'une version sans modification de style, et retirer l'intégralité des classes CSS. En ajoutant une à une les classes, nous pouvons ainsi trouver celle qui comporte la propriété CSS qui pose problème.
L'erreur vient de la propriété left: -9999px;
sur l'input
qui est mal interprétée par Talkback dans Android 5.0. On va donc changer la propriété à 0 et cacher l'input aux lecteurs d'écran par précaution.
.item-radio input {
left: 0;
}
.item-radio input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
L'icône présente pour marquer l'état reste focusable mais n'est pas vocalisée sous Android 5.0. L'état étant déjà vocalisé par l'input
, il est préférable de la cacher complètement avec la propriété aria-hidden="true"
dans la directive.
.directive('ionRadioAlly', function() {
return {
restrict: 'E',
replace: true,
require: '?ngModel',
transclude: true,
template:
'<label class="item item-radio">' +
'<input type="radio" name="radio-group">' +
'<div class="item-content disable-pointer-events" ng-transclude></div>' +
'<i aria-hidden="true" class="radio-icon disable-pointer-events icon ion-checkmark"></i>' +
'</label>',
compile: function(element, attr) {
if (attr.icon) {
element.children().eq(2).removeClass('ion-checkmark').addClass(attr.icon);
}
var input = element.find('input');
angular.forEach({
'name': attr.name,
'value': attr.value,
'disabled': attr.disabled,
'ng-value': attr.ngValue,
'ng-model': attr.ngModel,
'ng-disabled': attr.ngDisabled,
'ng-change': attr.ngChange,
'ng-required': attr.ngRequired,
'required': attr.required
}, function(value, name) {
if (angular.isDefined(value)) {
input.attr(name, value);
}
});
return function(scope, element, attr) {
scope.getValue = function() {
return scope.ngValue || attr.value;
};
};
}
};
})
Nous nous basons ici sur les critères présents dans la proposition d'extension du RGAA pour les mobiles/tactiles (voir https://github.com/DISIC/referentiel-mobile-tactile/blob/master/refentiel-mobile-tactile-liste-criteres.md ).
Le critère 14.3 comporte le test suivant :
Test 14.3.1 : Chaque interaction gestuelle déclenchant une action respecte-t-elle ces conditions ?
- l'action est déclenchée uniquement à la fin de l'interaction gestuelle ;
- l'action n'est pas déclenchée si l'élément déclencheur perd le focus.
Le premier test invalide plusieurs gestes :
- Le geste on-hold va déclencher l'action pendant l'appui et non à la fin de l'interaction ;
- Le geste on-touch va déclencher l'action avant la fin de touchend ou mouseup ;
- Les actions on-drag, on-drag-* vont déclencher l'action avant la fin de touchend ou mouseup.
De la même manière le deuxième test invalide plusieurs gestes :
- Les gestes on-swipe, on-swipe-* peuvent être déclenchés même si le focus est perdu ;
- Le geste on-release est déclenché peu importe où le focus se trouve.
Il reste 2 gestes valides :
- on-tap pour les appuis courts ;
- on-double-tap pour les doubles appuis.
Documentation de $ionicModal dans Ionic: http://ionicframework.com/docs/1.0.1/api/service/$ionicModal/
La fenêtre modale Ionic affiche du contenu temporaire à l'utilisateur. On peut aussi bien afficher des actions, du contenu ou un formulaire à l'intérieur.
Pour le composant $ionicModal les erreurs relevées sont :
- L'utilisateur utilisant un lecteur d'écran ne peut pas interagir avec les éléments à l'intérieur de la modale.
- Le focus n'est pas renvoyé sur le premier élément à l'ouverture.
- Le focus peut sortir de la fenêtre modale en cours d'ouverture.
- À la fermeture le focus ne revient pas sur l'élément ayant permis d'ouvrir la fenêtre.
- La touche Echap ne ferme pas la fenêtre.
- Absence de l'attribut role="dialog".
- Absence de label pour la modale.
L'attribut data-tap-disabled="true"
ne permet pas de désactiver la réduction du délai de 300 ms. La désactivation du CSS et des propriétés pointer-event sont sans effet. Il est donc impossible pour un utilisateur avec un lecteur d'écran actif d'utiliser les fenêtres modales Ionic et aucune correction simple n'a été mise en évidence.
Il est préférable d'utiliser une fenêtre modale déjà accessible. AngularJs étant un framework assez flexible, on peut ajouter aussi bien une fenêtre modale jQuery, React ou AngularJS.
Le même problème se posera pour $ionicPopover et $ionicActionSheet.
Lors de ce tutoriel, nous remarquons bien que Ionic n'a pas été conçu pour être accessible. La première raison est sûrement l'interception des événements click pour réduire le délai de 300ms, qui empêche toutes les actions avec un lecteur d'écran. Ensuite, nous nous rendons compte qu'aucun test n'a été fait avec un lecteur d'écran VoiceOver, car il est impossible de défiler dans l'application avec un lecteur d'écran. Sur l'ensemble des composants, seulement une petit partie sera effectivement corrigeable (ion Forms: Inline Labels et Stacked Labels, ion-checkbox, ion-toggle, ion-radio) ce qui limite l'intérêt de l'utilisation d'Ionic pour créer une application accessible.
Si vous souhaitez faire une application iOS, il est préférable d'attendre que l'anomalie sur le défilement soit corrigée, il est possible d'utiliser Ionic mais cela risque d'être très chronophage en l'état et de provoquer des régressions ou des comportements inattendus.
Si vous souhaitez faire une application Android, il est très important de tester l'application sur Android > 5 et Android 4.4. Il peut y avoir beaucoup de comportements différents entre les deux versions OS, en effet la WebView n'est pas la même et l'accessibilité peut comporter des anomalies sur une seule des versions.
De manière générale, Ionic 1.0.1 n'est pas recommandable pour créer une application accessible. Il est préférable d'utiliser Cordova et de créer sa propre application. Nous pourrions éventuellement travailler sans le module JavaScript Ionic et charger seulement la feuille CSS, mais il faudrait prendre garde à la structure HTML, aux propriétés pointer-events:none
et aux problèmes éventuels de contrastes.
Ce document est la propriété du Secrétariat général à la modernisation de l'action publique français (SGMAP). Il est placé sous la licence ouverte 1.0 ou ultérieure, équivalente à une licence Creative Commons BY. Pour indiquer la paternité, ajouter un lien vers la version originale du document disponible sur le compte Github de la DInSIC.