Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
20 KiB
État « obligatoire » cohérent + normalisation email — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Exposer une prop required cohérente avec astérisque rouge dans le label sur toute la famille formulaire, et ajouter une sanitisation à la saisie (suppression des espaces + option lowercase) à MalioInputEmail.
Architecture : Un composant présentational partagé MalioRequiredMark (astérisque aria-hidden, token text-m-danger) est importé explicitement et rendu dans le <label> de chaque composant quand required est vrai. Les 4 composants sans la prop la reçoivent (+ câblage aria-required là où il n'y a pas de required natif). MalioInputEmail.onInput sanitise la valeur avant émission.
Tech Stack : Nuxt 4 layer, Vue 3 <script setup lang="ts">, Tailwind (palette m-*), tailwind-merge, Vitest + @vue/test-utils (jsdom).
Spec : docs/superpowers/specs/2026-06-03-required-asterisk-email-sanitization-design.md
Conventions de test (rappel) : chaque fichier *.test.ts définit son propre helper de montage (nom variable : mountInput, mountDate, mountCheckbox, mountTime, mountComponent…) ou monte en inline. Le tableau de chaque tâche indique le helper exact à réutiliser.
⚠️ Suite flaky : des timeouts intermittents existent sur diverses suites. Si un test échoue par timeout sans rapport avec le changement, relancer le fichier ciblé ; ne pas conclure à un échec sans relance. Le hook pre-commit lance les tests — si un timeout flaky bloque un commit déjà vérifié manuellement, utiliser git commit --no-verify.
Branche : feature/MUI-41-props-required-asterisque-dans-le-label-sur-les-co (rester dessus, ne pas créer de branche).
Task 1 : Composant partagé MalioRequiredMark
Files:
-
Create:
app/components/malio/shared/RequiredMark.vue -
Test:
app/components/malio/shared/RequiredMark.test.ts -
Step 1 : Écrire le test qui échoue
Create app/components/malio/shared/RequiredMark.test.ts :
import {describe, expect, it} from 'vitest'
import {mount} from '@vue/test-utils'
import RequiredMark from './RequiredMark.vue'
describe('MalioRequiredMark', () => {
it('rend un astérisque', () => {
const wrapper = mount(RequiredMark)
expect(wrapper.text()).toBe('*')
})
it('est masqué pour les technologies d’assistance', () => {
const wrapper = mount(RequiredMark)
expect(wrapper.get('[data-test="required-mark"]').attributes('aria-hidden')).toBe('true')
})
it('utilise le token de couleur danger', () => {
const wrapper = mount(RequiredMark)
expect(wrapper.get('[data-test="required-mark"]').classes()).toContain('text-m-danger')
})
})
- Step 2 : Lancer le test, vérifier l'échec
Run: npm run test -- app/components/malio/shared/RequiredMark.test.ts
Expected: FAIL — Failed to resolve import './RequiredMark.vue' (le composant n'existe pas encore).
- Step 3 : Créer le composant
Create app/components/malio/shared/RequiredMark.vue :
<template>
<span
data-test="required-mark"
aria-hidden="true"
class="ml-0.5 select-none text-m-danger"
>*</span>
</template>
<script setup lang="ts">
defineOptions({name: 'MalioRequiredMark', inheritAttrs: false})
</script>
- Step 4 : Lancer le test, vérifier le succès
Run: npm run test -- app/components/malio/shared/RequiredMark.test.ts
Expected: PASS (3 tests).
- Step 5 : Commit
git add app/components/malio/shared/RequiredMark.vue app/components/malio/shared/RequiredMark.test.ts
git commit -m "feat(ui): composant partagé MalioRequiredMark (astérisque champ obligatoire)"
Task 2 : Prop required + a11y + astérisque sur les 4 composants sans la prop
Composants : Select, SelectCheckbox, InputUpload, InputRichText. Chacun reçoit la prop required, le câblage a11y adapté, l'import + le rendu de l'astérisque, et un test.
Files:
-
Modify:
app/components/malio/select/Select.vue,app/components/malio/select/SelectCheckbox.vue,app/components/malio/input/InputUpload.vue,app/components/malio/input/InputRichText.vue -
Test:
app/components/malio/select/Select.test.ts,app/components/malio/select/SelectCheckbox.test.ts,app/components/malio/input/InputUpload.test.ts,app/components/malio/input/InputRichText.test.ts -
Step 1 : Écrire les tests qui échouent (un par composant)
Patron d'assertion (à adapter au helper de chaque fichier) :
it('affiche l’astérisque quand required est vrai', () => {
const wrapper = /* monter avec { label: 'Champ', required: true, ...props requises } */
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(true)
})
it('n’affiche pas l’astérisque par défaut', () => {
const wrapper = /* monter avec { label: 'Champ', ...props requises } */
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
})
Montage par fichier :
| Fichier test | Montage |
|---|---|
select/Select.test.ts |
inline : mount(SelectForTest, {props: {label: 'Champ', required: true, options: [{label: 'A', value: 'a'}]}}) (et sans required pour le 2ᵉ test) |
select/SelectCheckbox.test.ts |
inline : mount(SelectCheckboxForTest, {props: {label: 'Champ', required: true, options: [{label: 'A', value: 'a'}]}}) |
input/InputUpload.test.ts |
helper existant mountComponent({label: 'Champ', required: true}) |
input/InputRichText.test.ts |
helper existant mountComponent({label: 'Champ', required: true}) |
Note : pour
Select/SelectCheckbox, reprendre la forme exacte desoptionset lesglobal.stubsdéjà utilisés dans les autresit()du fichier (copier un montage voisin).
- Step 2 : Lancer les tests, vérifier l'échec
Run: npm run test -- app/components/malio/select/Select.test.ts app/components/malio/select/SelectCheckbox.test.ts app/components/malio/input/InputUpload.test.ts app/components/malio/input/InputRichText.test.ts
Expected: FAIL sur les nouveaux tests « affiche l’astérisque » (la prop/le rendu n'existent pas encore).
- Step 3 : Ajouter la prop
required(type + défaut) dans les 4 composants
Dans chaque defineProps<{…}>(), ajouter la ligne :
required?: boolean
Dans chaque withDefaults(…, { … }), ajouter :
required: false,
- Step 4 : Câbler l'accessibilité (un élément interactif par composant)
Select.vue — sur le <button> déclencheur (là où sont déjà :aria-expanded, :aria-controls), ajouter :
:aria-required="required || undefined"
SelectCheckbox.vue — idem, sur son <button> déclencheur :
:aria-required="required || undefined"
InputUpload.vue — sur l'<input type="file">, ajouter l'attribut natif :
:required="required"
InputRichText.vue — sur le wrapper éditeur identifié par :id="editorId" (le conteneur de <EditorContent> en mode éditable), ajouter :
:aria-required="required || undefined"
- Step 5 : Importer et rendre l'astérisque dans les 4 composants
Dans le <script setup> de chacun, ajouter l'import (chemin relatif depuis family/Component.vue) :
import MalioRequiredMark from '../shared/RequiredMark.vue'
Dans le <template>, remplacer le rendu du libellé {{ label }} (celui à l'intérieur du <label> du champ — pas un {{ opt.label }}) par :
{{ label }}<MalioRequiredMark v-if="required" />
Respecter l'indentation existante de chaque fichier. Pour
Select/SelectCheckbox, viser le{{ label }}du<label>flottant, pas le{{ opt.label }}des options.
- Step 6 : Lancer les tests, vérifier le succès
Run: npm run test -- app/components/malio/select/Select.test.ts app/components/malio/select/SelectCheckbox.test.ts app/components/malio/input/InputUpload.test.ts app/components/malio/input/InputRichText.test.ts
Expected: PASS (anciens + nouveaux tests). En cas de timeout flaky non lié, relancer le fichier concerné.
- Step 7 : Commit
git add app/components/malio/select/Select.vue app/components/malio/select/SelectCheckbox.vue app/components/malio/input/InputUpload.vue app/components/malio/input/InputRichText.vue app/components/malio/select/Select.test.ts app/components/malio/select/SelectCheckbox.test.ts app/components/malio/input/InputUpload.test.ts app/components/malio/input/InputRichText.test.ts
git commit -m "feat(ui): prop required + aria-required + astérisque sur Select/SelectCheckbox/Upload/RichText"
Task 3 : Astérisque sur les composants ayant déjà required
Ces composants ont déjà la prop required (câblée nativement). On ajoute uniquement l'import + le rendu de l'astérisque + un test.
Files (16 composants → 13 via CalendarField mutualisé) :
Composant .vue |
Import à ajouter | Fichier test | Helper de montage |
|---|---|---|---|
input/InputText.vue |
'../shared/RequiredMark.vue' |
input/Input.test.ts |
mountInput({label:'Champ', required:true}) |
input/InputEmail.vue |
'../shared/RequiredMark.vue' |
input/InputEmail.test.ts |
mountComponent({label:'Champ', required:true}) |
input/InputPhone.vue |
'../shared/RequiredMark.vue' |
input/InputPhone.test.ts |
mountComponent({label:'Champ', required:true}) |
input/InputPassword.vue |
'../shared/RequiredMark.vue' |
input/InputPassword.test.ts |
mountComponent({label:'Champ', required:true}) |
input/InputTextArea.vue |
'../shared/RequiredMark.vue' |
input/InputTextArea.test.ts |
helper du fichier (mount<…> ; copier un montage voisin) |
input/InputAmount.vue |
'../shared/RequiredMark.vue' |
input/InputAmount.test.ts |
helper du fichier |
input/InputNumber.vue |
'../shared/RequiredMark.vue' |
input/InputNumber.test.ts |
helper du fichier |
input/InputAutocomplete.vue |
'../shared/RequiredMark.vue' |
input/InputAutocomplete.test.ts |
mountComponent({label:'Champ', required:true, …props requises}) |
checkbox/Checkbox.vue |
'../shared/RequiredMark.vue' |
checkbox/Checkbox.test.ts |
mountCheckbox({label:'Champ', required:true}) |
radio/RadioButton.vue |
'../shared/RequiredMark.vue' |
radio/RadioButton.test.ts |
helper du fichier |
time/Time.vue |
'../shared/RequiredMark.vue' |
time/Time.test.ts |
mountTime({label:'Champ', required:true}) |
time/TimePicker.vue |
'../shared/RequiredMark.vue' |
time/TimePicker.test.ts |
helper du fichier |
date/internal/CalendarField.vue |
'../../shared/RequiredMark.vue' |
date/Date.test.ts |
mountDate({label:'Champ', required:true}) |
CalendarFieldrend le label de tout le date family (Date,DateTime,DateRange,DateWeek). Une seule modif + un seul test (viaDate.test.ts) couvrent les quatre.
- Step 1 : Écrire les tests qui échouent (un couple par fichier test du tableau)
Pour chaque fichier test listé, ajouter :
it('affiche l’astérisque quand required est vrai', () => {
const wrapper = /* helper du tableau, avec required: true */
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(true)
})
it('n’affiche pas l’astérisque par défaut', () => {
const wrapper = /* helper du tableau, sans required */
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
})
- Step 2 : Lancer les tests, vérifier l'échec
Run: npm run test -- app/components/malio/input app/components/malio/checkbox app/components/malio/radio app/components/malio/time app/components/malio/date/Date.test.ts
Expected: FAIL sur les nouveaux tests « affiche l’astérisque ».
- Step 3 : Ajouter l'import + le rendu de l'astérisque dans les 13
.vue
Dans chaque <script setup>, ajouter l'import indiqué dans la colonne « Import à ajouter ».
Dans chaque <template>, transformer le libellé du champ :
{{ label }}<MalioRequiredMark v-if="required" />
(Le {{ label }} est à l'intérieur du <label v-if="label"> du champ. Respecter l'indentation propre à chaque fichier.)
- Step 4 : Lancer les tests, vérifier le succès
Run: npm run test -- app/components/malio/input app/components/malio/checkbox app/components/malio/radio app/components/malio/time app/components/malio/date/Date.test.ts
Expected: PASS. (Vérifier notamment que input/InputEmail.test.ts « renders the label text » → 'Adresse email' passe toujours : pas de required dans ce test, donc pas d'astérisque.) Relancer en cas de timeout flaky.
- Step 5 : Commit
git add app/components/malio/input app/components/malio/checkbox app/components/malio/radio app/components/malio/time app/components/malio/date/internal/CalendarField.vue app/components/malio/date/Date.test.ts
git commit -m "feat(ui): astérisque required dans le label de la famille formulaire"
Task 4 : Sanitisation de MalioInputEmail
Files:
-
Modify:
app/components/malio/input/InputEmail.vue -
Test:
app/components/malio/input/InputEmail.test.ts -
Step 1 : Écrire les tests qui échouent
Ajouter à input/InputEmail.test.ts :
it('supprime tous les espaces saisis', async () => {
const wrapper = mountComponent()
await wrapper.get('input').setValue(' a b @ c.com ')
const emits = wrapper.emitted('update:modelValue')!
expect(emits[emits.length - 1]).toEqual(['ab@c.com'])
expect(wrapper.get('input').element.value).toBe('ab@c.com')
})
it('conserve la casse par défaut', async () => {
const wrapper = mountComponent()
await wrapper.get('input').setValue('User@Example.COM')
const emits = wrapper.emitted('update:modelValue')!
expect(emits[emits.length - 1]).toEqual(['User@Example.COM'])
})
it('met en minuscules quand lowercase est vrai', async () => {
const wrapper = mountComponent({lowercase: true})
await wrapper.get('input').setValue('User@Example.COM')
const emits = wrapper.emitted('update:modelValue')!
expect(emits[emits.length - 1]).toEqual(['user@example.com'])
})
Ajouter
lowercase?: booleanau typeInputEmailPropsen tête du fichier de test (sinon TS refuse la prop dans le 3ᵉ test).
- Step 2 : Lancer les tests, vérifier l'échec
Run: npm run test -- app/components/malio/input/InputEmail.test.ts
Expected: FAIL — les espaces ne sont pas supprimés / lowercase inconnu.
- Step 3 : Ajouter la prop
lowercase
Dans defineProps<{…}>() de InputEmail.vue, ajouter :
lowercase?: boolean
Dans withDefaults(…, { … }), ajouter :
lowercase: false,
- Step 4 : Ajouter la fonction de sanitisation et réécrire
onInput
Ajouter la fonction pure (au-dessus de onInput) :
const sanitizeEmail = (v: string) => {
let out = v.replace(/\s+/g, '')
if (props.lowercase) out = out.toLowerCase()
return out
}
Remplacer le onInput existant par :
const onInput = (event: Event) => {
const target = event.target as HTMLInputElement
const raw = target.value
const sanitized = sanitizeEmail(raw)
if (sanitized !== raw) {
// `<input type="email">` ne supporte pas l'API de sélection :
// selectionStart vaut null, setSelectionRange lève. On garde defensivement.
const caret = target.selectionStart
target.value = sanitized
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
}
emit('update:modelValue', sanitized)
}
- Step 5 : Lancer les tests, vérifier le succès
Run: npm run test -- app/components/malio/input/InputEmail.test.ts
Expected: PASS (anciens tests inclus, dont « emits update:modelValue on input change » avec 'new@example.com' qui n'a pas d'espace → inchangé).
- Step 6 : Commit
git add app/components/malio/input/InputEmail.vue app/components/malio/input/InputEmail.test.ts
git commit -m "feat(inputs): sanitisation email (suppression des espaces + option lowercase)"
Task 5 : Documentation (COMPONENTS.md + CHANGELOG.md)
Files:
-
Modify:
COMPONENTS.md,CHANGELOG.md -
Step 1 :
COMPONENTS.md— lignesrequiredmanquantes
Pour les sections MalioSelect, MalioSelectCheckbox, MalioInputUpload, MalioInputRichText, ajouter dans le tableau des props la ligne (au même format que les autres composants) :
| `required` | `boolean` | `false` | Champ requis (affiche un astérisque rouge dans le label) |
-
Step 2 :
COMPONENTS.md— note astérisque + proplowercase -
Dans l'introduction de la famille formulaire (ou la section des props communes), ajouter une phrase : « Lorsque
requiredest vrai, un astérisque rouge est ajouté dans le label (visuel ; la sémantique est portée par l'attributrequired/aria-required). » -
Dans la section
MalioInputEmail, ajouter la ligne de prop :
| `lowercase` | `boolean` | `false` | Normalise la saisie en minuscules à la frappe |
et préciser que les espaces sont supprimés automatiquement à la saisie (pas de masque ; la validation de format reste à la couche error).
- Step 3 :
CHANGELOG.md— entrées
Sous le ### Added de la version en cours (format * [#…] …), ajouter :
* [#MUI-41] Prop `required` cohérente + astérisque rouge dans le label sur la famille formulaire
* [#MUI-41] InputEmail : sanitisation à la saisie (suppression des espaces, option `lowercase`)
- Step 4 : Commit
git add COMPONENTS.md CHANGELOG.md
git commit -m "docs: required/astérisque + lowercase email (COMPONENTS + CHANGELOG)"
Task 6 : Exemples playground + vérification finale
Files:
-
Modify: page(s) playground des composants concernés (selon
.playground/; cf. mémoire « Architecture playground ») -
Step 1 : Ajouter des exemples légers
Sur la page playground d'un composant représentatif (ex. InputText/Select), ajouter une instance :required="true". Sur la page InputEmail, ajouter une instance :lowercase="true". Si le coût d'intégration dépasse quelques minutes (routage/nav à câbler), le noter et passer — c'est hors scope strict du ticket.
- Step 2 : Lint
Run: npm run lint
Expected: 0 erreur. Corriger le cas échéant.
- Step 3 : Suite de tests complète des fichiers touchés
Run: npm run test -- app/components/malio/shared app/components/malio/input app/components/malio/select app/components/malio/checkbox app/components/malio/radio app/components/malio/time app/components/malio/date
Expected: PASS. En cas de timeout flaky, relancer le(s) fichier(s) concerné(s) individuellement.
- Step 4 : Commit (si exemples playground ajoutés)
git add .playground
git commit -m "docs(playground): exemples required + email lowercase"
Récapitulatif des commits attendus
feat(ui): composant partagé MalioRequiredMark (astérisque champ obligatoire)feat(ui): prop required + aria-required + astérisque sur Select/SelectCheckbox/Upload/RichTextfeat(ui): astérisque required dans le label de la famille formulairefeat(inputs): sanitisation email (suppression des espaces + option lowercase)docs: required/astérisque + lowercase email (COMPONENTS + CHANGELOG)docs(playground): exemples required + email lowercase(optionnel)