diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9fa83..25e2ece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.12] - 2025-07-28 +### Added +- Modal compoenent +- Confirm modal component +- useConfirmModal composable + ## [1.0.11] - 2025-07-27 ### Added - Alert compoenent diff --git a/README.md b/README.md index 7c32bf8..3ee4794 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # visua-vue -**Current version: v1.0.11** \ No newline at end of file +**Current version: v1.0.12** \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f60c67f..195d81c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cellule-financiere-pmo/visua-vue", - "version": "1.0.11", + "version": "1.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cellule-financiere-pmo/visua-vue", - "version": "1.0.11", + "version": "1.0.12", "license": "ISC", "dependencies": { "@cellule-financiere-pmo/visua": "1.1.3", diff --git a/package.json b/package.json index 249783b..75bb529 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cellule-financiere-pmo/visua-vue", - "version": "1.0.11", + "version": "1.0.12", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.vue b/src/App.vue index 0fec676..a6a508f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,7 +10,9 @@ // import VProgressBarView from '../template/VProgressBarView.vue'; // import VMessageView from '../template/VMessageView.vue'; // import VFileUploadView from '../template/VFileUploadView.vue'; -import VAlertView from '../template/VAlertView.vue'; +// import VAlertView from '../template/VAlertView.vue'; +// import VModalView from '../template/VModalView.vue'; +import VConfirmModalView from '../template/VConfirmModalView.vue'; @@ -26,5 +28,7 @@ import VAlertView from '../template/VAlertView.vue'; - + + + diff --git a/src/assets/style/primevue-configuration.css b/src/assets/style/primevue-configuration.css index 9a185a9..9b3a74c 100644 --- a/src/assets/style/primevue-configuration.css +++ b/src/assets/style/primevue-configuration.css @@ -16,3 +16,5 @@ @import './primevue-style/fileupload.css'; @import './primevue-style/scrollpanel.css'; @import './primevue-style/toast.css'; +@import './primevue-style/dialog.css'; +@import './primevue-style/confirmdialog.css'; diff --git a/src/assets/style/primevue-style/confirmdialog.css b/src/assets/style/primevue-style/confirmdialog.css new file mode 100644 index 0000000..65c6f8b --- /dev/null +++ b/src/assets/style/primevue-style/confirmdialog.css @@ -0,0 +1,5 @@ +:root { + --p-confirmdialog-content-gap: 1rem; + --p-confirmdialog-icon-size: var(--titles-H6-XXS-size); + --p-confirmdialog-icon-color: var(--text-title-grey); +} diff --git a/src/assets/style/primevue-style/dialog.css b/src/assets/style/primevue-style/dialog.css new file mode 100644 index 0000000..dbeb53e --- /dev/null +++ b/src/assets/style/primevue-style/dialog.css @@ -0,0 +1,14 @@ +:root{ + --p-dialog-background: var(--background-lifted-grey); + --p-dialog-border-color: var(--border-default-grey); + --p-dialog-color: var(--text-title-grey); + --p-dialog-border-radius: 0px; + --p-dialog-shadow: var(--shadow); + --p-dialog-header-padding: 1rem; + --p-dialog-header-gap: 0.5rem; + --p-dialog-title-font-size: var(--titles-H4-SM-size); + --p-dialog-title-font-weight: var( --titles-H4-SM-weight); + --p-dialog-content-padding: 0 1rem 1rem 1rem; + --p-dialog-footer-padding: 1rem; + --p-dialog-footer-gap: 1rem; +} diff --git a/src/components/composable/useConfirmModal.ts b/src/components/composable/useConfirmModal.ts new file mode 100644 index 0000000..0bf333b --- /dev/null +++ b/src/components/composable/useConfirmModal.ts @@ -0,0 +1,42 @@ +import type { ConfirmationOptions } from "primevue/confirmationoptions"; +import { useConfirm } from "primevue"; +import VButton from "../button/VButton.vue"; + +export function useConfirmModal() { + const confirm = useConfirm(); + + const showConfirmModal = ({ + acceptProps = VButton, + rejectProps = VButton, + group = '', + header = '', + message = '', + icon = '', + accept = Function, + reject = Function, + onHide = Function, + onShow = Function, + modal = true, + blockScroll = true, + position = 'center', + appendTo = 'body', + }: ConfirmationOptions) => { + confirm.require({ + group, + header, + message, + icon, + rejectProps, + acceptProps, + accept, + reject, + onHide, + onShow, + modal, + blockScroll, + position, + appendTo, + }) + } + return {showConfirmModal} +} diff --git a/src/components/modal/IVModal.type.ts b/src/components/modal/IVModal.type.ts new file mode 100644 index 0000000..253df63 --- /dev/null +++ b/src/components/modal/IVModal.type.ts @@ -0,0 +1,205 @@ +import type { HTMLAttributes } from 'vue'; +import type IVButton from '../button/IVButton.type'; +import { type DialogBreakpoints } from 'primevue'; +import type { HintedString } from '@primevue/core'; + +/** + * Interface representing the properties for a Modal component. + */ +export interface IModal { + /** + * Optional unique identifier for the modal element. + */ + modalId?: string + + /** + * Flag indicating whether the modal is currently open. + */ + opened?: boolean + + /** + * List of actions (buttons) displayed in the modal. + */ + actions?: IVButton[] + + /** + * Determines if the modal should behave like an alert dialog. + */ + isAlert?: boolean + + /** + * Reference to the originating element that triggered the modal, + * used to return focus when the modal is closed. + */ + origin?: { focus: () => void } + + /** + * Title displayed at the top of the modal. + */ + title: string + + /** + * Optional icon name or path to render next to the title. + */ + icon?: string + + /** + * Defines the modal size: 'sm' = small, 'md' = medium, 'lg' = large. + */ + size?: 'sm' | 'md' | 'lg' + + /** + * Accessible label (aria-label) for the close button. + */ + closeButtonLabel?: string + + /** + * Tooltip (title attribute) shown when hovering over the close button. + */ + closeButtonTitle?: string +} + +/** + * Defines valid properties in Dialog component. + */ +export interface DialogProps { + /** + * Title content of the dialog. + */ + header?: string | undefined; + /** + * Footer content of the dialog. + */ + footer?: string | undefined; + /** + * Specifies the visibility of the dialog. + * @defaultValue false + */ + visible?: boolean | undefined; + /** + * Defines if background should be blocked when dialog is displayed. + * @defaultValue false + */ + modal?: boolean | undefined; + /** + * Style of the content section. + */ + contentStyle?: unknown; + /** + * Style class of the content section. + */ + contentClass?: unknown; + /** + * Used to pass all properties of the HTMLDivElement to the overlay Dialog inside the component. + */ + contentProps?: HTMLAttributes | undefined; + /** + * Adds a close icon to the header to hide the dialog. + * @defaultValue true + */ + closable?: boolean | undefined; + /** + * Specifies if clicking the modal background should hide the dialog. + * @defaultValue false + */ + dismissableMask?: boolean | undefined; + /** + * Specifies if pressing escape key should hide the dialog. + * @defaultValue true + */ + closeOnEscape?: boolean | undefined; + /** + * Whether to show the header or not. + * @defaultValue true + */ + showHeader?: boolean | undefined; + /** + * Whether background scroll should be blocked when dialog is visible. + * @defaultValue false + */ + blockScroll?: boolean | undefined; + /** + * Base zIndex value to use in layering. + * @defaultValue 0 + */ + baseZIndex?: number | undefined; + /** + * Whether to automatically manage layering. + * @defaultValue true + */ + autoZIndex?: boolean | undefined; + /** + * Position of the dialog. + * @defaultValue center + */ + position?: HintedString<'center' | 'top' | 'bottom' | 'left' | 'right' | 'topleft' | 'topright' | 'bottomleft' | 'bottomright'> | undefined; + /** + * Whether the dialog can be displayed full screen. + * @defaultValue false + */ + maximizable?: boolean | undefined; + /** + * Object literal to define widths per screen size. + */ + breakpoints?: DialogBreakpoints; + /** + * Enables dragging to change the position using header. + * @defaultValue true + */ + draggable?: boolean | undefined; + /** + * Keeps dialog in the viewport when dragging. + * @defaultValue true + */ + keepInViewport?: boolean | undefined; + /** + * Minimum value for the left coordinate of dialog in dragging. + * @defaultValue 0. + */ + minX?: number | undefined; + /** + * Minimum value for the top coordinate of dialog in dragging. + * @defaultValue 0 + */ + minY?: number | undefined; + /** + * A valid query selector or an HTMLElement to specify where the dialog gets attached. + * @defaultValue body + */ + appendTo?: HintedString<'body' | 'self'> | undefined | HTMLElement; + /** + * Icon to display in the dialog close button. + */ + closeIcon?: string | undefined; + /** + * Icon to display in the dialog maximize button when dialog is not maximized. + */ + maximizeIcon?: string | undefined; + /** + * Icon to display in the dialog maximize button when dialog is minimized. + */ + minimizeIcon?: string | undefined; + /** + * Used to pass all properties of the ButtonProps to the Button component. + * @type {ButtonProps} + * @defaultValue { severity: 'secondary', rounded: true, text: true } + */ + closeButtonProps?: object | undefined; + /** + * Used to pass all properties of the ButtonProps to the Button component. + * @type {ButtonProps} + * @defaultValue { severity: 'secondary', rounded: true, text: true } + */ + maximizeButtonProps?: object | undefined; +} + +/** + * Extended Modal interface that includes configuration for responsive breakpoints. + */ +export default interface IVModal extends Partial>, Partial { + /** + * Breakpoints used to control responsive modal behavior. + * Compatible with PrimeVue Dialog breakpoints format. + */ + breakpoints?: DialogBreakpoints +} diff --git a/src/components/modal/VConfirmModal.vue b/src/components/modal/VConfirmModal.vue new file mode 100644 index 0000000..1cb0459 --- /dev/null +++ b/src/components/modal/VConfirmModal.vue @@ -0,0 +1,68 @@ + + + + + + diff --git a/src/components/modal/VModal.vue b/src/components/modal/VModal.vue new file mode 100644 index 0000000..db18711 --- /dev/null +++ b/src/components/modal/VModal.vue @@ -0,0 +1,137 @@ + + + diff --git a/src/main.ts b/src/main.ts index 0197873..590d577 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,10 +3,12 @@ import { createApp } from 'vue' import App from './App.vue' import primeVue from 'primevue/config' import ToastService from 'primevue/toastservice' +import ConfirmationService from 'primevue/confirmationservice' const app = createApp(App) app.use(primeVue) app.use(ToastService) +app.use(ConfirmationService) app.mount('#app') diff --git a/test/VModal.spec.ts b/test/VModal.spec.ts new file mode 100644 index 0000000..d25fdd9 --- /dev/null +++ b/test/VModal.spec.ts @@ -0,0 +1,69 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import VModal from '../src/components/modal/VModal.vue'; +import type IVButton from '../src/components/button/IVButton.type'; + +describe('VModal.vue', () => { + let actions: IVButton[]; + let visible: boolean; + + beforeEach(() => { + visible = true; + actions = [ + { + label: 'Bouton primaire', + onClick: vi.fn(), + title: 'primaire', + }, + { + label: 'Bouton secondaire', + secondary: true, + onClick: vi.fn(), + title: 'secondaire', + }, + { + label: 'Bouton tertiaire', + tertiary: true, + onClick: vi.fn(), + title: 'tertiaire', + }, + ]; + }); + + it('emits update:visible when internal visibility changes', async () => { + const wrapper = mount(VModal, { + props: { + visible: true, + actions, + }, + }); + + // Simulation d’un changement interne + await wrapper.vm.$emit('update:visible', false); + await nextTick(); + + const emitted = wrapper.emitted('update:visible'); + expect(emitted).toBeTruthy(); + expect(emitted?.[0]).toEqual([false]); + +}); + + + it('emits lifecycle events (show/hide)', async () => { + const wrapper = mount(VModal, { + props: { + visible, + actions, + }, + }); + + wrapper.vm.$emit('show'); + wrapper.vm.$emit('hide'); + + await nextTick(); + + expect(wrapper.emitted('show')).toBeTruthy(); + expect(wrapper.emitted('hide')).toBeTruthy(); + }); +});