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
Nom | Type | Défaut | Obligatoire | Description |
---|---|---|---|---|
content | string | ✅ | Le texte à afficher dans l'infobulle. | |
onHover | boolean | false | Si true , l'infobulle s'affiche au survol. | |
id | string | getRandomId('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 selononHover
).
📝 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 ! 🎩🐇✨