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"/>
<kuik:menu menu="${menuViewModel}" closeIconTitle="Fermer"/>
| Name | Type | Required | Description |
|---|---|---|---|
| menu | fr.kosmos.web.kore.attributes.interfaces.MenuItem | true | Information du menu |
| variant | java.lang.String | false | Variation |
| columns | java.lang.Integer | false | Nombre de colonnes |
| closeIconTitle | java.lang.String | false | Le libellé du bouton de fermeture |
<%@ 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" %>
<%@ attribute name="menu" required="true" type="fr.kosmos.web.kore.attributes.interfaces.MenuItem" description="Information du menu" %>
<%@ 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 libellé du bouton de fermeture" %>
<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>
<%--@elvariable id="subSubItemViewModel" type="fr.kosmos.web.kore.attributes.interfaces.MenuItem"--%>
<div class="menu__popup" id="${menu.id}" popover="">
<kuik:button status="neutral" size="sm" icon="true" popovertarget="${menu.id}" popovertargetaction="hide">
<kuik:icon title="${pageScope.closeIconTitle}" source="icon://ui/close"/>
<span class="visually-hidden"> <c:out value="${pageScope.closeIconTitle}" /></span>
</kuik:button>
<div class="u-vspacer-lg"></div>
<div class="menu__popup-header">
<c:if test="${not empty menu.icon}">
<kuik:icon title="" source="${menu.icon}"/>
</c:if>
<div class="menu__popup-title">${menu.title}</div>
<c:if test="${not empty menu.link}">
<div class="u-vspacer-sm"></div>
<kuik:link link="${menu.link}" class="button button--secondary button--md" aria-label="${menu.ariaLabel}">
${menu.label}
<kuik:icon title="" source="icon://ui/arrow-right" size="md"/>
</kuik:link>
</c:if>
</div>
<div class="u-vspacer-lg"></div>
<ul class="menu-grid menu-grid--${classesVariant}" style="--items: ${menu.items.size()}; --columns: ${columnsNumber}">
<c:forEach var="subItem" items="${menu.items}">
<li class="menu-grid__item">
<kuik:link link="${subItem.link}" class="menu-grid__link" data-level="2">
<c:choose>
<c:when test="${not empty subItem.icon}">
<kuik:icon-content icon="${subItem.icon}" iconTitle="">
<c:out value="${subItem.title}"/>
</kuik:icon-content>
</c:when>
<c:otherwise>
<c:out value="${subItem.title}"/>
</c:otherwise>
</c:choose>
</kuik:link>
<c:if test="${not empty subItem.items}">
<ul class="menu-grid">
<c:forEach var="subSubItem" items="${subItem.items}">
<li class="menu-grid__item">
<kuik:link link="${subSubItem.link}" class="menu-grid__link" data-level="3">
<c:if test="${not empty subSubItem.icon}">
<kuik:icon-content icon="${subSubItem.icon}" iconTitle="">
<c:out value="${subSubItem.title}"/>
</kuik:icon-content>
</c:if>
</kuik:link>
</li>
</c:forEach>
</ul>
</c:if>
</li>
</c:forEach>
</ul>
<c:if test="${not empty menu.teaser && not empty menu.teaserView}">
<div class="u-vspacer-lg"></div>
<c:set var="teaser" value="${menu.teaser}" scope="request"/>
<jsp:include page="${menu.teaserView}"/>
</c:if>
</div>
@use 'mixins/mixins-lists' as lists;
@use 'variables/variables-breakpoints' as breakpoints;
.menu-grid {
@include lists.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: breakpoints.$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;
}
@use 'mixins/mixins-customizables' as custom;
@use 'mixins/mixins-lists' as lists;
$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 lists.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.component($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.component($prefix-menu-list-link);
outline: none;
.icon {
@include custom.icon;
}
}
}
.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.component($prefix-menu-list-link);
.icon {
@include custom.icon;
}
}
.menu-list__link--current {
@include custom.component($prefix-menu-list-link);
&[href^='#']:not([href='#'])::after {
background-color: var(--neutral-80);
}
.icon {
@include custom.icon;
}
}
.menu-list__link--back {
@include custom.component($prefix-menu-list-link-back, true);
.icon {
@include custom.icon;
}
&:hover,
&:focus {
@include custom.component($prefix-menu-list-link-back);
.icon {
@include custom.icon;
}
}
&::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.component($prefix-menu-list-link-back);
.icon {
@include custom.icon;
}
}
@use 'mixins/mixins-customizables' as custom;
$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.component($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.component($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;
}
}
.menu__link:hover {
@include custom.component($prefix-link);
.icon {
@include custom.icon;
}
}
.menu__link:focus,
.menu__item:has(> :popover-open) > .menu__link {
@include custom.component($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;
}
}
.menu__link--current {
@include custom.component($prefix-link);
.icon {
@include custom.icon;
}
}
.menu__popup {
@include custom.component($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.component($prefix-popup-header, true);
.icon {
@include custom.icon;
}
}
.menu-list__item {
.icon {
@include custom.icon;
}
}
.menu-list__item-title,
.menu__popup-title {
@include custom.component($prefix-popup-title, true);
}
.menu--sm,
.menu--md,
.menu--lg,
.menu--xl {
@include custom.component($prefix);
.menu__link {
@include custom.component($prefix-link);
@include custom.component($prefix-link);
@include custom.component($prefix-link);
}
.menu-list__item,
.menu__popup-header {
@include custom.component($prefix-popup-header);
@include custom.component($prefix-popup-header);
.icon {
@include custom.icon;
}
}
.menu-list__item-title,
.menu__popup-title {
@include custom.component($prefix-popup-title);
}
}
package fr.kosmos.web.kore.attributes.interfaces;
import java.util.Collection;
/**
* Modèle du menu.
*/
public interface Menu {
String getLabel();
void setLabel(String label);
String getAriaLabel();
void setAriaLabel(String label);
Collection
package fr.kosmos.web.kore.attributes.interfaces;
import fr.kosmos.web.kore.attributes.DefaultLink;
/**
* Modèle d'un item de menu
*/
public interface MenuItem extends Menu {
String getTitle();
void setTitle(String title);
String getLabel();
void setLabel(String label);
String getAriaLabel();
void setAriaLabel(String label);
DefaultLink getLink();
void setLink(DefaultLink link);
String getIcon();
void setIcon(String icon);
String getId();
void setId(String id);
String getTeaser();
void setTeaser(String teaser);
String getTeaserView();
void setTeaserView(String teaserView);
boolean isCurrentAscendant();
void setCurrentAscendant(boolean currentAscendant);
boolean isCurrent();
void setCurrent(boolean current);
String getPriority();
void setPriority(String priority);
}