diff --git a/src/App.vue b/src/App.vue index c7eecad..3c30836 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,12 +1,14 @@ diff --git a/src/assets/style/primevue-configuration.css b/src/assets/style/primevue-configuration.css index c080610..3736d1a 100644 --- a/src/assets/style/primevue-configuration.css +++ b/src/assets/style/primevue-configuration.css @@ -1 +1,2 @@ @import './primevue-style/button.css'; +@import './primevue-style/accordion.css'; diff --git a/src/assets/style/primevue-style/accordion.css b/src/assets/style/primevue-style/accordion.css new file mode 100644 index 0000000..ffca950 --- /dev/null +++ b/src/assets/style/primevue-style/accordion.css @@ -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; +} diff --git a/src/components/accordion/IVAccordion.type.ts b/src/components/accordion/IVAccordion.type.ts new file mode 100644 index 0000000..1b61e80 --- /dev/null +++ b/src/components/accordion/IVAccordion.type.ts @@ -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; +} diff --git a/src/components/accordion/VAccordion.vue b/src/components/accordion/VAccordion.vue new file mode 100644 index 0000000..1eca4ce --- /dev/null +++ b/src/components/accordion/VAccordion.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/components/accordion/VAccordionChild.vue b/src/components/accordion/VAccordionChild.vue new file mode 100644 index 0000000..e5c208d --- /dev/null +++ b/src/components/accordion/VAccordionChild.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/test/VAccordion.spec.ts b/test/VAccordion.spec.ts new file mode 100644 index 0000000..bdcf631 --- /dev/null +++ b/test/VAccordion.spec.ts @@ -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: ` + + + + + + + + + `, + }, + 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) + }) +})