From 60c3a450dee7b61ce9c1d37a2ee78f399b7498d8 Mon Sep 17 00:00:00 2001
From: Mathieu Ponton <mponton@grandlyon.com>
Date: Thu, 12 Sep 2024 13:17:32 +0000
Subject: [PATCH] feat(input): add required + autocomplete + example for
 invalid inputs

---
 src/app/contact/contact.component.html        | 14 ++-
 .../account-info/account-info.component.html  |  8 ++
 .../structure-contact.component.html          |  2 +
 .../structure-orientator.component.html       | 12 ++-
 .../mediation-beneficiary-info.component.html | 13 ++-
 .../mediation-beneficiary-info.component.ts   |  2 +-
 src/app/login/login.component.html            |  6 ++
 .../newsletter-subscription.component.html    |  1 +
 src/app/profile/edit/edit.component.html      | 21 +++-
 .../structure-add-member-modal.component.html |  1 +
 .../forgot-password.component.html            |  6 +-
 .../reset-password.component.html             |  3 +
 .../components/input/input.component.html     |  9 +-
 .../components/input/input.component.scss     |  9 +-
 .../components/input/input.component.ts       | 95 ++++++++++++++++---
 .../shared/components/input/input.stories.ts  |  8 ++
 .../select-or-create.component.html           |  2 +
 .../select-or-create.component.scss           |  5 +-
 .../textarea/textarea.component.html          |  9 +-
 .../textarea/textarea.component.scss          | 40 ++++----
 .../components/textarea/textarea.component.ts |  3 +
 .../components/textarea/textarea.stories.ts   | 33 ++++---
 .../more-filters/more-filters.component.scss  |  3 -
 src/app/utils/CustomRegExp.ts                 |  1 +
 src/styles.scss                               | 10 +-
 25 files changed, 246 insertions(+), 70 deletions(-)

diff --git a/src/app/contact/contact.component.html b/src/app/contact/contact.component.html
index 0b76199a2..92ef14404 100644
--- a/src/app/contact/contact.component.html
+++ b/src/app/contact/contact.component.html
@@ -1,22 +1,26 @@
 <form class="contactForm" [formGroup]="contactForm" (ngSubmit)="onSubmit()">
   <h2>Nous contacter</h2>
+  <p class="required-text-help">Les champs marqués d'un <sup>*</sup> sont obligatoires.</p>
 
   <app-input
     id="name"
     label="Nom"
     size="large"
-    [status]="contactForm.get('name').invalid ? null : 'success'"
+    autocomplete="on"
+    [required]="true"
+    [status]="contactForm.get('name').value ? (contactForm.get('name').invalid ? 'error' : 'success') : null"
     [value]="contactForm.get('name').value"
     (valueChange)="contactForm.get('name').setValue($event)"
   />
 
   <app-input
     id="email"
+    type="email"
     label="Adresse mail"
-    autocomplete="on"
     size="large"
+    autocomplete="on"
+    [required]="true"
     [status]="contactForm.get('email').value ? (contactForm.get('email').invalid ? 'error' : 'success') : null"
-    [statusText]="contactForm.get('email').hasError('alreadyExist') ? 'Cet email est déjà utilisé' : null"
     [value]="contactForm.get('email').value"
     (valueChange)="contactForm.get('email').setValue($event)"
   />
@@ -25,6 +29,8 @@
     id="phone"
     label="Téléphone"
     size="large"
+    type="tel"
+    autocomplete="on"
     [status]="contactForm.get('phone').value ? (contactForm.get('phone').invalid ? 'error' : 'success') : null"
     [value]="contactForm.get('phone').value"
     (valueChange)="contactForm.get('phone').setValue($event); utils.modifyPhoneInput(contactForm, 'phone', $event)"
@@ -34,6 +40,7 @@
     id="subject"
     label="Objet du message"
     size="large"
+    [required]="true"
     [status]="contactForm.get('subject').invalid ? null : 'success'"
     [value]="contactForm.get('subject').value"
     (valueChange)="contactForm.get('subject').setValue($event)"
@@ -43,6 +50,7 @@
     id="message"
     label="Message"
     placeholder="Exemple : J'aimerais avoir de l'aide sur Rés'in."
+    [required]="true"
     [value]="contactForm.get('message').value"
     (valueChange)="contactForm.get('message').setValue($event)"
   />
diff --git a/src/app/form/form-view/account-form/account-info/account-info.component.html b/src/app/form/form-view/account-form/account-info/account-info.component.html
index c54f8283e..7d82fa96e 100644
--- a/src/app/form/form-view/account-form/account-info/account-info.component.html
+++ b/src/app/form/form-view/account-form/account-info/account-info.component.html
@@ -2,6 +2,7 @@
   <div class="title">
     <h3>Qui êtes-vous&nbsp;?</h3>
     <p>Ces informations seront visibles dans l’annuaire des acteurs, accessible uniquement en version connectée</p>
+    <p class="required-text-help">Les champs marqués d'un <sup>*</sup> sont obligatoires.</p>
   </div>
 
   <div class="formGroup">
@@ -9,6 +10,8 @@
       id="name"
       label="Prénom"
       size="large"
+      autocomplete="on"
+      [required]="true"
       [status]="accountForm.get('name').value ? (accountForm.get('name').invalid ? 'error' : 'success') : null"
       [value]="accountForm.get('name').value"
       (valueChange)="accountForm.get('name').setValue($event); setValidationsForm()"
@@ -17,6 +20,8 @@
       id="surname"
       label="Nom"
       size="large"
+      autocomplete="on"
+      [required]="true"
       [status]="accountForm.get('surname').value ? (accountForm.get('surname').invalid ? 'error' : 'success') : null"
       [value]="accountForm.get('surname').value"
       (valueChange)="accountForm.get('surname').setValue($event); setValidationsForm()"
@@ -25,6 +30,9 @@
       id="phone"
       label="Téléphone"
       size="large"
+      autocomplete="on"
+      type="tel"
+      [required]="true"
       [status]="accountForm.get('phone').value ? (accountForm.get('phone').invalid ? 'error' : 'success') : null"
       [value]="accountForm.get('phone').value"
       (valueChange)="
diff --git a/src/app/form/form-view/structure-form/structure-contact/structure-contact.component.html b/src/app/form/form-view/structure-form/structure-contact/structure-contact.component.html
index 0090c36ff..bdbbd6870 100644
--- a/src/app/form/form-view/structure-form/structure-contact/structure-contact.component.html
+++ b/src/app/form/form-view/structure-form/structure-contact/structure-contact.component.html
@@ -9,6 +9,7 @@
       id="email"
       label="Email de la structure"
       size="large"
+      type="email"
       [status]="getContactMailStatus()"
       [value]="structureForm.get('contactMail').value"
       (valueChange)="structureForm.get('contactMail').setValue($event); setValidationsForm()"
@@ -17,6 +18,7 @@
       id="phone"
       label="Téléphone"
       size="large"
+      type="tel"
       [status]="getContactPhoneStatus()"
       [value]="structureForm.get('contactPhone').value"
       (valueChange)="utils.modifyPhoneInput(structureForm, 'contactPhone', $event); setValidationsForm()"
diff --git a/src/app/form/orientation-form-view/global-components/structure-orientator/structure-orientator.component.html b/src/app/form/orientation-form-view/global-components/structure-orientator/structure-orientator.component.html
index 02c32a754..42cc3156b 100644
--- a/src/app/form/orientation-form-view/global-components/structure-orientator/structure-orientator.component.html
+++ b/src/app/form/orientation-form-view/global-components/structure-orientator/structure-orientator.component.html
@@ -1,6 +1,7 @@
 <div class="orientationForm">
   <div class="orientation-header">
     <h2>Quelle structure oriente la personne&nbsp;?</h2>
+    <p *ngIf="!hasStructures" class="required-text-help">Les champs marqués d'un <sup>*</sup> sont obligatoires.</p>
     <div *ngIf="hasStructures && structuresLinked.length >= 2" class="number">
       {{ structuresLinked.length }} structures sont associées à votre compte
     </div>
@@ -35,6 +36,7 @@
           id="structureName"
           label="Nom de votre structure"
           size="large"
+          [required]="true"
           [status]="form.get('structureName').value ? (form.get('structureName').invalid ? 'error' : 'success') : null"
           [value]="form.get('structureName').value"
           (valueChange)="updatedForm('structureName', $event)"
@@ -42,10 +44,12 @@
 
         <app-input
           id="structureMail"
-          label="Email de votre structure<sup class='sup-input'>1</sup>"
+          label="Email de votre structure"
           description="Facultatif"
           autocomplete="on"
           size="large"
+          sup="1"
+          type="email"
           [status]="getStatus('structureMail')"
           [statusText]="getStatusText('structureMail')"
           [value]="form.get('structureMail').value"
@@ -54,10 +58,12 @@
 
         <app-input
           id="structurePhone"
-          label="Téléphone de votre structure<sup class='sup-input'>1</sup>"
+          label="Téléphone de votre structure"
           description="Facultatif"
           autocomplete="on"
           size="large"
+          sup="1"
+          type="tel"
           [status]="getStatus('structurePhone')"
           [statusText]="getStatusText('structurePhone')"
           [value]="form.get('structurePhone').value"
@@ -68,7 +74,7 @@
   </div>
 
   <p *ngIf="!hasStructures">
-    <sup class="sup-input">1</sup
+    <sup>1</sup
     ><span class="footnote">
       Un moyen de communication (email et/ou téléphone) est obligatoire pour valider cette étape</span
     >
diff --git a/src/app/form/orientation-form-view/online-demarch/online-mediation/mediation-beneficiary-info/mediation-beneficiary-info.component.html b/src/app/form/orientation-form-view/online-demarch/online-mediation/mediation-beneficiary-info/mediation-beneficiary-info.component.html
index 698717397..753e8eed6 100644
--- a/src/app/form/orientation-form-view/online-demarch/online-mediation/mediation-beneficiary-info/mediation-beneficiary-info.component.html
+++ b/src/app/form/orientation-form-view/online-demarch/online-mediation/mediation-beneficiary-info/mediation-beneficiary-info.component.html
@@ -5,6 +5,7 @@
   <div *ngIf="!isOrientationRdv" class="title">
     <h2>Quel est le nom de la personne que vous orientez&nbsp;?</h2>
     <p>Ces informations apparaîtront sur une fiche d’orientation à imprimer et à transmettre à la personne</p>
+    <p class="required-text-help">Les champs marqués d'un <sup>*</sup> sont obligatoires.</p>
   </div>
 
   <div class="formGroup">
@@ -13,6 +14,7 @@
       label="Prénom"
       autocomplete="on"
       size="large"
+      [required]="true"
       [status]="form.get('name').value ? (form.get('name').invalid ? 'error' : 'success') : null"
       [value]="form.get('name').value"
       (valueChange)="updatedForm('name', $event)"
@@ -23,6 +25,7 @@
       label="Nom"
       autocomplete="on"
       size="large"
+      [required]="true"
       [status]="form.get('surname').value ? (form.get('surname').invalid ? 'error' : 'success') : null"
       [value]="form.get('surname').value"
       (valueChange)="updatedForm('surname', $event)"
@@ -33,7 +36,9 @@
       id="email"
       autocomplete="on"
       size="large"
-      [label]="'Email' + getSup()"
+      type="email"
+      label="Email"
+      [sup]="getSup()"
       [status]="form.get('email').value ? (form.get('email').invalid ? 'error' : 'success') : null"
       [statusText]="form.get('email').value ? (form.get('email').invalid ? 'Email invalide' : 'Email valide') : null"
       [value]="form.get('email').value"
@@ -45,7 +50,9 @@
       id="phone"
       autocomplete="on"
       size="large"
-      [label]="'Téléphone' + getSup()"
+      label="Téléphone"
+      type="tel"
+      [sup]="getSup()"
       [status]="form.get('phone').value ? (form.get('phone').invalid ? 'error' : 'success') : null"
       [statusText]="
         form.get('phone').value ? (form.get('phone').invalid ? 'Téléphone invalide' : 'Téléphone valide') : null
@@ -56,7 +63,7 @@
   </div>
 
   <p *ngIf="isOrientationRdv && isPhone() && isEmail()">
-    <sup class="sup-input">1</sup
+    <sup>1</sup
     ><span class="footnote">
       Un moyen de communication (email et/ou téléphone) est obligatoire pour valider cette étape</span
     >
diff --git a/src/app/form/orientation-form-view/online-demarch/online-mediation/mediation-beneficiary-info/mediation-beneficiary-info.component.ts b/src/app/form/orientation-form-view/online-demarch/online-mediation/mediation-beneficiary-info/mediation-beneficiary-info.component.ts
index 477a33de7..ac62cd667 100644
--- a/src/app/form/orientation-form-view/online-demarch/online-mediation/mediation-beneficiary-info/mediation-beneficiary-info.component.ts
+++ b/src/app/form/orientation-form-view/online-demarch/online-mediation/mediation-beneficiary-info/mediation-beneficiary-info.component.ts
@@ -24,7 +24,7 @@ export class MediationBeneficiaryInfoComponent implements OnInit {
     return Boolean(this.form.get('email'));
   }
   public getSup(): string {
-    return this.isPhone() && this.isEmail() ? "<sup class='sup-input'>1</sup>" : '';
+    return this.isPhone() && this.isEmail() ? '1' : '';
   }
 
   public updatedForm(field: string, value: string): void {
diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html
index dbe8c8630..cf61a117e 100644
--- a/src/app/login/login.component.html
+++ b/src/app/login/login.component.html
@@ -3,6 +3,7 @@
     <div class="title">
       <h3>{{ isWelcome ? 'Bienvenue !' : 'Connexion' }}</h3>
       <p>Saisissez votre email pour vous connecter</p>
+      <p class="required-text-help">Les champs marqués d'un <sup>*</sup> sont obligatoires.</p>
     </div>
     <p *ngIf="verificationIssue" class="incorrectId">
       Une erreur est survenue lors de la validation de votre email... Veuillez envoyer un mail au support.
@@ -13,9 +14,11 @@
           id="login"
           label="Identifiant"
           size="large"
+          [required]="true"
           [status]="f.email.value ? (f.email.invalid || authFailed || isUnverifiedEmail ? 'error' : 'success') : null"
           [statusText]="getLoginStatusText()"
           [value]="f.email.value"
+          [wide]="true"
           (valueChange)="onChange(); f.email.setValue($event)"
         />
         <app-button
@@ -31,13 +34,16 @@
           label="Mot de passe"
           size="large"
           type="password"
+          [externalStatusControl]="true"
           [statusText]="
             f.password.invalid
               ? 'Le mot de passe doit obligatoirement contenir : 8&nbsp;caractères, une majuscule, une minuscule, un caractère spécial et un chiffre'
               : ''
           "
+          [required]="true"
           [status]="f.password.value ? (f.password.invalid || authFailed ? 'error' : 'success') : 'info'"
           [value]="f.password.value"
+          [wide]="true"
           (valueChange)="onChange(); f.password.setValue($event)"
         />
       </div>
diff --git a/src/app/newsletter-subscription/newsletter-subscription.component.html b/src/app/newsletter-subscription/newsletter-subscription.component.html
index 49b52d3e1..292b4597f 100644
--- a/src/app/newsletter-subscription/newsletter-subscription.component.html
+++ b/src/app/newsletter-subscription/newsletter-subscription.component.html
@@ -7,6 +7,7 @@
     label="Adresse email"
     autocomplete="on"
     size="large"
+    type="email"
     [wide]="true"
     [status]="
       subscriptionForm.get('email').value ? (subscriptionForm.get('email').invalid ? 'error' : 'success') : null
diff --git a/src/app/profile/edit/edit.component.html b/src/app/profile/edit/edit.component.html
index bfffb8af5..9f3c57a6b 100644
--- a/src/app/profile/edit/edit.component.html
+++ b/src/app/profile/edit/edit.component.html
@@ -51,6 +51,7 @@
           [(value)]="userProfile.surname"
         />
         <app-input
+          type="tel"
           [id]="'phone'"
           [label]="'Téléphone'"
           [size]="'large'"
@@ -166,15 +167,23 @@
     (closed)="$event ? confirm() : closeModal()"
   >
     <div class="modal-content">
+      <p class="required-text-help">Les champs marqués d'un <sup>*</sup> sont obligatoires.</p>
       <app-input
+        id="email"
+        type="email"
+        [required]="true"
         [label]="'Nouvel email'"
         [size]="'large'"
         [status]="getEmailStatus(newEmail)"
+        [statusText]="getEmailStatusText()"
         [wide]="true"
         [(value)]="newEmail"
       />
       <app-input
-        [label]="'Confirmer le nouvel email'"
+        id="confirm-email"
+        type="email"
+        [required]="true"
+        [label]="'Confirmation du nouvel email'"
         [size]="'large'"
         [status]="getEmailStatus(newEmailConfirm)"
         [statusText]="getEmailStatusText()"
@@ -192,7 +201,10 @@
     (closed)="$event ? confirm() : closeModal()"
   >
     <div class="modal-content">
+      <p class="required-text-help">Les champs marqués d'un <sup>*</sup> sont obligatoires.</p>
       <app-input
+        id="old-password"
+        [required]="true"
         [label]="'Ancien mot de passe'"
         [size]="'large'"
         [autocomplete]="'on'"
@@ -200,18 +212,24 @@
         [statusText]="getOldPasswordStatusText(getPasswordStatus(oldPassword))"
         [type]="'password'"
         [wide]="true"
+        [externalStatusControl]="true"
         [(value)]="oldPassword"
       />
       <app-input
+        id="new-password"
+        [required]="true"
         [label]="'Nouveau mot de passe'"
         [size]="'large'"
         [status]="getPasswordStatus(newPassword)"
         [statusText]="getPasswordStatusText(getPasswordStatus(newPassword))"
         [type]="'password'"
         [wide]="true"
+        [externalStatusControl]="true"
         [(value)]="newPassword"
       />
       <app-input
+        id="confirm-new-password"
+        [required]="true"
         [label]="'Confirmer le nouveau mot de passe'"
         [size]="'large'"
         [autocomplete]="'on'"
@@ -219,6 +237,7 @@
         [statusText]="getNewPasswordStatusText(getPasswordStatus(newPasswordConfirm, newPassword))"
         [type]="'password'"
         [wide]="true"
+        [externalStatusControl]="true"
         [(value)]="newPasswordConfirm"
       />
     </div>
diff --git a/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.html b/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.html
index 11b21962a..acb6f334f 100644
--- a/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.html
+++ b/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.html
@@ -7,6 +7,7 @@
 >
   <app-input
     autocomplete="on"
+    type="email"
     [label]="'Email du membre à ajouter'"
     [size]="'large'"
     [wide]="true"
diff --git a/src/app/reset-password/forgot-password.component.html b/src/app/reset-password/forgot-password.component.html
index 7c84780f8..c458a4fa5 100644
--- a/src/app/reset-password/forgot-password.component.html
+++ b/src/app/reset-password/forgot-password.component.html
@@ -2,15 +2,19 @@
   <div class="title">
     <h1>Mot de passe oublié</h1>
     <p>Saisissez votre email afin de réinitialiser votre mot de passe</p>
+    <p class="required-text-help">Les champs marqués d'un <sup>*</sup> sont obligatoires.</p>
   </div>
   <form [formGroup]="forgotPasswordForm" (ngSubmit)="onSubmit()">
     <div class="fields">
       <app-input
+        id="account-email"
+        type="email"
         size="large"
         [placeholder]="'exemple@mail.com'"
         [label]="'Email du compte'"
         [value]="email.value"
-        [status]="email.value ? (email.invalid ? 'error' : 'success') : null"
+        [required]="true"
+        [wide]="true"
         (valueChange)="email.setValue($event)"
       />
     </div>
diff --git a/src/app/reset-password/reset-password.component.html b/src/app/reset-password/reset-password.component.html
index b8c859982..64eef7016 100644
--- a/src/app/reset-password/reset-password.component.html
+++ b/src/app/reset-password/reset-password.component.html
@@ -1,6 +1,7 @@
 <div class="resetPage">
   <div class="title">
     <h1>Réinitialisation du mot de passe</h1>
+    <p class="required-text-help">Les champs marqués d'un <sup>*</sup> sont obligatoires.</p>
   </div>
   <form [formGroup]="resetPasswordForm" (ngSubmit)="onSubmit()">
     <div class="fields">
@@ -10,6 +11,7 @@
         size="large"
         type="password"
         [value]="password.value"
+        [required]="true"
         (valueChange)="password.setValue($event)"
       />
       <div class="passwordConditions">
@@ -63,6 +65,7 @@
         label="Vérification du mot de passe"
         size="large"
         type="password"
+        [required]="true"
         [status]="confirmPassword.value ? (confirmPassword.invalid ? 'error' : 'success') : null"
         [value]="confirmPassword.value"
         (valueChange)="confirmPassword.setValue($event)"
diff --git a/src/app/shared/components/input/input.component.html b/src/app/shared/components/input/input.component.html
index 7bcaf6453..4330dd269 100644
--- a/src/app/shared/components/input/input.component.html
+++ b/src/app/shared/components/input/input.component.html
@@ -1,6 +1,9 @@
 <div class="container" [ngClass]="{ disabled: disabled, readonly: readonly, wide: wide }">
   <div class="label">
-    <label [ngClass]="status" [htmlFor]="id" [innerHtml]="label"></label>
+    <div>
+      <label [ngClass]="status" [htmlFor]="id + '_Input'">{{ label }}</label>
+      <sup *ngIf="sup">{{ sup }}</sup>
+    </div>
     <span *ngIf="description" class="description" [ngClass]="{ disabled: disabled, readOnly: readonly }">{{
       description
     }}</span>
@@ -8,13 +11,15 @@
 
   <div class="inputContainer" [ngClass]="{ hasIconInField: hasIconInField }">
     <input
+      [attr.aria-required]="required ? 'true' : 'false'"
       [type]="type"
-      [id]="id"
+      [id]="id + '_Input'"
       [disabled]="disabled"
       [readonly]="readonly"
       [autocomplete]="autocomplete"
       [ngClass]="classes"
       [placeholder]="placeholder"
+      [attr.maxlength]="255"
       [(ngModel)]="value"
       (ngModelChange)="onChange()"
       (blur)="onFinishedEditing()"
diff --git a/src/app/shared/components/input/input.component.scss b/src/app/shared/components/input/input.component.scss
index 5593ae056..3c1d7b022 100644
--- a/src/app/shared/components/input/input.component.scss
+++ b/src/app/shared/components/input/input.component.scss
@@ -36,7 +36,12 @@
       }
     }
 
-    span {
+    sup {
+      margin-left: 4px;
+      cursor: default;
+    }
+
+    .description {
       font-size: $font-size-xxsmall;
       color: $grey-3;
       cursor: default;
@@ -62,7 +67,7 @@
       box-sizing: border-box;
       border-radius: 4px;
       border: 1px solid $grey-4;
-      padding: 8px 0px 8px 16px;
+      padding: 8px 0 8px 16px;
       font-size: $font-size-small;
       transition: all 0.3s ease-in-out;
 
diff --git a/src/app/shared/components/input/input.component.ts b/src/app/shared/components/input/input.component.ts
index 1259e448e..fe985ce54 100644
--- a/src/app/shared/components/input/input.component.ts
+++ b/src/app/shared/components/input/input.component.ts
@@ -1,4 +1,5 @@
 import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { CustomRegExp } from 'src/app/utils/CustomRegExp';
 
 @Component({
   selector: 'app-input',
@@ -9,7 +10,7 @@ export class InputComponent implements OnInit {
   /** HTML id associated with for */
   @Input() id: string;
 
-  @Input() type: 'text' | 'password' | 'time' = 'text';
+  @Input() type: 'text' | 'password' | 'time' | 'email' | 'tel' = 'text';
 
   @Input() disabled = false;
 
@@ -41,22 +42,44 @@ export class InputComponent implements OnInit {
 
   @Input() value: string;
 
+  @Input() required = false;
+
+  /** Additional sup information next to the label */
+  @Input() sup = '';
+
+  /** Determines if the parent component controls the status and statusText */
+  @Input() externalStatusControl = false;
+
   /** Triggers when input changes */
   @Output() valueChange = new EventEmitter<string>();
 
   @Output() finishedEditing = new EventEmitter<string>();
 
-  hasIconInField = false;
-
-  ngOnInit(): void {
-    if (this.type === 'password' || this.readonly) this.hasIconInField = true;
-  }
+  public hasIconInField = false;
+  private example = '';
 
   public get classes(): string {
     return [this.size, this.status, this.wide ? 'wide' : ''].join(' ');
   }
 
+  ngOnInit(): void {
+    if (this.type === 'password' || this.readonly) this.hasIconInField = true;
+    if (this.required) this.sup = '*';
+    console.log({ required: this.required, sup: this.sup });
+  }
+
   public onChange(): void {
+    if (!this.externalStatusControl) {
+      if (this.type === 'text' && this.isInvalidText()) {
+        this.setStatusError(this.getStatusText());
+      } else if (this.type === 'email' && !Boolean(this.value.match(CustomRegExp.EMAIL))) {
+        this.setStatusError(this.getExampleText());
+      } else if (this.type === 'tel' && !Boolean(this.value.match(CustomRegExp.PHONE))) {
+        this.setStatusError(this.getExampleText());
+      } else {
+        this.setStatusSuccess();
+      }
+    }
     this.valueChange.emit(this.value);
   }
 
@@ -69,15 +92,61 @@ export class InputComponent implements OnInit {
   }
 
   getStatusText(): string {
-    if (this.statusText) return this.statusText;
+    if (this.externalStatusControl) {
+      return this.statusText || '';
+    }
+    if (this.value === '') {
+      return '';
+    }
+    if (this.status === 'error') {
+      this.example = this.getExampleText();
+      return `${this.label} invalide. ${this.example}`;
+    }
+    if (this.status === 'success') {
+      return `${this.label} valide`;
+    }
+    return '';
+  }
 
-    switch (this.status) {
-      case 'success':
-        return `${this.label} valide`;
-      case 'error':
-        return `${this.label} invalide`;
+  private isInvalidText(): boolean {
+    return (
+      (this.id.startsWith('name') || this.id.startsWith('surname') || this.id === 'subject') &&
+      !CustomRegExp.NAME.test(this.value)
+    );
+  }
+
+  private setStatusError(message: string): void {
+    this.status = 'error';
+    this.statusText = message;
+  }
+
+  private setStatusSuccess(): void {
+    this.status = 'success';
+    this.statusText = '';
+  }
+
+  private getExampleText(): string {
+    switch (true) {
+      case this.type === 'email':
+        return 'Exemple: ml@gmail.com';
+      case this.type === 'tel':
+        return 'Exemple: 06 11 22 33 44';
+      case this.id.startsWith('name'):
+        return this.getNameExampleText();
+      case this.id.startsWith('surname'):
+        return 'Exemple: Bocuse';
       default:
-        break;
+        return '';
+    }
+  }
+
+  private getNameExampleText(): string {
+    if (this.id.startsWith('name')) {
+      if (this.label === 'Prénom') {
+        return 'Exemple : Paul';
+      } else {
+        return 'Exemple : Paul Bocuse';
+      }
     }
   }
 }
diff --git a/src/app/shared/components/input/input.stories.ts b/src/app/shared/components/input/input.stories.ts
index 11c5b288a..b4ff594ad 100644
--- a/src/app/shared/components/input/input.stories.ts
+++ b/src/app/shared/components/input/input.stories.ts
@@ -88,3 +88,11 @@ export const InputReadonly: Story = {
     readonly: true,
   },
 };
+
+export const InputWithSup: Story = {
+  args: {
+    ...Input.args,
+    id: 'input8',
+    sup: '1',
+  },
+};
diff --git a/src/app/shared/components/select-or-create/select-or-create.component.html b/src/app/shared/components/select-or-create/select-or-create.component.html
index 9e84c456d..8f4f28d64 100644
--- a/src/app/shared/components/select-or-create/select-or-create.component.html
+++ b/src/app/shared/components/select-or-create/select-or-create.component.html
@@ -11,6 +11,7 @@
       [status]="!isAddingNewItem ? (isSelectedItem ? 'success' : 'error') : null"
       [description]="'Recherchez votre ' + name.toLowerCase() + ' dans la liste suivante'"
       [value]="isAddingNewItem ? null : value"
+      [externalStatusControl]="true"
       (valueChange)="onAutocompleteValueChange($event)"
       (click)="onAutocompleteValueChange(value || '')"
     />
@@ -45,6 +46,7 @@
         [placeholder]="'exemple : ' + name.toLowerCase()"
         [description]="'Renseignez le nom de votre ' + name.toLowerCase()"
         [status]="isFieldValid() ? 'success' : 'error'"
+        [externalStatusControl]="true"
         (valueChange)="onCreateValueChange($event)"
       />
       <app-button label="Annuler la création" [variant]="'primaryBlack'" (click)="toggleAddItem()" />
diff --git a/src/app/shared/components/select-or-create/select-or-create.component.scss b/src/app/shared/components/select-or-create/select-or-create.component.scss
index 3b28dfc46..776ea7317 100644
--- a/src/app/shared/components/select-or-create/select-or-create.component.scss
+++ b/src/app/shared/components/select-or-create/select-or-create.component.scss
@@ -9,7 +9,6 @@
 
   .select {
     .autocomplete-items {
-      top: -28px;
       position: relative;
       > div {
         border-bottom: 1px solid $grey-7;
@@ -29,10 +28,8 @@
     gap: 16px;
     .createForm {
       display: flex;
-      flex-direction: row;
+      flex-direction: column;
       gap: 16px;
-      align-items: center;
-      flex-grow: 1;
     }
     p {
       @include font-regular-16;
diff --git a/src/app/shared/components/textarea/textarea.component.html b/src/app/shared/components/textarea/textarea.component.html
index 1cc8cec25..261ec67ae 100644
--- a/src/app/shared/components/textarea/textarea.component.html
+++ b/src/app/shared/components/textarea/textarea.component.html
@@ -1,7 +1,12 @@
 <div class="inputContainer" [ngClass]="{ disabled: disabled }">
-  <label [htmlFor]="'textarea-' + id" [ngClass]="status">{{ label }}</label>
+  <div class="label">
+    <div>
+      <label [htmlFor]="'textarea-' + id" [ngClass]="status">{{ label }}</label>
+      <span *ngIf="required" class="required">*</span>
+    </div>
+    <span *ngIf="description" class="description" [ngClass]="{ disabled: disabled }">{{ description }}</span>
+  </div>
 
-  <span *ngIf="description" class="description" [ngClass]="{ disabled: disabled }">{{ description }}</span>
   <textarea
     rows="8"
     [value]="value"
diff --git a/src/app/shared/components/textarea/textarea.component.scss b/src/app/shared/components/textarea/textarea.component.scss
index 72754051c..c8285e157 100644
--- a/src/app/shared/components/textarea/textarea.component.scss
+++ b/src/app/shared/components/textarea/textarea.component.scss
@@ -4,8 +4,9 @@
 .inputContainer {
   display: flex;
   flex-direction: column;
-  width: 100vw;
+  width: 100%;
   max-width: 600px;
+
   &.disabled {
     cursor: not-allowed;
     label {
@@ -13,23 +14,30 @@
     }
   }
 
-  label {
-    line-height: 150%;
-    color: $grey-1;
-    &.error {
-      color: $red;
+  .label {
+    label {
+      line-height: 150%;
+      color: $grey-1;
+      &.error {
+        color: $red;
+      }
+      &.success {
+        color: $info-success;
+      }
     }
-    &.success {
-      color: $info-success;
+    .required {
+      margin-left: 4px;
+      font-size: $font-size-xxsmall;
+      color: $red;
+      cursor: default;
     }
-  }
-
-  span.description {
-    margin-top: 4px;
-    font-size: $font-size-xxsmall;
-    color: $grey-3;
-    &.disabled {
-      color: $grey-6;
+    span.description {
+      margin-top: 4px;
+      font-size: $font-size-xxsmall;
+      color: $grey-3;
+      &.disabled {
+        color: $grey-6;
+      }
     }
   }
 
diff --git a/src/app/shared/components/textarea/textarea.component.ts b/src/app/shared/components/textarea/textarea.component.ts
index ee6b4dc6f..30b48b674 100644
--- a/src/app/shared/components/textarea/textarea.component.ts
+++ b/src/app/shared/components/textarea/textarea.component.ts
@@ -32,6 +32,9 @@ export class TextareaComponent {
   /** Value */
   @Input() value = '';
 
+  /** Is the textarea required ? */
+  @Input() required = false;
+
   @Output() valueChange = new EventEmitter<string>();
 
   public onValueChange(): void {
diff --git a/src/app/shared/components/textarea/textarea.stories.ts b/src/app/shared/components/textarea/textarea.stories.ts
index f4d4f03a7..cc25a8143 100644
--- a/src/app/shared/components/textarea/textarea.stories.ts
+++ b/src/app/shared/components/textarea/textarea.stories.ts
@@ -22,43 +22,52 @@ type Story = StoryObj<TextareaComponent>;
 
 export const Textarea: Story = {
   args: {
-    id: 'input1',
+    id: 'textarea1',
     label: 'Label',
   },
 };
 
-export const InputWithDescription: Story = {
+export const TextareaWithDescription: Story = {
   args: {
     ...Textarea.args,
-    id: 'input2',
+    id: 'textarea2',
     description: 'Texte de description additionnel',
   },
 };
 
-export const InputDisabled: Story = {
+export const TextareaDisabled: Story = {
   args: {
-    ...InputWithDescription.args,
-    id: 'input3',
+    ...TextareaWithDescription.args,
+    id: 'textarea3',
     disabled: true,
   },
 };
 
-export const InputWithError: Story = {
+export const TextareaWithError: Story = {
   args: {
-    ...InputWithDescription.args,
-    id: 'input4',
+    ...TextareaWithDescription.args,
+    id: 'textarea4',
     label: 'Label',
     status: 'error',
     statusText: "Texte d'erreur obligatoire",
   },
 };
 
-export const InputWithSuccess: Story = {
+export const TextareaWithSuccess: Story = {
   args: {
-    ...InputWithDescription.args,
-    id: 'input5',
+    ...TextareaWithDescription.args,
+    id: 'textarea5',
     label: 'Label',
     status: 'success',
     statusText: 'Texte de succès',
   },
 };
+
+export const TextareaRequired: Story = {
+  args: {
+    ...Textarea.args,
+    id: 'textarea6',
+    label: 'This is required',
+    required: true,
+  },
+};
diff --git a/src/app/structure-list/components/more-filters/more-filters.component.scss b/src/app/structure-list/components/more-filters/more-filters.component.scss
index 3cbfff626..2e6430faf 100644
--- a/src/app/structure-list/components/more-filters/more-filters.component.scss
+++ b/src/app/structure-list/components/more-filters/more-filters.component.scss
@@ -80,7 +80,6 @@
         #categoryName {
           padding: 0 0;
         }
-
       }
       .collapseContent {
         display: flex;
@@ -89,7 +88,6 @@
         padding-block: 16px;
         padding-left: 24px;
         border-top: 1px solid $grey-4;
-
       }
     }
   }
@@ -100,4 +98,3 @@
     padding-block: 16px;
   }
 }
-
diff --git a/src/app/utils/CustomRegExp.ts b/src/app/utils/CustomRegExp.ts
index 9dfc15812..c23979fe6 100644
--- a/src/app/utils/CustomRegExp.ts
+++ b/src/app/utils/CustomRegExp.ts
@@ -35,4 +35,5 @@ export class CustomRegExp {
    * Validate a location request in search bar
    */
   public static readonly LOCATION: RegExp = /^\d+\s[A-z]+\s[A-z]+/g; // NOSONAR
+  public static readonly NAME: RegExp = /^[\p{L}\s'-]*$/u; // NOSONAR
 }
diff --git a/src/styles.scss b/src/styles.scss
index a8322ad8d..3c155b8ce 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -329,12 +329,9 @@ p {
 }
 
 sup {
+  font-size: $font-size-xxsmall;
   color: $red;
 }
-.sup-input {
-  font-size: $font-size-xxxxsmall;
-  color: unset;
-}
 
 .formGroup {
   display: flex;
@@ -361,3 +358,8 @@ div.inline {
 .sb-colorRow {
   margin-block: 12px !important;
 }
+
+p.required-text-help {
+  color: $grey-1;
+  font-size: $font-size-xsmall !important;
+}
-- 
GitLab