Groupe de Bouton radio (et bouton radio riche) - DsfrRadioButtonSet 
🌟 Introduction 
Les groupes de boutons radio (riches) permettent d’éviter d’écrire plusieurs fois le composant DsfrRadioButton, il est fortement conseillé de l’utiliser plutôt que de répéter DsfrRadioButton.
🏅 La documentation sur le bouton radio et le bouton radio riche sur le DSFR
La story sur le groupe de boutons radio sur le storybook de VueDsfr📐 Structure 
Le composant DsfrRadioButtonSet est composé des éléments suivants :
- Un élément <div>englobant l'ensemble du groupe de radio.
- Un élément <fieldset>contenant les boutons radio et les messages associés.
- Une légende (legend) définie par la proplegendet personnalisable avec le slotlegend.
- Un hint (hint) définie par la prophintet personnalisable avec le slothint.
- Un groupe de boutons radio individuels rendus par le composant DsfrRadioButton.
- Un message d'information, d'erreur ou de validation, affiché en dessous du groupe de boutons radio (facultatif).
🛠️ Props 
| Nom | Type | Description | Obligatoire | 
|---|---|---|---|
| titleId | string | Identifiant unique du champ (générée automatiquement si non fournie) | Non | 
| disabled | boolean | Indique si l'ensemble des boutons radio est désactivé | Non | 
| required | boolean | Indique si le groupe de radio est obligatoire | Non | 
| small | boolean | Affiche les boutons radio en taille réduite | Non | 
| inline | boolean | Affiche les boutons radio en ligne (par défaut : non) | Non | 
| name | string | Nom du champ <input>associé à l'ensemble des boutons radio du tableau données dans la propoptions, cf. plus loin | Oui | 
| errorMessage | string | Message d'erreur global à afficher | Non | 
| validMessage | string | Message de validation global à afficher | Non | 
| legend | string | Texte de la légende | Non | 
| hint | string | Texte du hint | Non | 
| modelValue | stringounumberouboolean | Valeur courante du composant (sélection courante) | Non | 
| options | Omit<DsfrRadioButtonProps, 'modelValue'>[] | Tableau d'options définissant les boutons radio individuels | Oui | 
Important
L’attribut name des boutons radio est très important. Sa valeur doit être identique pour tous les boutons radio d’un même groupe. Il y a différentes façons de donner la valeur de cet attribut :
- Soit dans la prop namedeDsfrRadioButtonSetsi la propoptionsest utilisée et que le slot par défaut n’est pas utilisé, et que chaque élément du tableau d’optionsne contient pas de propriété dont la clé estname(exemple 1).
- Soit en tant que propriété de chaque objet du tableau passé en prop optionsàDsfrRadioButtonSet(exemple 2)
- Soit en tant que prop de chaque DsfrRadioButtondans le slot par défaut deDsfrRadioButtonSet(exemple 3)
La prop name est inutile sur DsfrRadioButtonSet si le slot par défaut est utilisé (déconseillé) avec des DsfrRadioButton (sur lesquels il faudra obligatoirement la même prop name).
Exemples:
<script>
const options = [
  {
    label: 'Première valeur',
    id: 'name1-1',
    value: 'name1-1',
    hint: 'Description de la première valeur',
  },
  {
    label: 'Deuxième valeur',
    id: 'name1-2',
    value: 'name1-2',
    hint: 'Description de la deuxième valeur',
  },
] // pas de propriété `name` sur les objets de `options`
</script>
<template>
  <DsfrRadioButtonSet
    name="name-1"
    :options="options"
  />
</template><script>
const options = [
  {
    label: 'Première valeur',
    id: 'name1-1',
    name: 'name1', // propriété `name` identique sur chaque objet de `options`
    value: 'name1-1',
    hint: 'Description de la première valeur',
  },
  {
    label: 'Deuxième valeur',
    id: 'name1-2',
    name: 'name1', // propriété `name` identique sur chaque objet de `options`
    value: 'name1-2',
    hint: 'Description de la deuxième valeur',
  },
]
</script>
<template>
  <DsfrRadioButtonSet
    :options="options"
  />
</template><script>
const options = [
  {
    label: 'Première valeur',
    id: 'name1-1',
    value: 'name1-1',
    hint: 'Description de la première valeur',
  },
  {
    label: 'Deuxième valeur',
    id: 'name1-2',
    value: 'name1-2',
    hint: 'Description de la deuxième valeur',
  },
] // pas de propriété `name` sur les objets de `options`
</script>
<template>
  <DsfrRadioButtonSet>
    <DsfrRadioButton
      v-for="option of options"
      :key="option.id"
      v-model="modelValue1"
      name="name-1"
      v-bind="option"
    />
  </DsfrRadioButtonSet>
</template><script>
const options = [
  {
    label: 'Première valeur',
    id: 'name1-1',
    value: 'name1-1',
    hint: 'Description de la première valeur',
  },
  {
    label: 'Deuxième valeur',
    id: 'name1-2',
    value: 'name1-2',
    hint: 'Description de la deuxième valeur',
  },
] // pas de propriété `name` sur les objets de `options`
</script>
<template>
  <!-- Il manque la prop name sur DsfrRadioButtonSet -->
  <DsfrRadioButtonSet
    :options="options"
  />
</template><script>
const options = [
  {
    label: 'Première valeur',
    id: 'name1-1',
    value: 'name1-1',
    hint: 'Description de la première valeur',
  },
  {
    label: 'Deuxième valeur',
    id: 'name1-2',
    value: 'name1-2',
    hint: 'Description de la deuxième valeur',
  },
] // pas de propriété `name` sur les objets de `options`
</script>
<template>
  <DsfrRadioButtonSet>
    <!-- Il manque la prop name sur DsfrRadioButton -->
    <DsfrRadioButton
      v-for="option of options"
      :key="option.id"
      v-model="modelValue1"
      v-bind="option"
    />
  </DsfrRadioButtonSet>
</template>📡 Événements 
DsfrRadioButtonSet émet l'événement suivant :
| Nom | Description | 
|---|---|
| update:modelValue | Est émis lorsque la valeur d'un bouton radio est sélectionnée | 
🧩 Slots 
DsfrRadioButtonSet fournit les slots suivants pour la personnalisation :
- legend: Permet de personnaliser le contenu de la légende.
- hint: Permet de personnaliser le contenu d'un hint.
- required-tip: Permet d'ajouter un astérisque indiquant que le champ est obligatoire (fonctionne uniquement si l'attribut- requiredest défini sur le composant).
🪆 Relation avec DsfrRadioButton 
Le composant DsfrRadioButtonSet utilise le composant DsfrRadioButton pour rendre visuellement chaque option du groupe. Chaque bouton radio individuel hérite des props du composant DsfrRadioButtonSet excepté modelValue.
📝 Exemples 
<script lang="ts" setup>
import { ref } from 'vue'
import DsfrRadioButtonSet from '../DsfrRadioButtonSet.vue'
const modelValue1 = ref()
const modelValue2 = ref()
const modelValue3 = ref()
const modelValue4 = ref()
const modelValue5 = ref()
const modelValue6 = ref()
const options1 = [
  {
    label: 'Première valeur',
    id: 'name1-1',
    value: 'name1-1',
    hint: 'Description one',
  },
  {
    label: 'Deuxième valeur',
    id: 'name1-2',
    value: 'name1-2',
    hint: 'Description two',
  },
  {
    label: 'Troisième valeur',
    id: 'name1-3',
    value: 'name1-3',
    hint: 'Description three',
  },
]
const options2 = structuredClone(options1).map(option => Object.fromEntries(
  Object.entries(option).map(([key, value]) => [key, value.replace('name1', 'name2')]),
))
const options3 = structuredClone(options1).map(option => Object.fromEntries(
  Object.entries(option).map(([key, value]) => [key, value.replace('name1', 'name3')]),
))
const options4 = structuredClone(options1).map(option => Object.fromEntries(
  Object.entries(option).filter(([key]) => key !== 'hint').map(([key, value]) => [key, value.replace('name1', 'name4')]),
))
const options5 = structuredClone(options1).map(option => Object.fromEntries(
  Object.entries(option).filter(([key]) => key !== 'hint').map(([key, value]) => [key, value.replace('name1', 'name5')]),
))
const options6 = structuredClone(options1).map(option => Object.fromEntries(
  Object.entries(option).filter(([key]) => key !== 'hint').map(([key, value]) => [key, value.replace('name1', 'name6')]),
))
</script>
<template>
  <div class="fr-container fr-my-2v">
    <div>
      <DsfrRadioButtonSet
        v-model="modelValue1"
        legend="Groupe de boutons radio simple"
        hint="Texte de description additionnel"
        :options="options1"
        name="name-1"
      />
      <p>
        modelValue1: {{ modelValue1 }}
      </p>
    </div>
    <div>
      <DsfrRadioButtonSet
        v-model="modelValue2"
        legend="Groupe de boutons radio avec message d’erreur"
        :options="options2"
        name="name-2"
        error-message="Message d’erreur"
      />
    </div>
    <div>
      <DsfrRadioButtonSet
        v-model="modelValue3"
        legend="Groupe de boutons radio avec message de validation"
        :options="options3"
        name="name-3"
        valid-message="Message de validation"
      />
    </div>
    <div>
      <DsfrRadioButtonSet
        v-model="modelValue4"
        legend="Groupe de boutons radio en ligne"
        :options="options4"
        name="name-4"
        inline
      />
      <p>
        modelValue4: {{ modelValue4 }}
      </p>
    </div>
    <div>
      <DsfrRadioButtonSet
        v-model="modelValue5"
        legend="Groupe de boutons radio en ligne avec message d’erreur"
        :options="options5"
        name="name-5"
        inline
        error-message="Message d’erreur"
      />
    </div>
    <div>
      <DsfrRadioButtonSet
        v-model="modelValue6"
        legend="Groupe de boutons radio en ligne avec message de validation"
        :options="options6"
        name="name-6"
        inline
        valid-message="Message de validation"
      />
    </div>
  </div>
</template>⚙️ Code source du composant 
<script lang="ts" setup>
import type { DsfrRadioButtonSetProps } from './DsfrRadioButton.types'
import { computed } from 'vue'
import { useRandomId } from '../../utils/random-utils'
import DsfrRadioButton from './DsfrRadioButton.vue'
export type { DsfrRadioButtonSetProps }
const props = withDefaults(defineProps<DsfrRadioButtonSetProps>(), {
  titleId: () => useRandomId('radio-button', 'group'),
  errorMessage: '',
  validMessage: '',
  legend: '',
  hint: '',
  options: () => [],
})
const emit = defineEmits<{ (e: 'update:modelValue', payload: string | number | boolean): void }>()
const message = computed(() => props.errorMessage || props.validMessage)
const additionalMessageClass = computed(() => props.errorMessage ? 'fr-error-text' : 'fr-valid-text')
const onChange = ($event: string) => {
  if ($event === props.modelValue) {
    return
  }
  emit('update:modelValue', $event)
}
const describedByElement = computed(() => message.value ? `messages-${props.titleId}` : undefined)
</script>
<template>
  <div class="fr-form-group">
    <fieldset
      class="fr-fieldset"
      :class="{
        'fr-fieldset--error': errorMessage,
        'fr-fieldset--valid': validMessage,
      }"
      :disabled="disabled"
      :aria-labelledby="titleId"
      :aria-describedby="describedByElement"
      :aria-invalid="ariaInvalid"
      :role="(errorMessage || validMessage) ? 'group' : undefined"
    >
      <legend
        v-if="legend || $slots.legend || hint || $slots.hint"
        :id="titleId"
        class="fr-fieldset__legend fr-fieldset__legend--regular"
      >
        <!-- @slot Slot pour personnaliser tout le contenu de la balise <legend> cf. [DsfrInput](/?path=/story/composants-champ-de-saisie-champ-simple-dsfrinput--champ-avec-label-personnalise). Une **props porte le même nom pour une légende simple** (texte sans mise en forme) -->
        <slot name="legend">
          {{ legend }}
          <span
            v-if="hint || $slots.hint"
            class="fr-hint-text"
          >
            <slot name="hint">
              {{ hint }}
            </slot>
          </span>
          <!-- @slot Slot pour indiquer que le champ est obligatoire. Par défaut, met une astérisque si `required` est à true (dans un `<span class="required">`) -->
          <slot name="required-tip">
            <span
              v-if="required"
              class="required"
            > *</span>
          </slot>
        </slot>
      </legend>
      <slot>
        <DsfrRadioButton
          v-for="(option, i) of options"
          :key="typeof option.value === 'boolean' ? i : (option.value || i)"
          :name="name"
          :aria-disabled="option.disabled"
          v-bind="option"
          :small="small"
          :inline="inline"
          :model-value="modelValue"
          @update:model-value="onChange($event as string)"
        />
      </slot>
      <div
        v-if="message"
        :id="`messages-${titleId}`"
        class="fr-messages-group"
        aria-live="assertive"
        role="alert"
      >
        <p
          class="fr-message  fr-message--info  flex  items-center"
          :class="additionalMessageClass"
        >
          {{ message }}
        </p>
      </div>
    </fieldset>
  </div>
</template>export type DsfrRadioButtonProps = {
  id?: string
  name?: string
  modelValue: string | number | boolean | undefined
  disabled?: boolean
  small?: boolean
  inline?: boolean
  value: string | number | boolean
  label?: string
  hint?: string
  rich?: boolean
  img?: string
  imgTitle?: string
  svgPath?: string
  svgAttrs?: Record<string, unknown>
}
export type DsfrRadioButtonOptions = (Omit<DsfrRadioButtonProps, 'modelValue'>)[]
export type DsfrRadioButtonSetProps = {
  titleId?: string
  disabled?: boolean
  required?: boolean
  small?: boolean
  inline?: boolean
  name?: string
  errorMessage?: string
  validMessage?: string
  legend?: string
  hint?: string
  modelValue?: string | number | boolean | undefined
  options?: Omit<DsfrRadioButtonProps, 'modelValue'>[]
  ariaInvalid?: boolean | 'grammar' | 'spelling'
}