Menu
Le composant menu présente un menu pouvant aller de 1 à 3 niveaux.
Il prend en paramètre les éléments suivants :
menu: L'objet Menu à affichersize: La taille du menuvariant: La variation du menu balances/smartcolumns: Le nombre de colonnes pour le menu
Classes du composant
Modificateurs de tailles
.menu--lg: Large.menu--xl: Extra-large
Classes du sous-composant menu-grid
.menu-grid--smart: Affichage vertical des items- Nécessite l'attribution obligatoire de la variable
--items(nombre d'items enfants directs présents à l'intérieur du menu) dans l'attributstylede l'élément.
- Nécessite l'attribution obligatoire de la variable
.menu-grid--balanced: Affichage vertical des items tentant d'équilibrer les colonnes
Documentation du fonctionnement
Par défaut, les items sont disposés en grille, horizontalement (les uns après les autres, de gauche à droite, puis ligne après ligne).
Le fonctionnement de la variante balanced utilise le module CSS columns.
Pour la variante smart, on utilise un algorithme de répartition défini par les critères suivants :
-
Considérant C, le nombre de colonnes MAXIMUM souhaité.
-
Considérant N, le nombre d'items à l'intérieur du menu. N prend les valeurs successives de 1, 2, 3, 4, etc.
-
Pour N ≤ C, on remplit la 1è colonne verticalement
-
Pour N > C, on continue de remplir la 2è colonne, puis la 3è, etc.
-
Pour N > C * C, on rajoute une ligne et on répartit les items sur les C colonnes.
-
Pour N > C * (C + 1), on rajoute encore une ligne et on répartit à nouveau les items
-
etc.
=> On peut en déduire le nombre de lignes comme étant la formule suivante :
LIGNES = MAX(C, CEIL(N / C))
Exemple pour C = 3 et N = 2
Item 1
Item 2
Exemple pour C = 3 et N = 5
Item 1 - Item 4
Item 2 - Item 5
Item 3
Exemple pour C = 3 et N = 7
Item 1 - Item 4 - Item 7
Item 2 - Item 5
Item 3 - Item 6
Exemple pour C = 3 et N = 10
Item 1 - Item 5 - Item 9
Item 2 - Item 6 - Item 10
Item 3 - Item 7
Item 4 - Item 8
Large classic
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="kuik" uri="kuik" %>
<%--@elvariable id="menuViewModel" type="fr.kosmos.web.kore.attributes.interfaces.Menu"--%>
<kuik:heading size="lg" skin="a" title="Large classic" level="3"/>
<p>
<kuik:menu menu="${menuViewModel}" closeIconTitle="Fermer"/>
</p>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
| Name | Type | Required | Description |
|---|---|---|---|
| menu | fr.kosmos.web.kore.attributes.interfaces.Menu | true | Information du menu |
| size | java.lang.String | false | Taille (défaut : md) |
| variant | java.lang.String | false | Variation |
| columns | java.lang.Integer | false | Nombre de colonnes |
| closeIconTitle | java.lang.String | false | Le title du bouton de fermeture |
.menu-grid {
@include list-reset;
--columns: 1;
--column-gap: var(--space-6);
--row-gap: var(--space-4);
// Règle responsive : sur les écrans moyens, on divise par 2 (arrondi à l'inférieur) le nombre de colonnes affichées, minimum affiché : 1
--_columns-small: max(1, round(down, calc(var(--columns) / 2)));
--_columns-large: var(--columns);
--_columns: var(--_columns-small);
column-gap: var(--column-gap);
display: grid;
grid-template-columns: repeat(var(--_columns), 1fr);
&:has(.menu-grid) {
row-gap: var(--row-gap);
[data-level="2"] {
border-bottom-color: var(--neutral-40);
font-weight: var(--font-weight-700);
height: auto;
}
}
@media (min-width: $screen-md) {
--_columns: var(--_columns-large);
}
}
.menu-grid--balanced {
column-count: var(--_columns);
column-fill: balance;
display: block;
}
.menu-grid--smart {
--items: 0;
--_rows: max(var(--_columns), round(up, var(--items) / var(--_columns)));
grid-auto-flow: column;
grid-template-columns: repeat(var(--columns), 1fr);
grid-template-rows: repeat(var(--_rows), auto);
}
.menu-grid__link {
border-bottom: var(--line-thin) var(--neutral-30);
break-inside: avoid;
display: block;
height: 100%;
padding: var(--space-3) var(--space-2);
text-decoration: none;
}
$prefix-menu-list-link: 'menu-list-link';
$prefix-menu-list-link-back: 'menu-list-link-back';
.menu-list {
--padding-h: var(--space-4);
--padding-v: var(--space-4);
@include list-reset;
left: 0;
padding: var(--padding-v) var(--padding-h);
position: absolute;
top: 0;
transition: var(--duration-immediate) visibility var(--duration-normal) linear;
visibility: hidden;
width: 100%;
.menu-list {
left: 100%;
}
.panel__body > & {
position: relative;
}
&.menu-list--current { // assure l'affichage du niveau 1 en no-script
transition-delay: var(--duration-immediate);
visibility: visible;
}
}
@media (scripting: none) {
.menu-list {
scroll-snap-align: start;
&:target,
&:has(:target),
.panel__body > &:not(:has(.menu-list--curent)) {
transition-delay: var(--duration-immediate);
visibility: visible;
}
}
}
.menu-list__item {
border-bottom: var(--line-thin) var(--neutral-30);
}
.menu-list__item--top {
border-bottom: none;
margin-block-end: var(--space-6);
}
.menu-list__item--bottom {
border-bottom: none;
margin-block-start: var(--space-6);
}
.menu-list__link {
@include custom-typo($prefix-menu-list-link, true);
@include custom-color($prefix-menu-list-link, true);
@include custom-border-radius($prefix-menu-list-link, true);
@include custom-space($prefix-menu-list-link, true);
align-items: center;
border: none;
cursor: pointer;
display: flex;
justify-content: space-between;
line-height: var(--line-height-condensed);
text-align: start;
width: 100%;
&[href^='#']:not([href='#'])::after {
aspect-ratio: 1;
background-color: var(--color1-40);
content: '';
height: var(--size-4);
mask-image: url('data:image/svg+xml,');
mask-position: center;
mask-size: cover;
}
.icon {
@include custom-icon;
}
&:hover,
&:focus {
@include custom-color($prefix-menu-list-link);
outline: none;
.icon {
@include custom-icon-color;
}
}
}
.menu-list--sm .menu-list__link,
.menu-list--md .menu-list__link,
.menu-list--lg .menu-list__link,
.menu-list--xl .menu-list__link {
@include custom-typo($prefix-menu-list-link);
@include custom-space($prefix-menu-list-link);
@include custom-border-radius($prefix-menu-list-link);
.icon {
@include custom-icon-size;
}
}
.menu-list__link--current {
@include custom-color($prefix-menu-list-link);
&[href^='#']:not([href='#'])::after {
background-color: var(--neutral-80);
}
.icon {
@include custom-icon-color;
}
}
.menu-list__link--back {
@include custom-typo($prefix-menu-list-link-back, true);
@include custom-color($prefix-menu-list-link-back, true);
@include custom-border-radius($prefix-menu-list-link-back, true);
@include custom-space($prefix-menu-list-link-back, true);
.icon {
@include custom-icon;
}
&:hover,
&:focus {
@include custom-color($prefix-menu-list-link-back);
.icon {
@include custom-icon-color;
}
}
&::after {
display: none;
}
}
.menu-list--sm .menu-list__link--back,
.menu-list--md .menu-list__link--back,
.menu-list--lg .menu-list__link--back,
.menu-list--xl .menu-list__link--back {
@include custom-typo($prefix-menu-list-link-back);
@include custom-space($prefix-menu-list-link-back);
@include custom-border-radius($prefix-menu-list-link-back);
.icon {
@include custom-icon-size;
}
}
$prefix: 'menu';
$prefix-link: 'menu-link';
$prefix-popup: 'menu-popup';
$prefix-popup-header: 'menu-popup-header';
$prefix-popup-title: 'menu-popup-title';
.menu {
@include custom-color-fill($prefix, true);
@include custom-color-stroke($prefix, true);
@include custom-border($prefix, true);
@include custom-space($prefix, true);
anchor-name: var(--anchor-name, true);
}
.menu__list {
align-items: stretch;
display: inline flex;
flex-wrap: wrap;
gap: var(--space-4);
height: 100%;
list-style: none;
margin: var(--space-0);
padding: var(--space-0);
}
.menu__link {
@include custom-typo($prefix-link, true);
@include custom-color($prefix-link, true);
@include custom-border($prefix-link, true);
@include custom-space($prefix-link, true);
align-items: center;
cursor: pointer;
display: flex;
height: 100%;
&[popovertarget]::after {
aspect-ratio: 1;
background-color: currentcolor;
content: '';
display: inline-block;
flex-shrink: 0;
height: 1lh;
mask-image: url('data:image/svg+xml,');
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}
.icon {
@include custom-icon-color;
}
}
.menu__link:hover {
@include custom-color($prefix-link);
.icon {
@include custom-icon-color;
}
}
.menu__link:focus,
.menu__item:has(> :popover-open) > .menu__link {
@include custom-color($prefix-link);
box-shadow: 0 0 0 var(--line-xs) var(--links-focus-radius-color), 0 0 0 var(--line-2xs) var(--white);
outline: none;
.icon {
@include custom-icon-color;
}
}
.menu__link--current {
@include custom-color($prefix-link);
.icon {
@include custom-icon-color;
}
}
.menu__popup {
@include custom-color-fill($prefix-popup, true);
border: none;
display: none;
left: 0;
margin: var(--space-0);
opacity: 0;
padding: var(--space-8);
position: absolute;
position-anchor: var(--anchor-name, --root-anchor);
top: anchor(bottom);
transition: var(--duration-normal) opacity var(--transition-function),
var(--duration-normal) translate var(--transition-function),
var(--duration-normal) display allow-discrete;
translate: 0 calc(-1 * var(--space-4));
width: 100%;
&:popover-open {
display: block;
opacity: 1;
translate: 0 0;
}
}
@starting-style {
.menu__popup:popover-open {
opacity: 0;
translate: 0 calc(-1 * var(--space-4));
}
}
.menu__popup-header {
@include custom-space($prefix-popup-header, true);
@include custom-border-radius($prefix-popup-header, true);
.icon {
@include custom-icon-size;
}
}
.menu-list__item {
.icon {
@include custom-icon-size;
}
}
.menu-list__item-title,
.menu__popup-title {
@include custom-typo($prefix-popup-title, true);
@include custom-color-content($prefix-popup-title, true);
}
.menu--sm,
.menu--md,
.menu--lg,
.menu--xl {
@include custom-border($prefix);
@include custom-space($prefix);
.menu__link {
@include custom-typo($prefix-link);
@include custom-border($prefix-link);
@include custom-space($prefix-link);
}
.menu-list__item,
.menu__popup-header {
@include custom-space($prefix-popup-header);
@include custom-border-radius($prefix-popup-header);
.icon {
@include custom-icon-size;
}
}
.menu-list__item-title,
.menu__popup-title {
@include custom-typo($prefix-popup-title);
}
}
// CONSTANTS
const EMPTY_STR = '';
const HASH_CHAR = '#';
const HREF_ATTR = 'href';
const CLICK_EVENT = 'click';
const BEFORE_TOGGLE_EVENT = 'beforetoggle';
const TOGGLE_EVENT = 'toggle';
const SCROLLEND_EVENT = 'scrollend';
const TAB_INDEX_ATTR = 'tabindex';
const DISABLED_TAB_INDEX_VALUE = '-1';
const HASH_START_REGEX = /^.*#/;
const TARGET_EVENT_STATUS_OPEN = 'open';
const DATA_ATTACH_TO_PANEL_ATTR = 'data-attach-to-panel';
const SCOPE_SELECTOR = ':scope';
const JS_MENU_WRAPPER_SELECTOR = '.js-menu-list';
const SCOPE_JS_MENU_WRAPPER_SELECTOR = `${SCOPE_SELECTOR} > .panel__body .js-menu-list`;
const SCOPE_JS_CURRENT_MENU_SELECTOR = '[aria-current="page"]';
const MENU_LIST_SELECTOR = '.menu-list';
const MENU_LIST_CURRENT_CLASSNAME = 'menu-list--current';
const MENU_LIST_ITEM_SELECTOR = '.menu-list__item';
const MENU_LIST_LINK_SELECTOR = '.menu-list__link';
const MENU_LIST_LINK_BACK_CLASSNAME = 'menu-list__link--back';
const PANEL_BODY = '.panel__body';
const SCOPE_PANEL_BODY = `${SCOPE_SELECTOR} > .panel__body`;
const PANEL_BODY_SMOOTH_SCROLL_CLASSNAME = 'panel__body--smooth-scroll';
const SUBMENU_ITEMS_SELECTOR = `[${HREF_ATTR}^="${HASH_CHAR}"]:not([${HREF_ATTR}="${HASH_CHAR}"])`;
const SCOPE_LIST_ITEM_SELECTOR = `${SCOPE_SELECTOR} > ${MENU_LIST_ITEM_SELECTOR}`;
const SCOPE_MENU_SELECTOR = `${SCOPE_SELECTOR} > ${MENU_LIST_SELECTOR}`;
const ONSCROLLEND_EVENTKEY = `on${SCROLLEND_EVENT}`;
const TARGET_ATTR_SELECTOR = id => `[${HREF_ATTR}="${HASH_CHAR}${id}"]`;
const PX = value => `${value}px`;
// VARIABLES
// Utilisée pour stocker l'URL de la page, afin de pouvoir la restaurer quand le menu est fermé
let _previousUrl
/**
* Renvoie tous les éléments "focusable" à l'intérieur de l'élément.
* NOTE : cette fonction ne prend VOLONTAIREMENT pas en compte la présence d'un éventuel attribut [tabindex="-1"] puisqu'elle est utilisée
* dans l'objectif de contrôler la navigation au clavier pour l'accessibilité du menu.
* @param {HTMLElement} element Élément HTML parent
* @returns {Array} Tableau d'éléments HTML
*/
function findFocusableChildrenOf(element) {
return Array.from(element.querySelectorAll(`
input:not([disabled]),
select:not([disabled]),
textarea:not([disabled]),
button:not([disabled]),
a[href]:not([disabled]),
[tabindex]:not([disabled])
`) ?? [ ]);
}
/**
* Désactive la navigation au clavier sur tous les éléments cliquables du menu qui ne sont pas dans le menu affiché
* @param {HTMLElement} menu Menu de référence
*/
function disableAllItemsExceptFor(menu) {
const focusables = findFocusableChildrenOf(menu.closest(JS_MENU_WRAPPER_SELECTOR));
for (const focusable of focusables) {
const parentMenu = focusable.closest(MENU_LIST_SELECTOR);
if (parentMenu === menu) {
focusable.removeAttribute(TAB_INDEX_ATTR);
} else {
focusable.setAttribute(TAB_INDEX_ATTR, DISABLED_TAB_INDEX_VALUE);
}
}
}
/**
* Assigne le focus à un élément du menu.
* Si le paramètre "id" n'est pas défini, alors ce sera le premier élément cliquable du menu qui recevra le focus.
* S'il est défini, "id" N'indique PAS l'identifiant de l'item qui doit être sélectionné MAIS l'identifiant de sa cible. Cela permet de
* retrouver un item dans la liste et de le cibler quand on sort de son sous-menu.
* @param {HTMLElement} menu Menu à utiliser
* @param {String} id Identifiant qui sera utilisé pour le ciblage
*/
function setFocusToCurrentMenu(menu, id = undefined) {
let focusable = null;
// Liste tous les sous-items directs
const menuItems = menu.querySelectorAll(SCOPE_LIST_ITEM_SELECTOR);
for (const menuItem of menuItems) {
// Récupère le sous-menu s'il est présent
const subMenuElement = menuItem.querySelector(SCOPE_MENU_SELECTOR);
// Récupère la liste des éléments de l'item qui peuvent avoir le focus, hors sous-menu
const focusables = findFocusableChildrenOf(menuItem).filter(e => !subMenuElement || !(subMenuElement.contains(e)));
// Si l'id est défini, on récupère l'élément focusable qui lui correspond, sinon, on prend le premier qu'on trouve
[ focusable ] = id ? [ focusables.find(i => i.matches(TARGET_ATTR_SELECTOR(id))) ] : focusables;
if (focusable) {
break;
}
}
// On applique le focus à l'élément focusable récupéré s'il n'est pas déjà ailleurs
if (focusable && document.activeElement === document.body) {
focusable.focus({ preventScroll: true });
}
}
/**
* Renvoie le menu ciblé par un item.
* @param {HTMLElement} item Item du menu à utiliser
* @returns {HTMLElement} Menu ciblé
*/
function retrieveTargetedMenu(item) {
let targetedMenu = null;
const idSelector = item.getAttribute(HREF_ATTR)?.replace(HASH_START_REGEX, EMPTY_STR);
if (idSelector) {
targetedMenu = document.getElementById(idSelector);
}
return targetedMenu;
}
/**
* Fonction qui affiche le bon menu et donne le focus à l'item souhaité
* @param {HTMLElement} menu Menu ciblé
* @param {String} id Identifiant du menu ciblé
* @param {boolean} immediate Indique s'il faut afficher le menu directement ou attendre la fin de l'animation
* @returns {Promise} Possibilité d'attendre la fin du scroll horizontal pour faire autre chose
*/
function setCurrentMenu(menu, id = undefined, immediate = false) {
return new Promise((resolve) => {
// Remet tous les tabindex du menu à -1 sauf pour le menu courant
disableAllItemsExceptFor(menu);
const panelBody = menu.closest(PANEL_BODY);
// Désigne le menu courant par une classe
const menus = Array.from(panelBody.querySelectorAll(MENU_LIST_SELECTOR));
for (const m of menus) {
m.classList.toggle(MENU_LIST_CURRENT_CLASSNAME, m === menu);
}
const shouldListenToScrollEvent = ONSCROLLEND_EVENTKEY in panelBody && !immediate;
// Donne le focus à l'élément souhaité dans le menu
// Ne le fait que quand on est sûr que l'animation de scroll est terminée (ou si la demande est immédiate)
if (shouldListenToScrollEvent) {
panelBody.addEventListener(SCROLLEND_EVENT, () => {
setFocusToCurrentMenu(menu, id);
resolve();
}, { once: true });
}
// Assure que le menu courant est bien en place
scrollToMenu(menu);
if (!shouldListenToScrollEvent) {
setFocusToCurrentMenu(menu, id);
resolve();
}
});
}
/**
* Fonction exécutée lorsqu'un élément est cliqué (fonctionne aussi quand la touche Entrée est appuyée si l'élément a le focus).
* @param {MouseEvent} e Événement JS
*/
async function itemClickEventHandler(e) {
let id = '';
const target = e.target.closest(MENU_LIST_LINK_SELECTOR);
// Traitement spécifique si on clique sur l'item de Retour qui permet de revenir au niveau parent
const isBackItem = target.classList.contains(MENU_LIST_LINK_BACK_CLASSNAME);
if (isBackItem) {
id = target.closest(MENU_LIST_SELECTOR)?.id;
}
// Récupère le menu ciblé
const menu = retrieveTargetedMenu(target);
if (menu) {
// Empêche le changement de hash dans l'URL de la page
e.preventDefault();
e.stopPropagation();
// Affiche le bon menu et donne le focus à l'item souhaité
await setCurrentMenu(menu, id);
}
}
/**
* Ajoute le contrôle du clic sur les items du menu et ses sous-items.
* @param {HTMLElement} menu Menu à utiliser
*/
function bindClicksOnItems(menu) {
const submenuItems = menu.querySelectorAll(SUBMENU_ITEMS_SELECTOR);
for (const submenuItem of submenuItems) {
submenuItem.addEventListener(CLICK_EVENT, itemClickEventHandler);
}
}
/**
* Retire les contrôles ajoutés aux items du menu.
* @param {HTMLElement} menu Menu à utiliser
*/
function unbindClicksOnItems(menu) {
const submenuItems = menu.querySelectorAll(SUBMENU_ITEMS_SELECTOR);
for (const submenuItem of submenuItems) {
submenuItem.removeEventListener(CLICK_EVENT, itemClickEventHandler);
}
}
/**
* Événement exécuté avant que le panneau ne s'ouvre ou se ferme
* @param {ToggleEvent} e Événement JS
*/
function beforePanelToggleHandler(e) {
const panel = e.target;
const menu = panel.querySelector(SCOPE_JS_MENU_WRAPPER_SELECTOR);
const panelBody = panel.querySelector(SCOPE_PANEL_BODY);
if (!panelBody) return;
if (e.newState === TARGET_EVENT_STATUS_OPEN) {
_previousUrl = document.location.href
bindClicksOnItems(menu);
} else {
if (_previousUrl) {
history.pushState({ }, EMPTY_STR, _previousUrl);
_previousUrl = undefined;
}
unbindClicksOnItems(menu);
panel.querySelectorAll('.menu-list--current').forEach((currentLink) => {
currentLink.classList.remove('menu-list--current');
});
}
}
/**
* Événement exécuté après l'ouverture ou la fermeture du panneau
* @param {ToggleEvent} toggleEvent Événement JS
*/
async function afterPanelToggleHandler(toggleEvent) {
const panel = toggleEvent.target;
const menu = panel.querySelector(SCOPE_JS_CURRENT_MENU_SELECTOR)?.closest('.menu-list') || panel.querySelector(SCOPE_JS_MENU_WRAPPER_SELECTOR);
if (toggleEvent.newState === TARGET_EVENT_STATUS_OPEN) {
const durationNormal = Number.parseFloat(window.getComputedStyle(document.querySelector('body')).getPropertyValue('--duration-normal')) * 1000;
if (durationNormal === 0.0) {
await afterPanelToggleSetCurrentMenu(panel, menu);
} else {
handlePanelToggle(panel, menu, durationNormal);
}
}
}
/**
* Gère le basculement d'un panneau avec une transition et met à jour le menu une fois la transition terminée.
* Dans le cas où le transitionend est mal géré par le navigateur, un timeout 10% supérieur à la durée de la transition est mis en place.
*
* @param {HTMLElement} panel Le panneau à basculer.
* @param {HTMLElement} menu Le menu à mettre à jour après le basculement.
* @param {number} durationNormal Durée de la transition en millisecondes.
*/
function handlePanelToggle(panel, menu, durationNormal) {
const controller = new AbortController();
const panelToggleTimeout = setTimeout(async () => {
controller.abort();
await afterPanelToggleSetCurrentMenu(panel, menu);
}, durationNormal * 1.1)
panel.addEventListener('transitionend', async () => await afterPanelToggleSetCurrentMenu(panel, menu, panelToggleTimeout), {once: true, signal: controller.signal});
}
/**
* Met à jour le menu actif après le basculement du panneau.
*
* @param {HTMLElement} panel - Le panneau dont le comportement de défilement est modifié.
* @param {HTMLElement} menu - Le menu à définir comme actif.
*/
async function afterPanelToggleSetCurrentMenu(panel, menu, panelToggleTimeout = null) {
if (panelToggleTimeout) {
clearTimeout(panelToggleTimeout);
}
setScrollBehavior(panel, false);
await setCurrentMenu(menu, undefined, true);
setScrollBehavior(panel, true);
}
/**
* Définit le comportement de défilement pour l'élément de panneau donné.
*
* @param {HTMLElement} panel - L'élément du panneau à configurer.
* @param {boolean} activate - Indique si le défilement fluide doit être activé (true) ou désactivé (false).
*/
function setScrollBehavior(panel, activate) {
const panelBody = panel.querySelector(SCOPE_PANEL_BODY);
if (panelBody) {
panelBody.classList.toggle(PANEL_BODY_SMOOTH_SCROLL_CLASSNAME, activate);
}
}
/**
* Assure que le menu affiché est bien en place
* @param {HTMLElement} menu Menu à positionner
*/
function scrollToMenu(menu) {
const panelBody = menu.closest(PANEL_BODY);
// On change la hauteur du menu pour ne garder que la hauteur nécessaire.
panelBody.style.height = PX(menu.clientHeight);
// On déplace le scroll
const { x } = menu.getBoundingClientRect();
panelBody.scrollTo(x + panelBody.scrollLeft, 0);
}
/**
* Ajoute les contrôles d'événement nécessaires si le menu se trouve dans un composant "Panneau".
* @param {HTMLElement} menu Menu concerné
*/
function bindPanelEvents(menu) {
if (!(menu.hasAttribute(DATA_ATTACH_TO_PANEL_ATTR))) return;
const id = menu.getAttribute(DATA_ATTACH_TO_PANEL_ATTR);
const panel = document.getElementById(id);
panel?.addEventListener(BEFORE_TOGGLE_EVENT, beforePanelToggleHandler);
panel?.addEventListener(TOGGLE_EVENT, afterPanelToggleHandler);
}
/**
* Retire les contrôles d'événements nécessaires quand le menu se trouve dans un élément "Panneau".
* @param {HTMLElement} menu Menu concerné
*/
function unbindPanelEvents(menu) {
if (!(menu.hasAttribute(DATA_ATTACH_TO_PANEL_ATTR))) return;
const id = menu.getAttribute(DATA_ATTACH_TO_PANEL_ATTR);
const panel = document.getElementById(id);
panel?.removeEventListener(BEFORE_TOGGLE_EVENT, beforePanelToggleHandler);
panel?.removeEventListener(TOGGLE_EVENT, afterPanelToggleHandler);
}
/**
* Initialise un menu mobile.
* @param {HTMLElement} menu Menu à initialiser
*/
function initMenuMobile(menu) {
bindPanelEvents(menu);
}
/**
* Détruit la gestion associée à un menu mobile.
* @param {HTMLElement} menu Menu concerné
*/
function destroyMenuMobile(menu) {
unbindPanelEvents(menu);
}
/**
* Initialise tous les menus mobile trouvés sur la page.
*/
function initAllMenuMobile() {
document.querySelectorAll(JS_MENU_WRAPPER_SELECTOR).forEach(initMenuMobile);
}
/**
* Détruit la gestion associée à tous les menus mobile trouvés sur la page.
*/
function destroyAllMenuMobile() {
document.querySelectorAll(JS_MENU_WRAPPER_SELECTOR).forEach(destroyMenuMobile);
}
export { initAllMenuMobile, initMenuMobile, destroyAllMenuMobile, destroyMenuMobile };
window.addEventListener('load', initAllMenuMobile);
<%@ tag pageEncoding="UTF-8" trimDirectiveWhitespaces="true" dynamic-attributes="dynattrs" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="kuik" uri="kuik" %>
<%@ tag import="java.util.UUID" %>
<%@ attribute name="menu" required="true" type="fr.kosmos.web.kore.attributes.interfaces.Menu" description="Information du menu" %>
<%@ attribute name="size" required="false" type="java.lang.String" description="Taille (défaut : md)" %>
<%@ attribute name="variant" required="false" type="java.lang.String" description="Variation" %>
<%@ attribute name="columns" required="false" type="java.lang.Integer" description="Nombre de colonnes" %>
<%@ attribute name="closeIconTitle" required="false" type="java.lang.String" description="Le title du bouton de fermeture" %>
<%--@elvariable id="itemViewModel" type="fr.kosmos.web.kore.attributes.interfaces.MenuItem"--%>
<c:if test="${not empty menu.items}">
<c:set var="classesSize" value="menu--md"/>
<c:if test="${not empty pageScope.size}">
<c:set var="classesSize" value="menu--${size}"/>
</c:if>
<c:set var="key" value="${UUID.randomUUID().toString()}" />
<c:set value="menu ${classesSize}" var="classes"/>
<c:set var="classesVariant" value="rows" scope="request"/>
<c:if test="${not empty pageScope.variant}">
<c:set var="classesVariant" value="${variant}" scope="request"/>
</c:if>
<c:set var="columnsNumber" value="4" scope="request"/>
<c:if test="${not empty pageScope.columns}">
<c:set var="columnsNumber" value="${pageScope.columns}" scope="request"/>
</c:if>
<c:set var="attributes">
<c:forEach items="${dynattrs}" var="a">
<c:choose>
<c:when test="${a.key == 'class'}">
<c:set value="${classes} ${a.value}" var="classes"/>
</c:when>
<c:otherwise>${a.key}="${a.value}" </c:otherwise>
</c:choose>
</c:forEach>
</c:set>
<nav class="${pageScope.classes}" ${pageScope.attributes} aria-label="principale" style="--anchor-name: --${key};">
<ul class="menu__list">
<c:forEach var="item" items="${pageScope.menu.items}">
<li class="menu__item">
<c:choose>
<c:when test="${not empty item.items}">
<button class="button button--tertiary button--sm button--no-wrap menu__link ${item.current ? 'menu__link--current' : ''}" ${item.current ? 'aria-current="page"' : ''} popovertarget="${item.id}">
<c:choose>
<c:when test="${not empty item.icon}">
<kuik:icon-content icon="${item.icon}" class="icon-content--centered" iconTitle="">
<c:out value="${item.title}" />
</kuik:icon-content>
</c:when>
<c:otherwise>
<c:out value="${item.title}"/>
</c:otherwise>
</c:choose>
</button>
<kuik:sub-menu closeIconTitle="${closeIconTitle}" menu="${item}" variant="${classesVariant}" columns="${columnsNumber}" />
</c:when>
<c:otherwise>
<kuik:link link="${item.link}" current="${item.current}" class="menu__link ${item.current ? 'menu__link--current' : ''}">
<c:if test="${not empty item.icon}">
<kuik:icon-content icon="${item.icon}" class="icon-content--centered" iconTitle="">
<c:out value="${item.title}" />
</kuik:icon-content>
</c:if>
</kuik:link>
</c:otherwise>
</c:choose>
</li>
</c:forEach>
</ul>
</nav>
</c:if>