Skip to content

Enhance/accessibility - Increase lighthouse accessibility score to 99%#239

Open
Samuelfaure wants to merge 29 commits into
developfrom
enhance/accessibility
Open

Enhance/accessibility - Increase lighthouse accessibility score to 99%#239
Samuelfaure wants to merge 29 commits into
developfrom
enhance/accessibility

Conversation

@Samuelfaure

@Samuelfaure Samuelfaure commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Avant
entreprise_before
particuier_before

Après
entreprise_after
particulier_after

Reste à faire: check manuel des rendus

closes API-6742, API-6743, API-6951, API-6952, API-6953, API-6954, API-6955, API-6956, API-6957, API-6958

@Samuelfaure Samuelfaure changed the title Enhance/accessibility Enhance/accessibility - Increase lighthouse accessibility score to 99% Jun 30, 2026
@skelz0r

skelz0r commented Jun 30, 2026

Copy link
Copy Markdown
Member

@Samuelfaure y'a déjà plusieurs PRs d'ouvertes sur l'a11y, tu peux checker si ça se recoupe, et si oui, review/merge et/ou intégrer dans ta PR en mentionnant les issues linear ?

@Samuelfaure

Copy link
Copy Markdown
Contributor Author

@skelz0r Les issues linear ont comme sujet de faire un audit, ici c'est pas vraiment un audit 🤔

Je vais recouper avec les autres PRs oui 👍

@skelz0r

skelz0r commented Jul 1, 2026

Copy link
Copy Markdown
Member

Isa a fait l'audit déjà :D

@Samuelfaure Samuelfaure force-pushed the enhance/accessibility branch from 254a23e to f32052a Compare July 1, 2026 08:28
@linear

linear Bot commented Jul 1, 2026

Copy link
Copy Markdown

Samuelfaure and others added 23 commits July 1, 2026 13:44
…ne (API-6739)

- Pictos décoratifs (_show et _others) : alt="" au lieu de l'uid technique
- <label> non associé à un contrôle → <p> (RGAA 8.9)
- Lien DataPass target="_blank" : ajout rel="noopener noreferrer" et avertissement sr-only (RGAA 13.2)

Ref: https://linear.app/pole-api/issue/API-6739
Le composant fr-sidemenu nécessite un <button class="fr-sidemenu__btn">
avec aria-controls/aria-expanded pour que le DSFR JS puisse afficher
le menu de catégories sur mobile. Sans lui, la navigation par catégorie
est inaccessible au clavier sur petits écrans.

Ref: https://linear.app/pole-api/issue/API-6738
…I-6734)

Les logos des fournisseurs dans les cartes du catalogue n'avaient pas
d'attribut alt : Rails génère alors un alt par défaut (nom de fichier)
vocalisé par les lecteurs d'écran. Ces logos sont décoratifs — le nom
du fournisseur est déjà dans le texte adjacent. alt="" explicite.

Idem pour le picto FranceConnect dans la carte API Particulier.

Ref: https://linear.app/pole-api/issue/API-6734
…I-6736)

RGAA 8.2 / 8.9 / 9.3 — <ul> et <p> imbriqués dans <p> :
- Section CGU (Entreprise et Particulier) : <ul> sorti du <p>
- Bloc deprecated (Entreprise et Particulier) : idem
- Bloc incoming : <p> imbriqué dans <p> → deux paragraphes séparés

RGAA 9.2 — nav sans aria-label :
- <nav role="navigation"> → <nav aria-label="Sommaire de la page">
  (role="navigation" redondant sur <nav> supprimé, class="" vide supprimée)

RGAA 1.1 — alt non pertinent :
- Logo fournisseur : "Logo du fournisseur de données" → "Logo de {nom}"

Ref: https://linear.app/pole-api/issue/API-6736
Typographic single quotes (U+2018/U+2019) were inadvertently introduced
in the ERB files instead of ASCII single quotes, making the compiled
Ruby code unparseable by Prism in certain contexts.
…C 8.6, 9.2, 12.1)

- admin.html.erb: corrige typo "Entreculier" → "API Entreprise / API Particulier" (NC 8.6)
- admin/editor/provider: ajout lien d'évitement DSFR (NC 12.1)
- admin: <div fr-container> → <main id="content"> (NC 9.2)
- editor/provider: <div fr-container> → <main id="content" class="fr-container ..."> (NC 9.2)
- shared/admin/_header.html.erb: correction même typo dans le titre service
…title (NC 8.5, 9.2, 12.1)

- layouts AE et AP : support content_for(:title) pour des titres de page dynamiques (NC 8.5)
- layouts AE et AP : ajout lien d'évitement DSFR href="#main-content" (NC 12.1)
- _containerized_body AE et AP : <div> → <main id="main-content"> (NC 9.2)
… 1.1, 1.2, 7.5, 9.1)

- _modal: image loading.gif alt="" (NC 1.1)
- _loader: alt="" + role="status" + fr-sr-only "Chargement en cours" (NC 1.1, 7.5)
- _impersonate: role="alert" sur la div d'avertissement (NC 7.5)
- section_access AE/AP: pictogrammes SVG alt="" (redondants avec le texte) (NC 1.2)
  et h4 → h3 pour la colonne droite (NC 9.1)
- section_developers AE: aria-live="polite" sur le badge Hyperping (NC 7.5)

Partially solves API-6954, API-6955
- footers AE/AP: fr-sr-only "(nouvelle fenêtre)" sur le lien licence etalab
- menus AE/AP: fr-sr-only sur le lien "Demander un accès" (DataPass)
- header tools AE/AP: fr-sr-only sur le lien page de statut
- section_howto AE: fr-sr-only sur les liens DataPass et annuaire-entreprises
- section_partners AE/AP: suppression target="_blank" sur routes internes
- section_demonstrateur AP: link_to bloc avec fr-sr-only
- locales AE/AP: fr-sr-only injecté dans les liens simplifions.data.gouv.fr en YAML

Partially solves API-6952
…externe (NC 1.2, 3.1, 6.1, 7.5, 8.5, 9.1, 9.3, 13.2)

- status.html.erb: lien <a> vide → aria-label statut + fr-sr-only (NC 6.1, 13.2)
  cercle CSS-only → aria-hidden + texte sr-only (NC 3.1)
- index AE/AP: content_for :title (NC 8.5) + aria-live sur div "Aucun résultat" (NC 7.5)
- show AE/AP: content_for :title (NC 8.5) + <h3 fr-alert__title> → <p> (NC 9.1)
- _details AE: divs format/paramètres → <ul><li> (NC 9.3) + fr-sr-only liens status/tests (NC 13.2)
- _details AP: fr-sr-only sur liens externes + alt="" sur pictogramme FranceConnect (NC 1.2, 13.2)
- _property.html.erb: auto_link avec fr-sr-only sur les liens auto-détectés (NC 13.2)
- _use_cases.html.erb: fr-sr-only sur les liens "Voir en détail" (NC 13.2)

Partially solves API-6952
…NC 1.2, 7.1, 8.5, 9.1, 13.2)

- _scopes.html.erb: <span tabindex="0"> → <h3 class="fr-accordion__title"> (NC 7.1)
- show: aria-hidden="true" sur l'icône flèche (NC 1.2)
- ask_for_prolongation: aria-hidden icône + fr-sr-only sur lien DataPass (NC 1.2, 13.2)
- prolong: aria-hidden icône + fr-sr-only sur liens DataPass et CTA (NC 1.2, 13.2)
- renew: fr-sr-only "(nouvelle fenêtre)" sur le CTA (NC 13.2)
- stats: content_for :title + h4 → h2, h5 → h3 (NC 8.5, 9.1)

Partially solves API-6952
…tut, DataPass (NC 3.1, 5.3, 9.1, 13.2)

- sessions/_new: h2 → h1 class="fr-h2" (NC 9.1) + fr-sr-only sur lien ProConnect (NC 13.2)
- wizard/_header: h2 → h1 class="fr-h2" (NC 9.1) + fr-sr-only sur lien DataPass (NC 13.2)
- wizard/contacts: h4 → h3 (hiérarchie correcte sous h1) (NC 9.1)
- wizard/prolonged: h3.fr-alert__title → p.fr-alert__title (NC 9.1)
- wizard/owner + authorization_requests/_header: fr-sr-only sur liens DataPass (NC 13.2)
- authorization_requests/show: caption tableau + h2 contact_technique → h3 (NC 5.3, 9.1)
- pages/current_status: aria-hidden sur cercle + fr-sr-only statut textuel (NC 3.1, 13.2)

Partially solves API-6952
….3, 6.1, 8.5, 9.3, 13.2)

Titres de page (NC 8.5):
- home AE/AP, accessibility, mentions, errors, profile, changelogs: content_for :title

Cas d'usages:
- _others: div.use_cases → ul.fr-raw-list + li (NC 9.3)
  + aria-label contextuel sur les liens "En savoir plus" (NC 6.1)
- _simplifions_card: fr-sr-only "(ouverture dans un nouvel onglet)" (NC 13.2)
- _endpoints_table: <caption class="fr-sr-only"> (NC 5.3)
- index AE/AP: content_for :title (NC 8.5)

Partially solves API-6952
Partner logo alt describes link destination rather than the image itself.
Decorative pictos alongside headings get empty alt.

Closes https://linear.app/pole-api/issue/API-6954
Adds external_link_to helper that appends a fr-sr-only "(nouvelle fenêtre)"
span and enforces target=_blank + rel=noopener noreferrer. Migrates all
external links in shared layout, footer, header, newsletter and home sections.
Also removes target=_blank from the internal partner link and fixes the
taget=_blank typo in the footer.

Closes https://linear.app/pole-api/issue/API-6952
… (RGAA 6.1)

Two issues fixed:
- _donnees_personnelles partials (both APIs) had `href` without `mailto:` prefix,
  making the links non-functional
- accessibility.html.erb, mentions.html.erb and _donnees_personnelles partials
  use raw email addresses as link text; added `title` attributes so screen
  readers announce the purpose when reading link lists out of context

Closes https://linear.app/pole-api/issue/API-6953
Two issues on api_entreprise home:
- _section_access: right-column cards used <h4> creating an h2→h4 jump
  relative to a preceding sibling h2; changed to <h3> (same level as
  the info items in the left column). Same fix applied to api_particulier.
- _section_howto: "Accès à mon compte" sub-block used <h2> at the same
  level as the section title; demoted to <h3>.

DSFR visual classes (fr-h4 etc.) are unaffected — only semantic level changed.

Closes https://linear.app/pole-api/issue/API-6955
… 8.7)

- "newsletter" → "lettre d'information" (DSFR official term) in
  api_particulier newsletter_title and api_entreprise iframe title
- "changelog" in newsletter section titles wrapped with <span lang="en">
- "Swagger" and "OpenAPI" in section_developers wrapped with <span lang="en">;
  swagger_description values now rendered with .html_safe to allow HTML
- "back-office" in api_entreprise section_use_cases description wrapped
  with <span lang="en">

Closes https://linear.app/pole-api/issue/API-6957
Covers API Entreprise and API Particulier. Skip link targets real ids
already present in the DOM (#contenu, #navigation-header-menu, #footer).

Non-regression: landmarks asserted on home, cas usages, catalogue, FAQ
and an authenticated account page for both APIs (65 examples).

Closes https://linear.app/pole-api/issue/API-6743
Two-layer strategy:
- Views call `content_for(:page_title)` for dynamic titles (endpoint name,
  cas_usage name, blog post title, authorization request intitulé)
- Static pages declare `page_title:` i18n keys under their controller/action;
  empty string = use site name only
- `full_page_title` helper: checks content_for first, falls back to i18n,
  raises in local/test env if neither is set — guarantees no silent gaps

Added `AbstractBlogPost#title` to parse the first `# ` heading from markdown.

Closes https://linear.app/pole-api/issue/API-6951
@Samuelfaure Samuelfaure force-pushed the enhance/accessibility branch from f4c5f67 to 5e8696a Compare July 1, 2026 11:56
…g_posts, redoc v2/v3 (NC 8.5)

Adds fallback page_title keys so static_page_title does not raise in
Rails.env.local? when content_for(:page_title) is a no-op (nil intitule,
nil blog post title, or view that never sets the key).

- authorization_requests#show — both AE and AP
- blog_posts#show — both AE and AP
- pages#redoc_v2 and pages#redoc_v3 — AP only
…el (NC 1.2, 13.2)

AP _section_partners had kept the old decorative alt "Logo fournisseur de
données - …" while AE was already updated to the link-purpose label "Voir
les API de …" (RGAA 6.1).

Three different sr-only strings were used for the same semantic action
(link opens in a new window): "(ouvre une nouvelle fenêtre)",
"(ouverture dans un nouvel onglet)", and "(nouvelle fenêtre)". Normalised
to the canonical "(nouvelle fenêtre)" from external_link_to across all
templates.
…sing container

external_link_to reassigned html_options to {} in the block-given branch
when href was not a Hash, silently discarding any options passed as the
third positional argument (aria-*, data-*, class). Changed to only
override html_options when href is actually a Hash (the intended block-form
calling convention).

Admin layout <main> had no CSS classes while editor and provider layouts
both use fr-container fr-mb-5w fr-mt-5w, causing admin pages to render
full-width without layout constraints.
@Samuelfaure

Copy link
Copy Markdown
Contributor Author
image slight increase 👍

@Samuelfaure Samuelfaure requested review from Un3x and skelz0r and removed request for skelz0r July 1, 2026 12:21
@Samuelfaure Samuelfaure marked this pull request as ready for review July 1, 2026 12:21
@Samuelfaure Samuelfaure requested a review from skelz0r July 1, 2026 12:21
@Isalafont

Copy link
Copy Markdown
Collaborator

J'ai fait un Pré-audit assistée par IA (20% du RGAA environ), il restera encore toute la partie audit "humaine" à faire mais c'est top cela va me permettre d'aller plus vite sur les pages. 👍🏻

@Isalafont Isalafont left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super travail !!
Cela couvre une bonne partie des problèmes détectés et cela va grandement me faciliter l'audit "humaine" page par page restante à faire 🚀

J'ai ajouté des petits commentaires et des suggestions. N'hésites pas si tu veux que l'on en discute car tu connais le site bien mieux que moi et tu saura mieux m'orienter vers la bonne solution à te conseiller.

Je ne sais plus si dans les tickets que tu as mergé, il y avait celui du plan du site inclus ?

Ce que je peux te suggérer, mais c'est à toi de me dire, vu que la PR est déjà volumineuse, on peut très bien se dire que pour les premiers commentaires suggéré, on créé des tickets dédiés pour n'avoir à traiter que ces points séparément dans une autre PR.
Si tu préfères cela, on pourra merger celle ci. A toi de me dire !

Comment thread site/app/views/layouts/api_entreprise/application.html.erb Outdated
Comment thread site/app/views/shared/api_particulier/_containerized_body.html.erb Outdated
Comment thread site/app/views/shared/api_entreprise/_containerized_body.html.erb Outdated
Comment thread site/app/views/layouts/api_entreprise/application.html.erb
page_title: Mon compte
authorization_requests:
index:
page_title: Mes demandes

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: je changerais le titre en Mes habilitations qui me semble plus pertinent.
Le H1 de cette page est Habilitation API Particulier et la différence entre les 2 titres pourraient perturber certains utilisateurs.

index:
page_title: Mes demandes
show:
page_title: Mon habilitation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il me semble que ce titre n'est jamais atteint.

Cela correspond bien à cette page ?
Sur API Particulier, une fois connecté, la page show d’une habilitation n’affiche jamais le titre attendu Mon habilitation. Le <title> reste par exemple sur Mairie de Bordeaux, alors que l’utilisateur consulte bien le détail d’une habilitation.

Image

Titre de page dynamique ignoré

Cela vient probablement du fait que la vue définit un bloc content_for :title, mais que le helper full_page_title ne lit que :page_title. Le titre dynamique défini par la vue n’est donc jamais pris en compte.

Suggestion : faire lire :title par full_page_title, ou harmoniser les vues pour utiliser uniquement content_for :page_title.

Exemple robuste côté helper :

def full_page_title
  page_title =
    if content_for?(:page_title)
      content_for(:page_title)
    elsif content_for?(:title)
      content_for(:title)
    else
      t('.page_title')
    end

  [page_title, current_app_name].compact.join(' | ')
end

Cela permettrait aux pages comme la show d’une habilitation d’exposer correctement leur titre dynamique
dans <title>.

<% if properties['description'] %>
<div class="description fr-highlight--beige-gris-galet fr-text--sm fr-pl-2w">
<%= simple_format(auto_link(properties['description'], target: '_blank'), {}, wrapper_tag: 'div') %>
<%= simple_format(auto_link(properties['description'], target: '_blank', rel: 'noopener noreferrer') { |url| "#{url}<span class=\"fr-sr-only\">(nouvelle fenêtre)</span>".html_safe }, {}, wrapper_tag: 'div') %>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Le bloc passé à auto_link semble destiné à ajouter l’indication accessible (nouvelle fenêtre) aux URLs détectées automatiquement :

auto_link(properties['description'], target: '_blank', rel: 'noopener noreferrer') do |url|
  "#{url}<span class=\"fr-sr-only\">(nouvelle fenêtre)</span>".html_safe
end

Mais dans le rendu actuel, ces URLs ne semblent pas ouvrir de nouvelle fenêtre / nouvel onglet. Dans ce cas, l’indication (nouvelle fenêtre) ne doit pas être ajoutée.

Il faudrait donc clarifier l’intention :

  • soit ces liens doivent s’ouvrir dans le même onglet : supprimer le bloc et éviter target: '_blank' ;
  • soit ces liens doivent s’ouvrir dans un nouvel onglet : s’assurer que target="_blank" est bien rendu et que le wrapper auto_link transmet le bloc.

Aujourd’hui, le bloc est trompeur car il suggère un comportement “nouvelle fenêtre” qui ne correspond pas au rendu observé.

Si je ne me trompe pas ça doit correspondre en test a une url se trouvant par exemple dans cette page
http://particulier.api.localtest.me:3000/catalogue/mesri/statut_etudiant

Image

et pour tester :
dans :
commons/endpoints/_swagger_shared/mesri.yml:123

Et cette description est rendue par :

site/app/views/shared/endpoints/_property.html.erb:26

  1. Ouvrir http://particulier.api.localtest.me:3000/catalogue/mesri/statut_etudiant
  2. Aller dans la section des données.
  3. Chercher la propriété Établissement d'études.
  4. Inspecter le lien https://annuaire-education.fr.
  5. Vérifier si le HTML généré contient ou non le contenu attendu dans le lien.

Je suis preneuse d'en discuter avec toi 👍🏻

@Samuelfaure

Copy link
Copy Markdown
Contributor Author

@Isalafont La PR plan du site est ici #204 ça ne chevauchait pas sur celle ci donc j'ai pas touché

Je vais faire les quick-wins dans ta review et si y'a de gros morceaux on corrigera après 👍 comme ça on ship

Address review feedback from PR #239: containerized_body partials
opened a second <main> nested inside the layout's <main id="contenu">,
breaking the unique-landmark rule. Also add tabindex="-1" to the
three skip-link targets (#contenu, nav menu, footer) so Firefox/Safari
actually move focus on activation, matching Chrome's native behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants