mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
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:
2
TODO.md
2
TODO.md
@@ -11,6 +11,8 @@ Pending tasks have been split into topic files under [`todo/`](todo/README.md):
|
|||||||
|
|
||||||
## Recently completed (this session)
|
## Recently completed (this session)
|
||||||
|
|
||||||
|
- [x] WCAG 3.3.1 `autofocus` on first invalid field — `App::flashAutofocus()` / `consumeAutofocus()` added; `actions/formulaire.php` maps exception messages → field names and stores the autofocus hint in `$_SESSION['_flash_autofocus']`; `actions/edit.php` does the same; `add.php` consumes it via a `withAutofocus()` helper and injects `autofocus => true` into `$attrs` for `text-field.php` / `select-field.php` includes; `edit.php` uses inline ternary for the same; `text-field.php` and `select-field.php` partials now support boolean `true` values in `$attrs` (emit bare attribute names for `autofocus`, `required`, etc.)
|
||||||
|
|
||||||
- [x] `config/apropos.php` — extracted hardcoded contacts (Laurent Leprince, Xavier Gorgol, Brigitte Ledune) and credits into a config array (`contacts[]`, `credits[]`, `erg_url`); `public/apropos.php` now loops over the config with `htmlspecialchars` instead of embedding names/emails in HTML
|
- [x] `config/apropos.php` — extracted hardcoded contacts (Laurent Leprince, Xavier Gorgol, Brigitte Ledune) and credits into a config array (`contacts[]`, `credits[]`, `erg_url`); `public/apropos.php` now loops over the config with `htmlspecialchars` instead of embedding names/emails in HTML
|
||||||
- [x] `todo/02-php-components.md` — audited and marked 8 stale items as already done: all 5 form field partials (`text-field`, `select-field`, `checkbox-list`, `file-field`, `jury-fieldset`), `admin-alert.php`/`flash-messages.php` consolidation, `RateLimit` cache dir placement, and `apropos.php` contacts extraction
|
- [x] `todo/02-php-components.md` — audited and marked 8 stale items as already done: all 5 form field partials (`text-field`, `select-field`, `checkbox-list`, `file-field`, `jury-fieldset`), `admin-alert.php`/`flash-messages.php` consolidation, `RateLimit` cache dir placement, and `apropos.php` contacts extraction
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,18 @@ try {
|
|||||||
error_log("Edit action error: " . $e->getMessage());
|
error_log("Edit action error: " . $e->getMessage());
|
||||||
|
|
||||||
App::flash('error', $e->getMessage());
|
App::flash('error', $e->getMessage());
|
||||||
|
|
||||||
|
// WCAG 3.3.1 — map error to the field that caused it so the form can autofocus it.
|
||||||
|
$msg = $e->getMessage();
|
||||||
|
$autofocusField = null;
|
||||||
|
if (str_contains($msg, 'titre') || str_contains($msg, 'Titre')) $autofocusField = 'titre';
|
||||||
|
elseif (str_contains($msg, 'année') || str_contains($msg, 'année')) $autofocusField = 'année';
|
||||||
|
elseif (str_contains($msg, 'synopsis') || str_contains($msg, 'Synopsis')) $autofocusField = 'synopsis';
|
||||||
|
elseif (str_contains($msg, 'auteur') || str_contains($msg, 'Auteur')) $autofocusField = 'auteurice';
|
||||||
|
if ($autofocusField !== null) {
|
||||||
|
App::flashAutofocus($autofocusField);
|
||||||
|
}
|
||||||
|
|
||||||
header('Location: ../edit.php?id=' . $thesisId);
|
header('Location: ../edit.php?id=' . $thesisId);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,6 +322,24 @@ try {
|
|||||||
App::flash('error', $e->getMessage());
|
App::flash('error', $e->getMessage());
|
||||||
$_SESSION['form_data'] = $_POST;
|
$_SESSION['form_data'] = $_POST;
|
||||||
|
|
||||||
|
// WCAG 3.3.1 — identify which field caused the error and request autofocus.
|
||||||
|
// Mapping is based on the exception messages thrown above.
|
||||||
|
$msg = $e->getMessage();
|
||||||
|
$autofocusField = null;
|
||||||
|
if (str_contains($msg, "Nom/Prénom/Pseudo")) $autofocusField = 'auteurice';
|
||||||
|
elseif (str_contains($msg, "Titre du mémoire")) $autofocusField = 'titre';
|
||||||
|
elseif (str_contains($msg, "Synopsis")) $autofocusField = 'synopsis';
|
||||||
|
elseif (str_contains($msg, "Année invalide")) $autofocusField = 'année';
|
||||||
|
elseif (str_contains($msg, "orientation")) $autofocusField = 'orientation';
|
||||||
|
elseif (str_contains($msg, "Atelier Pratique")) $autofocusField = 'ap';
|
||||||
|
elseif (str_contains($msg, "finalité")) $autofocusField = 'finality';
|
||||||
|
elseif (str_contains($msg, "langue")) $autofocusField = 'languages';
|
||||||
|
elseif (str_contains($msg, "mots-clés")) $autofocusField = 'tag';
|
||||||
|
elseif (str_contains($msg, "Lien URL")) $autofocusField = 'lien';
|
||||||
|
if ($autofocusField !== null) {
|
||||||
|
App::flashAutofocus($autofocusField);
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect back to form with preserved data
|
// Redirect back to form with preserved data
|
||||||
header('Location: ../add.php');
|
header('Location: ../add.php');
|
||||||
exit();
|
exit();
|
||||||
|
|||||||
@@ -24,10 +24,23 @@ try {
|
|||||||
die("Erreur lors du chargement du formulaire.");
|
die("Erreur lors du chargement du formulaire.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$formData = $_SESSION["form_data"] ?? [];
|
$formData = $_SESSION['form_data'] ?? [];
|
||||||
unset($_SESSION["form_data"]);
|
unset($_SESSION['form_data']);
|
||||||
|
$autofocusField = App::consumeAutofocus();
|
||||||
// Flash error consumed by the flash-messages partial below.
|
// 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 = "") {
|
function old($key, $default = "") {
|
||||||
global $formData;
|
global $formData;
|
||||||
return isset($formData[$key]) ? htmlspecialchars($formData[$key]) : $default;
|
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">
|
<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"]) ?>">
|
<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 = '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 $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'; ?>
|
<?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;
|
$name = 'année'; $label = 'Année :'; $value = old('année'); $required = true;
|
||||||
$type = 'number';
|
$type = 'number';
|
||||||
$placeholder = date('Y');
|
$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';
|
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 = '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 = '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 -->
|
<!-- Synopsis -->
|
||||||
<div>
|
<div>
|
||||||
<label for="synopsis">Synopsis :</label>
|
<label for="synopsis">Synopsis :</label>
|
||||||
<textarea id="synopsis" name="synopsis"
|
<textarea id="synopsis" name="synopsis"
|
||||||
rows="7" required><?= old('synopsis') ?></textarea>
|
rows="7" required
|
||||||
|
<?= $autofocusField === 'synopsis' ? 'autofocus' : '' ?>><?= old('synopsis') ?></textarea>
|
||||||
</div>
|
</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 = '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 = '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'; ?>
|
<?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'; ?>
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ try {
|
|||||||
// Set page title for header
|
// Set page title for header
|
||||||
$pageTitle = "Éditer TFE - " . htmlspecialchars($thesis['title']);
|
$pageTitle = "Éditer TFE - " . htmlspecialchars($thesis['title']);
|
||||||
|
|
||||||
|
// WCAG 3.3.1 — consume the autofocus hint stored by the edit action on validation failure.
|
||||||
|
$autofocusField = App::consumeAutofocus();
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log("Error loading edit page: " . $e->getMessage());
|
error_log("Error loading edit page: " . $e->getMessage());
|
||||||
die("Erreur lors du chargement: " . $e->getMessage());
|
die("Erreur lors du chargement: " . $e->getMessage());
|
||||||
@@ -70,12 +73,13 @@ try {
|
|||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||||
<input type="hidden" name="thesis_id" value="<?= $thesisId ?>">
|
<input type="hidden" name="thesis_id" value="<?= $thesisId ?>">
|
||||||
|
|
||||||
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = htmlspecialchars($thesis['authors']); $required = true; $attrs = ['autocomplete' => 'name']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = htmlspecialchars($thesis['authors']); $required = true; $attrs = array_merge(['autocomplete' => 'name'], $autofocusField === 'auteurice' ? ['autofocus' => true] : []); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||||
<?php $name = 'mail'; $label = 'Contact :'; $value = ''; $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
<?php $name = 'mail'; $label = 'Contact :'; $value = ''; $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$name = 'année'; $label = 'Année :'; $value = htmlspecialchars((string)$thesis['year']); $required = true;
|
$name = 'année'; $label = 'Année :'; $value = htmlspecialchars((string)$thesis['year']); $required = true;
|
||||||
$type = 'number';
|
$type = 'number';
|
||||||
|
$attrs = $autofocusField === 'année' ? ['autofocus' => true] : [];
|
||||||
include APP_ROOT . '/templates/partials/form/text-field.php';
|
include APP_ROOT . '/templates/partials/form/text-field.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -129,13 +133,14 @@ try {
|
|||||||
|
|
||||||
<?php $name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes; $selected = $currentLicenseId; $placeholder = '- Inconnue -'; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
<?php $name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes; $selected = $currentLicenseId; $placeholder = '- Inconnue -'; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||||
|
|
||||||
<?php $name = 'titre'; $label = 'Titre :'; $value = htmlspecialchars($thesis['title']); $required = true; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
<?php $name = 'titre'; $label = 'Titre :'; $value = htmlspecialchars($thesis['title']); $required = true; $attrs = $autofocusField === 'titre' ? ['autofocus' => true] : []; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||||
<?php $name = 'subtitle'; $label = 'Sous-titre :'; $value = htmlspecialchars($thesis['subtitle'] ?? ''); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
<?php $name = 'subtitle'; $label = 'Sous-titre :'; $value = htmlspecialchars($thesis['subtitle'] ?? ''); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||||
|
|
||||||
<!-- Synopsis (textarea — not covered by text-field partial) -->
|
<!-- Synopsis (textarea — not covered by text-field partial) -->
|
||||||
<div>
|
<div>
|
||||||
<label for="synopsis">Synopsis :</label>
|
<label for="synopsis">Synopsis :</label>
|
||||||
<textarea id="synopsis" name="synopsis" rows="7" required><?= htmlspecialchars($thesis['synopsis'] ?? '') ?></textarea>
|
<textarea id="synopsis" name="synopsis" rows="7" required
|
||||||
|
<?= $autofocusField === 'synopsis' ? 'autofocus' : '' ?>><?= htmlspecialchars($thesis['synopsis'] ?? '') ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php $name = 'languages'; $label = 'Langue(s) :'; $options = $languages; $checked = $currentLanguages; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
<?php $name = 'languages'; $label = 'Langue(s) :'; $options = $languages; $checked = $currentLanguages; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||||||
|
|||||||
21
src/App.php
21
src/App.php
@@ -83,6 +83,26 @@ class App
|
|||||||
$_SESSION["_flash_{$type}"] = $message;
|
$_SESSION["_flash_{$type}"] = $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the name of the field that should receive autofocus after a
|
||||||
|
* validation failure (WCAG 3.3.1).
|
||||||
|
*/
|
||||||
|
public static function flashAutofocus(string $fieldName): void
|
||||||
|
{
|
||||||
|
$_SESSION['_flash_autofocus'] = $fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume and return the autofocus field name, then clear it.
|
||||||
|
* Returns null when no autofocus hint is present.
|
||||||
|
*/
|
||||||
|
public static function consumeAutofocus(): ?string
|
||||||
|
{
|
||||||
|
$field = $_SESSION['_flash_autofocus'] ?? null;
|
||||||
|
unset($_SESSION['_flash_autofocus']);
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consume and return flash messages, then clear them from the session.
|
* Consume and return flash messages, then clear them from the session.
|
||||||
*
|
*
|
||||||
@@ -117,6 +137,7 @@ class App
|
|||||||
$_SESSION['form_error']
|
$_SESSION['form_error']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Note: autofocus is consumed separately via consumeAutofocus().
|
||||||
return ['error' => $error, 'success' => $success];
|
return ['error' => $error, 'success' => $success];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,24 @@ $required = $required ?? false;
|
|||||||
$placeholder = array_key_exists('placeholder', get_defined_vars()) ? $placeholder : '';
|
$placeholder = array_key_exists('placeholder', get_defined_vars()) ? $placeholder : '';
|
||||||
$id = $id ?? $name;
|
$id = $id ?? $name;
|
||||||
$hint = $hint ?? null;
|
$hint = $hint ?? null;
|
||||||
|
$attrs = $attrs ?? [];
|
||||||
?>
|
?>
|
||||||
<div>
|
<div>
|
||||||
<label for="<?= htmlspecialchars($id) ?>"><?= htmlspecialchars($label) ?></label>
|
<label for="<?= htmlspecialchars($id) ?>"><?= htmlspecialchars($label) ?></label>
|
||||||
|
<?php
|
||||||
|
$selectAttrStr = '';
|
||||||
|
foreach ($attrs as $k => $v) {
|
||||||
|
if ($v === true) {
|
||||||
|
$selectAttrStr .= ' ' . htmlspecialchars($k);
|
||||||
|
} else {
|
||||||
|
$selectAttrStr .= ' ' . htmlspecialchars($k) . '="' . htmlspecialchars((string)$v) . '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
<select id="<?= htmlspecialchars($id) ?>"
|
<select id="<?= htmlspecialchars($id) ?>"
|
||||||
name="<?= htmlspecialchars($name) ?>"
|
name="<?= htmlspecialchars($name) ?>"
|
||||||
<?= $required ? 'required' : '' ?>>
|
<?= $required ? 'required' : '' ?>
|
||||||
|
<?= $selectAttrStr ?>>
|
||||||
<?php if ($placeholder !== null): ?>
|
<?php if ($placeholder !== null): ?>
|
||||||
<option value=""><?= htmlspecialchars($placeholder) ?></option>
|
<option value=""><?= htmlspecialchars($placeholder) ?></option>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -46,4 +58,4 @@ $hint = $hint ?? null;
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
unset($required, $placeholder, $id, $hint);
|
unset($required, $placeholder, $id, $hint, $attrs, $selectAttrStr, $k, $v);
|
||||||
|
|||||||
@@ -25,8 +25,12 @@ $attrs = $attrs ?? [];
|
|||||||
|
|
||||||
$attrStr = '';
|
$attrStr = '';
|
||||||
foreach ($attrs as $k => $v) {
|
foreach ($attrs as $k => $v) {
|
||||||
|
if ($v === true) {
|
||||||
|
$attrStr .= ' ' . htmlspecialchars($k);
|
||||||
|
} else {
|
||||||
$attrStr .= ' ' . htmlspecialchars($k) . '="' . htmlspecialchars((string)$v) . '"';
|
$attrStr .= ' ' . htmlspecialchars($k) . '="' . htmlspecialchars((string)$v) . '"';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<div>
|
<div>
|
||||||
<label for="<?= htmlspecialchars($id) ?>"><?= htmlspecialchars($label) ?></label>
|
<label for="<?= htmlspecialchars($id) ?>"><?= htmlspecialchars($label) ?></label>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
## 3.3.1 Error identification
|
## 3.3.1 Error identification
|
||||||
|
|
||||||
- [x] **`add.php`/`edit.php` validation errors** — `flash-messages.php` already emits `<p role="alert" data-type="error">` for errors and `<p role="status">` for success
|
- [x] **`add.php`/`edit.php` validation errors** — `flash-messages.php` already emits `<p role="alert" data-type="error">` for errors and `<p role="status">` for success
|
||||||
- [ ] **`add.php`/`edit.php` `autofocus` on first invalid field** — requires controller to pass back which field failed; deferred (larger refactor)
|
- [x] **`add.php`/`edit.php` `autofocus` on first invalid field** — `App::flashAutofocus(fieldName)` stores the failing field in `$_SESSION['_flash_autofocus']`; action handlers map exception messages to field names; `add.php` consumes via `withAutofocus()` helper + injects into `$attrs`; `edit.php` uses inline ternary; partials support boolean `true` in `$attrs` to emit bare attribute names
|
||||||
|
|
||||||
## 3.3.2 Labels or instructions
|
## 3.3.2 Labels or instructions
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user