Merge branch 'accordion' into 'main'
Accordion See merge request cellule-financiere-pmo/design-system/visua-vue!4
This commit is contained in:
commit
ac76e4451b
|
@ -1,12 +1,14 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// import VButtonView from '../template/VButtonView.vue'
|
// import VButtonView from '../template/VButtonView.vue'
|
||||||
// import VButtonGroupView from '../template/VButtonGroupView.vue';
|
// import VButtonGroupView from '../template/VButtonGroupView.vue';
|
||||||
import VLinkView from '../template/VLinkView.vue';
|
// import VLinkView from '../template/VLinkView.vue';
|
||||||
|
import VAccordionView from '../template/VAccordionView.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- <VButtonView/> -->
|
<!-- <VButtonView/> -->
|
||||||
<!-- <VButtonGroupView/> -->
|
<!-- <VButtonGroupView/> -->
|
||||||
<VLinkView/>
|
<!-- <VLinkView/> -->
|
||||||
|
<VAccordionView/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
@import './primevue-style/button.css';
|
@import './primevue-style/button.css';
|
||||||
|
@import './primevue-style/accordion.css';
|
||||||
|
|
42
src/assets/style/primevue-style/accordion.css
Normal file
42
src/assets/style/primevue-style/accordion.css
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
:root{
|
||||||
|
/* panel */
|
||||||
|
--p-accordion-transition-duration: 0.2s;
|
||||||
|
--p-accordion-panel-border-width: 0px;
|
||||||
|
--p-accordion-panel-border-color: none;
|
||||||
|
/* header */
|
||||||
|
--p-accordion-header-color: var(--text-action-high-blue-france);
|
||||||
|
--p-accordion-header-hover-color: var(--text-action-high-blue-france);
|
||||||
|
--p-accordion-header-active-color: var(--text-action-high-blue-france);
|
||||||
|
--p-accordion-header-active-hover-color: var(--text-action-high-blue-france);
|
||||||
|
--p-accordion-header-padding: 0.75rem 1rem;
|
||||||
|
--p-accordion-header-font-weight: var(--text-body-MD-standard-text-Medium-weight);
|
||||||
|
--p-accordion-header-border-radius: 0px;
|
||||||
|
--p-accordion-header-border-width: 1px;
|
||||||
|
--p-accordion-header-border-color: var(--border-default-grey);
|
||||||
|
--p-accordion-header-background: var(--background-transparent);
|
||||||
|
--p-accordion-header-hover-background: var(--background-transparent-hover);
|
||||||
|
--p-accordion-header-active-background: var(--background-open-blue-france);
|
||||||
|
--p-accordion-header-active-hover-background: var(--background-open-blue-france-hover);
|
||||||
|
/* header first top border */
|
||||||
|
--p-accordion-header-first-top-border-radius: 0px;
|
||||||
|
--p-accordion-header-first-border-width: 1px;
|
||||||
|
/* header last bottom border */
|
||||||
|
--p-accordion-header-last-bottom-border-radius: 0px;
|
||||||
|
--p-accordion-header-last-active-bottom-border-radius: 0px;
|
||||||
|
/* focus */
|
||||||
|
--p-accordion-header-focus-ring-width: var(--focus-width);
|
||||||
|
--p-accordion-header-focus-ring-style: var(--focus-style);
|
||||||
|
--p-accordion-header-focus-ring-color: var(--focus-color);
|
||||||
|
--p-accordion-header-focus-ring-offset: var(--focus-offset);
|
||||||
|
/* icon */
|
||||||
|
--p-accordion-header-toggle-icon-color: var(--text-action-high-blue-france);
|
||||||
|
--p-accordion-header-toggle-icon-hover-color: var(--text-action-high-blue-france);
|
||||||
|
--p-accordion-header-toggle-icon-active-color: var(--text-action-high-blue-france);
|
||||||
|
--p-accordion-header-toggle-icon-active-hover-color: var(--text-action-high-blue-france);
|
||||||
|
/* content */
|
||||||
|
--p-accordion-content-border-width: 0px;
|
||||||
|
--p-accordion-content-border-color: none;
|
||||||
|
--p-accordion-content-background: var(--background-transparent);
|
||||||
|
--p-accordion-content-color: var(--text-default-grey);
|
||||||
|
--p-accordion-content-padding: 0px;
|
||||||
|
}
|
28
src/components/accordion/IVAccordion.type.ts
Normal file
28
src/components/accordion/IVAccordion.type.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* Interface representing the configuration and state of an Accordion component.
|
||||||
|
*/
|
||||||
|
export default interface IVAccordion {
|
||||||
|
/**
|
||||||
|
* Optional unique identifier for the accordion instance.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the accordion is disabled.
|
||||||
|
* When true, user interaction is prevented.
|
||||||
|
*/
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current value(s) of the accordion.
|
||||||
|
* Can be a single value or an array of values, depending on the selection mode.
|
||||||
|
* Accepts string, number, or null.
|
||||||
|
*/
|
||||||
|
value?: null | string | number | string[] | number[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value associated with a specific panel within the accordion.
|
||||||
|
* Useful for identifying or controlling individual panels.
|
||||||
|
*/
|
||||||
|
panelValue?: undefined | string | number;
|
||||||
|
}
|
45
src/components/accordion/VAccordion.vue
Normal file
45
src/components/accordion/VAccordion.vue
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Accordion from 'primevue/accordion';
|
||||||
|
import VAccordionChild from './VAccordionChild.vue';
|
||||||
|
import type IVAccordion from './IVAccordion.type';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<IVAccordion>(), {
|
||||||
|
value: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the event emitter for v-model binding
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:value',
|
||||||
|
])
|
||||||
|
|
||||||
|
// Local reactive value to sync with the parent v-model
|
||||||
|
const localValue = ref(props.value);
|
||||||
|
|
||||||
|
// Watch for external changes to the prop and update localValue accordingly
|
||||||
|
watch(() => props.value, (newVal) => {
|
||||||
|
if(localValue.value !== newVal) {
|
||||||
|
localValue.value = newVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for internal changes to localValue and emit them to the parent
|
||||||
|
watch(localValue, (newVal) => {
|
||||||
|
if(props.value !== newVal){
|
||||||
|
emit('update:value', newVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Accordion
|
||||||
|
v-model:value="localValue"
|
||||||
|
class="p-accordion"
|
||||||
|
style="width: 100%;"
|
||||||
|
@update:value="emit('update:value', $event)"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<VAccordionChild/>
|
||||||
|
</slot>
|
||||||
|
</Accordion>
|
||||||
|
</template>
|
62
src/components/accordion/VAccordionChild.vue
Normal file
62
src/components/accordion/VAccordionChild.vue
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AccordionPanel from 'primevue/accordionpanel';
|
||||||
|
import AccordionHeader from 'primevue/accordionheader';
|
||||||
|
import AccordionContent from 'primevue/accordioncontent';
|
||||||
|
import type IVAccordion from './IVAccordion.type';
|
||||||
|
import { useId } from 'vue';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<IVAccordion>(), {
|
||||||
|
id: () => useId(),
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AccordionPanel
|
||||||
|
:id="props.id"
|
||||||
|
:value="panelValue"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
:aria-disabled="props.disabled"
|
||||||
|
:class="['p-accordionpanel', {'disabled': props.disabled}]">
|
||||||
|
<AccordionHeader class="p-accordionheader" role="heading" :aria-level="panelValue">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionContent class="p-accordioncontent">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionPanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.p-accordionpanel.disabled {
|
||||||
|
/* header */
|
||||||
|
--p-accordion-header-color: var(--text-disabled-grey);
|
||||||
|
--p-accordion-header-hover-color: var(--text-disabled-grey);
|
||||||
|
--p-accordion-header-active-color: var(--text-disabled-grey);
|
||||||
|
--p-accordion-header-active-hover-color: var(--text-disabled-grey);
|
||||||
|
--p-accordion-header-border-color: var(--border-disabled-grey);
|
||||||
|
--p-accordion-content-border-color: var(--border-disabled-grey);
|
||||||
|
/* focus header */
|
||||||
|
--p-accordion-header-focus-ring-style: none;
|
||||||
|
--p-accordion-header-focus-ring-color: transparent;
|
||||||
|
--p-accordion-header-focus-ring-width: 0px;
|
||||||
|
/* background header */
|
||||||
|
--p-accordion-header-border-color: var(--border-default-grey);
|
||||||
|
--p-accordion-header-background: var(--background-disabled-grey);
|
||||||
|
--p-accordion-header-hover-background: var(--background-disabled-grey);
|
||||||
|
--p-accordion-header-active-background: var(--background-disabled-grey);
|
||||||
|
--p-accordion-header-active-hover-background: var(--background-disabled-grey);
|
||||||
|
/* icon */
|
||||||
|
--p-accordion-header-toggle-icon-color: var(--text-disabled-grey);
|
||||||
|
--p-accordion-header-toggle-icon-hover-color: var(--text-disabled-grey);
|
||||||
|
--p-accordion-header-toggle-icon-active-color: var(--text-disabled-grey);
|
||||||
|
--p-accordion-header-toggle-icon-active-hover-color: var(--text-disabled-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-accordionheader{
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-accordioncontent{margin: 1rem;}
|
||||||
|
</style>
|
153
test/VAccordion.spec.ts
Normal file
153
test/VAccordion.spec.ts
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import { describe, test, expect } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { h } from 'vue'
|
||||||
|
import VAccordion from '../src/components/accordion/VAccordion.vue'
|
||||||
|
import VAccordionChild from '../src/components/accordion/VAccordionChild.vue'
|
||||||
|
|
||||||
|
describe('VAccordion', () => {
|
||||||
|
const factory = () =>
|
||||||
|
mount(VAccordion, {
|
||||||
|
props: {
|
||||||
|
value: '0',
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: () => [
|
||||||
|
h(VAccordionChild, { panelValue: '0' }, {
|
||||||
|
header: () => 'Un titre d\'accordéon 1',
|
||||||
|
content: () => h('p', 'Contenu 1'),
|
||||||
|
}),
|
||||||
|
h(VAccordionChild, { panelValue: '1', disabled: true }, {
|
||||||
|
header: () => 'Un titre d\'accordéon 2',
|
||||||
|
content: () => h('p', 'Contenu 2'),
|
||||||
|
}),
|
||||||
|
h(VAccordionChild, { panelValue: '2' }, {
|
||||||
|
header: () => 'Un titre d\'accordéon 3',
|
||||||
|
content: () => h('p', 'Contenu 3'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
components: {
|
||||||
|
VAccordionChild,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
test('renders three accordion panels', () => {
|
||||||
|
const wrapper = factory()
|
||||||
|
const panels = wrapper.findAll('.p-accordionpanel')
|
||||||
|
expect(panels).toHaveLength(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('activates the first panel by default', () => {
|
||||||
|
const wrapper = factory()
|
||||||
|
const firstPanel = wrapper.findAll('.p-accordionpanel')[0]
|
||||||
|
expect(firstPanel.classes()).toContain('p-accordionpanel-active')
|
||||||
|
expect(firstPanel.attributes('data-p-active')).toBe('true')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('disables the second panel', () => {
|
||||||
|
const wrapper = factory()
|
||||||
|
const secondPanel = wrapper.findAll('.p-accordionpanel')[1]
|
||||||
|
expect(secondPanel.classes()).toContain('p-disabled')
|
||||||
|
expect(secondPanel.attributes('data-p-disabled')).toBe('true')
|
||||||
|
expect(secondPanel.find('button').attributes('disabled')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not emit update:value when a disabled panel is clicked', async () => {
|
||||||
|
const wrapper = mount(VAccordion, {
|
||||||
|
props: {
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: () => [
|
||||||
|
h(VAccordionChild, { panelValue: '0', disabled: true }, {
|
||||||
|
header: () => 'Panneau désactivé',
|
||||||
|
content: () => h('p', 'Contenu'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
components: {
|
||||||
|
VAccordionChild,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const header = wrapper.find('.p-accordionheader')
|
||||||
|
expect(header.attributes('disabled')).toBeDefined()
|
||||||
|
await header.trigger('click')
|
||||||
|
expect(wrapper.emitted('update:value')).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('emits update:value when a panel is clicked', async () => {
|
||||||
|
const wrapper = mount(VAccordion, {
|
||||||
|
props: {
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: `
|
||||||
|
<VAccordionChild panel-value="0">
|
||||||
|
<template #header>Panel 1</template>
|
||||||
|
<template #content><p>Contenu 1</p></template>
|
||||||
|
</VAccordionChild>
|
||||||
|
<VAccordionChild panel-value="1">
|
||||||
|
<template #header>Panel 2</template>
|
||||||
|
<template #content><p>Contenu 2</p></template>
|
||||||
|
</VAccordionChild>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
components: {
|
||||||
|
VAccordionChild,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const headers = wrapper.findAll('.p-accordionheader')
|
||||||
|
await headers[1].trigger('click')
|
||||||
|
|
||||||
|
// Checks that the event has been issued with the correct value
|
||||||
|
expect(wrapper.emitted('update:value')).toBeTruthy()
|
||||||
|
expect(wrapper.emitted('update:value')![0]).toEqual(['1'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('toggles an active panel when clicked again', async () => {
|
||||||
|
const wrapper = mount(VAccordion, {
|
||||||
|
props: {
|
||||||
|
value: '0',
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: () => [
|
||||||
|
h(VAccordionChild, { panelValue: '0' }, {
|
||||||
|
header: () => 'Panneau 1',
|
||||||
|
content: () => h('p', 'Contenu 1'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
components: {
|
||||||
|
VAccordionChild,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const header = wrapper.find('.p-accordionheader')
|
||||||
|
|
||||||
|
// Vérifie que le panneau est actif au départ
|
||||||
|
const panel = wrapper.find('.p-accordionpanel')
|
||||||
|
expect(panel.attributes('data-p-active')).toBe('true')
|
||||||
|
|
||||||
|
// Clique une première fois pour le refermer
|
||||||
|
await header.trigger('click')
|
||||||
|
|
||||||
|
// Vérifie que le panneau est maintenant inactif
|
||||||
|
expect(panel.attributes('data-p-active')).toBe('false')
|
||||||
|
|
||||||
|
// Vérifie que l'événement a bien été émis avec `null` ou une valeur vide
|
||||||
|
const emitted = wrapper.emitted('update:value')
|
||||||
|
expect(emitted).toBeTruthy()
|
||||||
|
const lastEmission = emitted![emitted!.length - 1]
|
||||||
|
expect(lastEmission[0]).toBe(null)
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user