Merge branch 'modal' into 'main'
Modal See merge request cellule-financiere-pmo/design-system/visua-vue!12
This commit is contained in:
commit
88f6ee8fef
|
@ -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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# visua-vue
|
||||
|
||||
**Current version: v1.0.11**
|
||||
**Current version: v1.0.12**
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@cellule-financiere-pmo/visua-vue",
|
||||
"version": "1.0.11",
|
||||
"version": "1.0.12",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
@ -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';
|
||||
</script>
|
||||
|
||||
|
||||
|
@ -26,5 +28,7 @@ import VAlertView from '../template/VAlertView.vue';
|
|||
<!-- <VProgressBarView/> -->
|
||||
<!-- <VMessageView/> -->
|
||||
<!-- <VFileUploadView/> -->
|
||||
<VAlertView/>
|
||||
<!-- <VAlertView/> -->
|
||||
<!-- <VModalView/> -->
|
||||
<VConfirmModalView/>
|
||||
</template>
|
||||
|
|
|
@ -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';
|
||||
|
|
5
src/assets/style/primevue-style/confirmdialog.css
Normal file
5
src/assets/style/primevue-style/confirmdialog.css
Normal file
|
@ -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);
|
||||
}
|
14
src/assets/style/primevue-style/dialog.css
Normal file
14
src/assets/style/primevue-style/dialog.css
Normal file
|
@ -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;
|
||||
}
|
42
src/components/composable/useConfirmModal.ts
Normal file
42
src/components/composable/useConfirmModal.ts
Normal file
|
@ -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}
|
||||
}
|
205
src/components/modal/IVModal.type.ts
Normal file
205
src/components/modal/IVModal.type.ts
Normal file
|
@ -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<Omit<IModal, 'opened'>>, Partial<DialogProps> {
|
||||
/**
|
||||
* Breakpoints used to control responsive modal behavior.
|
||||
* Compatible with PrimeVue Dialog breakpoints format.
|
||||
*/
|
||||
breakpoints?: DialogBreakpoints
|
||||
}
|
68
src/components/modal/VConfirmModal.vue
Normal file
68
src/components/modal/VConfirmModal.vue
Normal file
|
@ -0,0 +1,68 @@
|
|||
<script setup lang="ts">
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import type { ConfirmDialogProps } from 'primevue/confirmdialog';
|
||||
import VButtonGroup from '../button/VButtonGroup.vue';
|
||||
import styles from '@visua/typography.module.css';
|
||||
|
||||
const props = withDefaults(defineProps<ConfirmDialogProps>(), {
|
||||
group: '',
|
||||
draggable: true,
|
||||
breakpoints: undefined,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmDialog
|
||||
:group="props.group"
|
||||
:draggable="props.draggable"
|
||||
:breakpoints="props.breakpoints"
|
||||
role="alert"
|
||||
>
|
||||
<template #container="slotProps">
|
||||
<div class="container">
|
||||
<div
|
||||
class='header'
|
||||
:class="[styles['titles-H6-XXS']]"
|
||||
:style="('danger' in slotProps.message.acceptProps) ? {color: 'var(--text-default-error)'} : {color: 'var(--text-default-warning)'}"
|
||||
>
|
||||
<i :class="slotProps.message.icon" style="font-weight:lighter;"></i>
|
||||
<span style="width: 100%;">{{ slotProps.message.header }}</span>
|
||||
</div>
|
||||
<span>{{ slotProps.message.message }}</span>
|
||||
<VButtonGroup
|
||||
:title="slotProps.message.group"
|
||||
inline-layout-when="always"
|
||||
size="sm"
|
||||
align="right"
|
||||
:buttons="[
|
||||
{
|
||||
...slotProps.message.acceptProps,
|
||||
onclick: slotProps.acceptCallback
|
||||
},
|
||||
{
|
||||
...slotProps.message.rejectProps,
|
||||
onclick: slotProps.rejectCallback
|
||||
}
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.container{
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--p-dialog-header-gap);
|
||||
align-items: baseline;
|
||||
}
|
||||
</style>
|
||||
|
137
src/components/modal/VModal.vue
Normal file
137
src/components/modal/VModal.vue
Normal file
|
@ -0,0 +1,137 @@
|
|||
<script setup lang="ts">
|
||||
import Dialog from 'primevue/dialog';
|
||||
import type IVModal from './IVModal.type';
|
||||
import { useId, computed, watch, ref } from 'vue';
|
||||
import VButtonGroup from '../button/VButtonGroup.vue';
|
||||
import VButton from '../button/VButton.vue';
|
||||
import styles from '@visua/typography.module.css';
|
||||
|
||||
const props = withDefaults(defineProps<IVModal>(), {
|
||||
modalId: () => useId(),
|
||||
actions: () => [],
|
||||
icon: undefined,
|
||||
size: 'md',
|
||||
closeButtonLabel: 'Fermer',
|
||||
closeButtonTitle: 'Fermer la fenêtre modale',
|
||||
title: undefined,
|
||||
visible: false,
|
||||
isAlert: false,
|
||||
breakpoints: undefined,
|
||||
modal: true,
|
||||
dismissableMask: false,
|
||||
blockScroll: true,
|
||||
position: 'center',
|
||||
maximizable: false,
|
||||
draggable: true,
|
||||
showHeader: true,
|
||||
closeOnEscape: true,
|
||||
})
|
||||
|
||||
const role = computed(() => {
|
||||
return props.isAlert ? 'alertdialog' : 'dialog'
|
||||
})
|
||||
|
||||
const modalSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'lg':
|
||||
return `width: 49.5rem;`
|
||||
case 'md':
|
||||
return `width: 36.75rem;`
|
||||
case 'sm':
|
||||
return `width: 24rem;`
|
||||
default:
|
||||
return `width: 36.75rem;`
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:visible',
|
||||
'hide',
|
||||
'after-hide',
|
||||
'show',
|
||||
'maximize',
|
||||
'unmaximize',
|
||||
'dragstart',
|
||||
'dragend'
|
||||
]);
|
||||
|
||||
const localVisible = ref(props.visible);
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if(localVisible.value !== newVal){
|
||||
localVisible.value = newVal;
|
||||
}
|
||||
})
|
||||
watch(localVisible, (newVal) => {
|
||||
if(props.visible !== newVal){
|
||||
emit('update:visible', newVal);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:header="props.title"
|
||||
id="modal"
|
||||
:modal="props.modal"
|
||||
:dismissable-mask="props.dismissableMask"
|
||||
:role="role"
|
||||
:aria-model="true"
|
||||
:aria-labelledby="`modal-${props.modalId}-dialog`"
|
||||
ref="modal"
|
||||
:style="modalSize"
|
||||
:breakpoints="props.breakpoints"
|
||||
:block-scroll="props.blockScroll"
|
||||
:position="props.position"
|
||||
:maximizable="props.maximizable"
|
||||
:draggable="props.draggable"
|
||||
:show-header="props.showHeader"
|
||||
:close-on-escape="props.closeOnEscape"
|
||||
v-model:visible="localVisible"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
@after-hide="emit('after-hide', $event)"
|
||||
@dragend="emit('dragend', $event)"
|
||||
@dragstart="emit('dragstart', $event)"
|
||||
@hide="emit('hide', $event)"
|
||||
@maximize="emit('maximize', $event)"
|
||||
@unmaximize="emit('unmaximize', $event)"
|
||||
@show="emit('show', $event)"
|
||||
>
|
||||
<template #header>
|
||||
<slot name="header" v-if="props.icon !== undefined">
|
||||
<span :class="[styles['titles-H6-XXS']]">
|
||||
<i :class="props.icon"></i>
|
||||
{{ props.title }}
|
||||
</span>
|
||||
</slot>
|
||||
</template>
|
||||
<slot name="content"/>
|
||||
<template #footer>
|
||||
<slot name="footer">
|
||||
<VButtonGroup
|
||||
v-if="props.actions?.length"
|
||||
inline-layout-when="always"
|
||||
align="right"
|
||||
:buttons="props.actions"
|
||||
size="sm"
|
||||
title="groupe de boutons"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
<template #closebutton="{closeCallback}">
|
||||
<VButton
|
||||
tertiary
|
||||
noOutline
|
||||
size="sm"
|
||||
icon="ri-close-line"
|
||||
iconRight
|
||||
@click="closeCallback"
|
||||
:label="closeButtonLabel"
|
||||
:title="closeButtonTitle"
|
||||
ref="closeBtn"
|
||||
aria-controls="modal-1"
|
||||
type="button"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
|
@ -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')
|
||||
|
|
69
test/VModal.spec.ts
Normal file
69
test/VModal.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user