docs: plan d'implémentation MUI-41 + précisions spec (caret email, exclusion SiteSelector)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@ Pendant ERP-63, deux manques ont bloqué la mise en place de champs obligatoires
|
||||
|---|---|
|
||||
| Périmètre `required` + astérisque | **Toute la famille formulaire**, y compris `InputUpload`, `InputRichText`, `SiteSelector` |
|
||||
| Prop `lowercase` (email) | **Opt-in, défaut `false`** |
|
||||
| Espaces email | **Supprimer tous les espaces** (début, milieu, fin), avec préservation du curseur |
|
||||
| Espaces email | **Supprimer tous les espaces** (début, milieu, fin) ; préservation du curseur *best-effort* (voir caveat ci-dessous) |
|
||||
| Accessibilité astérisque | `aria-hidden="true"` — la sémantique est portée par l'attribut HTML natif `required` |
|
||||
|
||||
## Section 1 — Indicateur « obligatoire »
|
||||
@@ -63,11 +63,25 @@ L'astérisque vit **à l'intérieur du `<label>`** → il flotte avec le floatin
|
||||
|
||||
### Props à ajouter
|
||||
|
||||
`required?: boolean` (défaut `false`) sur les 5 composants qui ne l'ont pas : `Select`, `SelectCheckbox`, `InputUpload`, `InputRichText`, `SiteSelector`. Câblage sur l'attribut natif quand le composant a un élément de formulaire sous-jacent qui le supporte.
|
||||
`required?: boolean` (défaut `false`) sur les **4** composants qui ne l'ont pas et qui possèdent un label de champ : `Select`, `SelectCheckbox`, `InputUpload`, `InputRichText`.
|
||||
|
||||
### Câblage accessibilité (a11y)
|
||||
|
||||
L'astérisque est `aria-hidden` : la sémantique « obligatoire » doit donc être portée par le DOM.
|
||||
|
||||
- **Élément natif `required` déjà câblé** (asterisque suffit) : `InputText`, `InputEmail`, `InputPhone`, `InputPassword`, `InputTextArea`, `InputAmount`, `InputNumber`, `InputAutocomplete`, `Checkbox`, `RadioButton`, `Time`, `TimePicker`, et `CalendarField` (date family).
|
||||
- **Pas de `required` natif** → ajouter `:aria-required="required || undefined"` sur l'élément interactif :
|
||||
- `Select` / `SelectCheckbox` : le `<button>` déclencheur (combobox).
|
||||
- `InputRichText` : le wrapper éditeur (`#editorId`, contenteditable via TipTap).
|
||||
- `InputUpload` : possède un `<input type="file">` natif → on câble `:required="required"` dessus (natif).
|
||||
|
||||
### Composants concernés par le rendu de l'astérisque
|
||||
|
||||
Famille formulaire complète : `InputText`, `InputEmail`, `InputPhone`, `InputPassword`, `InputTextArea`, `InputAmount`, `InputNumber`, `InputAutocomplete`, `InputUpload`, `InputRichText`, `Select`, `SelectCheckbox`, `Checkbox`, `RadioButton`, `Date`, `DateTime`, `DateRange`, `DateWeek`, `Time`, `TimePicker`, `SiteSelector`.
|
||||
`InputText`, `InputEmail`, `InputPhone`, `InputPassword`, `InputTextArea`, `InputAmount`, `InputNumber`, `InputAutocomplete`, `InputUpload`, `InputRichText`, `Select`, `SelectCheckbox`, `Checkbox`, `RadioButton`, `Time`, `TimePicker`, et `CalendarField` (rendu mutualisé pour `Date`, `DateTime`, `DateRange`, `DateWeek`).
|
||||
|
||||
### Exclusion : `SiteSelector`
|
||||
|
||||
`MalioSiteSelector` est un **radiogroup de tuiles** (segmented control) : il n'a **pas de label de champ** (son `labelClass` style le nom de chaque tuile). Y placer un astérisque n'a pas de sens. Il est **exclu** du périmètre `required`/astérisque. À rouvrir si un besoin de « groupe obligatoire » émerge (ce serait alors un libellé de groupe distinct, hors de ce ticket).
|
||||
|
||||
### Alternative écartée
|
||||
|
||||
@@ -98,10 +112,19 @@ const onInput = (event: Event) => {
|
||||
const sanitized = sanitizeEmail(raw)
|
||||
|
||||
if (sanitized !== raw) {
|
||||
const caret = target.selectionStart ?? raw.length
|
||||
const newCaret = sanitizeEmail(raw.slice(0, caret)).length
|
||||
// `<input type="email">` ne supporte PAS l'API de sélection :
|
||||
// selectionStart vaut null, setSelectionRange lève une exception.
|
||||
// On garde donc la repositionnement défensif (no-op sur type=email).
|
||||
const caret = target.selectionStart
|
||||
target.value = sanitized
|
||||
target.setSelectionRange(newCaret, newCaret)
|
||||
if (caret !== null) {
|
||||
const newCaret = sanitizeEmail(raw.slice(0, caret)).length
|
||||
try {
|
||||
target.setSelectionRange(newCaret, newCaret)
|
||||
} catch {
|
||||
/* type d'input sans support de sélection — ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isControlled.value) localValue.value = sanitized
|
||||
@@ -111,8 +134,8 @@ const onInput = (event: Event) => {
|
||||
|
||||
Points clés :
|
||||
|
||||
- **Curseur** : position recalculée en sanitisant la portion à gauche du curseur → taper un espace au milieu ne fait pas sauter le curseur. `lowercase` ne change pas la longueur, donc n'affecte pas la position.
|
||||
- **Resynchro DOM** : `target.value = sanitized` même en mode contrôlé, pour que l'affichage colle toujours à la valeur émise.
|
||||
- **Caveat curseur** : la spec HTML interdit l'API de sélection sur `type="email"` (`selectionStart` = `null`, `setSelectionRange` lève). La repositionnement est donc **best-effort** et inactif sur l'email : sur le cas rare d'une suppression d'espace en milieu de chaîne, le curseur peut aller en fin. Les cas courants (espace en fin, collage) gardent naturellement le curseur en fin. Le code est gardé (`caret !== null` + `try/catch`) pour ne jamais lever.
|
||||
- **Collage** couvert (paste déclenche `input`).
|
||||
- **Inchangé** : `type="email"`, `inputmode="email"`, icône, et **aucune validation de format**.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user