Files
xamxam/TODO.md
Pontoporeia e17246c850 Add field-level aria-errormessage, aria-invalid, and aria-describedby across the TFE form
WCAG 3.3.1 (Error Identification): failing fields now get
aria-errormessage pointing to the flash-error container and
aria-invalid="true". WCAG 3.3.3 (Error Suggestion): <small>
hint text on inputs, selects, and file fields is now linked via
aria-describedby (always, not just on error).

Changes:
- text-field.php, select-field.php, checkbox-list.php: accept
  $errorFieldName; add aria-errormessage/aria-invalid on match;
  add id to <small> and aria-describedby on the control
- fieldset-tfe-info.php: aria-invalid on synopsis textarea
- fichiers-fragment.php: aria-describedby on cover, note
  d'intention, TFE, annexes, and website inputs; aria-invalid
  on format checkboxes when error matches 'formats'
- form.php: id="flash-error" + tabindex="-1" on flash-error
  div; accept $errorFieldName from callers
- admin/add.php: set $errorFieldName, wire $withAutofocusFn
  (was identity default)
- admin/edit.php: set $errorFieldName
- partage/index.php: consume autofocus field, wire autofocus
  function, add App::flashAutofocus() in submit catch block

Also fixes WCAG standards issue: removed invalid 'required'
HTML attribute from <fieldset> elements in checkbox-list.php
and fichiers-fragment.php (only aria-required stays). Added
role="group" for explicit ARIA semantics.
2026-06-11 10:23:47 +02:00

132 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TODO
## HTMX v2 Migration
Reference: `docs/autosave-system.md` → "HTMX v2 Migration Plan" section.
- [x] `contenus-edit.php` (pages): Add `hx-*` attrs, add `overtype:change` dispatch in OverType `onChange`
- [x] `contenus-edit.php` (form_help): Add `hx-*` attrs, add `overtype:change` dispatch in OverType `onChange`
- [x] `apropos-groups-form.php` (contacts): Add `hx-*` attrs only
- [x] `contenus-edit.php` (sidebar_links): Add `hx-*` attrs only
- [x] Add `handleAutosaveResponse()` shared handler + `htmx:beforeRequest` loading state
- [x] Delete `autosave.js`
- [x] Fix backend `$isAjax` detection: also recognize `HX-Request` header (page.php, apropos.php, form-help.php)
- [x] Form-help inline editors: add OverType toolbar + HTMX auto-save + remove save buttons
- [x] Markdown cheatsheet modal: reusable dialog on all OverType editors
## FilePond crash on TFE upload forms
- [x] Analyze root cause → `docs/filepond-crash-analysis.md`
- [x] Partial fixes (Content-Type headers, onerror cleanup, load object) — insufficient, crash still reproduces
- [x] HTMX/destroy race hypothesis investigation → `docs/filepond-race-investigation.md` (verdict: REFUTED)
- [x] Diagnostic probes + deep analysis: confirmed load-file-error dispatch path, traced via error.stack to fileValidateSizeFilter line 389
- [x] **ROOT CAUSE FIXED**: fileValidateSizeFilter accessed `item.filename` but FileValidateSize's LOAD_FILE filter passes the raw File/Blob (which has `.name`, not `.filename`). Changed to `item.filename || item.name`. Also added null guard to getExt().
- [x] Defensive: Wt and Fr crash guards in filepond.min.js prevent action.status.main crash
- [x] process.onload: replaced throw with error-marker return (prevents FilePond crash when server returns HTML)
- [x] Routing: partage index.php now routes /partage/actions/* directly to PHP files (was treating 'actions' as a slug and returning full HTML page)
- [x] **All crashes resolved** — verified working on partage form
## Form Accessibility & Resilience — Assessment Follow-up
Reference: Assessment against progressive-enhancement / WCAG-AA / "never lose data" / low-common-denominator guidelines.
### 1. WCAG AA: Add field-level `aria-errormessage` + `aria-invalid` to TFE form
**Current state:** Flash error divs have `role="alert"` but individual fields are never linked to their error via `aria-errormessage`. The `autofocusFieldForError()` mechanism focuses the field after a validation redirect but does not announce the error to screen readers. Help `<small>` text is not linked via `aria-describedby`.
**To do:**
- [x] Extend `text-field.php`, `select-field.php`, `checkbox-list.php` partials to accept optional `$errorFieldName` variable
- [x] When `$errorFieldName` matches the field, add `aria-errormessage="flash-error"` and `aria-invalid="true"` on the input
- [x] On validation redirect, populate `$errorFieldName` from `App::consumeAutofocus()` so each failing field references the flash error container
- [x] Add `aria-describedby` linking each `<small>` hint to its input (always, not just on error)
- [x] Give flash-error div `id="flash-error"` and `tabindex="-1"` for programmatic reference
- [x] Wire `App::flashAutofocus()` into partage submit catch block (was missing)
- [x] Wire `$withAutofocusFn` in admin add template (was defaulting to identity)
- [x] Apply `aria-invalid` + `aria-errormessage` on synopsis textarea (not in text-field partial)
- [x] Apply `aria-describedby` on file inputs in `fichiers-fragment.php`
- [x] Apply format checkboxes `aria-invalid` support in `fichiers-fragment.php`
- [ ] Test with VoiceOver and NVDA on the full add/edit/partage form flows
### 2. No-JS file uploads silently fail (data loss)
**Current state:** `form.php` hardcodes `<input type="hidden" name="filepond_mode" value="1">`. Without JS, no `queue_file[]` hidden inputs are populated → server gets `filepond_mode=1` with empty queue → all files silently dropped. The form is supposed to work without JS.
**To do:**
- [ ] Change the hidden input to `<input type="hidden" name="filepond_mode" value="0" disabled>` by default; JS enables it and sets `value="1"` on DOMContentLoaded
- [ ] Add server-side fallback in `ThesisCreateController::submit()` and `ThesisEditController::save()`: when `filepond_mode=1` but no `queue_file` data is present, fall through to the legacy `$_FILES` path
- [ ] Test end-to-end: submit the partage form with JS disabled, verify files arrive via `$_FILES`
### 3. Autosave text fields on partage form
**Current state:** Only FilePond files are persisted to the server before submit. Text/select/checkbox fields live only in DOM and are lost on tab close / crash / network failure. `beforeunload-guard.js` warns but cannot recover data. The admin panel already has an HTMX-based autosave pattern (`contenus-edit.php`).
**To do:**
- [ ] Implement a per-session draft endpoint (`POST /partage/<slug>/draft`) that saves form data to a session-backed store
- [ ] On field blur/change, POST the field name+value via HTMX (debounced 500ms)
- [ ] On page load, hydrate all fields from the draft endpoint
- [ ] Draft is cleared on successful submission; expires after 24h with session
- [ ] Add a visible "Brouillon enregistré" indicator to reassure the student
### 4. FilePond temp file IDs lost on partage validation redirect
**Current state:** After a validation error, `storePrimedFiles()` shows file *names* so the user knows what to re-select. But the actual FilePond hex IDs (server-side temp files) are not preserved — the user must re-upload everything. For large files on slow connections, this is painful.
**To do:**
- [ ] After a validation error in the partage form, preserve FilePond temp file IDs in `$_SESSION['share_filepond_ids_' . $slug]` by queue type
- [ ] Use these IDs to build the `data-existing-files` JSON attribute on the re-rendered form, seeding the FilePond pools
- [ ] Ensure the temp files' server-side lifetime covers at least one validation round-trip (extend `FilepondHandler` TTL if needed)
### 5. Two-phase commit: protect against crash during file moves
**Current state:** DB transaction commits, then files are moved from tmp to storage. If the process crashes between COMMIT and file move completion, the DB has a thesis record with no files (or partial files). This is a public-facing service — no acceptable failure rate.
**To do:**
- [ ] Add an `is_published` or `status` flag to theses table (if not already present)
- [ ] In the partage submit flow: INSERT thesis with `status='draft'` inside the transaction → COMMIT → move files → UPDATE `status='active'`
- [ ] On partage submission error recovery, roll back draft theses that are older than N minutes with no files
- [ ] Add a periodic cleanup job (`just cleanup-drafts`) for orphaned drafts
### 6. Mobile-responsive form layout
**Current state:** Form field rows use `grid-template-columns: 260px 1fr` with no breakpoint. Below ~520px viewport width, labels wrap and inputs are cramped.
**To do:**
- [ ] Add `@media (max-width: 600px)` rule in `form.css` switching to `grid-template-columns: 1fr` with labels stacked above inputs
- [ ] Test on 320px wide viewport (PSP, low-end Android)
- [ ] Ensure touch targets are at least 44×44px (WCAG 2.5.5)
### 7. Split form.css: shared base vs admin-only styles
**Current state:** `form.css` is 36KB (1,500+ lines). The partage form loads all of it but uses only ~40% (base input/fieldset styles). Admin-specific styles (`.admin-jury-*`, `.admin-backoffice-*`, etc.) are dead weight for students.
**To do:**
- [ ] Split `form.css` into `form-base.css` (~15KB: inputs, selects, textareas, fieldsets, legends, validation states, flash messages, `aria-invalid` styling) and `form-admin.css` (~21KB: admin layout, jury fieldset, backoffice, tag pills, autocomplete)
- [ ] Load `form-base.css` in the partage form; load both in admin forms
- [ ] Update `head.php` to support `$extraCssAdmin` for admin-only stylesheets
### 8. Fix `aria-required` on `<fieldset>` (standards compliance)
**Current state:** `checkbox-list.php` and `fichiers-fragment.php` put `required aria-required="true"` on `<fieldset>` elements. The `required` attribute is not valid on `<fieldset>`. Screen readers may not interpret this correctly.
**To do:**
- [x] Remove `required` attribute from `<fieldset>` in `checkbox-list.php` — keep `aria-required="true"` only
- [x] Remove `required` attribute from `<fieldset>` in `fichiers-fragment.php` — keep `aria-required="true"` only
- [x] Add `role="group"` on both `<fieldset>` elements for explicit ARIA semantics
### 9. Refactor partage form page wrapper to a template
**Current state:** `renderShareLinkForm()` in `partage/index.php` outputs an entire `<html>` document inline (~120 lines of HTML inside a PHP function). This duplicates `head.php`/`header.php` logic.
**To do:**
- [ ] Extract the partage form page chrome to `templates/partage/form-page.php`
- [ ] Use `App::render()` or a dedicated `renderPartageForm()` helper
- [ ] Share meta tags, favicon references, CSRF meta, and live-reload script from `head.php`
### 10. Reduce form bootstrap duplication across 3 entry points
**Current state:** `admin/add.php`, `admin/edit.php`, and `partage/index.php` (inside `renderShareLinkForm()`) each repeat ~4060 lines of identical setup: load `ThesisCreateController`, call `loadFormData()`, build `$helpFn`, load site settings, set up jury/default access variables, etc.
**To do:**
- [ ] Add a `ThesisFormSetup` helper class (or static method on the controllers) that returns a standardised array of form variables
- [ ] Each entry point becomes: `$formVars = ThesisFormSetup::prepare($mode, $slug); extract($formVars); include '...form.php';`