Skip to content

Textarea ​

Composant Textarea flexible avec compteur de caractères, ajustement automatique de hauteur et conformité aux normes W3C d'accessibilité.

Description ​

SuTextarea est un composant de zone de texte enrichi qui étend les capacités d'un élément <textarea> natif. Il offre des fonctionnalités avancées telles que le compteur de caractères, l'ajustement automatique de la hauteur, et une gestion complète de l'accessibilité.

Exemples d'utilisation ​

Utilisation simple (sans SuFormField) ​

Textarea de base

vue
<script setup>
import { ref } from 'vue'

const text = ref('')
</script>

<template>
  <SuTextarea 
    placeholder="Entrez votre texte..."
    :rows="4"
    v-model="text"
  />
</template>

Avec compteur de caractères ​

Compteur de caractères

vue
<script setup>
import { ref } from 'vue'

const comment = ref('')
</script>

<template>
  <SuTextarea 
    placeholder="Votre commentaire..."
    :maxLength="200"
    :showCounter="true"
    :rows="3"
    v-model="comment"
  />
</template>

Ajustement automatique de hauteur ​

Auto-resize

vue
<script setup>
import { ref } from 'vue'

const message = ref('')
</script>

<template>
  <SuTextarea 
    placeholder="Tapez votre message..."
    :autoResize="true"
    :minRows="2"
    :maxRows="8"
    v-model="message"
  />
</template>

Tailles ​

Tailles disponibles

vue
<template>
  <SuTextarea 
    size="sm" 
    placeholder="Petit textarea" 
    :rows="2"
  />
  
  <SuTextarea 
    size="md" 
    placeholder="Textarea moyen" 
    :rows="3"
  />
  
  <SuTextarea 
    size="lg" 
    placeholder="Grand textarea" 
    :rows="4"
  />
</template>

États ​

États visuels

vue
<template>
  <SuTextarea 
    placeholder="État par défaut"
  />
  
  <SuTextarea 
    state="error"
    value="Texte avec erreur"
  />
  
  <SuTextarea 
    state="success"
    value="Texte validé"
  />
  
  <SuTextarea 
    state="warning"
    value="Texte avec avertissement"
  />
</template>

Utilisation avec SuFormField ​

Le composant SuTextarea peut être utilisé avec SuFormField pour bénéficier d'une structure de formulaire complète avec label, message et gestion des états.

Usage basique avec SuFormField ​

vue
<template>
  <SuFormField 
    label="Description" 
    message="Décrivez votre projet en quelques phrases"
  >
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Entrez votre description..."
        :rows="4"
        v-bind="slotProps"
        v-model="description"
      />
    </template>
  </SuFormField>
</template>

Avec destructuration des slot props ​

vue
<template>
  <SuFormField 
    label="Commentaire"
    :required="true"
    message="Partagez votre avis"
  >
    <template #default="{ fieldId, messageId, state, disabled }">
      <SuTextarea 
        :id="fieldId"
        :aria-describedby="messageId"
        :state="state"
        :disabled="disabled"
        placeholder="Votre commentaire..."
        :rows="3"
        v-model="comment"
      />
    </template>
  </SuFormField>
</template>

Validation avec états ​

Textarea avec validation

vue
<script setup>
import { ref, computed } from 'vue'

const message = ref('')
const messageError = ref('')

const validateMessage = () => {
  if (!message.value) {
    messageError.value = 'Le message est requis'
  } else if (message.value.length < 10) {
    messageError.value = 'Le message doit contenir au moins 10 caractères'
  } else {
    messageError.value = ''
  }
}

const messageState = computed(() => {
  if (!message.value) return 'default'
  return messageError.value ? 'error' : 'success'
})
</script>

<template>
  <SuFormField 
    label="Message"
    :required="true"
    :state="messageState"
    :message="messageError || 'Rédigez votre message'"
  >
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Tapez votre message..."
        :rows="3"
        v-bind="slotProps"
        v-model="message"
        @blur="validateMessage"
      />
    </template>
  </SuFormField>
</template>

Avec compteur de caractères et FormField ​

vue
<template>
  <SuFormField 
    label="Tweet"
    message="Partagez vos pensées"
    :required="true"
  >
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Que se passe-t-il ?"
        :maxLength="280"
        :showCounter="true"
        :autoResize="true"
        :minRows="2"
        :maxRows="6"
        v-bind="slotProps"
        v-model="tweet"
      />
    </template>
  </SuFormField>
</template>

Auto-resize avec FormField ​

vue
<template>
  <SuFormField 
    label="Description du projet"
    message="La hauteur s'ajuste automatiquement au contenu"
  >
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Décrivez votre projet..."
        :autoResize="true"
        :minRows="3"
        :maxRows="12"
        v-bind="slotProps"
        v-model="projectDescription"
      />
    </template>
  </SuFormField>
</template>

Label et message personnalisés ​

vue
<script setup>
import { DocumentTextIcon, InformationCircleIcon } from '@heroicons/vue/24/outline'
</script>

<template>
  <SuFormField 
    label="Contenu" 
    :required="true"
    message="Rédigez votre article"
  >
    <template #label="{ label, required, htmlFor }">
      <label 
        :for="htmlFor" 
        class="flex items-center gap-2 font-bold text-gray-900"
      >
        <DocumentTextIcon class="w-4 h-4" />
        {{ label }}
        <span v-if="required" class="text-red-500">*</span>
      </label>
    </template>
    
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Commencez à écrire..."
        :autoResize="true"
        :minRows="4"
        :maxRows="12"
        :maxLength="5000"
        :showCounter="true"
        v-bind="slotProps"
        v-model="content"
      />
    </template>
    
    <template #message="{ state }">
      <div class="flex items-center gap-2 text-sm text-gray-600">
        <InformationCircleIcon class="w-4 h-4" />
        <span v-if="state === 'error'">⚠️ Le contenu est trop court</span>
        <span v-else>Rédigez votre article (minimum 100 caractères)</span>
      </div>
    </template>
  </SuFormField>
</template>

Limitation de caractères avec feedback ​

vue
<script setup>
import { ref, computed } from 'vue'

const feedback = ref('')

const remainingChars = computed(() => 500 - feedback.value.length)
const feedbackState = computed(() => {
  if (!feedback.value) return 'default'
  if (feedback.value.length < 10) return 'error'
  if (feedback.value.length > 500) return 'error'
  return 'success'
})
</script>

<template>
  <SuFormField 
    label="Feedback produit"
    :required="true"
    :state="feedbackState"
    :message="remainingChars < 0 ? 'Limite dépassée' : `${remainingChars} caractères restants`"
  >
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Partagez votre expérience..."
        :maxLength="500"
        :showCounter="true"
        :autoResize="true"
        :minRows="3"
        :maxRows="8"
        v-bind="slotProps"
        v-model="feedback"
      />
    </template>
  </SuFormField>
</template>

Champ désactivé ou en lecture seule ​

vue
<template>
  <!-- Désactivé -->
  <SuFormField 
    label="Textarea désactivé"
    :disabled="true"
    message="Ce champ est temporairement indisponible"
  >
    <template #default="slotProps">
      <SuTextarea 
        value="Ce contenu est désactivé"
        v-bind="slotProps"
      />
    </template>
  </SuFormField>
  
  <!-- Lecture seule -->
  <SuFormField 
    label="Textarea en lecture seule"
    message="Consultation uniquement"
  >
    <template #default="{ fieldId, messageId }">
      <SuTextarea 
        :id="fieldId"
        :aria-describedby="messageId"
        :readonly="true"
        value="Ce contenu ne peut pas être modifié"
      />
    </template>
  </SuFormField>
</template>

API ​

Props ​

PropTypeDefaultDescription
modelValuestringundefinedValeur du textarea (v-model)
size'sm' | 'md' | 'lg''md'Taille du textarea
state'default' | 'error' | 'success' | 'warning''default'État visuel
disabledbooleanfalseDésactive le textarea
readonlybooleanfalseTextarea en lecture seule
requiredbooleanfalseChamp requis
placeholderstringundefinedTexte de placeholder
rowsnumber3Nombre de lignes par défaut
minRowsnumber2Nombre minimum de lignes (auto-resize)
maxRowsnumber10Nombre maximum de lignes (auto-resize)
maxLengthnumberundefinedNombre maximum de caractères
showCounterbooleanfalseAfficher le compteur
autoResizebooleanfalseAjustement automatique de hauteur
spellcheckbooleantrueVérification orthographique
wrap'soft' | 'hard' | 'off''soft'Mode de retour à la ligne

Props d'accessibilité (héritées de SuFormField) ​

Lorsque utilisé avec SuFormField, le composant reçoit automatiquement :

PropTypeDescription
idstringID unique du champ (fieldId)
aria-describedbystringID du message associé (messageId)
statestringÉtat de validation
disabledbooleanÉtat désactivé

Attributs d'accessibilité supplémentaires ​

PropTypeDefaultDescription
ariaLabelstringundefinedLabel accessible
ariaInvalidbooleanundefinedIndique si la valeur est invalide
ariaRequiredbooleanundefinedIndique si le champ est requis
autocompletestringundefinedAttribut autocomplete HTML

Events ​

EventTypeDescription
@update:modelValue(value: string) => voidÉmis lors du changement de valeur (v-model)
@input(event: Event) => voidÉmis lors de la saisie
@change(event: Event) => voidÉmis lors du changement
@focus(event: FocusEvent) => voidÉmis lors du focus
@blur(event: FocusEvent) => voidÉmis lors de la perte de focus
@keydown(event: KeyboardEvent) => voidÉmis lors de l'appui sur une touche
@keyup(event: KeyboardEvent) => voidÉmis lors du relâchement d'une touche

Méthodes exposées ​

MéthodeTypeDescription
focus()() => voidDonne le focus au textarea
select()() => voidSélectionne le texte
blur()() => voidRetire le focus
textareaRefRef<HTMLTextAreaElement>Référence à l'élément textarea natif

Accessibilité ​

Le composant Textarea respecte les normes WCAG 2.1 AA et les bonnes pratiques W3C :

✅ Fonctionnalités d'accessibilité ​

  • Attributs ARIA : Support complet (aria-label, aria-describedby, aria-invalid, etc.)
  • Compteur accessible : Annonces vocales pour les limites de caractères
  • Messages d'état : Via aria-describedby (messageId) avec aria-live
  • Contraste des couleurs : Ratios conformes WCAG AA (4.5:1)
  • Focus visible : Indicateur de focus clair avec outline
  • Tailles minimales : Cible tactile respectée
  • Mode sombre : Contraste adapté
  • Réduction d'animation : Respect de prefers-reduced-motion

🎯 Bonnes pratiques d'utilisation ​

vue
<!-- ✅ BON : Utilisation avec SuFormField pour l'accessibilité complète -->
<SuFormField 
  label="Description"
  :required="true"
  :state="descriptionError ? 'error' : 'default'"
  :message="descriptionError || 'Minimum 50 caractères'"
>
  <template #default="slotProps">
    <SuTextarea 
      placeholder="Décrivez votre projet..."
      :maxLength="500"
      :showCounter="true"
      :autoResize="true"
      v-bind="slotProps"
      v-model="description"
    />
  </template>
</SuFormField>

<!-- ✅ BON : Utilisation standalone avec attributs ARIA manuels -->
<label for="comment-textarea">Commentaire</label>
<SuTextarea 
  id="comment-textarea"
  aria-describedby="comment-help"
  aria-required="true"
  placeholder="Votre commentaire..."
  v-model="comment"
/>
<span id="comment-help">Partagez votre avis (minimum 10 caractères)</span>

<!-- ❌ MAUVAIS : Sans label ni description -->
<SuTextarea 
  placeholder="Texte..."
  v-model="text"
/>

Fonctionnalités avancées ​

🔄 Auto-resize intelligent ​

Le textarea ajuste automatiquement sa hauteur selon le contenu :

  • Hauteur minimale : Définie par minRows
  • Hauteur maximale : Définie par maxRows (défilement au-delà)
  • Natif quand c'est possible : field-sizing: content en CSS, sans JavaScript
  • Repli : calcul JS basé sur la hauteur de ligne réelle pour les navigateurs plus anciens

📊 Compteur de caractères ​

Le compteur offre un feedback visuel et vocal :

  • Affichage dynamique : caractères_saisis/limite_max
  • États visuels : Normal, proche limite, dépassement
  • Annonces vocales : Messages pour lecteurs d'écran
  • Couleurs adaptées : Vert, orange, rouge selon l'état
ToucheAction
TabNaviguer vers/depuis le textarea
Shift + TabNavigation inverse
Ctrl/Cmd + ASélectionner tout le texte
Ctrl/Cmd + ZAnnuler
Ctrl/Cmd + YRefaire

Exemples d'usage avancés ​

Formulaire de contact complet ​

vue
<script setup>
import { ref, computed } from 'vue'

const formData = ref({
  subject: '',
  message: ''
})

const errors = ref({})

const validateForm = () => {
  errors.value = {}
  
  if (!formData.value.subject || formData.value.subject.length < 5) {
    errors.value.subject = 'Minimum 5 caractères'
  }
  
  if (!formData.value.message || formData.value.message.length < 20) {
    errors.value.message = 'Minimum 20 caractères'
  }
  
  return Object.keys(errors.value).length === 0
}
</script>

<template>
  <form @submit.prevent="validateForm">
    <h2>Nous contacter</h2>
    
    <SuFormField 
      label="Sujet"
      :required="true"
      :state="errors.subject ? 'error' : 'default'"
      :message="errors.subject || 'Sujet de votre message'"
    >
      <template #default="slotProps">
        <SuInput 
          placeholder="Sujet..."
          v-bind="slotProps"
          v-model="formData.subject"
        />
      </template>
    </SuFormField>
    
    <SuFormField 
      label="Message"
      :required="true"
      :state="errors.message ? 'error' : 'default'"
      :message="errors.message || 'Décrivez votre demande en détail'"
    >
      <template #default="slotProps">
        <SuTextarea 
          placeholder="Votre message..."
          :maxLength="2000"
          :showCounter="true"
          :autoResize="true"
          :minRows="4"
          :maxRows="12"
          v-bind="slotProps"
          v-model="formData.message"
        />
      </template>
    </SuFormField>
    
    <button type="submit">Envoyer</button>
  </form>
</template>

Éditeur avec statistiques ​

vue
<script setup>
import { ref, computed } from 'vue'

const content = ref('')

const stats = computed(() => ({
  words: content.value.trim().split(/\s+/).filter(w => w.length > 0).length,
  chars: content.value.length,
  readTime: Math.ceil(content.value.trim().split(/\s+/).filter(w => w.length > 0).length / 200)
}))
</script>

<template>
  <div class="editor">
    <div class="stats">
      <span>{{ stats.words }} mots</span>
      <span>{{ stats.chars }} caractères</span>
      <span>~{{ stats.readTime }} min</span>
    </div>
    
    <SuFormField 
      label="Contenu"
      message="Rédigez votre article"
    >
      <template #default="slotProps">
        <SuTextarea 
          placeholder="Commencez à écrire..."
          :autoResize="true"
          :minRows="8"
          :maxRows="20"
          :maxLength="5000"
          :showCounter="true"
          v-bind="slotProps"
          v-model="content"
        />
      </template>
    </SuFormField>
  </div>
</template>

Composant SuTextareaField ​

Pour une utilisation encore plus simple, vous pouvez utiliser le composant SuTextareaField qui combine automatiquement SuFormField et SuTextarea :

vue
<template>
  <!-- Au lieu de -->
  <SuFormField label="Description" message="Décrivez votre projet">
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Entrez votre description..."
        v-bind="slotProps"
        v-model="description"
      />
    </template>
  </SuFormField>
  
  <!-- Vous pouvez simplement écrire -->
  <SuTextareaField 
    label="Description"
    message="Décrivez votre projet"
    placeholder="Entrez votre description..."
    v-model="description"
  />
</template>

Le composant SuTextareaField accepte toutes les props de SuFormField et SuTextarea combinées, offrant une syntaxe plus concise tout en conservant toutes les fonctionnalités.

Voir la documentation complète de SuTextareaField pour plus de détails.

Publié sous licence MIT.