Skip to content

Curseur - DsfrRange

🌟 Introduction

Bienvenue dans la documentation du DsfrRange, un composant Vue qui va slider dans votre coeur comme un croissant bien chaud glisse dans votre petit déjeuner. Ce composant est un véritable couteau suisse pour les curseurs, capable de tout faire, de l'affichage simple à la gestion de valeurs doubles. Mettez vos ceintures, on décolle !

Les curseurs sont des entrées numériques qui permettent de voir graphiquement une sélection par rapport a une valeur minimale et maximale. Ils servent à montrer en temps réel les options choisies et éclairer la prise de décision. ("Why so serious?" 🦇🃏)

🏅 La documentation sur le curseur importante sur le DSFR

La story sur le curseur importante sur le storybook de VueDsfr

📐 Structure

  • Le composant est encapsulé dans une div avec la classe fr-range-group, qui peut afficher un message d'erreur via message.
  • Le label est affiché en haut, suivi par un texte d'indice (hint) si fourni.
  • Le curseur (input type="range") est stylisé avec des classes pour gérer la taille et l'état désactivé.
  • Les valeurs minimales et maximales sont affichées si hideIndicators est false.
  • Un second curseur est présent si la prop double est true.
  • Les messages d'erreur ou autres sont affichés dans une div spécifique.

🛠️ Props

NomTypeDéfautDescription
idstringgetRandomId('range')Identifiant unique du curseur. Si non fourni, un id est généré aléatoirement.
minnumber0Valeur minimale du curseur.
maxnumber100Valeur maximale du curseur.
modelValuenumber0Valeur actuelle du curseur.
labelstring-Texte de l'étiquette associée au curseur.
hintstringundefinedTexte d'indice optionnel.
messagestringundefinedMessage à afficher en cas d'erreur.
prefixstringundefinedTexte à afficher avant la valeur.
suffixstringundefinedTexte à afficher après la valeur.
smallbooleanundefinedSi true, réduit la taille du curseur.
hideIndicatorsbooleanundefinedCache les indicateurs de valeur min/max si true.
stepnumberundefinedPas d'incrément du curseur.
doublebooleanundefinedActive un second curseur si true.
disabledbooleanundefinedDésactive le curseur si true.

📡 Évenements

  • update:modelValue: Émis lors de la modification de la valeur du curseur. Renvoie la nouvelle valeur.

📝 Exemple

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

import DsfrRange from '../DsfrRange.vue'

const value = ref<number>(100)
const value2 = ref<number>(100)
const lowerValue = ref<number>(0)
</script>

<template>
  <div class="fr-container fr-py-4w">
    <div>
      <DsfrRange
        v-model="value"
        label="Label du curseur"
      />
    </div>
    <p>
      {{ value }}
    </p>
    <div>
      <DsfrRange
        v-model="value2"
        v-model:lower-value="lowerValue"
        label="Label du curseur"
      />
    </div>
    <p>
      {{ lowerValue }} - {{ value2 }}
    </p>
  </div>
</template>
vue
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'

import { getRandomId } from '../../utils/random-utils'

import type { DsfrRangeProps } from './DsfrRange.types'

const props = withDefaults(defineProps<DsfrRangeProps>(), {
  id: () => getRandomId('range'),
  min: 0,
  max: 100,
  modelValue: 0,
  lowerValue: undefined,
  hint: undefined,
  message: undefined,
  prefix: undefined,
  suffix: undefined,
  step: undefined,
})

const emit = defineEmits<{
  (e: 'update:modelValue', payload: string | number): void
  (e: 'update:lowerValue', payload: string | number): void
}>()

const input = ref<HTMLInputElement>()
const output = ref<HTMLSpanElement>()
const inputWidth = ref()

const double = computed(() => props.lowerValue !== undefined)

const outputStyle = computed(() => {
  if (props.lowerValue === undefined) {
    const translateXValue = (props.modelValue - props.min) / (props.max - props.min) * inputWidth.value
    return `transform: translateX(${translateXValue}px) translateX(-${props.modelValue}%);`
  }
  const translateXValue = (props.modelValue + props.lowerValue - props.min) / 2 / (props.max - props.min) * inputWidth.value
  return `transform: translateX(${translateXValue}px) translateX(-${props.lowerValue + ((props.modelValue - props.lowerValue) / 2)}%);`
})

const rangeStyle = computed(() => {
  const progressRight = (props.modelValue - props.min) / (props.max - props.min) * inputWidth.value - (double.value ? 12 : 0)
  const progressLeft = ((props.lowerValue ?? 0) - props.min) / (props.max - props.min) * inputWidth.value

  return {
    '--progress-right': `${progressRight + 24}px`,
    ...(double.value ? { '--progress-left': `${progressLeft + 12}px` } : {}),
  }
})

watch([() => props.modelValue, () => props.lowerValue], ([upper, lower]) => {
  if (lower === undefined) {
    return
  }

  if (double.value && upper < lower) {
    emit('update:lowerValue', upper)
  }
  if (double.value && lower > upper) {
    emit('update:modelValue', lower)
  }
})

const outputValue = computed(() => {
  return (props.prefix ?? '')
    .concat(double.value ? `${props.lowerValue} - ` : '')
    .concat(`${props.modelValue}`)
    .concat(props.suffix ?? '')
})

onMounted(() => {
  inputWidth.value = input.value?.offsetWidth
})
</script>

<template>
  <div
    :id="`${id}-group`"
    class="fr-range-group"
    :class="{ 'fr-range-group--error': message }"
  >
    <label
      :id="`${id}-label`"
      class="fr-label"
    >
      <slot name="label">
        {{ label }}
      </slot>
      <span class="fr-hint-text">
        <slot name="hint">
          {{ hint }}
        </slot>
      </span>
    </label>
    <div
      class="fr-range"
      data-fr-js-range="true"
      :class="{
        'fr-range--sm': small,
        'fr-range--double': double,
        'fr-range-group--disabled': disabled,
      }"
      :data-fr-prefix="prefix ?? undefined"
      :data-fr-suffix="suffix ?? undefined"
      :style="rangeStyle"
    >
      <span
        ref="output"
        class="fr-range__output"
        data-fr-js-range-output="true"
        :style="outputStyle"
      >{{ outputValue }}</span>
      <input
        v-if="double"
        :id="`${id}-2`"
        type="range"
        :min="min"
        :max="max"
        :step="step"
        :value="lowerValue"
        :disabled="disabled"
        :aria-disabled="disabled"
        :aria-labelledby="`${id}-label`"
        :aria-describedby="`${id}-messages`"
        @input="emit('update:lowerValue', +($event.target as HTMLInputElement)?.value)"
      >
      <input
        :id="id"
        ref="input"
        type="range"
        :min="min"
        :max="max"
        :step="step"
        :value="modelValue"
        :disabled="disabled"
        :aria-disabled="disabled"
        :aria-labelledby="`${id}-label`"
        :aria-describedby="`${id}-messages`"
        @input="emit('update:modelValue', +($event.target as HTMLInputElement)?.value)"
      >

      <span
        v-if="!hideIndicators"
        class="fr-range__min"
        aria-hidden="true"
        data-fr-js-range-limit="true"
      >{{ min }}</span>
      <span
        v-if="!hideIndicators"
        class="fr-range__max"
        aria-hidden="true"
        data-fr-js-range-limit="true"
      >{{ max }}</span>
    </div>
    <div
      v-if="message || $slots.messages"
      :id="`${id}-messages`"
      class="fr-messages-group"
      aria-live="polite"
      role="alert"
    >
      <slot name="messages">
        <p
          v-if="message"
          :id="`${id}-message-error`"
          class="fr-message fr-message--error"
        >
          {{ message }}
        </p>
      </slot>
    </div>
  </div>
</template>
ts
export type DsfrRangeProps = {
  id?: string
  min?: number
  max?: number
  modelValue?: number
  lowerValue?: number
  label: string
  hint?: string
  message?: string
  prefix?: string
  suffix?: string
  small?: boolean
  hideIndicators?: boolean
  step?: number
  disabled?: boolean
}

Et voilà ! Notre DsfrRange est prêt à être croqué dans vos interfaces comme une baguette bien croustillante. N'oubliez pas de l'assaisonner avec vos styles et logiques pour qu'il s'intègre parfaitement dans le festin visuel de votre application. Bon codage ! 🥖💻