Aller au contenu

PD-230 — Plan d'implémentation


📚 Navigation User Story | Document | | | ---------- | -- | | 📋 [Spécification](PD-230-specification.md) | | | 🛠️ **Plan d'implémentation** | *(ce document)* | | ✅ [Critères d'acceptation](PD-230-acceptability.md) | | | 📝 [Retour d'expérience](PD-230-rex.md) | | [← Retour à site-vitrine](../PD-225-epic.md) · [↑ Index User Story](index.md)

1. Découpage en composants

Structure accessibilité

src/
├── styles/
│   └── accessibility.css     # Focus visible, skip links
├── components/
│   ├── SkipLink.astro        # Lien "Aller au contenu"
│   └── *.astro               # Composants accessibles
├── layouts/
│   └── BaseLayout.astro      # Structure sémantique
└── ...

Configuration:
├── .eslintrc.js              # eslint-plugin-astro/jsx-a11y
├── pa11y.config.js           # Configuration pa11y-ci
└── .gitlab-ci.yml            # Job audit accessibilité

Composants clés

Composant Responsabilité
SkipLink.astro Navigation rapide clavier
accessibility.css :focus-visible, touch targets
BaseLayout.astro Landmarks ARIA, lang
pa11y.config.js Audit automatisé

2. Flux techniques

Audit accessibilité CI

git push
GitLab CI
    ├── npm run build
    ├── npm run lint (eslint a11y)
    └── npm run test:a11y (pa11y-ci)
            ├── Lance serveur local dist/
            ├── Teste chaque page
            └── Rapport WCAG violations
Tab
SkipLink (visible au focus)
Header navigation
Main content (#main-content)
Footer

3. Mapping invariants → mécanismes

Invariant Mécanisme technique
INV-1 : Contraste minimum Tokens couleurs (ratio 4.5:1)
INV-2 : Navigation clavier tabindex, :focus-visible

Styles accessibilité

/* src/styles/accessibility.css */

/* Skip link */
.skip-link {
  position: absolute;
  top: -100%;
  left: 0;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-bleu-securite);
  color: white;
  z-index: 9999;
  text-decoration: none;
}

.skip-link:focus {
  top: 0;
}

/* Focus visible */
:focus-visible {
  outline: 3px solid var(--color-bleu-securite);
  outline-offset: 2px;
}

:focus:not(:focus-visible) {
  outline: none;
}

/* Touch targets (WCAG 2.5.5) */
button,
a,
input,
select,
textarea,
[role="button"] {
  min-width: 48px;
  min-height: 48px;
}

/* Taille texte minimum */
body {
  font-size: max(1rem, 13px);
}

small,
.text-sm {
  font-size: max(0.875rem, 13px);
}

/* Réduire les animations si préférence utilisateur */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
---
// src/components/SkipLink.astro
const { lang } = Astro.props;
const label = lang === 'en'
  ? 'Skip to main content'
  : 'Aller au contenu principal';
---

<a href="#main-content" class="skip-link">
  {label}
</a>

Structure sémantique BaseLayout

---
// src/layouts/BaseLayout.astro (extrait)
const { lang } = Astro.props;
---

<!DOCTYPE html>
<html lang={lang}>
<head>...</head>
<body>
  <SkipLink lang={lang} />

  <header role="banner">
    <nav role="navigation" aria-label="Navigation principale">
      ...
    </nav>
  </header>

  <main id="main-content" role="main">
    <slot />
  </main>

  <footer role="contentinfo">
    ...
  </footer>
</body>
</html>

Configuration ESLint a11y

// .eslintrc.js
module.exports = {
  extends: [
    'plugin:astro/recommended',
    'plugin:astro/jsx-a11y-recommended'
  ],
  rules: {
    'astro/jsx-a11y/alt-text': 'error',
    'astro/jsx-a11y/anchor-has-content': 'error',
    'astro/jsx-a11y/click-events-have-key-events': 'error',
    'astro/jsx-a11y/heading-has-content': 'error',
    'astro/jsx-a11y/html-has-lang': 'error',
    'astro/jsx-a11y/img-redundant-alt': 'error',
    'astro/jsx-a11y/interactive-supports-focus': 'error',
    'astro/jsx-a11y/label-has-associated-control': 'error',
    'astro/jsx-a11y/no-autofocus': 'error',
    'astro/jsx-a11y/no-redundant-roles': 'error',
    'astro/jsx-a11y/role-has-required-aria-props': 'error',
    'astro/jsx-a11y/tabindex-no-positive': 'error'
  }
};

Configuration pa11y-ci

// pa11y.config.js
module.exports = {
  defaults: {
    standard: 'WCAG2AA',
    runners: ['axe', 'htmlcs'],
    timeout: 30000,
    wait: 1000
  },
  urls: [
    'http://localhost:4321/fr/',
    'http://localhost:4321/en/',
    'http://localhost:4321/fr/product',
    'http://localhost:4321/en/product',
    'http://localhost:4321/fr/pricing',
    'http://localhost:4321/en/pricing',
    'http://localhost:4321/fr/faq',
    'http://localhost:4321/en/faq'
  ]
};

4. Gestion des erreurs

Erreur Cause Mitigation
Contraste insuffisant Mauvaise couleur Tokens avec ratios validés
Image sans alt Oubli développeur ESLint bloque le build
Focus invisible CSS override :focus-visible obligatoire
Touch target < 48px Élément trop petit CSS min-width/height

Job CI accessibilité

# .gitlab-ci.yml (extrait)
a11y:
  stage: test
  script:
    - npm ci
    - npm run build
    - npm run preview &
    - sleep 5
    - npx pa11y-ci --config pa11y.config.js
  artifacts:
    reports:
      junit: pa11y-results.xml

5. Impacts sécurité

Aspect Mesure
XSS via aria-label Échappement Astro natif
Clickjacking X-Frame-Options: DENY
Social engineering Pas de liens trompeurs

6. Hypothèses techniques

Hypothèse Justification
Navigateurs modernes :focus-visible supporté >95%
pa11y-ci disponible Package npm stable
axe-core comme runner Standard industrie
Pas de contenu dynamique Audit statique suffisant

7. Points de vigilance

Point Risque Action
Images décoratives Alt vide vs alt manquant alt="" explicite
Formulaires Labels non associés Vérifier for/id
Modales Focus trap absent CSS-only ou <dialog>
Accordéons <details> non accessible Tester lecteurs d'écran
Liens "Cliquez ici" vague Texte descriptif

Fichiers à créer

Fichier Description
src/styles/accessibility.css Styles a11y
src/components/SkipLink.astro Skip link
.eslintrc.js Config ESLint a11y
pa11y.config.js Config pa11y-ci
.gitlab-ci.yml Job audit a11y