En-tête - DsfrHeader
🌟 Introduction
Salut les développeurs ! Découvrez DsfrHeader
, le composant d'en-tête ultra-flexible pour vos applications Vue. Conçu pour mettre en valeur votre service et vos partenaires avec style, il intègre une barre de recherche, des liens rapides, et même un emplacement pour un logo personnalisé. Préparez-vous à donner à votre application une tête bien pensée !
🏅 La documentation sur l’en-tête sur le DSFR
La story sur l’en-tête sur le storybook de VueDsfrStructure
L’en-tête est composé :
- du bloc Marque - obligatoire.
- du nom de site optionnel.
- d’une ‘baseline’ (description) sous le nom de site.
- d’une partie fonctionnelle optionnelle - proposant des accès rapides et/ou une barre de recherche et/ou un sélecteur de langue - adaptée aux besoins particuliers de chaque site.
🛠️ Props
Nom | Type | Défaut | Obligatoire | Description |
---|---|---|---|---|
searchbarId | string | 'searchbar-header' | valeur de l’attribut id de l’input de la searchbar. | |
serviceTitle | string | undefined | Titre du service affiché dans l'en-tête. | |
serviceDescription | string | undefined | Description courte du service. | |
homeTo | string | '/' | Lien de la page d'accueil. | |
logoText | string | string[] | 'Gouvernement' | Texte ou texte alternatif du logo. | |
modelValue | string | '' | Valeur pour la barre de recherche. | |
operatorImgAlt | string | '' | Texte alternatif pour l'image de l'opérateur. | |
operatorImgSrc | string | '' | Source de l'image de l'opérateur. | |
operatorImgStyle | StyleValue | () => ({}) | Style CSS pour l'image de l'opérateur. | |
placeholder | string | 'Rechercher...' | Placeholder pour la barre de recherche. | |
quickLinks | DsfrHeaderMenuLinkProps[] | () => [] | Liens rapides à afficher dans l'en-tête. | |
languageSelector | DsfrLanguageSelectorProps | undefined | Liens rapides à afficher dans l'en-tête. | |
searchLabel | string | 'Recherche' | Label pour la barre de recherche. | |
quickLinksAriaLabel | string | 'Menu secondaire' | Label ARIA pour les liens rapides. | |
showSearch | boolean | false | Affiche ou non la barre de recherche. | |
showBeta | boolean | false | Affiche ou non l'indicateur BETA. | |
showSearchLabel | string | 'Recherche' | Label du bouton pour afficher la recherche. | |
menuLabel | string | 'Menu' | Label du menu. | |
menuModalLabel | string | 'Menu modal' | Label du menu en mode modal. | |
closeMenuModalLabel | string | 'Fermer' | Label du bouton de fermeture du menu en mode modal. | |
homeLabel | string | 'Accueil' | Label de l'accueil composant le titre du lien présentant le service. |
📡 Événements
Nom | Description | Charge utile |
---|---|---|
update:modelValue | Émis lors de la mise à jour de la barre de recherche. | Contenu (string ) du champ de saisie pour la recherche |
search | Émis lorsqu’une recherche est effectuée. | Contenu (string ) du champ de saisie pour la recherche |
languageSelect | Émis lorsque l’utilisateur change la langue du site. | Contenu (string ) du champ de saisie pour la recherche |
🧩 Slots
Nom | Description |
---|---|
operator | Slot pour le logo de l'opérateur. |
before-quick-links | Slot pour ajouter du contenu avant les liens rapides. |
after-quick-links | Slot pour ajouter du contenu après les liens rapides. |
mainnav | Slot pour le menu de navigation principal. |
default | Slot par défaut pour le contenu supplémentaire dans l'en-tête. |
📝 Exemples
vue
<script lang="ts" setup>
import { getCurrentInstance, ref, watch } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import VIcon from '../../VIcon/VIcon.vue'
import DsfrHeader, { type DsfrHeaderProps } from '../DsfrHeader.vue'
import type { DsfrLanguageSelectorElement } from '@/components/DsfrLanguageSelector/DsfrLanguageSelector.types'
const logoText = ['Ministère', 'de l’intérieur']
const serviceTitle = 'Nom du Site/Service'
const serviceDescription = 'baseline - précisions sur l‘organisation'
const placeholder = ''
const homeTo = '/'
const quickLinks: DsfrHeaderProps['quickLinks'] = [
{ label: 'Créer un espace', to: '/space/create', icon: 'ri-add-circle-line', iconRight: true },
{ label: 'Se connecter', to: '/login', class: 'fr-icon-user-fill' },
{ label: 'S’enregistrer', to: '/signup', icon: 'ri-account-circle-line', iconRight: true, iconAttrs: { animation: 'spin', speed: 'slow' } },
]
const languageSelector = ref({
id: 'language-selector',
languages: [
{ label: 'Français', codeIso: 'fr' },
{ label: 'English', codeIso: 'en' },
{ label: 'Deutsch', codeIso: 'de' },
],
currentLanguage: 'fr',
})
const app = getCurrentInstance()
app?.appContext.app.use(
createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: { template: '<div>Accueil</div>' } },
{ path: '/space/create', component: { template: '<div>Espace</div>' } },
{ path: '/login', component: { template: '<div>login</div>' } },
{ path: '/signup', component: { template: '<div>signup</div>' } },
],
}),
).component('VIcon', VIcon)
const search = ref('')
watch(search, (newValue) => {
console.log('search', newValue) // eslint-disable-line no-console
})
const selectLanguage = (language: DsfrLanguageSelectorElement) => {
languageSelector.value.currentLanguage = language.codeIso
}
</script>
<template>
<!-- Attention, il faut au moins vue 3.4 pour les props raccourcies -->
<!-- cf. https://blog.vuejs.org/posts/vue-3-4#v-bind-same-name-shorthand -->
<DsfrHeader
:logo-text
:service-title
:service-description
:placeholder
:home-to
:quick-links
:language-selector
@language-select="selectLanguage($event)"
/>
</template>
Exemple plus complet sur l’application de demo (dont le code source est disponible ici).
⚙️ Code source du composant
vue
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, provide, ref, toRef, useSlots } from 'vue'
import DsfrLanguageSelector, { type DsfrLanguageSelectorElement } from '../DsfrLanguageSelector/DsfrLanguageSelector.vue'
import DsfrLogo from '../DsfrLogo/DsfrLogo.vue'
import DsfrSearchBar from '../DsfrSearchBar/DsfrSearchBar.vue'
import DsfrHeaderMenuLinks from './DsfrHeaderMenuLinks.vue'
import { registerNavigationLinkKey } from './injection-key'
import type { DsfrHeaderProps } from './DsfrHeader.types'
export type { DsfrHeaderProps }
const props = withDefaults(defineProps<DsfrHeaderProps>(), {
searchbarId: 'searchbar-header',
languageSelector: undefined,
serviceTitle: undefined,
serviceDescription: undefined,
homeTo: '/',
logoText: () => 'Gouvernement',
modelValue: '',
operatorImgAlt: '',
operatorImgSrc: '',
operatorImgStyle: () => ({}),
placeholder: 'Rechercher...',
quickLinks: () => [],
searchLabel: 'Recherche',
quickLinksAriaLabel: 'Menu secondaire',
showSearchLabel: 'Recherche',
menuLabel: 'Menu',
menuModalLabel: 'Menu modal',
closeMenuModalLabel: 'Fermer',
homeLabel: 'Accueil',
})
const emit = defineEmits<{
(e: 'update:modelValue', payload: string): void
(e: 'search', payload: string): void
(e: 'languageSelect', payload: DsfrLanguageSelectorElement): void
}>()
const languageSelector = toRef(props, 'languageSelector')
const menuOpened = ref(false)
const searchModalOpened = ref(false)
const modalOpened = ref(false)
const hideModal = () => {
modalOpened.value = false
menuOpened.value = false
searchModalOpened.value = false
document.getElementById('button-menu')?.focus()
}
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
hideModal()
}
}
onMounted(() => {
document.addEventListener('keydown', onKeyDown)
})
onUnmounted(() => {
document.removeEventListener('keydown', onKeyDown)
})
const showMenu = () => {
modalOpened.value = true
menuOpened.value = true
searchModalOpened.value = false
document.getElementById('close-button')?.focus()
}
const showSearchModal = () => {
modalOpened.value = true
menuOpened.value = false
searchModalOpened.value = true
}
const onQuickLinkClick = hideModal
const title = computed(() => [props.homeLabel, props.serviceTitle].filter(x => x).join(' - '))
const slots = useSlots()
const isWithSlotOperator = computed(() => Boolean(slots.operator?.().length) || !!props.operatorImgSrc)
const isWithSlotNav = computed(() => Boolean(slots.mainnav))
provide(registerNavigationLinkKey, () => {
return hideModal
})
</script>
<template>
<header
role="banner"
class="fr-header"
>
<div class="fr-header__body">
<div class="fr-container width-inherit">
<div class="fr-header__body-row">
<div class="fr-header__brand fr-enlarge-link">
<div class="fr-header__brand-top">
<div class="fr-header__logo">
<RouterLink
:to="homeTo"
:title
>
<DsfrLogo
:logo-text="logoText"
data-testid="header-logo"
/>
</RouterLink>
</div>
<div
v-if="isWithSlotOperator"
class="fr-header__operator"
>
<!-- @slot Slot nommé operator pour le logo opérateur. Sera dans `<div class="fr-header__operator">` -->
<slot name="operator">
<img
v-if="operatorImgSrc"
class="fr-responsive-img"
:src="operatorImgSrc"
:alt="operatorImgAlt"
:style="operatorImgStyle"
>
</slot>
</div>
<div
v-if="showSearch || isWithSlotNav || quickLinks?.length"
class="fr-header__navbar"
>
<button
v-if="showSearch"
class="fr-btn fr-btn--search"
aria-controls="header-search"
:aria-label="showSearchLabel"
:title="showSearchLabel"
:data-fr-opened="searchModalOpened"
@click.prevent.stop="showSearchModal()"
/>
<button
v-if="isWithSlotNav || quickLinks?.length"
id="button-menu"
class="fr-btn--menu fr-btn"
:data-fr-opened="showMenu"
aria-controls="header-navigation"
aria-haspopup="menu"
:aria-label="menuLabel"
:title="menuLabel"
data-testid="open-menu-btn"
@click.prevent.stop="showMenu()"
/>
</div>
</div>
<div
v-if="serviceTitle"
class="fr-header__service"
>
<RouterLink
:to="homeTo"
:title
v-bind="$attrs"
>
<p class="fr-header__service-title">
{{ serviceTitle }}
<span
v-if="showBeta"
class="fr-badge fr-badge--sm fr-badge--green-emeraude"
>
BETA
</span>
</p>
</RouterLink>
<p
v-if="serviceDescription"
class="fr-header__service-tagline"
>
{{ serviceDescription }}
</p>
</div>
<div
v-if="!serviceTitle && showBeta"
class="fr-header__service"
>
<p class="fr-header__service-title">
<span class="fr-badge fr-badge--sm fr-badge--green-emeraude">BETA</span>
</p>
</div>
</div>
<div class="fr-header__tools">
<div
v-if="quickLinks?.length || languageSelector"
class="fr-header__tools-links"
>
<DsfrHeaderMenuLinks
v-if="!menuOpened"
:links="quickLinks"
:nav-aria-label="quickLinksAriaLabel"
/>
<template v-if="languageSelector">
<DsfrLanguageSelector
v-bind="languageSelector"
@select="emit('languageSelect', $event)"
/>
</template>
</div>
<div
v-if="showSearch"
class="fr-header__search fr-modal"
>
<DsfrSearchBar
:searchbar-id="searchbarId"
:label="searchLabel"
:model-value="modelValue"
:placeholder="placeholder"
style="justify-content: flex-end"
@update:model-value="emit('update:modelValue', $event)"
@search="emit('search', $event)"
/>
</div>
</div>
</div>
<div
v-if="showSearch || isWithSlotNav || (quickLinks && quickLinks.length) || languageSelector"
id="header-navigation"
class="fr-header__menu fr-modal"
:class="{ 'fr-modal--opened': modalOpened }"
:aria-label="menuModalLabel"
role="dialog"
aria-modal="true"
>
<div class="fr-container">
<button
id="close-button"
class="fr-btn fr-btn--close"
aria-controls="header-navigation"
data-testid="close-modal-btn"
@click.prevent.stop="hideModal()"
>
{{ closeMenuModalLabel }}
</button>
<div class="fr-header__menu-links">
<template v-if="languageSelector">
<DsfrLanguageSelector
v-bind="languageSelector"
@select="languageSelector.currentLanguage = $event.codeIso"
/>
</template>
<DsfrHeaderMenuLinks
v-if="menuOpened"
role="navigation"
:links="quickLinks"
:nav-aria-label="quickLinksAriaLabel"
@link-click="onQuickLinkClick"
/>
</div>
<template v-if="modalOpened">
<slot
name="mainnav"
:hidemodal="hideModal"
/>
</template>
<div
v-if="searchModalOpened"
class="flex justify-center items-center"
>
<DsfrSearchBar
:searchbar-id="searchbarId"
:model-value="modelValue"
:placeholder="placeholder"
@update:model-value="emit('update:modelValue', $event)"
@search="emit('search', $event)"
/>
</div>
</div>
</div>
<!-- @slot Slot par défaut pour le contenu du fieldset (sera dans `<div class="fr-header__body-row">`) -->
<slot />
</div>
</div>
<div class="fr-header__menu fr-modal">
<div
v-if="isWithSlotNav && !modalOpened"
class="fr-container"
>
<!-- @slot Slot nommé mainnav pour le menu de navigation principal -->
<slot
name="mainnav"
:hidemodal="hideModal"
/>
</div>
</div>
</header>
</template>
ts
import type { HTMLAttributes, StyleValue } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import type { DsfrLanguageSelectorProps } from '../DsfrLanguageSelector/DsfrLanguageSelector.types'
import type VIcon from '../VIcon/VIcon.vue'
export type DsfrHeaderMenuLinkProps = {
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
/**
* @deprecated Use the prop `to` instead
*/
href?: string
/**
* @deprecated Use the prop `to` instead
*/
path?: string
}
export type DsfrHeaderProps = {
searchbarId?: string
serviceTitle?: string
serviceDescription?: string
homeTo?: string
logoText?: string | string[]
modelValue?: string
operatorImgAlt?: string
operatorImgSrc?: string
operatorImgStyle?: StyleValue
placeholder?: string
quickLinks?: (DsfrHeaderMenuLinkProps & HTMLAttributes)[]
languageSelector?: DsfrLanguageSelectorProps
searchLabel?: string
quickLinksAriaLabel?: string
showSearch?: boolean
showSearchLabel?: string
showBeta?: boolean
menuLabel?: string
menuModalLabel?: string
closeMenuModalLabel?: string
homeLabel?: string
}
ts
import type { InjectionKey } from 'vue'
type RegisterNavigationLink = () => () => void
export const registerNavigationLinkKey: InjectionKey<RegisterNavigationLink> = Symbol('header')