| 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 <malio@yuno.malio.fr> Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Reviewed-on: #88 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #88.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import {describe, expect, it, vi} from 'vitest'
|
||||
import {mount} from '@vue/test-utils'
|
||||
import type {DefineComponent} from 'vue'
|
||||
import {defineComponent, nextTick, ref, type DefineComponent} from 'vue'
|
||||
import {Icon as IconifyIcon} from '@iconify/vue'
|
||||
import InputAutocomplete from './InputAutocomplete.vue'
|
||||
|
||||
@@ -569,4 +569,40 @@ describe('MalioInputAutocomplete', () => {
|
||||
expect(msg.exists()).toBe(true)
|
||||
expect(msg.classes()).not.toContain('min-h-[1rem]')
|
||||
})
|
||||
|
||||
// MUI-48 : après avoir sélectionné une option dans la liste, le champ garde le focus DOM
|
||||
// mais isFocused interne passe à false (clic option en mousedown.prevent). Un collage qui
|
||||
// remplace tout (Ctrl+A puis Ctrl+V) déclenche update:modelValue(null) ; le watch ne doit
|
||||
// PAS vider la valeur collée. Régression : le champ se vidait au lieu de prendre le texte collé.
|
||||
it('MUI-48 : un collage après sélection dans la liste remplace la valeur (ne la vide pas)', async () => {
|
||||
const Harness = defineComponent({
|
||||
components: {InputAutocomplete},
|
||||
setup() {
|
||||
const val = ref<string | number | null>(null)
|
||||
const opts = ref([{label: '10 Rue de la Paix', value: '10 Rue de la Paix'}])
|
||||
return {val, opts}
|
||||
},
|
||||
template: '<InputAutocomplete v-model="val" :options="opts" allow-create />',
|
||||
})
|
||||
|
||||
const wrapper = mount(Harness, {
|
||||
attachTo: document.body,
|
||||
global: {stubs: {IconifyIcon: {template: '<span data-test="icon" v-bind="$attrs" />'}}},
|
||||
})
|
||||
const input = wrapper.get('input')
|
||||
|
||||
// saisie puis sélection d'une suggestion (commit, focus DOM conservé)
|
||||
await input.trigger('focus')
|
||||
await input.setValue('10')
|
||||
await wrapper.findAll('[data-test="option"]')[0].trigger('click')
|
||||
await nextTick()
|
||||
expect(input.element.value).toBe('10 Rue de la Paix')
|
||||
|
||||
// Ctrl+A puis Ctrl+V : input toujours focalisé DOM, aucun nouvel évènement focus
|
||||
await input.setValue('25 Avenue Victor Hugo')
|
||||
await nextTick()
|
||||
|
||||
expect(input.element.value).toBe('25 Avenue Victor Hugo')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -393,6 +393,11 @@ const scheduleSearch = () => {
|
||||
|
||||
const onInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
// Un évènement input prouve que le champ est en cours d'édition : on resynchronise
|
||||
// isFocused, qu'une sélection précédente (onSelect) a pu passer à false tout en gardant
|
||||
// le focus DOM (clic option en mousedown.prevent). Sans ça, le watch ci-dessous remettrait
|
||||
// inputValue à '' au collage et la valeur collée serait perdue (MUI-48).
|
||||
isFocused.value = true
|
||||
inputValue.value = target.value
|
||||
if (!isOpen.value) isOpen.value = true
|
||||
activeIndex.value = -1
|
||||
|
||||
@@ -107,19 +107,24 @@ describe('MalioSidebar', () => {
|
||||
expect(links[2].attributes('href')).toBe('/fournisseurs')
|
||||
})
|
||||
|
||||
it('hover : fond + couleur + semi-bold tous portés par le <li> (texte non figé sur le <a>)', () => {
|
||||
it('hover : fond + couleur + semi-bold portés par le <li>', () => {
|
||||
const wrapper = mountComponent({sections})
|
||||
const li = wrapper.find('li')
|
||||
expect(li.classes()).toContain('hover:bg-m-primary/10')
|
||||
expect(li.classes()).toContain('hover:text-m-primary')
|
||||
expect(li.classes()).toContain('hover:font-semibold')
|
||||
expect(li.classes()).toContain('text-black')
|
||||
expect(li.classes()).toContain('pt-1')
|
||||
expect(li.classes()).toContain('pb-1')
|
||||
// Le <a> ne fige PAS sa couleur (sinon le texte resterait noir sur les bandes
|
||||
// pt-1/pb-1 hors du <a> alors que le fond du <li> est bleu).
|
||||
expect(wrapper.find('a').classes()).not.toContain('text-black')
|
||||
expect(wrapper.find('a').classes()).not.toContain('hover:text-m-primary')
|
||||
})
|
||||
|
||||
it('zone cliquable : le padding vertical est sur le <a>, pas sur le <li> (pas de bande morte au survol)', () => {
|
||||
const wrapper = mountComponent({sections})
|
||||
// Le padding vertical doit appartenir à la cible de clic (<a>) pour que toute
|
||||
// la bande survolée soit cliquable — sinon pt-1/pb-1 sur le <li> crée une
|
||||
// bande colorée mais non cliquable en haut et en bas du lien.
|
||||
const li = wrapper.find('li')
|
||||
expect(li.classes()).not.toContain('pt-1')
|
||||
expect(li.classes()).not.toContain('pb-1')
|
||||
expect(wrapper.find('a').classes()).toContain('py-1')
|
||||
})
|
||||
|
||||
it('actif : route exacte → lien en primary + semi-bold, sans fond', () => {
|
||||
@@ -239,6 +244,43 @@ describe('MalioSidebar', () => {
|
||||
expect(wrapper.find('img[alt="M"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders footer slot when expanded', () => {
|
||||
const wrapper = mountComponent({sections}, {
|
||||
footer: '<a href="/logout">Déconnexion</a>',
|
||||
})
|
||||
expect(wrapper.find('a[href="/logout"]').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('Déconnexion')
|
||||
})
|
||||
|
||||
it('renders footer-collapsed slot when collapsed', async () => {
|
||||
const wrapper = mountComponent({sections}, {
|
||||
'footer-collapsed': '<span>FC</span>',
|
||||
})
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(wrapper.text()).toContain('FC')
|
||||
})
|
||||
|
||||
it('footer is rendered after the nav (pushed to the bottom)', () => {
|
||||
const wrapper = mountComponent({sections}, {
|
||||
footer: '<span class="ft">Footer</span>',
|
||||
})
|
||||
const children = wrapper.find('aside').element.children
|
||||
const navIndex = Array.from(children).findIndex(el => el.tagName === 'NAV')
|
||||
const footerEl = wrapper.find('.ft').element
|
||||
const footerWrapperIndex = Array.from(children).findIndex(el => el.contains(footerEl))
|
||||
expect(footerWrapperIndex).toBeGreaterThan(navIndex)
|
||||
})
|
||||
|
||||
it('does not render a footer container when no footer slot is provided', () => {
|
||||
const wrapper = mountComponent({sections})
|
||||
// Seuls le bloc logo et le <nav> sont des conteneurs (+ le bouton toggle).
|
||||
// Aucun div de footer ne doit apparaître après le <nav>.
|
||||
const children = Array.from(wrapper.find('aside').element.children)
|
||||
const navIndex = children.findIndex(el => el.tagName === 'NAV')
|
||||
const after = children.slice(navIndex + 1)
|
||||
expect(after.some(el => el.tagName === 'DIV')).toBe(false)
|
||||
})
|
||||
|
||||
it('uses custom id when provided', () => {
|
||||
const wrapper = mountComponent({sections, id: 'my-sidebar'})
|
||||
expect(wrapper.find('aside').attributes('id')).toBe('my-sidebar')
|
||||
|
||||
@@ -49,14 +49,14 @@
|
||||
<li
|
||||
v-for="item in section.items"
|
||||
:key="item.to"
|
||||
:class="collapsed ? '' : 'text-black hover:bg-m-primary/10 hover:font-semibold hover:text-m-primary pt-1 pb-1'"
|
||||
:class="collapsed ? '' : 'text-black hover:bg-m-primary/10 hover:font-semibold hover:text-m-primary'"
|
||||
>
|
||||
<NuxtLink
|
||||
:to="item.to"
|
||||
active-class="!text-m-primary font-semibold"
|
||||
:class="twMerge(
|
||||
'block truncate text-[15px] leading-[150%]',
|
||||
collapsed ? 'px-3 text-center' : 'pl-[32px]',
|
||||
collapsed ? 'px-3 text-center' : 'pl-[32px] py-1',
|
||||
isActive(item) ? '!text-m-primary font-semibold' : '',
|
||||
)"
|
||||
>
|
||||
@@ -67,6 +67,20 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div
|
||||
v-if="$slots.footer || $slots['footer-collapsed']"
|
||||
:class="['px-[20px] py-[14px]', collapsed ? '' : 'mx-[10px] border-t-2 border-m-primary']"
|
||||
>
|
||||
<slot
|
||||
v-if="collapsed"
|
||||
name="footer-collapsed"
|
||||
/>
|
||||
<slot
|
||||
v-else
|
||||
name="footer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="collapsed ? 'Déplier le menu' : 'Plier le menu'"
|
||||
|
||||
@@ -43,6 +43,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Avec footer (collé en bas)">
|
||||
<div class="flex h-[600px] border rounded-lg overflow-hidden">
|
||||
<MalioSidebar
|
||||
v-model="collapsed3"
|
||||
:sections="sectionsLong"
|
||||
>
|
||||
<template #logo>
|
||||
<span class="text-2xl font-bold text-m-primary">Malio</span>
|
||||
</template>
|
||||
<template #logo-collapsed>
|
||||
<span class="text-2xl font-bold text-m-primary">M</span>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="leading-tight">
|
||||
<p class="text-[14px] font-semibold text-m-text">Tristan</p>
|
||||
<p class="text-[12px] text-m-muted">Administrateur</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer-collapsed>
|
||||
<span class="block text-center text-[14px] font-bold text-m-primary">T</span>
|
||||
</template>
|
||||
</MalioSidebar>
|
||||
|
||||
<div class="flex-1 p-6 bg-white">
|
||||
<p class="text-m-muted">
|
||||
Le footer reste collé en bas même quand la nav scrolle.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
@@ -94,6 +125,15 @@ entre les deux états.
|
||||
|
||||
- Contenu affiché en haut quand la sidebar est pliée.
|
||||
|
||||
### footer
|
||||
|
||||
- Contenu affiché en bas quand la sidebar est dépliée (profil, déconnexion, version…).
|
||||
- Toujours collé en bas : la nav occupe l'espace restant (`flex-1`) et pousse le footer.
|
||||
|
||||
### footer-collapsed
|
||||
|
||||
- Contenu affiché en bas quand la sidebar est pliée.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Comportement
|
||||
@@ -127,6 +167,7 @@ import MalioSidebar from '../../components/malio/sidebar/Sidebar.vue'
|
||||
|
||||
const collapsed1 = ref(false)
|
||||
const collapsed2 = ref(false)
|
||||
const collapsed3 = ref(false)
|
||||
|
||||
const sectionsShort = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user