Skip to content

Groupe de boutons - DsfrButtonGroup

🌟 Introduction

Les boutons dans le contexte d'un groupe suivent les même règles que le composant bouton :

  • Il prend en charge les 2 types de boutons (primaire, secondaire) ;
  • Il gère les 3 tailles (prop size valeurs sm, md, lg) et les variantes ( Icônes / texte seul, avec icônes à gauche / droite).

📐 Structure

Ce composant est une simple balise ul qui peut recevoir un tableau de DsfrButtonProps & ButtonHTMLAttributes pour mettre chaque bouton dans un li.

Le slot par défaut peut être utilisé pour mettre vos boutons si la prop buttons est absente (ou un tableau vide).

🛠️ Props

Aucune prop n’est obligatoire

NomTypeDéfautDescription
align'right' / 'center' / StringundefinedDéfinit l'alignement des boutons dans le groupe. Peut être 'right' ou 'center'.
buttons(DsfrButtonProps & ButtonHTMLAttributes)[]() => []Liste des boutons à afficher. Chaque bouton est un objet qui peut inclure toutes les pros d’un DsfrButton, y compris un gestionnaire onClick.
equisizedbooleanfalseSi true, tous les boutons du groupe auront la même largeur.
inlineLayoutWhenstring | boolean'never'Détermine quand les boutons doivent être affichés sur une seule linge. Peut être 'always', 'never', ou correspondre à une taille spécifique ('sm', 'md', 'lg').
iconRightbooleanfalseSi true, place les icônes à droite du texte dans tous les boutons.
size'sm' | 'md' | 'lg''md'Détermine la taille des boutons. Peut être 'sm' (petit), 'md' (moyen, défaut), 'lg' (grand).

🧩 Slots

Le slot par défaut peut être utilisé pour mettre des boutons personnalisés.

Important

Si vous utilisez le slot, il faut bien envelopper chaque bouton dans une balise <li> Cf. les exemples

📝 Exemples

vue
<script lang="ts" setup>
import { ref } from 'vue'

import DsfrButton from '../DsfrButton.vue'
import DsfrButtonGroup from '../DsfrButtonGroup.vue'

const nb1 = ref(0)
const nb2 = ref(0)

const buttons = [
  {
    label: 'Bouton du premier groupe',
    onclick: () => {
      nb1.value++
    },
  },
  {
    label: 'Bouton secondaire du premier groupe',
    secondary: true,
    onclick: () => {
      nb2.value++
    },
  },
]
</script>

<template>
  <div class="fr-container fr-my-2w">
    <div>
      <p class="fr-text--lg">
        Premier groupe, 2 petits boutons avec utilisation de la prop `buttons`
      </p>
      <DsfrButtonGroup
        size="sm"
        :buttons="buttons"
      />
      <p class="fr-text--sm">
        Bouton primaire cliqué {{ nb1 }} fois
      </p>
      <p class="fr-text--sm">
        Bouton secondaire cliqué {{ nb2 }} fois
      </p>
    </div>
    <div>
      <p class="fr-text--lg">
        Deuxième groupe, avec utilisation du slot
      </p>
      <DsfrButtonGroup
        equisized
        inline-layout-when="always"
      >
        <li>
          <DsfrButton
            label="1re"
            primary
          />
        </li>
        <li>
          <DsfrButton
            label="2re"
            secondary
          />
        </li>
        <li>
          <DsfrButton
            label="3re"
            tertiary
          />
        </li>
        <li>
          <DsfrButton
            label="3re ss bord"
            tertiary
            no-outline
          />
        </li>
      </DsfrButtonGroup>
    </div>
  </div>
</template>
vue
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'

import DsfrButton from './DsfrButton.vue'
import type { DsfrButtonGroupProps } from './DsfrButton.types'

export type { DsfrButtonGroupProps }

const props = withDefaults(defineProps<DsfrButtonGroupProps>(), {
  buttons: () => [],
  inlineLayoutWhen: 'never',
  size: 'md',
  align: undefined,
})

const buttonsEl = ref<HTMLUListElement | null>(null)

const sm = computed(() => ['sm', 'small'].includes(props.size))
const md = computed(() => ['md', 'medium'].includes(props.size))
const lg = computed(() => ['lg', 'large'].includes(props.size))

const inlineAlways = computed(() => ['always', '', true].includes(props.inlineLayoutWhen))
const inlineSm = computed(() => ['sm', 'small'].includes(props.inlineLayoutWhen as string))
const inlineMd = computed(() => ['md', 'medium'].includes(props.inlineLayoutWhen as string))
const inlineLg = computed(() => ['lg', 'large'].includes(props.inlineLayoutWhen as string))
const center = computed(() => props.align === 'center')
const right = computed(() => props.align === 'right')

const equisizedWidth = ref('auto')
const groupStyle = computed(() => `--equisized-width: ${equisizedWidth.value};`)

const computeEquisizedWidth = async () => {
  let maxWidth = 0
  await new Promise((resolve) => setTimeout(resolve, 100))
  buttonsEl.value?.querySelectorAll('.fr-btn').forEach((btn: Element) => {
    const button = btn as HTMLButtonElement
    const width = button.offsetWidth
    const buttonStyle = window.getComputedStyle(button)
    const marginLeft = +buttonStyle.marginLeft.replace('px', '')
    const marginRight = +buttonStyle.marginRight.replace('px', '')
    button.style.width = 'var(--equisized-width)'
    const newWidth = width + marginLeft + marginRight
    if (newWidth > maxWidth) {
      maxWidth = newWidth
    }
  })
  equisizedWidth.value = `${maxWidth}px`
}

onMounted(async () => {
  if (!buttonsEl.value || !props.equisized) {
    return
  }
  await computeEquisizedWidth()
})
</script>

<template>
  <ul
    ref="buttonsEl"
    :style="groupStyle"
    class="fr-btns-group"
    :class="{
      'fr-btns-group--equisized': equisized,
      'fr-btns-group--sm': sm,
      'fr-btns-group--md': md,
      'fr-btns-group--lg': lg,
      'fr-btns-group--inline-sm': inlineAlways || inlineSm,
      'fr-btns-group--inline-md': inlineAlways || inlineMd,
      'fr-btns-group--inline-lg': inlineAlways || inlineLg,
      'fr-btns-group--center': center,
      'fr-btns-group--right': right,
      'fr-btns-group--icon-right': iconRight,
      'fr-btns-group--inline-reverse': reverse,
    }"
    data-testid="fr-btns"
  >
    <li
      v-for="({ onClick, ...button }, i) in buttons"
      :key="i"
    >
      <DsfrButton
        v-bind="button"
        @click="onClick"
      />
    </li>
    <!-- @slot Slot par défaut pour le contenu de la liste de boutons. Sera dans `<ul class="fr-btns-group">` -->
    <slot />
  </ul>
</template>
ts
import type { ButtonHTMLAttributes } from 'vue'

import type VIcon from '../VIcon/VIcon.vue'

export type DsfrButtonProps = {
  disabled?: boolean
  label?: string
  secondary?: boolean
  tertiary?: boolean
  iconRight?: boolean
  iconOnly?: boolean
  noOutline?: boolean
  size?: 'sm' | 'small' | 'lg' | 'large' | 'md' | 'medium' | '' | undefined
  icon?: string | InstanceType<typeof VIcon>['$props']
  onClick?: ($event: MouseEvent) => void
}

export type DsfrButtonGroupProps = {
  buttons?: (DsfrButtonProps & ButtonHTMLAttributes)[]
  reverse?: boolean
  equisized?: boolean
  iconRight?: boolean
  align?: 'right' | 'center' | '' | undefined
  inlineLayoutWhen?: 'always' | 'never' | 'sm' | 'small' | 'lg' | 'large' | 'md' | 'medium' | '' | true | undefined
  size?: 'sm' | 'small' | 'lg' | 'large' | 'md' | 'medium' | undefined
}

Et voilà ! Vous êtes prêt à ajouter une touche de sophistication à votre interface avec DsfrButtonGroup. Bonne création ! 🎨✨