From 251c939ba050359a5d0d65a6b869e4e9d64fd51e Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 19 Jun 2026 14:01:41 +0000 Subject: [PATCH] fix: sidebar active style (#82) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié --------- Co-authored-by: admin malio Co-authored-by: THOLOT DECHENE Matthieu Co-authored-by: matthieu Reviewed-on: https://gitea.malio.fr/MALIO-DEV/malio-layer-ui/pulls/82 Co-authored-by: tristan Co-committed-by: tristan --- CHANGELOG.md | 1 + COMPONENTS.md | 5 +- app/components/malio/sidebar/Sidebar.test.ts | 59 +++++++++++++++++--- app/components/malio/sidebar/Sidebar.vue | 16 ++++++ 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa6dc2..dc9cb34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ Liste des évolutions de la librairie Malio layer UI * [#MUI-42] Button / ButtonIcon : l'anneau de focus passe du halo `ring-2 ring-m-primary/50` à l'anneau standard `.m-focus-ring` (outline plein, offset 2px), pour l'homogénéité avec les autres composants. ### Fixed +* Sidebar : le **lien actif** reste actif sur les **sous-routes** (match par préfixe via `useRoute().path` au lieu de l'`active-class` de NuxtLink qui dépendait de l'imbrication des routes) — ex. `/supplier` reste surligné sur `/supplier/1/edit`. Nouvelle option `exact: true` par item pour forcer le match strict. * Famille Date (CalendarField) : le **clic sur le picto calendrier** ouvre désormais le popover (le `` en overlay absolu interceptait le clic sans le traiter, et ne le laissait pas retomber sur l'input). Couvre Date, DateTime, DateRange, DateWeek. La croix d'effacement conserve son comportement (efface sans ouvrir). * Famille Date editable (MalioDate, MalioDateTime) : la saisie clavier est désormais **bornée par champ** sur le premier **et** le second chiffre (jour `01-31`, mois `01-12`, heure `00-23`, minute `00-59`) — une valeur hors plage (`99/99/9999`, un jour `33`, un mois `19`…) ne peut plus être tapée (auparavant saisissable puis rejetée a posteriori par la validation). Les impossibilités calendaires fines (`31/02`, 29/02 non bissextile, hors `min`/`max`) restent captées par la validation. Implémenté via `buildBoundedMask(template)` (CalendarField) : un `preProcess` maska valide chaque champ progressivement (un chiffre n'est accepté que s'il reste une complétion valide dans la plage) ; il distingue le mois des minutes (même lettre `M`) selon la présence d'heures dans le gabarit. * DataTable : pagination réalignée verticalement après l'introduction du `min-h-[1rem]` du Select — la barre pagination passe en `items-center`, et le MalioSelect du sélecteur de perPage est encapsulé dans un wrapper `h-12` qui borne sa taille flex à la hauteur du field (le slot vide déborde invisiblement en dessous). Span « Lignes : » et boutons Prev/Page/Next sont désormais centrés exactement sur le field (y=24) diff --git a/COMPONENTS.md b/COMPONENTS.md index 49fcd79..c453986 100644 --- a/COMPONENTS.md +++ b/COMPONENTS.md @@ -885,7 +885,10 @@ Barre latérale de navigation rétractable. | `sidebarClass` | `string` | `''` | Classes CSS sidebar | | `toggleClass` | `string` | `''` | Classes CSS bouton toggle | -**Type SidebarSection :** `{ title?: string, items: { label: string, icon?: string, to?: string, href?: string, active?: boolean }[] }` +**Type SidebarSection :** `{ label?: string, icon?: string, items: SidebarItem[] }` +**Type SidebarItem :** `{ label: string, to: string, exact?: boolean }` + +**Lien actif :** un lien est marqué actif (texte `m-primary` + semi-bold) quand la route courante **est ce lien ou une de ses sous-routes** (match par préfixe) — ex. `/supplier` reste actif sur `/supplier/1/edit`. Mettre `exact: true` sur l'item force le match strict (actif uniquement sur la route exacte). Indépendant de l'imbrication des routes côté consommateur. **Events :** `update:modelValue(value: boolean)` **Slots :** `logo` (sidebar ouverte), `logo-collapsed` (sidebar fermée) diff --git a/app/components/malio/sidebar/Sidebar.test.ts b/app/components/malio/sidebar/Sidebar.test.ts index 5f5f2d7..22f4846 100644 --- a/app/components/malio/sidebar/Sidebar.test.ts +++ b/app/components/malio/sidebar/Sidebar.test.ts @@ -1,12 +1,14 @@ import {describe, expect, it} from 'vitest' import {mount} from '@vue/test-utils' import type {DefineComponent} from 'vue' +import {createMemoryHistory, createRouter} from 'vue-router' import {Icon as IconifyIcon} from '@iconify/vue' import Sidebar from './Sidebar.vue' type SidebarItem = { label: string to: string + exact?: boolean } type SidebarSection = { @@ -50,14 +52,30 @@ const stubs = { }, } +function makeRouter(path = '/') { + const router = createRouter({ + history: createMemoryHistory(), + routes: [{path: '/:all(.*)*', component: {template: '
'}}], + }) + router.push(path) + return router +} + function mountComponent(props: SidebarProps, slots?: Record) { return mount(SidebarForTest, { props, slots, - global: {stubs}, + global: {stubs, plugins: [makeRouter()]}, }) } +// Monte avec le router positionné sur `path` (pour tester l'état actif). +async function mountAt(path: string, props: SidebarProps = {sections}) { + const router = makeRouter(path) + await router.isReady() + return mount(SidebarForTest, {props, global: {stubs, plugins: [router]}}) +} + describe('MalioSidebar', () => { it('renders expanded by default', () => { const wrapper = mountComponent({sections}) @@ -104,12 +122,39 @@ describe('MalioSidebar', () => { expect(wrapper.find('a').classes()).not.toContain('hover:text-m-primary') }) - it('actif : texte primary + semi-bold, sans fond, via active-class', () => { - const wrapper = mountComponent({sections}) - const activeClass = wrapper.find('a').attributes('active-class') ?? '' - expect(activeClass).toContain('text-m-primary') - expect(activeClass).toContain('font-semibold') - expect(activeClass).not.toContain('bg-') + it('actif : route exacte → lien en primary + semi-bold, sans fond', () => { + return mountAt('/reception').then((wrapper) => { + const link = wrapper.findAll('a')[0] + expect(link.classes()).toContain('font-semibold') + expect(link.classes()).toContain('!text-m-primary') + expect(link.classes().some(c => c.startsWith('bg-'))).toBe(false) + }) + }) + + it('actif : reste actif sur une sous-route (match par préfixe)', async () => { + const wrapper = await mountAt('/reception/1/edit') + expect(wrapper.findAll('a')[0].classes()).toContain('font-semibold') + }) + + it('actif : les autres liens ne sont pas actifs sur une sous-route', async () => { + const wrapper = await mountAt('/reception/1/edit') + expect(wrapper.findAll('a')[1].classes()).not.toContain('font-semibold') + }) + + it('exact : pas actif sur une sous-route', async () => { + const exactSections: SidebarSection[] = [ + {label: 'S', icon: 'mdi:home', items: [{label: 'R', to: '/reception', exact: true}]}, + ] + const wrapper = await mountAt('/reception/1/edit', {sections: exactSections}) + expect(wrapper.find('a').classes()).not.toContain('font-semibold') + }) + + it('exact : actif sur la route exacte', async () => { + const exactSections: SidebarSection[] = [ + {label: 'S', icon: 'mdi:home', items: [{label: 'R', to: '/reception', exact: true}]}, + ] + const wrapper = await mountAt('/reception', {sections: exactSections}) + expect(wrapper.find('a').classes()).toContain('font-semibold') }) it('renders section icons via IconifyIcon', () => { diff --git a/app/components/malio/sidebar/Sidebar.vue b/app/components/malio/sidebar/Sidebar.vue index d1bfa67..d8364f6 100644 --- a/app/components/malio/sidebar/Sidebar.vue +++ b/app/components/malio/sidebar/Sidebar.vue @@ -57,6 +57,7 @@ :class="twMerge( 'block truncate text-[15px] leading-[150%]', collapsed ? 'px-3 text-center' : 'pl-[32px]', + isActive(item) ? '!text-m-primary font-semibold' : '', )" > {{ item.label }} @@ -87,6 +88,7 @@