WCAG 3.3.1: autofocus first invalid field on add/edit form validation failure

Add App::flashAutofocus(fieldName) and consumeAutofocus() to the thin App
helper so action handlers can identify which field caused a validation error
and the form page can move browser focus directly to it on reload.

Changes:
- src/App.php — flashAutofocus() stores field name in _flash_autofocus
  session key; consumeAutofocus() drains it and returns the name (or null)
- actions/formulaire.php — catch block maps exception messages to field
  names (auteurice, titre, synopsis, année, orientation, ap, finality,
  languages, tag, lien) and calls App::flashAutofocus()
- actions/edit.php — catch block maps common edit errors to field names
  and calls App::flashAutofocus()
- add.php — consumes the hint via App::consumeAutofocus() into
  $autofocusField; withAutofocus() helper merges autofocus=>true into
  $attrs for every field include; synopsis textarea gets inline autofocus
- edit.php — same pattern with inline ternary merges and textarea autofocus
- templates/partials/form/text-field.php — $attrs loop now emits bare
  attribute names (no ="...") when value === true, supporting autofocus,
  disabled, readonly etc. without special-casing
- templates/partials/form/select-field.php — same boolean-attr support
  added; $attrs variable initialised to [] when caller omits it

Closes WCAG 3.3.1 autofocus item in todo/04-accessibility.md.
This commit is contained in:
Pontoporeia
2026-04-04 12:23:03 +02:00
parent 4c3f71b6e4
commit c2eff75789
9 changed files with 106 additions and 18 deletions

View File

@@ -24,10 +24,23 @@ try {
die("Erreur lors du chargement du formulaire.");
}
$formData = $_SESSION["form_data"] ?? [];
unset($_SESSION["form_data"]);
$formData = $_SESSION['form_data'] ?? [];
unset($_SESSION['form_data']);
$autofocusField = App::consumeAutofocus();
// Flash error consumed by the flash-messages partial below.
/**
* Merge autofocus into the $attrs array for a given field.
* Only adds the attribute when $autofocusField matches $fieldName.
*/
function withAutofocus(string $fieldName, array $attrs = []): array {
global $autofocusField;
if ($autofocusField === $fieldName) {
$attrs['autofocus'] = true;
}
return $attrs;
}
function old($key, $default = "") {
global $formData;
return isset($formData[$key]) ? htmlspecialchars($formData[$key]) : $default;
@@ -50,9 +63,9 @@ function wasSelected($key, $value) {
<form action="actions/formulaire.php" method="post" enctype="multipart/form-data" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
<?php $name = 'titre'; $label = 'Titre :'; $value = old('titre'); $required = true; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'titre'; $label = 'Titre :'; $value = old('titre'); $required = true; $attrs = withAutofocus('titre'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'subtitle'; $label = 'Sous-titre (si applicable) :'; $value = old('subtitle'); $required = false; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old('auteurice'); $required = true; $attrs = ['autocomplete' => 'name']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old('auteurice'); $required = true; $attrs = withAutofocus('auteurice', ['autocomplete' => 'name']); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'mail'; $label = 'Contact(s) (optionnel) [mail/site/insta/etc.] :'; $value = old('mail'); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php require APP_ROOT . '/templates/partials/form/jury-fieldset.php'; ?>
@@ -61,34 +74,35 @@ function wasSelected($key, $value) {
$name = 'année'; $label = 'Année :'; $value = old('année'); $required = true;
$type = 'number';
$placeholder = date('Y');
$attrs = ['min' => 2000, 'max' => date('Y') + 1];
$attrs = withAutofocus('année', ['min' => 2000, 'max' => date('Y') + 1]);
include APP_ROOT . '/templates/partials/form/text-field.php';
?>
<?php $name = 'orientation'; $label = 'Orientation :'; $options = $orientations; $selected = $formData['orientation'] ?? ''; $required = true; $placeholder = ''; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
<?php $name = 'orientation'; $label = 'Orientation :'; $options = $orientations; $selected = $formData['orientation'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('orientation'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
<?php $name = 'ap'; $label = 'Atelier pluridisciplinaire :'; $options = $apPrograms; $selected = $formData['ap'] ?? ''; $required = true; $placeholder = ''; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
<?php $name = 'ap'; $label = 'Atelier pluridisciplinaire :'; $options = $apPrograms; $selected = $formData['ap'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('ap'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
<?php $name = 'finality'; $label = 'Finalité du master :'; $options = $finalityTypes; $selected = $formData['finality'] ?? ''; $required = true; $placeholder = ''; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
<?php $name = 'finality'; $label = 'Finalité du master :'; $options = $finalityTypes; $selected = $formData['finality'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('finality'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
<?php $name = 'languages'; $label = 'Langue(s) :'; $options = $languages; $checked = $formData['languages'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
<?php $name = 'formats'; $label = 'Format(s) :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
<?php $name = 'tag'; $label = 'Mots-clés :'; $value = old('tag'); $placeholder = 'sociologie, anthropologie, ...'; $hint = 'Séparez par des virgules. Max 10 mots-clés.'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'tag'; $label = 'Mots-clés :'; $value = old('tag'); $placeholder = 'sociologie, anthropologie, ...'; $hint = 'Séparez par des virgules. Max 10 mots-clés.'; $attrs = withAutofocus('tag'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<!-- Synopsis -->
<div>
<label for="synopsis">Synopsis :</label>
<textarea id="synopsis" name="synopsis"
rows="7" required><?= old('synopsis') ?></textarea>
rows="7" required
<?= $autofocusField === 'synopsis' ? 'autofocus' : '' ?>><?= old('synopsis') ?></textarea>
</div>
<?php $name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes; $selected = $formData['license_id'] ?? ''; $placeholder = '— Inconnue —'; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
<?php $name = 'duration_info'; $label = 'Durée / Taille :'; $value = old('duration_info'); $placeholder = 'Ex : 84 pages'; $hint = 'Durée (minutes) ou nombre de pages.'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'lien'; $label = 'Lien (site / ressource) :'; $value = old('lien'); $type = 'url'; $placeholder = 'https://...'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'lien'; $label = 'Lien (site / ressource) :'; $value = old('lien'); $type = 'url'; $placeholder = 'https://...'; $attrs = withAutofocus('lien'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'couverture'; $label = 'Image de couverture :'; $accept = 'image/jpeg,image/png'; $hint = 'JPG, PNG. Taille max : 10 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>