Skip to content

Commit

Permalink
feat: ✨ Nouveau composant Bulle d'aide
Browse files Browse the repository at this point in the history
  • Loading branch information
DaBadBunny authored and laruiss committed Dec 22, 2023
1 parent f09021f commit 64d1c11
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 4 deletions.
4 changes: 2 additions & 2 deletions .vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ export default defineConfig({
link: '/composants/DsfrSegmented.md',
},
{
text: 'DsfrSegmentedSet',
link: '/composants/DsfrSegmentedSet.md',
text: 'DsfrTooltip',
link: '/composants/DsfrTooltip.md',
},
]
},
Expand Down
14 changes: 13 additions & 1 deletion .vitepress/theme/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* Colors
* -------------------------------------------------------------------------- */

@font-face {
@font-face {
font-family: Marianne;
src: url("@gouvfr/dsfr/dist/fonts/Marianne-Light.woff2") format("woff2"), url("@gouvfr/dsfr/dist/fonts/Marianne-Light.woff") format("woff");
font-weight: 300;
Expand Down Expand Up @@ -190,3 +190,15 @@
.w-full {
width: 100%;
}

.h-full {
width: 100%;
}

.flex {
display: flex;
}

.flex-end {
justify-content: flex-end;
}
2 changes: 1 addition & 1 deletion docs/_frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
layout: false
---

<div ref="el" class="flex h-4 flex-col px-6 pb-5"></div>
<div ref="el" class="flex h-full flex-col px-6 pb-5"></div>

<script setup lang="ts">
import { useStyleTag } from '@vueuse/core'
Expand Down
48 changes: 48 additions & 0 deletions src/components/DsfrTooltip/DsfrTooltip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 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.

## 🛠️ Les 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é. |

## 📡 Événements

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

## 🧩 Les 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

::: code-group

<Story data-title="Démo" min-h="300px">
<DsfrTooltipExample />
</Story>

<<< docs-demo/DsfrTooltipExample.vue [Code de la démo]

<<< DsfrTooltip.vue
:::

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

<script setup lang="ts">
import DsfrTooltipExample from './docs-demo/DsfrTooltipExample.vue'
</script>
95 changes: 95 additions & 0 deletions src/components/DsfrTooltip/DsfrTooltip.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import DsfrTooltip from './DsfrTooltip.vue'
export default {
component: DsfrTooltip,
title: 'Composants/DsfrTooltip',
argTypes: {
id: {
control: 'text',
description: '(optionnel) Valeur de l’attribut `id` du tooltip. Par défaut, un id pseudo-aléatoire sera donné.',
},
content: {
control: 'text',
description: 'Contenu de votre bulle d’aide : il s’agit d’un texte sans mise en forme.',
},
onHover: {
control: 'boolean',
description: 'Permet de définir si l’infobulle doit s’afficher au survol de l’élément (`true`) ou au clic (`false`, défaut).',
},
},
}

export const Infobulle = (args) => ({
components: {
DsfrTooltip,
},

data () {
return {
...args,
}
},

template: `
<DsfrTooltip
:content="content"
:on-hover="onHover"
>
Un élément intriguant
</DsfrTooltip>
`,

})
Infobulle.args = {
content: 'Un élément assez intriguant',
onHover: false,
}

export const InfobulleParDefaut = (args) => ({
components: {
DsfrTooltip,
},

data () {
return {
...args,
}
},

template: `
<DsfrTooltip
:content="content"
>
Un contenu qui n’apparaîtra que si hover est à \`true\`
</DsfrTooltip>
`,

})
InfobulleParDefaut.args = {
content: 'Un élément assez intriguant',
}

export const InfobulleAuSurvol = (args) => ({
components: {
DsfrTooltip,
},

data () {
return {
...args,
}
},

template: `
Du texte
<DsfrTooltip
:content="content"
onHover
>
avec une précision à donner ici
</DsfrTooltip>
`,

})
InfobulleAuSurvol.args = {
content: 'Texte précisant pourquoi ce texte est là',
}
142 changes: 142 additions & 0 deletions src/components/DsfrTooltip/DsfrTooltip.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<script setup lang="ts">
import { computed, defineProps, ref, watch, onUnmounted, onMounted } from 'vue'
import { getRandomId } from '../../utils/random-utils'
const props = withDefaults(defineProps<{
content: string
onHover?: boolean
id?: string,
}>(), {
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)
watch(show, async (value) => {
if (typeof document === 'undefined') {
return
}
if (!value) {
return
}
opacity.value = 0
await new Promise(resolve => setTimeout(resolve, 100))
const sourceTop = source.value?.offsetTop
const sourceHeight = source.value?.offsetHeight
const sourceWidth = source.value?.offsetWidth
const sourceLeft = source.value?.offsetLeft
const tooltipHeight = tooltip.value?.offsetHeight
const tooltipWidth = tooltip.value?.offsetWidth
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'
})
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
}
onMounted(() => {
document.documentElement.addEventListener('click', clickListener)
})
onUnmounted(() => {
document.documentElement.removeEventListener('click', clickListener)
})
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
: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="onClick()"
@mouseenter="onMouseEnter()"
@mouseleave="onMouseLeave()"
>
<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>
37 changes: 37 additions & 0 deletions src/components/DsfrTooltip/docs-demo/DsfrTooltipExample.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<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>

0 comments on commit 64d1c11

Please sign in to comment.