# aria-expanded qui ne change jamais parce que le bloc React ne se met pas à jour

> Le chevron visuel tourne bien au clic, mais l'attribut aria-expanded reste figé sur false dans l'éditeur. Un état React mal synchronisé explique ce décalage.

- Auteur : WordPress Développement
- Publié le : 2023-10-31
- Mis à jour le : 2023-10-31
- Catégorie : Accessibilité
- URL : https://www.wpmoderne.fr/accessibilite/aria-expanded-fige-bloc-react-etat-non-synchronise/

## L’essentiel

- Un attribut ARIA codé en dur ne suit jamais l'état réel du composant
- useState doit piloter à la fois le rendu visuel et l'attribut ARIA
- Le problème se limite ici à l'interface d'édition, pas au rendu public

Le chevron d'un panneau repliable, dans l'interface d'édition d'un bloc Gutenberg personnalisé, tourne bien de quatre-vingt-dix degrés à chaque clic. L'attribut `aria-expanded` posé sur le bouton qui le déclenche, lui, reste obstinément figé sur `false`, quel que soit le nombre de clics effectués. Un développement rapide en React masquait un bug de synchronisation d'état, resté invisible tant que personne n'avait testé le composant au clavier avec un lecteur d'écran.

Ce billet diagnostique ce bug précis, rencontré dans l'éditeur d'un bloc personnalisé développé pour l'écran d'administration d'un plugin interne. Il ne traite pas du rendu front public du bloc, qui repose sur un balisage HTML statique généré séparément, sans logique React embarquée à cet endroit.

## Symptôme

Le composant concerné affiche un panneau de réglages avancés, repliable par défaut, à l'intérieur du panneau latéral `InspectorControls` du bloc. Le code, tel qu'il existait avant correction, ressemblait à ceci :

```
function PanneauReglagesAvances( { children } ) {
  const [ ouvert, setOuvert ] = useState( false );

  return (
    <div className="wpm-panneau-repliable">
      <button
        type="button"
        aria-expanded={ false }
        onClick={ () => setOuvert( ! ouvert ) }
        className={ ouvert ? 'wpm-chevron-ouvert' : '' }
      >
        Réglages avancés
      </button>
      { ouvert && <div className="wpm-panneau-contenu">{ children }</div> }
    </div>
  );
}
```

Le rendu visuel fonctionne parfaitement : la classe `wpm-chevron-ouvert` s'ajoute bien selon l'état `ouvert`, ce qui fait pivoter le chevron via une règle CSS, et le contenu du panneau apparaît et disparaît correctement. Un test uniquement visuel, à la souris, ne révèle donc rien d'anormal.

## Diagnostic

La cause tient à une seule ligne : `aria-expanded={ false }` est codée en dur, littéralement la valeur booléenne `false`, plutôt que reliée à la variable d'état `ouvert` qui pilote par ailleurs tout le reste du comportement du composant. React ne recalcule cet attribut à chaque rendu que si sa valeur dépend effectivement d'une variable d'état ; ici, la valeur étant une constante littérale, elle reste identique à chaque nouveau rendu, quel que soit le nombre de clics sur le bouton.

Ce genre d'erreur se produit facilement lors d'un développement itératif : le composant a probablement été écrit d'abord sans état dynamique, avec `aria-expanded={ false }` comme valeur de départ raisonnable, puis la logique d'ouverture et de fermeture a été ajoutée ensuite pour le rendu visuel, sans revenir corriger cette ligne laissée de côté.

> L'essentiel à retenir : Un attribut ARIA codé en dur ne suit jamais l'état réel du composant ; useState doit piloter à la fois le rendu visuel et l'attribut ARIA ; Le problème se limite ici à l'interface d'édition, pas au rendu public

## Correctif

La correction consiste simplement à relier l'attribut à la même variable d'état que le reste du composant :

```
function PanneauReglagesAvances( { children } ) {
  const [ ouvert, setOuvert ] = useState( false );

  return (
    <div className="wpm-panneau-repliable">
      <button
        type="button"
        aria-expanded={ ouvert }
        onClick={ () => setOuvert( ! ouvert ) }
        className={ ouvert ? 'wpm-chevron-ouvert' : '' }
      >
        Réglages avancés
      </button>
      { ouvert && <div className="wpm-panneau-contenu">{ children }</div> }
    </div>
  );
}
```

Après correction, chaque clic sur le bouton déclenche un nouveau rendu du composant, et l'attribut `aria-expanded` reflète désormais fidèlement la variable `ouvert`, exactement comme la classe CSS qui pilote la rotation visuelle du chevron. Un lecteur d'écran annonce alors correctement l'état « développé » ou « réduit » du panneau à chaque activation du bouton.

## Prévention

Pour éviter que ce type d'oubli ne se reproduise sur d'autres composants du même plugin, l'équipe a ajouté une règle de revue de code systématique : toute recherche du motif `aria-expanded={ false }` ou `aria-expanded={ true }` dans une revue de pull request, avec valeur littérale plutôt que variable, déclenche une question explicite à l'auteur du changement. Un script de recherche simple permet de repérer ces occurrences avant même la revue humaine :

```
grep -rn "aria-expanded={ true }\|aria-expanded={ false }" src/ --include="*.js"
```

Un test automatisé complète ce filet de sécurité, en simulant un clic sur le bouton et en vérifiant que l'attribut change de valeur en conséquence, plutôt que de se contenter de vérifier l'apparition du contenu du panneau :

```
test( 'aria-expanded suit l\'état d\'ouverture du panneau', () => {
  const { getByRole } = render( <PanneauReglagesAvances>Contenu</PanneauReglagesAvances> );
  const bouton = getByRole( 'button', { name: /réglages avancés/i } );

  expect( bouton ).toHaveAttribute( 'aria-expanded', 'false' );
  fireEvent.click( bouton );
  expect( bouton ).toHaveAttribute( 'aria-expanded', 'true' );
} );
```

> Conseil maison : tester un composant repliable au clavier, sans jamais regarder l'écran, révèle immédiatement ce genre de décalage entre rendu visuel et attribut ARIA — l'oreille ne se laisse pas distraire par un chevron qui tourne correctement.

## En résumé

Un attribut ARIA codé en dur pendant une phase de développement rapide reste un piège classique : il fonctionne au premier essai, puis se fige silencieusement dès qu'une logique d'état dynamique est ajoutée ailleurs dans le composant sans qu'on pense à le relier. Vérifier systématiquement qu'un attribut d'état ARIA dépend bien de la même variable que le rendu visuel qu'il est censé refléter évite ce décalage, détectable uniquement par un test au clavier ou un test automatisé ciblé.
