Chapitre 1115 minDébutant
Ergonomie mobile et bureau : React Native et WinForms
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- adapter un écran mobile au doigt, au clavier virtuel et aux lecteurs d'écran ;
- régler l'ordre de tabulation, les raccourcis et la validation d'une fenêtre WinForms ;
- afficher les états chargement, vide, erreur et succès dans chaque technologie.
4 exercices corrigés
Rendre ergonomique une application mobile ou de bureau, c'est appliquer les mêmes principes qu'en web avec les composants natifs de la plateforme : sur un écran tactile, des cibles assez grandes pour le doigt, le bon clavier virtuel et des rôles et des noms pour VoiceOver et TalkBack ; dans une application de bureau, un ordre de tabulation logique, des raccourcis clavier, des noms accessibles et des erreurs qui n'interrompent pas le travail. Ce chapitre montre ces réglages sur l'écran de retour de Prêt'Matos, en React Native puis en WinForms, et compare la façon dont chaque technologie affiche les états d'un écran.
Le principe reste celui du chapitre 10 : partir des composants natifs (Pressable et TextInput en React Native, Button, TextBox et ComboBox en WinForms), qui savent déjà recevoir le focus et se présenter aux lecteurs d'écran, puis régler les quelques propriétés que les valeurs par défaut laissent de côté.
En React Native : penser au doigt et au lecteur d'écran mobile
Sur un téléphone ou une tablette, trois contraintes s'ajoutent : le doigt est moins précis que la souris, le clavier virtuel cache la moitié de l'écran, et les lecteurs d'écran (VoiceOver, TalkBack) se pilotent par gestes. Si la logistique de Sorbier Logistique avait une application native pour le comptoir du stock, l'écran de retour ressemblerait à ceci :
import { useState } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
export function RetourMateriel({ onEnregistrer }: { onEnregistrer: (numero: string) => void }) {
const [numero, setNumero] = useState('');
return (
<View style={styles.ecran}>
<Text style={styles.titre} accessibilityRole="header">
Enregistrer un retour
</Text>
<Text style={styles.libelle}>Numéro d'inventaire</Text>
<TextInput
style={styles.champ}
value={numero}
onChangeText={setNumero}
accessibilityLabel="Numéro d'inventaire"
accessibilityHint="Le numéro est sur l'étiquette du matériel, par exemple PC-07"
autoCapitalize="characters"
autoCorrect={false}
returnKeyType="done"
onSubmitEditing={() => onEnregistrer(numero)}
/>
<Pressable
style={({ pressed }) => [styles.bouton, pressed && styles.boutonEnfonce]}
accessibilityRole="button"
onPress={() => onEnregistrer(numero)}
>
<Text style={styles.texteBouton}>Enregistrer le retour</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
ecran: { flex: 1, padding: 16, gap: 8 },
titre: { fontSize: 24, fontWeight: '700' },
libelle: { fontSize: 16 },
champ: { minHeight: 48, borderWidth: 1, borderColor: '#6b7280', borderRadius: 6, paddingHorizontal: 12, fontSize: 18 },
bouton: { minHeight: 48, borderRadius: 6, backgroundColor: '#1d6b43', alignItems: 'center', justifyContent: 'center' },
boutonEnfonce: { opacity: 0.8 },
texteBouton: { color: '#ffffff', fontSize: 18, fontWeight: '700' },
});
| Règle | Dans le code |
|---|---|
| Cibles d'au moins 44 à 48 pixels | minHeight: 48 sur le champ et le bouton ; hitSlop agrandit la zone d'une petite icône sans changer son dessin |
| Un rôle et un nom pour le lecteur d'écran | accessibilityRole="header" sur le titre, "button" sur le bouton ; accessibilityLabel et accessibilityHint sur le champ |
| Le bon clavier, sans correction automatique | autoCapitalize="characters", autoCorrect={false} pour un code comme « PC-07 » ; keyboardType pour les nombres et les téléphones |
| La touche du clavier enchaîne | returnKeyType="done" et onSubmitEditing : Noah valide sans chercher le bouton |
| Un retour visuel au toucher | Le style pressed du Pressable |
| Les couleurs du design system | Les mêmes jetons que le Web : vert #1d6b43, contour #6b7280 |
Le texte de React Native suit par défaut la taille de police choisie dans les réglages du téléphone : ne bloque pas ce comportement, et vérifie que tes écrans le supportent. Le cours développement mobile détaille les composants et l'accessibilité en React Native, et son chapitre sur les formulaires gère le clavier qui recouvre les champs.
En WinForms : le clavier d'abord
Une application de gestion WinForms est utilisée au clavier toute la journée. Les contrôles natifs sont accessibles, à condition de régler quelques propriétés que le concepteur visuel laisse souvent à leur valeur par défaut. Voici l'équivalent WinForms de l'écran de retour ; ses contrôles (txtNumero, cboEtat, btnScanner, btnEnregistrer, btnAnnuler, le libellé lblErreurNumero et le composant errorProvider) ont été posés dans le concepteur :
using System.ComponentModel;
using System.Text.RegularExpressions;
public partial class FormRetour : Form
{
public FormRetour()
{
InitializeComponent();
// L'ordre de tabulation suit l'ordre de lecture, de haut en bas.
txtNumero.TabIndex = 0;
cboEtat.TabIndex = 1;
btnEnregistrer.TabIndex = 2;
btnAnnuler.TabIndex = 3;
AcceptButton = btnEnregistrer; // Entrée enregistre
CancelButton = btnAnnuler; // Échap ferme la fenêtre
btnAnnuler.CausesValidation = false; // Annuler ne déclenche pas la validation
btnEnregistrer.Text = "&Enregistrer le retour"; // raccourci Alt+E
// Le bouton n'affiche qu'une icône : il lui faut un nom pour les lecteurs d'écran.
btnScanner.AccessibleName = "Scanner l'étiquette";
// Une liste fermée plutôt qu'une saisie libre, avec la valeur la plus probable.
cboEtat.DropDownStyle = ComboBoxStyle.DropDownList;
cboEtat.Items.AddRange(new object[] { "Bon état", "Abîmé", "À réparer" });
cboEtat.SelectedIndex = 0;
txtNumero.Validating += TxtNumero_Validating;
}
private void TxtNumero_Validating(object? sender, CancelEventArgs e)
{
string numero = txtNumero.Text.Trim().ToUpperInvariant();
string message = "";
if (numero.Length == 0)
{
message = "Indiquez le numéro d'inventaire, par exemple PC-07.";
}
else if (!Regex.IsMatch(numero, @"^[A-Z]{2}-\d{2,3}$"))
{
message = "Écrivez le numéro comme sur l'étiquette : deux lettres, un tiret, des chiffres (PC-07).";
}
// L'icône de l'ErrorProvider ne montre son message qu'au survol :
// on l'écrit aussi sous le champ, dans un Label visible par tous.
errorProvider.SetError(txtNumero, message);
lblErreurNumero.Text = message;
txtNumero.AccessibleDescription = message; // lu par le lecteur d'écran au retour sur le champ
// e.Cancel reste à false : on signale l'erreur sans bloquer le focus dans le champ.
}
}
| Règle | Dans WinForms |
|---|---|
| Ordre de tabulation logique | TabIndex, ou menu Affichage > Ordre de tabulation dans le concepteur |
| Entrée et Échap qui font ce qu'on attend | AcceptButton et CancelButton du formulaire |
| Raccourcis clavier | & devant une lettre du texte d'un bouton ou d'un libellé |
| Nom pour les lecteurs d'écran | AccessibleName (et AccessibleDescription) sur les contrôles sans texte visible |
| Pas de saisie libre quand les valeurs sont connues | ComboBox en DropDownList, valeur par défaut |
| Message d'erreur visible et lisible | ErrorProvider pour le repère, et un Label pour le texte |
| Erreur annoncée au lecteur d'écran | AccessibleDescription du champ = le message |
| Ne pas piéger l'utilisateur | Laisser e.Cancel à false : il peut quitter le champ et y revenir |
| Annuler sans être bloqué | CausesValidation = false sur le bouton Annuler |
| Fenêtre qui s'adapte | Anchor et Dock pour les redimensionnements, mise à l'échelle selon la résolution de l'écran |
La validation à la sortie du champ (Validating) et l'ErrorProvider sont détaillés dans le cours C# : valider une saisie. Évite les MessageBox à chaque erreur de saisie : elles interrompent, disparaissent après un clic et obligent à mémoriser le message pendant qu'on corrige.
Les états, dans chaque technologie
Les états décrits dans la spécification (chapitre 9) doivent exister dans le code, et être perçus par tous :
| État | Web et React | React Native | WinForms |
|---|---|---|---|
| Chargement | Texte dans une région role="status" ; structure de la page en place | ActivityIndicator avec un texte ; annonce par AccessibilityInfo.announceForAccessibility | Curseur d'attente et Label « Chargement… » ; traitement en async pour ne pas figer la fenêtre |
| Vide | Phrase et action (« Voir le catalogue ») | Composant affiché quand la liste est vide (ListEmptyComponent d'une FlatList) | Label à la place de la grille vide |
| Erreur | Message role="alert" et bouton « Réessayer » | Message et bouton « Réessayer » | Message dans la fenêtre, bouton « Réessayer » |
| Succès | Message de confirmation, focus logique | Message, puis retour à l'écran précédent | Message dans la barre d'état ou la fenêtre, sans bloquer |
Pour WinForms, le cours C# montre comment garder une fenêtre réactive avec async et await pendant un chargement.
Exercices
Exercice 1 sur 4 Une fenêtre WinForms au clavier Réussi À revoir
Une fenêtre « Nouveau matériel » contient, de haut en bas : Désignation, Type, N° d'inventaire, puis les boutons Ajouter et Annuler. Le testeur constate que la touche Tab passe de Désignation à Annuler, puis à N° d'inventaire ; que la touche Entrée ne fait rien ; et qu'un bouton « ? » d'aide n'est pas annoncé par le lecteur d'écran. Quelles propriétés corriges-tu ?
Voir la réponseMasquer la réponse de l'exercice 1
TabIndex: 0 pour Désignation, 1 pour Type, 2 pour N° d'inventaire, 3 pour Ajouter, 4 pour Annuler (ou menu Affichage > Ordre de tabulation du concepteur).AcceptButtondu formulaire sur le bouton Ajouter (Entrée), etCancelButtonsur Annuler (Échap).AccessibleNamedu bouton « ? » : « Aide sur l'ajout d'un matériel ».
On peut ajouter des raccourcis (&Ajouter) et vérifier que « Type » est une liste fermée.
Tu avais trouvé ?
Exercice 2 sur 4 Un écran React Native à revoir Réussi À revoir
Voici le premier jet de l'écran mobile « Enregistrer un retour ». Relève ses défauts d'ergonomie et d'accessibilité, et propose une correction pour chacun.
<View>
<TextInput
placeholder="Numéro"
keyboardType="numeric"
value={numero}
onChangeText={setNumero}
style={{ height: 32 }}
/>
<Pressable onPress={() => onEnregistrer(numero)}>
<Image source={iconeValider} style={{ width: 24, height: 24 }} />
</Pressable>
</View>
Voir la réponseMasquer la réponse de l'exercice 2
| Défaut | Correction |
|---|---|
Pas de libellé visible : « Numéro » en placeholder disparaît à la frappe et ne dit pas de quel numéro il s'agit | Un Text « Numéro d'inventaire » au-dessus du champ, et un accessibilityLabel sur le champ |
keyboardType="numeric" alors que le numéro contient des lettres (« PC-07 ») | Le clavier par défaut, avec autoCapitalize="characters" et autoCorrect={false} |
| Champ de 32 pixels de haut | minHeight: 48 |
| Bouton réduit à une icône de 24 × 24 pixels, sans texte | Un libellé visible, « Enregistrer le retour », et minHeight: 48 ; à défaut, hitSlop pour agrandir la zone tactile et un accessibilityLabel |
| Le lecteur d'écran n'annonce pas un bouton | accessibilityRole="button" sur le Pressable |
| Aucun retour au toucher | Le style pressed du Pressable |
La version corrigée est l'écran RetourMateriel présenté plus haut.
Tu avais trouvé ?
Exercice 3 sur 4 Le bouton caché par le clavier Réussi À revoir
Dans un exercice du chapitre 8, le bouton « Confirmer la réservation » restait caché sous le clavier de l'iPhone, et deux participants abandonnaient. Si le formulaire était écrit en React Native, quels moyens aurais-tu pour éviter ce problème ? Cites-en trois.
Voir la réponseMasquer la réponse de l'exercice 3
- Faire remonter le contenu : envelopper le formulaire dans un
KeyboardAvoidingView, avecbehavior="padding"sur iOS, pour que le bas du formulaire reste visible au-dessus du clavier. - Permettre de faire défiler : placer le formulaire dans un
ScrollViewaveckeyboardShouldPersistTaps="handled"; sans ce réglage, le premier appui sur le bouton ne sert qu'à fermer le clavier. - Valider depuis le clavier :
returnKeyTypeetonSubmitEditingsur chaque champ, pour passer au champ suivant, puis envoyer le formulaire sans chercher le bouton.
Teste ensuite sur un vrai iPhone et un vrai téléphone Android : les deux systèmes ne gèrent pas le clavier de la même façon. Le cours développement mobile montre ces composants en situation.
Tu avais trouvé ?
Exercice 4 sur 4 Les états d'une fenêtre WinForms Réussi À revoir
La logistique dispose aussi d'une fenêtre WinForms « Aujourd'hui » : une DataGridView y affiche les sorties et les retours du jour, chargés depuis l'API. Pendant le chargement, la fenêtre reste figée plusieurs secondes ; quand rien n'est prévu, la grille reste vide sans explication ; quand l'API ne répond pas, une MessageBox affiche « System.Net.Http.HttpRequestException ». Propose une correction pour chacun de ces trois états.
Voir la réponseMasquer la réponse de l'exercice 4
- Chargement : appeler l'API avec
asyncetawait, pour que la fenêtre reste réactive, et afficher unLabel« Chargement des mouvements du jour… » avec le curseur d'attente. - Vide : masquer la grille et afficher « Aucune sortie ni aucun retour prévu aujourd'hui. » : ce n'est pas une erreur, c'est une information.
- Erreur : intercepter l'exception et afficher dans la fenêtre « Impossible de charger les mouvements du jour : vérifiez la connexion au réseau. » avec un bouton « Réessayer » ; le détail technique va dans le journal de l'application, pas à l'écran.
Les trois corrections relèvent du feedback immédiat et de la qualité des messages. La première évite aussi qu'une fenêtre figée trop longtemps soit marquée « (Ne répond pas) » par Windows.
Tu avais trouvé ?