a11y(jury-fieldset): fix WCAG 3.3.2, 4.1.2, 2.1.1 + audit 1.4.4/1.4.12

3.3.2 Labels or instructions
- Replace bare <label>Lecteur·ices :</label> (no 'for', no associated control)
  with <fieldset class="admin-jury-lecteurs"><legend>Lecteur·ices</legend>
  giving AT a proper programmatic label for the entire lecteur group

4.1.2 Name, role, value — Externe checkboxes lacked group context
- Add aria-label="Promoteur·ice — externe" on the promoteur Externe checkbox
- Add aria-label="Lecteur·ice N — nom" on every lecteur name input
- Add aria-label="Lecteur·ice N — externe" on every lecteur Externe checkbox
- All three attributes added to both PHP-rendered rows and the addJuryRow() JS
  that builds new rows dynamically

2.1.1 Keyboard — remove buttons already had aria-label; verified and updated
  label text to "Supprimer le lecteur·ice N" (consistent with new numbering)

CSS (admin.css)
- Add .admin-body fieldset fieldset.admin-jury-lecteurs rule: removes
  border/padding/background from the nested fieldset so it reads as a
  sub-group inside the outer jury fieldset, not a double-bordered card

Audit (no code change)
- WCAG 1.4.4: all font-size values use rem — no px text sizing anywhere
- WCAG 1.4.12: only overflow:hidden on media containers and .sr-only utility;
  no essential text content is clipped by text-spacing overrides
- WCAG 4.1.2 bulk JS: result is a redirect to flash-messages.php which already
  emits role="alert"/role="status" — no additional JS announcement needed
This commit is contained in:
Pontoporeia
2026-04-03 13:10:24 +02:00
parent 769d56fabc
commit d9f94eeb13
4 changed files with 85 additions and 51 deletions

36
TODO.md
View File

@@ -7,28 +7,34 @@ Pending tasks have been split into topic files under [`todo/`](todo/README.md):
| [todo/01-css-semantic-refactor.md](todo/01-css-semantic-refactor.md) | CSS class audit, semantic HTML (public + admin), inline style extraction, favicon |
| [todo/02-php-components.md](todo/02-php-components.md) | Form field partials, shared UI partials, controller extraction, backend maintenance |
| [todo/03-system-cache.md](todo/03-system-cache.md) | `system_cache` table, `SystemCache` class, `system.php` refactor |
| [todo/04-accessibility.md](todo/04-accessibility.md) | WCAG 2.1 AA remaining failures grouped by success criterion |
| [todo/04-accessibility.md](todo/04-accessibility.md) | WCAG 2.1 AA - remaining failures grouped by success criterion |
## Recently completed (this session)
- [x] `admin.css` added `[aria-current="page"]` rule for admin nav links (`border-bottom: 2px solid currentColor; padding-bottom: 1px`) fixing WCAG 1.4.1 (active nav link had no non-colour indicator)
- [x] `admin.css` fixed undefined `--admin-purple` variable in pagination hover; replaced with `--accent-primary` (same `#9557b5` value)
- [x] `todo/01-css-semantic-refactor.md` audited all CSS/HTML refactor tasks; marked ~15 items as already-done (`.admin-main`, `.admin-page-title`, `.admin-form-row`, `.admin-label`, `.admin-input/select/textarea`, `.admin-table`, `.admin-fieldset`, `tfe.css` class replacements, `search.css` selector, `login.php`/`edit.php` inline styles, `admin-alert` replacement, form partial hints)
- [x] `todo/04-accessibility.md` marked WCAG 1.4.1 admin nav and `--admin-purple` audit items as completed
- [x] `admin.css` - added `[aria-current="page"]` rule for admin nav links (`border-bottom: 2px solid currentColor; padding-bottom: 1px`) fixing WCAG 1.4.1 (active nav link had no non-colour indicator)
- [x] `admin.css` - fixed undefined `--admin-purple` variable in pagination hover; replaced with `--accent-primary` (same `#9557b5` value)
- [x] `todo/01-css-semantic-refactor.md` - audited all CSS/HTML refactor tasks; marked ~15 items as already-done (`.admin-main`, `.admin-page-title`, `.admin-form-row`, `.admin-label`, `.admin-input/select/textarea`, `.admin-table`, `.admin-fieldset`, `tfe.css` class replacements, `search.css` selector, `login.php`/`edit.php` inline styles, `admin-alert` replacement, form partial hints)
- [x] `todo/04-accessibility.md` - marked WCAG 1.4.1 admin nav and `--admin-purple` audit items as completed
- [x] `admin/index.php` server-side pagination (25/page); `Database::getThesesListCount()` added; `getThesesList()` extended with `$limit`/`$offset`; `access_type` JOIN added to query (was missing); result-count meta line added; `.pagination-wrap` + `.pagination-btn` + `.pagination-info` styles added to `admin.css`
- [x] `admin/index.php` - server-side pagination (25/page); `Database::getThesesListCount()` added; `getThesesList()` extended with `$limit`/`$offset`; `access_type` JOIN added to query (was missing); result-count meta line added; `.pagination-wrap` + `.pagination-btn` + `.pagination-info` styles added to `admin.css`
- [x] `checkbox-list.php` replaced `<div class="admin-checkbox-list">` with `<fieldset class="admin-checkbox-group"><legend class="sr-only"></legend><ul>` (WCAG 1.3.1 fix)
- [x] `admin.css` replaced `.admin-checkbox-list` with `.admin-body fieldset.admin-checkbox-group > ul` semantic selectors; added `span.admin-row-label` as visible label column counterpart
- [x] `login.php` wrapped content in `<main id="main-content">` landmark
- [x] `account.php` `<div class="admin-account-status">``<dl>`; `__row` divs kept; `__label` spans → `<dt>`; `admin-danger-zone__description` div → `<p>`
- [x] `index.php` maintenance bar `<div>``<aside role="status" aria-label="Statut du site">`
- [x] `add.php` / `edit.php` `autocomplete="name"` on author field, `autocomplete="email"` on contact field
- [x] `tags.php` all inline `style=` attributes removed; sizing/spacing moved to CSS (`.admin-input--inline`, `.admin-select--inline`, `.admin-inline-form + .admin-inline-form`, `.admin-tags-count`)
- [x] `checkbox-list.php` - replaced `<div class="admin-checkbox-list">` with `<fieldset class="admin-checkbox-group"><legend class="sr-only">...</legend><ul>` (WCAG 1.3.1 fix)
- [x] `admin.css` - replaced `.admin-checkbox-list` with `.admin-body fieldset.admin-checkbox-group > ul` semantic selectors; added `span.admin-row-label` as visible label column counterpart
- [x] `login.php` - wrapped content in `<main id="main-content">` landmark
- [x] `account.php` - `<div class="admin-account-status">``<dl>`; `__row` divs kept; `__label` spans → `<dt>`; `admin-danger-zone__description` div → `<p>`
- [x] `index.php` - maintenance bar `<div>``<aside role="status" aria-label="Statut du site">`
- [x] `add.php` / `edit.php` - `autocomplete="name"` on author field, `autocomplete="email"` on contact field
- [x] `tags.php` - all inline `style=` attributes removed; sizing/spacing moved to CSS (`.admin-input--inline`, `.admin-select--inline`, `.admin-inline-form + .admin-inline-form`, `.admin-tags-count`)
- [x] Marked already-done items in todo files: stats `<dl>`, `thanks.php` `<section>`, `scope="col"` on both tables, `tfe.php` inline styles, `role="alert"` on flash messages
- [x] `admin-submit-wrap``admin-form-footer` rename: updated all 6 admin templates (`add.php`, `edit.php`, `login.php`, `account.php`, `import.php`, `pages-edit.php`) and all 8 CSS selectors in `admin.css` (`.admin-form > div:not()` exclusion guards, `.admin-login-box` overrides). Closes `todo/01-css-semantic-refactor.md` submit-wrap task.
- [x] `admin-submit-wrap``admin-form-footer` rename: updated all 6 admin templates (`add.php`, `edit.php`, `login.php`, `account.php`, `import.php`, `pages-edit.php`) and all 8 CSS selectors in `admin.css` (`.admin-form > div:not(...)` exclusion guards, `.admin-login-box` overrides). Closes `todo/01-css-semantic-refactor.md` submit-wrap task.
- [x] Marked `status-badge.php` partial and WCAG 1.3.1 status-badge items as already-done in `todo/02-php-components.md` and `todo/04-accessibility.md` (partial + CSS were fully implemented; TODO had not been updated)
- [x] `public/index.php` — WCAG 2.4.4: home page cards now append `<span class="sr-only">, YEAR</span>` to each card's `<p>` link text so screen readers get unique link names when two theses share the same title
- [x] `public/index.php` — WCAG 2.4.4: home page cards now append `<span class="sr-only">, YEAR</span>` to each cards `<p>` link text so screen readers get unique link names when two theses share the same title
- [x] `todo/04-accessibility.md` — WCAG 2.4.3: marked back-link focus-order item as already done (`tfe-back-link` is already the first DOM element in `.tfe-left`, before `<h1>`)
- [x] `jury-fieldset.php` — WCAG 3.3.2: replaced bare `<label>Lecteur·ices :</label>` with `<fieldset class="admin-jury-lecteurs"><legend>Lecteur·ices</legend>` so the lecteur group has a proper programmatic label
- [x] `jury-fieldset.php` — WCAG 4.1.2: all "Externe" checkboxes (promoteur + each lecteur row, static + dynamically added via JS) now carry `aria-label="[Role] — externe"` providing group context without visible redundancy
- [x] `jury-fieldset.php` — WCAG 2.1.1: jury remove buttons verified to have descriptive `aria-label="Supprimer le lecteur·ice N"` on all rows (static + dynamic)
- [x] `admin.css` — added `.admin-body fieldset fieldset.admin-jury-lecteurs` rule: strips border/background on the nested lecteur fieldset so it renders as a visual sub-group, not a double-bordered card
- [x] `todo/04-accessibility.md` — WCAG 1.4.4 + 1.4.12 audited and marked done: all font-sizes are `rem`; no `overflow:hidden` on essential text content

View File

@@ -747,6 +747,26 @@
}
/* ── Jury fieldset ──────────────────────────────────────────────────────── */
/* Nested lecteur·ices fieldset sits inside the outer jury fieldset:
strip heavy border/background so it reads as a sub-group, not a card. */
.admin-body fieldset fieldset.admin-jury-lecteurs {
border: none;
padding: 0;
margin: 0.75rem 0 0;
background: transparent;
}
.admin-body fieldset fieldset.admin-jury-lecteurs > legend {
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--text-tertiary);
padding: 0;
margin-bottom: 0.5rem;
}
.admin-jury-row {
display: flex;
gap: 0.75rem;

View File

@@ -48,56 +48,64 @@ $juryIdx = max(count($juryLecteurs), 1);
value="<?= htmlspecialchars($juryPromoteur ?? '') ?>" placeholder="Nom">
<label class="admin-checkbox-label admin-jury-ext">
<input type="checkbox" name="jury_promoteur_ext" value="1"
<?= $juryPromoteurExt ? 'checked' : '' ?>> Externe
<?= $juryPromoteurExt ? 'checked' : '' ?>
aria-label="Promoteur·ice — externe"> Externe
</label>
</div>
</div>
<!-- Lecteur·ices (dynamic list) -->
<div>
<label>Lecteur·ices :</label>
<div>
<fieldset class="admin-jury-lecteurs">
<legend>Lecteur·ices</legend>
<div id="jury-lecteurs-list" class="admin-jury-list">
<?php if (empty($juryLecteurs)): ?>
<div class="admin-jury-entry">
<input type="text" name="jury_lecteurs[]" placeholder="Nom">
<input type="text" name="jury_lecteurs[]" placeholder="Nom"
id="jury_lecteur_0" aria-label="Lecteur·ice 1 — nom">
<label class="admin-checkbox-label admin-jury-ext">
<input type="checkbox" name="jury_lecteurs_ext[0]" value="1"> Externe
<input type="checkbox" name="jury_lecteurs_ext[0]" value="1"
aria-label="Lecteur·ice 1 — externe"> Externe
</label>
<button type="button" class="admin-btn-remove" onclick="removeJuryRow(this)"
aria-label="Supprimer ce lecteur"><span aria-hidden="true">✕</span></button>
aria-label="Supprimer le lecteur·ice 1"><span aria-hidden="true">✕</span></button>
</div>
<?php else: ?>
<?php foreach ($juryLecteurs as $li => $lm): ?>
<?php $lNum = $li + 1; ?>
<div class="admin-jury-entry">
<input type="text" name="jury_lecteurs[]"
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom">
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom"
id="jury_lecteur_<?= $li ?>" aria-label="Lecteur·ice <?= $lNum ?> — nom">
<label class="admin-checkbox-label admin-jury-ext">
<input type="checkbox" name="jury_lecteurs_ext[<?= $li ?>]" value="1"
<?= $lm['is_external'] ? 'checked' : '' ?>> Externe
<?= $lm['is_external'] ? 'checked' : '' ?>
aria-label="Lecteur·ice <?= $lNum ?> — externe"> Externe
</label>
<button type="button" class="admin-btn-remove" onclick="removeJuryRow(this)"
aria-label="Supprimer ce lecteur"><span aria-hidden="true">✕</span></button>
aria-label="Supprimer le lecteur·ice <?= $lNum ?>"><span aria-hidden="true">✕</span></button>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<button type="button" class="admin-btn-secondary admin-add-jury-btn"
onclick="addJuryRow()">+ Ajouter un·e lecteur·ice</button>
</div>
</div>
</fieldset>
</fieldset>
<script>
var juryIdx = <?= $juryIdx ?>;
function addJuryRow() {
var list = document.getElementById('jury-lecteurs-list');
var n = list.querySelectorAll('.admin-jury-entry').length + 1;
var div = document.createElement('div');
div.className = 'admin-jury-entry';
div.innerHTML = '<input type="text" name="jury_lecteurs[]" placeholder="Nom">'
div.innerHTML = '<input type="text" name="jury_lecteurs[]" placeholder="Nom"'
+ ' aria-label="Lecteur\u00b7ice ' + n + ' \u2014 nom">'
+ '<label class="admin-checkbox-label admin-jury-ext">'
+ '<input type="checkbox" name="jury_lecteurs_ext[' + juryIdx + ']" value="1"> Externe'
+ '<input type="checkbox" name="jury_lecteurs_ext[' + juryIdx + ']" value="1"'
+ ' aria-label="Lecteur\u00b7ice ' + n + ' \u2014 externe"> Externe'
+ '</label>'
+ '<button type="button" class="admin-btn-remove" onclick="removeJuryRow(this)" aria-label="Supprimer ce lecteur"><span aria-hidden="true">✕</span></button>';
+ '<button type="button" class="admin-btn-remove" onclick="removeJuryRow(this)"'
+ ' aria-label="Supprimer le lecteur\u00b7ice ' + n + '"><span aria-hidden="true">\u2715</span></button>';
list.appendChild(div);
juryIdx++;
}

View File

@@ -22,15 +22,15 @@
## 1.4.4 Resize text
- [ ] **Verify no text is set in `px`**ensure `width: 14px; height: 14px` on checkboxes and similar elements do not prevent text scaling
- [x] **Verify no text is set in `px`**audited all CSS files; every `font-size` uses `rem` or `em`; no `px` font-size found anywhere. No action needed.
## 1.4.12 Text spacing
- [ ] **No text-spacing override test done**verify WCAG 1.4.12 bookmarklet does not cause content clipping (especially `overflow: hidden` on `.card__media` and tight `aspect-ratio: 4/3`)
- [x] **No text-spacing override test done**audited all `overflow: hidden` instances: `.sr-only` (visually hidden utility, 1×1px — not text content), `.home-body figure` / `aside figure` / `.card` (media containers, not text). `.card__gradient-title` clamps decorative gradient text — not essential content (same info is in the `<p>` link). No WCAG 1.4.12 failure found.
## 2.1.1 Keyboard
- [ ] **Jury "✕" remove buttons in `add.php`/`edit.php`** add `aria-label` for keyboard discoverability
- [x] **Jury "✕" remove buttons in `add.php`/`edit.php`**`aria-label="Supprimer le lecteur·ice N"` already present on all remove buttons in `jury-fieldset.php` (both static and dynamically added rows)
## 2.4.3 Focus order
@@ -62,17 +62,17 @@
## 3.3.2 Labels or instructions
- [ ] **Admin jury "Lecteur·ices" label has no `for` attribute**wrap lecteur rows in `<fieldset>/<legend>` or use `aria-describedby`
- [x] **Admin jury "Lecteur·ices" label has no `for` attribute**replaced plain `<label>Lecteur·ices :</label>` with `<fieldset class="admin-jury-lecteurs"><legend>Lecteur·ices</legend>` in `jury-fieldset.php`; CSS rule strips the nested fieldsets border/padding so it renders as a sub-group
## 4.1.2 Name, role, value
- [ ] **Custom "Externe" checkbox for jury members has no group context** — add `aria-label="[Nom du promoteur] est externe"` dynamically via JS, or `aria-describedby` pointing to the adjacent name input
- [x] **Custom "Externe" checkbox for jury members has no group context** — all jury "Externe" checkboxes now carry explicit `aria-label` (e.g. `"Promoteur·ice — externe"`, `"Lecteur·ice N — externe"`); both static PHP-rendered rows and dynamically added rows via `addJuryRow()` receive the label
- [ ] **`<video>` elements on `tfe.php` have no captions** — add `<track kind="captions">` slot in template; document caption requirement in admin upload form
- [ ] **Admin `<select>` for visibility/access in `edit.php` uses truncated option text** — use full description in option text (or `title` attribute); keep truncated text only for visual display
- [ ] **Bulk publish/unpublish JS does not announce result to screen readers** — add `role="alert"` to error messages and `role="status"` to success messages across all admin pages
- [x] **Bulk publish/unpublish JS does not announce result to screen readers** — action result is a full-page redirect to a flash message rendered by `flash-messages.php` which already emits `role="alert"` (error) / `role="status"` (success); no additional JS announcement needed
## 5 - Motion & user preferences