Skip to content

Information contextuelle et Infobulle - DsfrTooltip

🌟 Introduction

Le DsfrTooltip est un composant Vue versatile, conçu pour fournir des infobulles contextuelles. Il supporte le déclenchement au survol ou au clic, et s'adapte automatiquement à la position de l'élément source pour une visibilité optimale. Ce composant est idéal pour ajouter des explications ou des informations supplémentaires sans encombrer l'interface utilisateur.

🛠️ Props

NomTypeDéfautObligatoireDescription
contentstringLe texte à afficher dans l'infobulle.
onHoverbooleanfalseSi true, l'infobulle s'affiche au survol.
idstringgetRandomId('tooltip')Identifiant unique pour l'infobulle. Utilisé pour l'accessibilité.

📡 Évenements

  • Aucun événement personnalisé n'est émis par ce composant.

🧩 Slots

  • default : Contenu personnalisé pour l'élément déclencheur de l'infobulle (peut être un lien ou un bouton selon onHover).

📝 Exemples

vue
<DsfrTooltip content="Voici une infobulle">
  Survolez-moi
</DsfrTooltip>

📝 Toutes les variantes 🌈 d’info-bulles

vue
<script lang="ts" setup>
import DsfrTooltip from '../DsfrTooltip.vue'
</script>

<template>
  <div
    class="flex flex-col justify-between w-full"
    style="height: 300px"
  >
    <div class="flex justify-between w-full">
      <DsfrTooltip
        on-hover
        content="Texte de l’info-bulle en haut à gauche qui peut être très très long"
      >
        Avec du texte ici
      </DsfrTooltip>
      <DsfrTooltip
        content="Texte de l’info-bulle en haut à droite qui peut être très très long"
      />
    </div>

    <div class="flex justify-center w-full">
      <DsfrTooltip
        content="Texte de l’info-bulle au centre qui peut être très très long"
      />
    </div>

    <div class="flex  justify-between w-full">
      <DsfrTooltip
        content="Texte de l’info-bulle en bas à gauche qui peut être très très long"
      />
      <DsfrTooltip
        content="Texte de l’info-bulle en bas à droite qui peut être très très long"
      />
    </div>
  </div>
</template>
vue
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'

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

import type { DsfrTooltipProps } from './DsfrTooltip.types'

export type { DsfrTooltipProps }

const props = withDefaults(defineProps<DsfrTooltipProps>(), {
  id: () => getRandomId('tooltip'),
})

const show = ref(false)

const source = ref<HTMLElement | null>(null)
const tooltip = ref<HTMLElement | null>(null)

const translateX = ref('0px')
const translateY = ref('0px')
const arrowX = ref('0px')
const top = ref(false)
const opacity = ref(0)

async function computePosition () {
  if (typeof document === 'undefined') {
    return
  }
  if (!show.value) {
    return
  }

  await new Promise(resolve => setTimeout(resolve, 100))
  const sourceTop = source.value?.getBoundingClientRect().top as number
  const sourceHeight = source.value?.offsetHeight as number
  const sourceWidth = source.value?.offsetWidth as number
  const sourceLeft = source.value?.getBoundingClientRect().left as number
  const tooltipHeight = tooltip.value?.offsetHeight as number
  const tooltipWidth = tooltip.value?.offsetWidth as number
  const isSourceAtTop = (sourceTop - tooltipHeight) < 0
  const isSourceAtBottom = !isSourceAtTop && (sourceTop + sourceHeight + tooltipHeight) >= document.documentElement.offsetHeight
  top.value = isSourceAtBottom
  const isSourceOnRightSide = (sourceLeft + sourceWidth) >= document.documentElement.offsetWidth
  const isSourceOnLeftSide = (sourceLeft + (sourceWidth / 2) - (tooltipWidth / 2)) <= 0

  translateY.value = isSourceAtBottom
    ? `${sourceTop - tooltipHeight + 8}px`
    : `${sourceTop + sourceHeight - 8}px`
  opacity.value = 1
  translateX.value = isSourceOnRightSide
    ? `${sourceLeft + sourceWidth - tooltipWidth - 4}px`
    : isSourceOnLeftSide
      ? `${sourceLeft + 4}px`
      : `${sourceLeft + (sourceWidth / 2) - (tooltipWidth / 2)}px`

  arrowX.value = isSourceOnRightSide
    ? `${(tooltipWidth / 2) - (sourceWidth / 2) + 4}px`
    : isSourceOnLeftSide
      ? `${-(tooltipWidth / 2) + (sourceWidth / 2) - 4}px`
      : '0px'
}

watch(show, computePosition, { immediate: true })

onMounted(() => {
  window.addEventListener('scroll', computePosition)
})
onUnmounted (() => {
  window.removeEventListener('scroll', computePosition)
})

const tooltipStyle = computed(() => (`transform: translate(${translateX.value}, ${translateY.value}); --arrow-x: ${arrowX.value}; opacity: ${opacity.value};'`))
const tooltipClass = computed(() => ({
  'fr-tooltip--shown': show.value,
  'fr-placement--top': top.value,
  'fr-placement--bottom': !top.value,
}))

const clickListener = (event: MouseEvent) => {
  if (!show.value) {
    return
  }
  if (event.target === source.value || source.value?.contains(event.target as Node)) {
    return
  }
  if (event.target === tooltip.value || tooltip.value?.contains(event.target as Node)) {
    return
  }
  show.value = false
}

const onEscapeKey = (event: KeyboardEvent) => {
  if (event.key === 'Escape') {
    show.value = false
  }
}

onMounted(() => {
  document.documentElement.addEventListener('click', clickListener)
  document.documentElement.addEventListener('keydown', onEscapeKey)
})

onUnmounted(() => {
  document.documentElement.removeEventListener('click', clickListener)
  document.documentElement.removeEventListener('keydown', onEscapeKey)
})

const onMouseEnter = () => {
  if (props.onHover) {
    show.value = true
  }
}

const onMouseLeave = () => {
  if (props.onHover) {
    show.value = false
  }
}

const onClick = () => {
  if (!props.onHover) {
    show.value = !show.value
  }
}
</script>

<template>
  <component
    v-bind="$attrs"
    :is="onHover ? 'a' : 'button'"
    :id="`link-${id}`"
    ref="source"
    :class="onHover ? 'fr-link' : 'fr-btn  fr-btn--tooltip'"
    :aria-describedby="id"
    :href="onHover ? '#' : undefined"
    @click.stop="onClick()"
    @mouseenter="onMouseEnter()"
    @mouseleave="onMouseLeave()"
    @focus="onMouseEnter()"
    @blur="onMouseLeave()"
  >
    <!-- @slot Slot par défaut pour le contenu de l’infobulle -->
    <slot />
  </component>
  <span
    :id="id"
    ref="tooltip"
    class="fr-tooltip  fr-placement"
    :class="tooltipClass"
    :style="tooltipStyle"
    role="tooltip"
    aria-hidden="true"
  >
    {{ content }}
  </span>
</template>

<style scoped>
.fr-tooltip {
  transition: opacity 0.3s ease-in-out;
}
</style>

Avec DsfrTooltip, révélez des informations cachées comme un magicien sort un lapin de son chapeau ! 🎩🐇✨