Pied de page - DsfrFooter
🌟 Introduction
Le DsfrFooter
est un composant Vue.js pour créer un pied de page personnalisé sur un site web. Il permet d'intégrer des logos, des liens vers des partenaires, des liens légaux, et d'autres éléments essentiels dans un pied de page.
Le pied de page est présent sur l’ensemble des pages du site. Il est situé en fin de page. Le trait bleu marque la séparation entre le corps de la page et le pied de page.
🏅 La documentation sur le pied de page sur le DSFR
La story sur le pied de page sur le storybook de VueDsfr📐 Structure
Ce composant se structure en plusieurs parties, incluant :
- Le Haut du Pied de Page: Peut être personnalisé avec des slots pour les listes de liens.
- Le Corps du Pied de Page: Contient la marque, des descriptions et des liens vers l'écosystème.
- Les Partenaires: Gérés par le composant
DsfrFooterPartners
. - Le Bas du Pied de Page: Inclut les liens obligatoires et la licence.
🛠️ Props
nom | type | défaut | obligatoire |
---|---|---|---|
a11yCompliance | string | 'non conforme' | |
a11yComplianceLink | import('vue-router').RouteLocationRaw | /a11y | |
legalLink | string | /mentions-legales | |
homeLink | import('vue-router').RouteLocationRaw | / | |
homeTitle | string | Retour à l’accueil | |
partners | DsfrFooterPartnersProps | undefined | |
personalDataLink | string | /donnees-personnelles | |
cookiesLink | string | /cookies | |
logoText | string | string[] | () => ['République', 'Française'] | |
descText | string | undefined | |
beforeMandatoryLinks | DsfrFooterLinkProps[] | () => [] | |
afterMandatoryLinks | DsfrFooterLinkProps[] | () => [] | |
mandatoryLinks | {label: string, to: import('vue-router').RouteLocationRaw | undefined}[] | Dynamique (voir script) | |
ecosystemLinks | {label: string, href: string}[] | Dynamique (voir script) | |
operatorLinkText | string | 'Revenir à l’accueil' | |
operatorTo | import('vue-router').RouteLocationRaw | undefined | / | |
operatorImgStyle | import('vue').StyleValue | undefined | |
operatorImgSrc | string | undefined | |
operatorImgAlt | string | '' | |
licenceTo | string | 'https://github.com/etalab/licence-ouverte/blob/master/LO.md' | |
licenceLinkProps | { href: string } | { to: import('vue-router').RouteLocationRaw | undefined } | undefined | |
licenceText | string | 'Sauf mention contraire, tous les textes de ce site sont sous' | |
licenceName | string | 'licence etalab-2.0' |
Des boutons après la liste de liens
Vous pouvez donc insérer un bouton après la liste de liens obligatoires (ou avant dans beforeMandatoryLink
) en ajoutant un élément avec un contenu similaire à celui-ci :
const afterMandatoryLinks = [
// (...)
{
label: 'Paramètres d’affichage',
button: true,
class: 'fr-icon-theme-fill fr-link--icon-left fr-px-2v',
to: '/settings',
onclick: () => console.log('Settings'),
},
// (...)
]
C’est le cas dans l’exemple.
📡 Événements
Aucun événement spécifique pour ce composant.
🧩 Slots
footer-link-lists
: Permet de personnaliser les listes de liens dans la partie supérieure du pied de page.description
: Pour personnaliser la description dans le corps du pied de page.
📝 Exemple
<script lang="ts" setup>
import { getCurrentInstance } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import VIcon from '../../VIcon/VIcon.vue'
import DsfrFooter from '../DsfrFooter.vue'
import { useScheme } from '@/composables'
const { setScheme, theme } = useScheme()
const changeTheme = () => {
setScheme(theme.value === 'light' ? 'dark' : 'light')
}
const beforeMandatoryLinks = [{ label: 'Before', to: '/before' }]
const afterMandatoryLinks = [
{ label: 'After', to: '/after' },
{
label: 'Paramètres d’affichage',
button: true,
class: 'fr-icon-theme-fill fr-link--icon-left fr-px-2v',
to: '/settings',
onclick: changeTheme,
},
]
const a11yCompliance = 'partiellement conforme'
const logoText = ['République', 'des châtons']
const legalLink = '/mentions-legales'
const personalDataLink = '/donnees-personnelles'
const cookiesLink = '/cookies'
const a11yComplianceLink = '/a11y-conformite'
const descText = 'Description'
const homeLink = '/'
const licenceText = undefined
const licenceTo = undefined
const licenceName = undefined
const licenceLinkProps = undefined
const ecosystemLinks = [
{
label: 'legifrance.gouv.fr',
href: 'https://legifrance.gouv.fr',
},
{
label: 'info.gouv.fr',
href: 'https://info.gouv.fr',
},
{
label: 'service-public.fr',
href: 'https://service-public.fr',
},
{
label: 'data.gouv.fr',
href: 'https://data.gouv.fr',
},
]
const app = getCurrentInstance()
app?.appContext.app.use(
createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: { template: '<div>Accueil</div>' } },
{ path: '/a11y-conformite', component: { template: '<div>Conformité RGAA</div>' } },
{ path: '/mentions-legales', component: { template: '<div>Mentions légales</div>' } },
{ path: '/donnees-personnelles', component: { template: '<div>Données personnelles</div>' } },
{ path: '/cookies', component: { template: '<div>cookies</div>' } },
{ path: '/after', component: { template: '<div>after</div>' } },
{ path: '/before', component: { template: '<div>before</div>' } },
{ path: '/_frame', component: { template: '<div>frame</div>' } },
],
}),
).component('VIcon', VIcon)
</script>
<template>
<DsfrFooter
:before-mandatory-links="beforeMandatoryLinks"
:after-mandatory-links="afterMandatoryLinks"
:a11y-compliance="a11yCompliance"
:logo-text="logoText"
:legal-link="legalLink"
:personal-data-link="personalDataLink"
:cookies-link="cookiesLink"
:a11y-compliance-link="a11yComplianceLink"
:desc-text="descText"
:home-link="homeLink"
:licence-text="licenceText"
:licence-to="licenceTo"
:licence-name="licenceName"
:licence-link-props="licenceLinkProps"
:ecosystem-links="ecosystemLinks"
/>
</template>
⚙️ Code source du composant
<script setup lang="ts">
import { computed, useSlots } from 'vue'
import type { RouteLocationRaw, RouterLink } from 'vue-router'
import DsfrFooterLink from '../DsfrFooter/DsfrFooterLink.vue'
import DsfrFooterPartners from '../DsfrFooter/DsfrFooterPartners.vue'
import DsfrLogo from '../DsfrLogo/DsfrLogo.vue'
import type { DsfrFooterProps } from './DsfrFooter.types'
export type { DsfrFooterProps }
export type {
DsfrFooterLinkListProps,
DsfrFooterLinkProps,
DsfrFooterPartner,
DsfrFooterPartnersProps,
} from './DsfrFooter.types'
const props = withDefaults(defineProps<DsfrFooterProps>(), {
a11yCompliance: 'non conforme',
a11yComplianceLink: '/a11y',
legalLink: '/mentions-legales',
homeLink: '/',
homeTitle: 'Retour à l’accueil',
// @ts-expect-error this is really undefined
partners: () => undefined,
personalDataLink: '/donnees-personnelles',
cookiesLink: '/cookies',
logoText: () => ['République', 'Française'],
descText: undefined,
beforeMandatoryLinks: () => [],
afterMandatoryLinks: () => [],
mandatoryLinks: (props) => [
{
label: `Accessibilité : ${props.a11yCompliance}`,
to: props.a11yComplianceLink,
},
{
label: 'Mentions légales',
to: props.legalLink,
'data-testid': '/mentions-legales',
},
{
label: 'Données personnelles',
to: props.personalDataLink,
},
{
label: 'Gestion des cookies',
to: props.cookiesLink,
},
],
ecosystemLinks: () => [
{
label: 'info.gouv.fr',
href: 'https://info.gouv.fr',
title: 'Informations gouvernementales, nouvelle fenêtre',
},
{
label: 'service-public.fr',
href: 'https://service-public.fr',
title: 'Informations et démarches administratives, nouvelle fenêtre',
},
{
label: 'legifrance.gouv.fr',
href: 'https://legifrance.gouv.fr',
title: 'Service public de diffusion du droit, nouvelle fenêtre',
},
{
label: 'data.gouv.fr',
href: 'https://data.gouv.fr',
title: 'Plateforme des données publiques, nouvelle fenêtre',
},
],
operatorLinkText: 'Revenir à l’accueil',
operatorTo: '/',
operatorImgStyle: undefined,
operatorImgSrc: undefined,
operatorImgAlt: '',
licenceText: 'Sauf mention explicite de propriété intellectuelle détenue par des tiers, les contenus de ce site sont proposés sous',
licenceTo: 'https://github.com/etalab/licence-ouverte/blob/master/LO.md',
// @ts-expect-error this is really undefined
licenceLinkProps: () => undefined,
licenceName: 'licence etalab-2.0',
})
const allLinks = computed(() => {
return [
...props.beforeMandatoryLinks,
...props.mandatoryLinks,
...props.afterMandatoryLinks,
]
})
const slots = useSlots()
const isWithSlotLinkLists = computed(() => {
return slots['footer-link-lists']?.().length
})
const isExternalLink = computed(() => {
const to = props.licenceTo || (props.licenceLinkProps as { to: RouteLocationRaw }).to
return to && typeof to === 'string' && to.startsWith('http')
})
const licenceLinkAttrs = computed(() => {
const { to, href, ...attrs } = props.licenceLinkProps ?? {}
return attrs
})
const routerLinkLicenceTo = computed(() => {
return isExternalLink.value ? '' : props.licenceTo
})
const aLicenceHref = computed(() => {
return isExternalLink.value ? props.licenceTo : ''
})
const externalOperatorLink = computed(() => {
return typeof props.operatorTo === 'string' && props.operatorTo.startsWith('http')
})
</script>
<template>
<footer
id="footer"
class="fr-footer"
role="contentinfo"
>
<div
v-if="isWithSlotLinkLists"
class="fr-footer__top"
>
<div class="fr-container">
<div class="fr-grid-row fr-grid-row--start fr-grid-row--gutters">
<!-- @slot Slot #footer-link-lists pour pouvoir changer les liens dans la rubrique en haut du pied de page -->
<slot name="footer-link-lists" />
</div>
</div>
</div>
<div class="fr-container">
<div class="fr-footer__body">
<div
v-if="operatorImgSrc"
class="fr-footer__brand fr-enlarge-link"
>
<DsfrLogo
:logo-text="logoText"
/>
<a
v-if="externalOperatorLink"
:href="(operatorTo as string)"
data-testid="card-link"
class="fr-footer__brand-link"
>
<img
class="fr-footer__logo"
:style="operatorImgStyle"
:src="operatorImgSrc"
:alt="operatorImgAlt"
>
</a>
<RouterLink
v-else
class="fr-footer__brand-link"
:to="homeLink"
:title="homeTitle"
>
<img
class="fr-footer__logo"
:style="operatorImgStyle"
:src="operatorImgSrc"
:alt="operatorImgAlt"
>
</RouterLink>
</div>
<div
v-else
class="fr-footer__brand fr-enlarge-link"
>
<RouterLink
:to="homeLink"
:title="homeTitle"
>
<DsfrLogo
:logo-text="logoText"
/>
</RouterLink>
</div>
<div class="fr-footer__content">
<p
class="fr-footer__content-desc"
>
<!-- @slot Slot #description pour le contenu de la description du footer. Sera dans `<p class="fr-footer__content-desc">` -->
<slot name="description">
{{ descText }}
</slot>
</p>
<ul class="fr-footer__content-list">
<li
v-for="({ href, label, title, ...attrs }, index) in ecosystemLinks"
:key="index"
class="fr-footer__content-item"
>
<a
class="fr-footer__content-link"
:href="href"
target="_blank"
rel="noopener noreferrer"
:title="title"
v-bind="attrs"
>
{{ label }}
</a>
</li>
</ul>
</div>
</div>
<DsfrFooterPartners
v-if="partners"
v-bind="partners"
/>
<div class="fr-footer__bottom">
<ul class="fr-footer__bottom-list">
<li
v-for="(link, index) in allLinks"
:key="index"
class="fr-footer__bottom-item"
>
<DsfrFooterLink
v-bind="link"
/>
</li>
</ul>
<div
v-if="licenceText"
class="fr-footer__bottom-copy"
>
<p>
{{ licenceText }}
<component
:is="isExternalLink ? 'a' : 'RouterLink'"
class="fr-link-licence no-content-after"
:to="isExternalLink ? undefined : routerLinkLicenceTo"
:href="isExternalLink ? aLicenceHref : undefined"
:target="isExternalLink ? '_blank' : undefined"
rel="noopener noreferrer"
v-bind="licenceLinkAttrs"
>
{{ licenceName }}
</component>
</p>
</div>
</div>
</div>
</footer>
</template>
<style scoped>
.fr-footer {
color: var(--text-default-grey);
}
.no-content-after {
--link-blank-content: '';
}
.ov-icon {
margin-bottom: 0;
}
</style>
import type { HTMLAttributes, StyleValue } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import type VIcon from '../VIcon/VIcon.vue'
export type DsfrFooterPartner = {
href: string
logo: string
name: string
}
export type DsfrFooterPartnersProps = {
mainPartner?: DsfrFooterPartner
subPartners?: DsfrFooterPartner[]
title?: string
}
export type DsfrFooterLinkProps = {
button?: boolean
icon?: string | InstanceType<typeof VIcon>['$props']
iconAttrs?: InstanceType<typeof VIcon>['$props'] & HTMLAttributes
iconRight?: boolean
label?: string
target?: string
onClick?: ($event: MouseEvent) => void
to?: RouteLocationRaw
href?: string
title?: string
}
export type DsfrFooterLinkListProps = {
categoryName: string
links: DsfrFooterLinkProps[]
}
export type DsfrFooterProps = {
a11yCompliance?: string
a11yComplianceLink?: RouteLocationRaw
legalLink?: string
homeLink?: RouteLocationRaw
homeTitle?: string
partners?: DsfrFooterPartnersProps
personalDataLink?: string
cookiesLink?: string
logoText?: string | string[]
descText?: string
beforeMandatoryLinks?: DsfrFooterLinkProps[]
afterMandatoryLinks?: DsfrFooterLinkProps[]
mandatoryLinks?: { label: string, to: RouteLocationRaw | undefined, title?: string }[]
ecosystemLinks?: { label: string, href: string, title: string, [key: string]: string }[]
operatorLinkText?: string
operatorTo?: RouteLocationRaw | undefined
operatorImgStyle?: StyleValue
operatorImgSrc?: string
operatorImgAlt?: string
licenceTo?: string
licenceLinkProps?: ({ href: string } | { to: RouteLocationRaw | undefined }) & Record<string, string>
licenceText?: string
licenceName?: string
}