Aller au contenu

SKILL-10 — Navigation

Ordre de focus, cohérence et positionnement dans l'application

Critères couverts

Critère RAAM Intitulé Niveau WCAG
10.1 L'utilisateur est informé de sa position dans l'application [A] WCAG 2.4.8
10.2 L'ordre de focus est cohérent et logique [A] WCAG 1.3.2, 2.4.3
10.3 Les raccourcis ne créent pas de conflits [A] WCAG 2.1.4
10.4 La navigation est cohérente entre les écrans [AA] WCAG 3.2.3, 3.2.4
10.5 L'indicateur de focus est visible [AA] WCAG 2.4.7
10.6 Le focus ne peut pas être piégé [AA] WCAG 2.1.2

Pourquoi c'est important

Le piège du focus

Une modale qui s'ouvre sans contraindre le focus est un défaut majeur. Le lecteur d'écran peut atteindre le contenu derrière et l'utilisateur est totalement perdu.

La navigation concerne l'ordre et le cheminement. L'ordre de focus de haut en bas et de gauche à droite doit être respecté pour que la lecture par VoiceOver ou TalkBack soit logique.

Patterns

1. Ordre de Focus [A]

✅ Bon
VStack(alignment: .leading) {
    Text("Titre de la carte")
        .font(.headline)
        .accessibilitySortPriority(1) // Lu en premier
    Text("Description détaillée...")
    Button("Action principale") { }
}
✅ Bon
Column(modifier = Modifier.padding(16.dp)) {
    Text(
        text = "Titre de la carte",
        modifier = Modifier.semantics { traversalIndex = -1f } // Index petit = en premier
    )
    Text("Description...", modifier = Modifier.semantics { traversalIndex = 0f })
    Button(onClick = {}, modifier = Modifier.semantics { traversalIndex = 1f }) {
        Text("Action principale")
    }
}

2. Position dans l'App [A]

✅ Bon
NavigationStack {
    List { Text("Contenu") }
    .navigationTitle("Accueil") // Titre unique annoncé
}
✅ Bon
Scaffold(
    topBar = {
        TopAppBar(
            title = { Text("Accueil", modifier = Modifier.semantics { heading() }) }
        )
    }
) { padding -> /* Contenu */ }

3. Focus Non Piégé [AA]

✅ Bon
if showModal {
    VStack {
        Text("Contenu de la modale").accessibilityAddTraits(.isHeader)
        Button("Fermer") { showModal = false }
    }
    .accessibilityAddTraits(.isModal) // Contraint le focus
    .accessibilityAction(.escape) { showModal = false }
}
✅ Bon
if (showDialog) {
    AlertDialog( // Gère nativement le focus trap
        onDismissRequest = { showDialog = false },
        title = { Text("Information") },
        text = { Text("Modale accessible.") },
        confirmButton = { TextButton(onClick = { showDialog = false }) { Text("Fermer") } }
    )
}

4. Navigation Cohérente [AA]

✅ Bon
TabView {
    Text("Contenu Accueil")
        .tabItem { Label("Accueil", systemImage: "house") }
        .accessibilityLabel("Onglet Accueil")
}
✅ Bon
NavigationBar {
    NavigationBarItem(
        icon = { Icon(Icons.Filled.Home, contentDescription = null) },
        label = { Text("Accueil") },
        selected = true,
        onClick = { },
        modifier = Modifier.semantics { contentDescription = "Onglet Accueil" }
    )
}

5. Focus VoiceOver : @AccessibilityFocusState [A]

@FocusState ≠ focus VoiceOver

@FocusState gère le focus clavier. Il ne déplace pas le curseur VoiceOver : utiliser @AccessibilityFocusState avec .accessibilityFocused(_:).

✅ Bon — retour du focus sur le déclencheur à la fermeture d'une sheet
@AccessibilityFocusState private var triggerFocused: Bool

Button("Filtrer les résultats") { showFilters = true }
    .accessibilityFocused($triggerFocused)
    .sheet(
        isPresented: $showFilters,
        onDismiss: { triggerFocused = true }, // sinon VoiceOver retombe en haut de l'écran
        content: { FiltersView().accessibilityAddTraits(.isModal) }
    )
✅ Bon — focus sur le champ en erreur après validation
@AccessibilityFocusState private var focusedField: Field?

TextField("exemple@domaine.fr", text: $email)
    .accessibilityLabel("Adresse email")
    .accessibilityFocused($focusedField, equals: .email)
// après échec de validation : focusedField = .email

Ce pattern est spécifique à iOS. Pour les dialogs Compose, voir le pattern 3 (AlertDialog).

6. Focus non masqué [2.2 · AA]

Hors RAAM 1.1, hors scoring

2.4.11 : une barre fixe ne doit pas cacher entièrement l'élément qui a le focus. iOS : .safeAreaInset(edge: .bottom) plutôt qu'un overlay ; Compose : Scaffold + paddingValues et imePadding(). Détails : Extensions WCAG 2.2.

Checklist [A] / [AA]

  • L'ordre de focus (lecture) correspond à la structure visuelle.
  • Les modales piègent correctement le focus (isModal / AlertDialog).
  • Le focus revient sur le déclencheur après la fermeture d'une modale.
  • Le titre des écrans est clair et annonce la position.

Références