[OCTO] Accessibilité sur mobile : La navigation clavier en Flutter

le 02/02/2024 par Rémi Dormoy
Tags: Software Engineering, Mobile

Moins connue dans l’univers mobile que la navigation par lecteur d’écran, la navigation clavier fait néanmoins partie des fonctionnalités à respecter si l’on veut être conforme aux principes WCAG. Elle s’adresse à trois groupes de personnes d’après le W3C :

  • Les personnes ayant une déficience visuelle peuvent bénéficier de certaines caractéristiques des claviers physiques par rapport aux claviers tactiles (par exemple, des touches distinctes, des rebords de touches et des dispositions de touches plus prévisibles).
  • Les personnes ayant des troubles de la dextérité ou de la mobilité peuvent bénéficier de claviers optimisés pour minimiser les frappes involontaires (par exemple, des touches de formes, d'espacements et de protections différentes) ou de méthodes d'entrée spécialisées qui imitent la saisie au clavier.
  • Les personnes qui peuvent être confuses à cause de la nature dynamique des claviers à l'écran peuvent bénéficier de la constance d'un clavier physique.
  • N’oublions pas que s’assurer de l'accessibilité d’un parcours permet également de s’assurer de sa cohérence et de son utilisabilité pour l’ensemble des utilisateurs.

Si elle présente beaucoup de similitudes avec la navigation des lecteurs d’écrans, certaines différences méritent toutefois de s’assurer de son bon fonctionnement en lui dédiant une recette spécifique.

Qu’est-ce que la navigation clavier ?

Comme vous l’aurez sûrement deviné, la navigation clavier consiste à utiliser un écran tactile sans utiliser la partie tactile, à l’aide d’un clavier physique (la plupart du temps un clavier externe connecté par Bluetooth ou USB par exemple). Pour cela, notre utilisateur va avoir à sa disposition un certain nombre de commandes qu’il nous faut prendre en compte :

  • Élément suivant (TAB) et élément précédent (SHIFT+TAB)
  • Déplacement haut, bas, droite et gauche (à l’aide des flèches)
  • Page-up (ctrl+haut), page-down (ctrl+bas), page-right (ctrl+droite) et page-left (ctrl+gauche) pour le scroll
  • Entrée ou espace pour sélectionner l’élément en cours
  • Cette liste n’est pas exhaustive

Au cours de cette navigation, plusieurs conditions doivent être respectées, afin de pouvoir garantir l’accessibilité de notre application :

  • Toutes les fonctionnalités sont accessibles via la navigation clavier
  • Lors de la navigation, il est possible de savoir quel élément est actuellement sélectionné
  • Seuls les éléments interactifs sont accessibles pendant la navigation
  • L’ordre de navigation est logique et cohérent
  • Les éléments sont regroupés lorsqu’ils sont liés
  • Il est possible de scroller les contenus pour accéder à l’ensemble des informations
  • Nous ne sommes jamais piégés dans une partie de l’écran, sans pouvoir sortir d’une boucle de navigation

Pour plus de détails, les règles WCAG sont la 2.1.1 pour la navigation clavier en général, la 2.1.2 pour éviter les pièges clavier, la 2.4.3 pour l’ordre de focus et 2.4.7 pour la visibilité du focus.

Cela vous semble toujours un peu abstrait ? Essayons avec un petit exemple.

Image animée montrant une navigation clavierSur l’exemple du dessus, on peut bien voir une navigation logique et cohérente, que ce soit avec les flèches haut, bas, droite, gauche, mais aussi avec l’appui sur suivant ou précédent. L’élément sélectionné apparaît également clairement (en jaune) ce qui permet de savoir où l’on est et où l’on peut cliquer.

Comment je peux tester mon Application ?

La majorité d’entre nous (les développeurs), nous utilisons notre smartphone via notre écran tactile, et il est possible que vous ne sachiez tout simplement pas comment tester la navigation clavier. Pas de panique, c’est en fait très simple, surtout sur Android. Trois solutions s’offrent à vous :

  • Si vous possédez un clavier Bluetooth : il y a de très fortes chances que vous puissiez le connecter à votre smartphone. Une fois connecté, vous pourrez ensuite utiliser directement les commandes vues plus haut.

  • En installant scrcpy : si vous ne connaissez pas, cet outil vous permet de connecter votre smartphone à votre ordi et de pouvoir utiliser votre souris pour cliquer sur l’écran, mais aussi d’utiliser votre clavier comme s'il était connecté au smartphone. Attention tout de même, certaines commandes comme Page-up ou Page-down ne seront pas accessibles si elles sont déjà utilisées par des raccourcis systèmes.

  • En installant Switch Access sur votre smartphone : c’est une application d’accessibilité, disponible sur le playstore, permettant de naviguer dans votre application sur un système similaire à la navigation clavier (Élément suivant/précédent, sélectionner). Vous pourrez naviguer :

    • Soit grâce à des expressions faciales (c’est amusant, mais finalement assez peu pratique).
    • Soit à l’aide de boutons physiques comme les boutons de volumes, vous trouverez ici comment les configurer.

Félicitations ! Vous avez maintenant tous les outils pour tester l’accessibilité de votre application en navigation clavier.

Implémentation en Flutter

Pour la grande majorité des cas, suivre les bonnes pratiques de développement Flutter suffit à proposer une navigation clavier accessible et il est donc probable que votre application soit déjà accessible :

  • si votre application n’est pas trop complexe
  • si votre code est bien structuré
  • si votre arbre de Widgets est bien propre
  • si vous n’utilisez pas de librairies développées sans avoir pensé à la navigation clavier
  • si vous n’avez pas pris de raccourcis dans le refactoring de Widget de vos collègues

En fait, il est assez peu probable que toute votre application soit accessible en navigation clavier si vous n’y avez jamais prêté attention. Mais la bonne nouvelle, c’est que les corrections sont la plupart du temps assez légères.

Rendre les éléments interactifs accessibles

Sur notre chemin vers une application accessible, la première étape est de vérifier que tous nos éléments interactifs sont accessibles. Comme souvent en accessibilité, il nous faut simplement utiliser les bons composants (ou ne pas utiliser les mauvais, selon votre point de vue).

  • Dans le cas où vous utilisez des Widgets Flutter fait pour être sélectionnés, pas de soucis : TextButton, FloatingActionButton ou Tab sont accessibles et vous n’avez pas à y faire attention.
  • Dans le cas où vous créez votre propre élément, il faut toujours utiliser Inkwell. Beaucoup de développeurs Flutter peuvent être tentés d’utiliser GestureDetector et sa méthode onTap, mais votre élément ne sera alors pas accessible au clavier.

Image montrant une navigation clavier qui oublie un élémentImage montrant une navigation clavier qui passe sur tous les élémentsSur l’image de gauche, le composant Feature 2, contient un GestureDetector et un appui sur Tab ou sur la flèche de droite nous emmène directement de Feature 1 à Feature 2. Sur l’image de droite, on remplace le GestureDetector par un Inkwell, et notre bouton revient dans la navigation.

Identifier les éléments sélectionnés

Maintenant que nos éléments sont bien accessibles, il faut vérifier qu’ils sont identifiables une fois sélectionnés. De la même manière qu'au-dessus, les composants Flutter exposés par le framework sont généralement identifiables sans que l’on ait rien à faire (une overlay grise apparaît généralement quand ils sont sélectionnés). Cependant, il est possible que cette overlay ne soit pas suffisante pour identifier notre composant comme sur l’exemple suivant : Image montrant une navigation ou il est difficile de discerner l'élément sélectionné

Pour ceux qui ont de (très) bons yeux, vous verrez peut-être le focus changer d’élément, mais ce n’est clairement pas satisfaisant. Pour pouvoir identifier notre élément sélectionné, nous avons plusieurs choix :

  • Changer sa couleur (comme sur les images précédentes)
  • L’entourer d’une bordure, un peu à la manière du lecteur d’écran
  • Changer sa taille (généralement le grossir quand il est sélectionné)

Image montrant une navigation ou il est facile de discerner l'élément sélectionné

Chacun préférera la solution qu’il trouve la plus belle, accessible ou originale, mais par souci de gentillesse, je vais vous montrer l’implémentation des 3.

Premier bouton, la couleur de focus

InkWell(
    focusColor: Colors.yellow,
     onTap: () {
            // Redirection vers la feature
          },
     child: [... contenu du bouton ...],
)

Rien de plus simple, il suffit simplement de rajouter une focusColor.

Second bouton, la bordure

class _Action2State extends State<_Action2> {

  bool _isFocused = false;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(30),
        border: Border.all(color: _isFocused ? Colors.yellow : Colors.transparent, width: 4),
      ),
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: ClipRRect(
          borderRadius: BorderRadius.circular(20),
          child: Material(
            color: Colors.white,
            child: InkWell(
              onFocusChange: (isFocused) {
                if (isFocused != _isFocused) {
                  setState(() {
                    _isFocused = isFocused;
                  });
                }
              },
              onTap: () {
                // Redirection vers la feature
              },
              child: [... Le contenu de notre bouton ...],
            ),
          ),
        ),
      ),
    );
  }
}

Un petit peu plus complexe, nous allons ici avoir besoin de passer notre Widget en Statefull et de garder une variable d’instance indiquant si notre bouton est sélectionné ou non. Cette information sera mise à jour dans le paramètre onFocusChange de notre Inkwell. Une fois setState appelé à cet endroit, il ne reste plus qu’à mettre à jour la couleur de bordure en fonction.

Dernier bouton, le grossissement :

class _Action3State extends State<_Action3> {

  bool _isFocused = false;

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      duration: Duration(milliseconds: 400),
      curve: Curves.fastEaseInToSlowEaseOut,
      scale: _isFocused ? 1.2 : 1,
      child: ClipRRect(
        borderRadius: BorderRadius.circular(20),
        child: Material(
          color: Colors.white,
          child: InkWell(
            onFocusChange: (isFocused) {
              if (isFocused != _isFocused) {
                setState(() {
                  _isFocused = isFocused;
                });
              }
            },
            onTap: () {
                 // Redirection vers la feature
            },
            child: [... Le contenu de notre bouton ...],
          ),
        ),
      ),
    );
  }
}

Sur le même principe que pour le second bouton, nous allons écouter la méthode onFocusChanged de notre InkWell, simplement changer la valeur de notre AnimatedScale et le tour est joué !

Garantir un ordre de navigation logique et cohérent

Dernière étape avant un utilisateur satisfait, garantir un ordre de navigation cohérent. Par défaut, la navigation au clavier va parcourir de la gauche vers la droite, puis du haut vers le bas. Il faut donc disposer nos éléments sur l’écran dans cet ordre. Si, pour une raison quelconque, vous voulez les disposer différemment, vous risquez d’avoir une liste désordonnée, comme sur l’exemple suivant : Image montrant une navigation passant par les éléments 1 puis 3 puis 2 puis 4

⚠️__Avertissement :__ Si vous avez besoin de changer l’ordre par défaut, il est probable que ce que vous faites n’est pas standard et donc probablement pas très logique et cohérent.

Si malgré tout, vous ne voulez pas changer votre UI mais que vous voulez réordonner votre liste, rien de trop compliqué non plus. Flutter a tout prévu pour vous, il suffit d’utiliser FocusTraversalGroup puis de “ranger” manuellement vos boutons dans des FocusTraversalOrder.

FocusTraversalGroup(
        policy: OrderedTraversalPolicy(),
        child: Row(
          children: [
            Column(
              mainAxisSize: MainAxisSize.max,
              children: [
                FocusTraversalOrder(
                  order: NumericFocusOrder(1),
                  child: _Action1(),
                ),
                FocusTraversalOrder(
                  order: NumericFocusOrder(2),
                  child: _Action2(),
                ),
              ],
            ),
            Column(
              children: [
                FocusTraversalOrder(
                  order: NumericFocusOrder(3),
                  child: _Action3(),
                ),
                FocusTraversalOrder(
                  order: NumericFocusOrder(4),
                  child: _Action4(),
                ),
              ],
            ),
          ],
        ),
      )

Et voilà !

Image montrant une navigation passant par les éléments 1 puis 2 puis 3 puis 4

Conclusion

La navigation clavier sur mobile est la plupart du temps simple à mettre en place si l’on suit les bonnes pratiques Flutter. Dans quelques rares cas où appliquer les bonnes pratiques ne vous suffit pas, nous avons pu voir quelques astuces pour y remédier. Si malgré tout, vous rencontrez un écran qui vous résiste et sur lequel vous n’arrivez pas à obtenir une navigation satisfaisante, deux cas de figure sont possibles :

  • Le framework Flutter comporte un bug, ou n’a pas encore implémenté l’accessibilité sur un de ses éléments, et il est donc conseillé d’ouvrir une issue (ou d’ajouter un pouce sur l’issue correspondante). N’ayez pas peur, Flutter est un framework encore jeune et les équipes qui le développent sont en recherche d’amélioration. Certains bugs comme celui du clavier externe sur iOS sont connus.
  • C’est votre design qu’il faut changer et pas votre implémentation. Parfois, quand c’est compliqué, c’est que c’est trop compliqué et il faut simplement faire autrement. D’ailleurs, si votre parcours ne fonctionne pas en navigation clavier, il est possible qu’il apparaisse comme illogique ou incohérent à un utilisateur classique.

PS : Dans un souci de mutualisation de vos efforts, je vous conseille fortement d’avoir une approche composant pour votre partie graphique, et de traiter l’accessibilité de chacun de ces composants à part, puis de les réutiliser au maximum. De cette manière, vous aurez probablement un “catalogue” de plusieurs boutons et/ou cartes personnalisables et accessibles.