Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 251c939ba0 |
@@ -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 `<Icon>` 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)
|
||||
|
||||
+4
-1
@@ -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)
|
||||
|
||||
@@ -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: '<div />'}}],
|
||||
})
|
||||
router.push(path)
|
||||
return router
|
||||
}
|
||||
|
||||
function mountComponent(props: SidebarProps, slots?: Record<string, string>) {
|
||||
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', () => {
|
||||
|
||||
@@ -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' : '',
|
||||
)"
|
||||
>
|
||||
<span v-if="!collapsed">{{ item.label }}</span>
|
||||
@@ -87,6 +88,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, useId} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import {Icon as IconifyIcon} from '@iconify/vue'
|
||||
import {twMerge} from 'tailwind-merge'
|
||||
|
||||
@@ -95,6 +97,10 @@ defineOptions({name: 'MalioSidebar', inheritAttrs: false})
|
||||
export type SidebarItem = {
|
||||
label: string
|
||||
to: string
|
||||
// Par défaut, un lien est actif sur sa route ET ses sous-routes (préfixe) :
|
||||
// `/supplier` reste actif sur `/supplier/1/edit`. `exact: true` force le match
|
||||
// strict (actif uniquement sur la route exacte).
|
||||
exact?: boolean
|
||||
}
|
||||
|
||||
export type SidebarSection = {
|
||||
@@ -123,6 +129,16 @@ const emit = defineEmits<{
|
||||
const generatedId = useId()
|
||||
const componentId = computed(() => props.id || `malio-sidebar-${generatedId}`)
|
||||
|
||||
const route = useRoute()
|
||||
// Actif si la route courante est le lien lui-même OU une de ses sous-routes
|
||||
// (match par préfixe), pour que `/supplier` reste actif sur `/supplier/1/edit`.
|
||||
// `item.exact` force le match strict.
|
||||
function isActive(item: SidebarItem): boolean {
|
||||
const path = route.path
|
||||
if (item.exact) return path === item.to
|
||||
return path === item.to || path.startsWith(`${item.to}/`)
|
||||
}
|
||||
|
||||
const isControlled = computed(() => props.modelValue !== undefined)
|
||||
const localValue = ref(false)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user