Merge branch 'progress' into 'main'

Progress

See merge request cellule-financiere-pmo/design-system/visua-vue!8
This commit is contained in:
Paul Valerie GOMA 2025-07-23 17:21:48 +00:00
commit b8458d6c2f
19 changed files with 990 additions and 6 deletions

View File

@ -5,6 +5,15 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.8] - 2025-07-23
### Added
- ProgressBar component
## [1.0.7] - 2025-07-23
### Added
- Select component
- Badge component
## [1.0.6] - 2025-07-23 ## [1.0.6] - 2025-07-23
### Added ### Added
- Checkbox component - Checkbox component

View File

@ -1,3 +1,3 @@
# visua-vue # visua-vue
**Current version: v1.0.6** **Current version: v1.0.8**

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@cellule-financiere-pmo/visua-vue", "name": "@cellule-financiere-pmo/visua-vue",
"version": "1.0.6", "version": "1.0.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@cellule-financiere-pmo/visua-vue", "name": "@cellule-financiere-pmo/visua-vue",
"version": "1.0.6", "version": "1.0.8",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@cellule-financiere-pmo/visua": "1.1.3", "@cellule-financiere-pmo/visua": "1.1.3",

View File

@ -1,6 +1,6 @@
{ {
"name": "@cellule-financiere-pmo/visua-vue", "name": "@cellule-financiere-pmo/visua-vue",
"version": "1.0.6", "version": "1.0.8",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -4,7 +4,10 @@
// import VLinkView from '../template/VLinkView.vue'; // import VLinkView from '../template/VLinkView.vue';
// import VAccordionView from '../template/VAccordionView.vue'; // import VAccordionView from '../template/VAccordionView.vue';
// import VInputView from '../template/VInputView.vue'; // import VInputView from '../template/VInputView.vue';
import VCheckboxView from '../template/VCheckboxView.vue'; // import VCheckboxView from '../template/VCheckboxView.vue';
// import VBadgeView from '../template/VBadgeView.vue';
// import VSelectView from '../template/VSelectView.vue';
import VProgressBarView from '../template/VProgressBarView.vue';
</script> </script>
@ -14,5 +17,8 @@ import VCheckboxView from '../template/VCheckboxView.vue';
<!-- <VLinkView/> --> <!-- <VLinkView/> -->
<!-- <VAccordionView/> --> <!-- <VAccordionView/> -->
<!-- <VInputView/> --> <!-- <VInputView/> -->
<VCheckboxView/> <!-- <VCheckboxView/> -->
<!-- <VBadgeView/> -->
<!-- <VSelectView/> -->
<VProgressBarView/>
</template> </template>

View File

@ -8,3 +8,8 @@
@import './primevue-style/various.css'; @import './primevue-style/various.css';
@import './primevue-style/form.css'; @import './primevue-style/form.css';
@import './primevue-style/checkbox.css'; @import './primevue-style/checkbox.css';
@import './primevue-style/tag.css';
@import './primevue-style/select.css';
@import './primevue-style/overlay.css';
@import './primevue-style/iconfield.css';
@import './primevue-style/progressbar.css';

View File

@ -0,0 +1,3 @@
:root{
--p-iconfield-icon-color: var(--input-color);
}

View File

@ -0,0 +1,17 @@
:root{
--p-overlay-modal-background: var(--background-lifted-grey);
--p-overlay-modal-border-color: var(--border-default-grey);
--p-overlay-modal-color: var(--text-default-grey);
--p-overlay-popover-background: var(--background-lifted-grey);
--p-overlay-popover-border-color: var(--border-default-grey);
--p-overlay-popover-color: var(--text-default-grey);
--p-overlay-navigation-shadow: var(--shadow);
--p-overlay-modal-border-radius: 0px;
--p-overlay-modal-padding: 1.25rem;
--p-overlay-modal-shadow: var(--shadow);
--p-overlay-popover-border-radius: 0px;
--p-overlay-popover-padding: 0.75rem;
--p-overlay-popover-shadow: var(--shadow);
--p-overlay-select-border-radius: 0px;
--p-overlay-select-shadow: var(--shadow);
}

View File

@ -0,0 +1,9 @@
:root{
--p-progressbar-background: var(--background-contrast-grey);
--p-progressbar-height: 1rem;
--p-progressbar-border-radius: 0.75rem;
--p-progressbar-value-background: var(--background-active-blue-france);
--p-progressbar-label-color: var(--text-inverted-grey);
--p-progressbar-label-font-size: var(--text-body-XS-mention-text-Medium-size);
--p-progressbar-label-font-weight: var(--text-body-XS-mention-text-Medium-weight);
}

View File

@ -0,0 +1,47 @@
:root{
--p-select-background: var(--input-background);
--p-select-disabled-background: var(--input-disabled-background);
--p-select-border-color: var(--input-border-color);
--p-select-hover-border-color: var(--input-border-color);
--p-select-color: var(--input-color);
--p-select-disabled-color: var(--input-disabled-color);
--p-select-placeholder-color: var(--input-color);
--p-select-padding-x: var(--input-padding-x);
--p-select-padding-y: var(--input-padding-y);
--p-select-border-radius: var(--input-border-raduis);
--p-select-dropdown-width: 2rem;
--p-select-dropdown-color: var(--input-color);
--p-select-overlay-background: var(--input-background);
--p-select-overlay-border-color: var(--input-border-color);
--p-select-overlay-border-radius: 0px 0px 0.25rem 0.25rem;
--p-select-overlay-color: var(--input-color);
--p-select-overlay-shadow: var(--shadow);
--p-select-list-padding: 0.25rem;
--p-select-list-gap: 0.125rem;
--p-select-list-header-padding: 0.5rem 1rem;
/* options */
--p-select-option-selected-background: var(--background-active-blue-france);
--p-select-option-selected-focus-background: var(--background-active-blue-france);
--p-select-option-color: var(--input-color);
--p-select-option-focus-color: var(--input-color);
--p-select-option-selected-color: var(--text-inverted-blue-france);
--p-select-option-padding: 0.5rem 0.75rem;
--p-select-option-border-radius: 0px;
--p-select-option-group-background: var(--input-background);
--p-select-option-group-color: var(--input-color);
--p-select-option-group-font-weight: var(--text-body-MD-standard-text-Regular-weight);
--p-select-option-group-padding: 0.25rem;
--p-select-clear-icon-color: var(--input-color);
--p-select-checkmark-color: var(--input-color);
--p-select-checkmark-gutter-start: 0.125rem;
--p-select-checkmark-gutter-end: 0.125rem;
--p-select-empty-message-padding: 0.25rem;
/* focus */
--p-select-focus-border-color: var(--input-border-color);
--p-select-focus-ring-width: var(--focus-width);
--p-select-focus-ring-style: var(--focus-style);
--p-select-focus-ring-color: var(--focus-color);
--p-select-focus-ring-offset: var(--focus-offset);
--p-select-option-focus-background: var(--background-transparent-active);
--p-select-option-selected-focus-color: var(--text-inverted-blue-france);
}

View File

@ -0,0 +1,23 @@
:root {
--p-tag-icon-size: var(--text-body-MD-standard-text-Regular-size);
--p-tag-font-size: var( --text-body-SM-detail-text-Maj-size);
--p-tag-font-weight: var( --text-body-SM-detail-text-Maj-weight);
--p-tag-padding: 0.25rem 0.5rem;
--p-tag-gap: 0.25rem;
--p-tag-border-radius: 0.25rem;
/* --p-tag-rounded-border-radius:
--p-tag-contrast-background: */
--p-tag-contrast-color: var(--p-surface-0);
--p-tag-danger-background: var(--background-contrast-error);
--p-tag-danger-color: var(--text-default-error);
--p-tag-warn-background: var(--background-contrast-warning);
--p-tag-warn-color: var(--text-default-warning);
--p-tag-info-background: var(--background-contrast-info);
--p-tag-info-color: var(--text-default-info);
--p-tag-success-background: var(--background-contrast-success);
--p-tag-success-color: var(--text-default-success);
--p-tag-secondary-background: var(--illustration-color-950-tournesol-default);
--p-tag-secondary-color: var(--illustration-color-sun-tournesol-default);
--p-tag-primary-background: var(--illustration-color-950-glycine-default);
--p-tag-primary-color: var(--illustration-color-sun-glycine-default);
}

View File

@ -0,0 +1,38 @@
/**
* Interface representing the properties of a Badge component.
*/
export default interface IVBadge {
/**
* The text label displayed inside the badge.
*/
label: string;
/**
* The type of badge, which determines its color and icon.
* - `success`: Indicates a positive or successful status.
* - `error`: Indicates an error or negative status.
* - `new`: Highlights something new or recently added.
* - `info`: Provides informational context.
* - `warning`: Warns the user about a potential issue.
* - **undefined**: Defaults to a neutral badge style.
*/
type?: 'success' | 'error' | 'new' | 'info' | 'warning' | undefined;
/**
* If true, the badge will be displayed without an icon.
* Defaults to false.
*/
noIcon?: boolean;
/**
* If true, the badge will be rendered in a smaller size.
* Useful for compact UI elements.
*/
small?: boolean;
/**
* Optional maximum width for the badge.
* Accepts any valid CSS width value (e.g., '100px', '10rem', '50%').
*/
maxWidth?: string;
}

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import type IVBadge from './IVBadge.type';
import Tag from 'primevue/tag';
import { computed } from 'vue';
const props = withDefaults(defineProps<IVBadge>(), {
type: undefined,
noIcon: false,
small: false,
})
const severity = computed(() => {
switch (props.type) {
case 'error': return 'danger';
case 'warning' : return 'warn';
case 'success' : return 'success';
case 'info': return 'info';
case 'new': return 'secondary';
default:
return undefined;
}
});
const icon = computed(() => {
switch (props.type) {
case 'error': return 'ri-close-circle-line';
case 'warning' : return 'ri-alert-line';
case 'success' : return 'ri-checkbox-circle-line';
case 'info': return 'ri-information-line';
case 'new': return 'ri-flashlight-line';
default:
return undefined;
}
})
const limit = computed(() => props.maxWidth);
</script>
<template>
<Tag
role="alert"
:value="props.label"
:severity="severity"
:title="props.label"
class="p-tag"
:class="{'small': props.small}"
>
<i v-if="!props.noIcon || props.type === undefined" :class="icon"></i>
<span :class="{'limit': props.maxWidth}">{{ props.label }}</span>
</Tag>
</template>
<style lang="css" scoped>
.p-tag{height: fit-content;}
.p-tag.small{
--p-tag-icon-size: var( --text-body-SM-detail-text-Maj-size);
--p-tag-font-size: var( --text-body-XS-mention-text-Maj-size);
--p-tag-font-weight: var( --text-body-XS-mention-text-Maj-weight);
--p-tag-padding: 0.125rem 0.325rem;
align-self: center;
}
.limit{
max-width: v-bind(limit);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@ -0,0 +1,30 @@
/**
* Interface representing the properties of a ProgressBar component.
*/
export default interface IVProgressBar {
/**
* Whether to display the numeric value of the progress.
* If true, the value will be shown alongside the progress bar.
* @default false
*/
showValue?: boolean;
/**
* The current progress value, typically between 0 and 100.
*/
value: number;
/**
* If true, the progress bar will be in indeterminate mode,
* indicating ongoing activity without a specific completion percentage.
* @default false
*/
indeterminate?: boolean;
/**
* If true, renders a smaller version of the progress bar.
* Useful for compact UI layouts.
* @default false
*/
small?: boolean;
}

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import ProgressBar from 'primevue/progressbar';
import type IVProgressBar from './IVProgressBar.type';
import { computed } from 'vue';
const props = withDefaults(defineProps<IVProgressBar>(), {
value: 0,
showValue: false,
indeterminate: false,
small: false,
});
const mode = computed(() => (props.indeterminate ? 'indeterminate' : 'determinate'));
</script>
<template>
<ProgressBar
role="progressbar"
:mode="mode"
:value="props.value"
:show-value="props.showValue && !props.small"
:class="['p-progressbar', {small: props.small}]"
style="width: 100%;"
>
<slot/>
</ProgressBar>
</template>
<style lang="css" scoped>
.p-progressbar.small {--p-progressbar-height: 0.5rem;}
</style>

View File

@ -0,0 +1,351 @@
import type { HintedString } from '@primevue/core';
import type { VirtualScrollerProps } from 'primevue/virtualscroller';
/**
* Represents the props for a custom VSelect component.
*/
export type option = string | number | null | undefined;
export interface ISelect {
/**
* Whether the select field is required.
*/
required?: boolean;
/**
* Whether the select field is disabled.
*/
disabled?: boolean;
/**
* The unique ID for the select element.
*/
selectId?: string;
/**
* The name attribute for the select element.
*/
name?: string;
/**
* Optional hint text displayed below the select field.
*/
hint?: string;
/**
* The currently selected value.
*/
modelValue?: option;
/**
* The label displayed above the select field.
*/
label?: string;
/**
* The list of options to display in the dropdown.
* Can be an array of strings, numbers, or objects.
*/
options?: Array<
string | number | Record<string, unknown> | {
value: unknown;
text: string;
disabled: boolean;
}
>;
/**
* Defines how to extract the label from an option.
* Can be a string key or a function.
*/
optionLabel?: string | ((option: unknown) => string);
/**
* Defines how to extract the value from an option.
* Can be a string key or a function.
*/
optionValue?: string | ((option: unknown) => unknown);
/**
* Message displayed when the selection is valid.
*/
successMessage?: string;
/**
* Message displayed when the selection is invalid.
*/
errorMessage?: string;
/**
* Text displayed when no option is selected.
*/
defaultUnselectedText?: string;
}
export interface SelectProps {
/**
* Value of the component.
*/
modelValue?: unknown;
/**
* The default value for the input when not controlled by `modelValue`.
*/
defaultValue?: unknown;
/**
* The name attribute for the element, typically used in form submissions.
*/
name?: string | undefined;
/**
* An array of select items to display as the available options.
*/
options?: unknown[];
/**
* Property name or getter function to use as the label of an option.
*/
optionLabel?: string | ((data: unknown) => string) | undefined;
/**
* Property name or getter function to use as the value of an option, defaults to the option itself when not defined.
*/
optionValue?: string | ((data: unknown) => unknown) | undefined;
/**
* Property name or getter function to use as the disabled flag of an option, defaults to false when not defined.
*/
optionDisabled?: string | ((data: unknown) => boolean) | undefined;
/**
* Property name or getter function to use as the label of an option group.
*/
optionGroupLabel?: string | ((data: unknown) => string) | undefined;
/**
* Property name or getter function that refers to the children options of option group.
*/
optionGroupChildren?: string | ((data: unknown) => unknown[]) | undefined;
/**
* Height of the viewport, a scrollbar is defined if height of list exceeds this value.
* @defaultValue 14rem
*/
scrollHeight?: string | undefined;
/**
* When specified, displays a filter input at header.
* @defaultValue false
*/
filter?: boolean | undefined;
/**
* Placeholder text to show when filter input is empty.
*/
filterPlaceholder?: string | undefined;
/**
* Locale to use in filtering. The default locale is the host environment's current locale.
*/
filterLocale?: string | undefined;
/**
* Defines the filtering algorithm to use when searching the options.
* @defaultValue contains
*/
filterMatchMode?: HintedString<'contains' | 'startsWith' | 'endsWith'> | undefined;
/**
* Fields used when filtering the options, defaults to optionLabel.
*/
filterFields?: string[] | undefined;
/**
* When present, custom value instead of predefined options can be entered using the editable input field.
* @defaultValue false
*/
editable?: boolean | undefined;
/**
* Default text to display when no option is selected.
*/
placeholder?: string | undefined;
/**
* Defines the size of the component.
*/
size?: HintedString<'small' | 'large'> | undefined;
/**
* When present, it specifies that the component should have invalid state style.
* @defaultValue false
*/
invalid?: boolean | undefined;
/**
* When present, it specifies that the component should be disabled.
* @defaultValue false
*/
disabled?: boolean | undefined;
/**
* Specifies the input variant of the component.
* @defaultValue null
*/
variant?: HintedString<'outlined' | 'filled'> | undefined | null;
/**
* A property to uniquely identify an option.
*/
dataKey?: string | undefined;
/**
* When enabled, a clear icon is displayed to clear the value.
* @defaultValue false
*/
showClear?: boolean | undefined;
/**
* Spans 100% width of the container when enabled.
* @defaultValue null
*/
fluid?: boolean | undefined;
/**
* @deprecated since v4.0. Use 'labelId' instead.
* Identifier of the underlying input element.
*/
inputId?: string | undefined;
/**
* @deprecated since v4.0. Use 'labelStyle' instead.
* Inline style of the input field.
*/
inputStyle?: object | undefined;
/**
* @deprecated since v4.0. Use 'labelClass' instead.
* Style class of the input field.
*/
inputClass?: string | object | undefined;
/**
* Identifier of the underlying label element.
*/
labelId?: string | undefined;
/**
* Inline style of the label field.
*/
labelStyle?: object | undefined;
/**
* Style class of the label field.
*/
labelClass?: string | object | undefined;
/**
* @deprecated since v4.0. Use 'overlayStyle' instead.
* Inline style of the overlay panel.
*/
panelStyle?: object | undefined;
/**
* @deprecated since v4.0. Use 'overlayClass' instead.
* Style class of the overlay panel.
*/
panelClass?: string | object | undefined;
/**
* Inline style of the overlay.
*/
overlayStyle?: object | undefined;
/**
* Style class of the overlay.
*/
overlayClass?: string | object | undefined;
/**
* A valid query selector or an HTMLElement to specify where the overlay gets attached.
* @defaultValue body
*/
appendTo?: HintedString<'body' | 'self'> | undefined | HTMLElement;
/**
* Whether the select is in loading state.
* @defaultValue false
*/
loading?: boolean | undefined;
/**
* Icon to display in clear button.
*/
clearIcon?: string | undefined;
/**
* Icon to display in the select.
*/
dropdownIcon?: string | undefined;
/**
* Icon to display in filter input.
*/
filterIcon?: string | undefined;
/**
* Icon to display in loading state.
*/
loadingIcon?: string | undefined;
/**
* Clears the filter value when hiding the select.
* @defaultValue false
*/
resetFilterOnHide?: boolean;
/**
* Clears the filter value when clicking on the clear icon.
* @defaultValue false
*/
resetFilterOnClear?: boolean;
/**
* Whether to use the virtualScroller feature. The properties of VirtualScroller component can be used like an object in it.
*/
virtualScrollerOptions?: VirtualScrollerProps;
/**
* Whether to focus on the first visible or selected element when the overlay panel is shown.
* @defaultValue false
*/
autoOptionFocus?: boolean | undefined;
/**
* Whether to focus on the filter element when the overlay panel is shown.
* @defaultValue false
*/
autoFilterFocus?: boolean | undefined;
/**
* When enabled, the focused option is selected.
* @defaultValue false
*/
selectOnFocus?: boolean | undefined;
/**
* When enabled, the focus is placed on the hovered option.
* @defaultValue true
*/
focusOnHover?: boolean | undefined;
/**
* Whether the selected option will be add highlight class.
* @defaultValue true
*/
highlightOnSelect?: boolean | undefined;
/**
* Whether the selected option will be shown with a check mark.
* @defaultValue false
*/
checkmark?: boolean | undefined;
/**
* Text to be displayed in hidden accessible field when filtering returns any results. Defaults to value from PrimeVue locale configuration.
* @defaultValue '{0} results are available'
*/
filterMessage?: string | undefined;
/**
* Text to be displayed in hidden accessible field when options are selected. Defaults to value from PrimeVue locale configuration.
* @defaultValue '{0} items selected'
*/
selectionMessage?: string | undefined;
/**
* Text to be displayed in hidden accessible field when any option is not selected. Defaults to value from PrimeVue locale configuration.
* @defaultValue No selected item
*/
emptySelectionMessage?: string | undefined;
/**
* Text to display when filtering does not return any results. Defaults to value from PrimeVue locale configuration.
* @defaultValue No results found
*/
emptyFilterMessage?: string | undefined;
/**
* Text to display when there are no options available. Defaults to value from PrimeVue locale configuration.
* @defaultValue No available options
*/
emptyMessage?: string | undefined;
/**
* Index of the element in tabbing order.
*/
tabindex?: number | string | undefined;
/**
* Defines a string value that labels an interactive element.
*/
ariaLabel?: string | undefined;
/**
* Identifier of the underlying input element.
*/
ariaLabelledby?: string | undefined;
/**
* Form control object, typically used for handling validation and form state.
*/
formControl?: Record<string, unknown> | undefined;
}
export default interface IVSelect extends Partial<Omit<SelectProps, 'modelValue' | 'options'>>, Partial<ISelect> {
optionTemplate?: boolean
}

View File

@ -0,0 +1,186 @@
<script setup lang="ts">
import Select from 'primevue/select';
import type IVSelect from './IVSelect.type';
import { useId, computed, watch, ref } from 'vue';
import VLabel from '../label/VLabel.vue';
import VHint from '../hint/VHint.vue';
import styles from '@visua/typography.module.css';
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<IVSelect>(), {
hint: '',
successMessage: '',
errorMessage: '',
selectId: () => `select-${useId()}`,
label: '',
required: false,
disabled: false,
options: undefined,
name: '',
defaultUnselectedText: 'Sélectionner une option',
filter: false,
editable: false,
optionLabel: undefined,
optionValue: undefined,
filterMessage: 'Les résultats sont disponibles',
selectionMessage: 'Elements sélectionnés',
emptySelectionMessage: 'Aucun élément sélectionné',
emptyFilterMessage: 'Aucun résultat trouvé',
emptyMessage: 'Aucune option disponible',
optionTemplate: false,
})
const emit = defineEmits([
'update:modelValue',
'value-change',
'change',
'focus',
'blur',
'before-show',
'before-hide',
'show',
'hide',
'filter',
])
const localModelValue = ref(props.modelValue);
watch(() => props.modelValue, (newVal) => {
if(localModelValue.value !== newVal){
localModelValue.value = newVal;
}
})
watch(localModelValue, (newVal) => {
if(props.modelValue !== newVal){
emit('update:modelValue', newVal);
}
});
const message = computed(() => {
return props.errorMessage || props.successMessage
})
const messageState = computed(() => {
return props.errorMessage ? 'error' : 'valid'
})
const typeMessage = computed(() => {
if (props.errorMessage) return 'alert';
else if (props.successMessage) return 'success';
else return undefined;
});
const labelState = computed(() => {
if(!props.errorMessage && !props.successMessage && !props.disabled) return 'default';
else if(!props.errorMessage && props.successMessage && !props.disabled) return 'success';
else if(props.errorMessage && !props.successMessage && !props.disabled) return 'error';
else return undefined
})
</script>
<template>
<div class="main-container">
<VLabel
:for="props.selectId"
:label="props.label"
:required="!props.disabled"
:disabled="props.disabled"
:type="labelState"
:hint="props.hint"
>
<template #required-type v-if="props.required">
<slot name="required-type"/>
</template>
</VLabel>
<Select
:name="props.name || props.selectId"
:options="props.options"
:optionLabel="props.optionLabel"
:optionValue="props.optionValue"
:disabled="props.disabled"
:aria-disabled="props.disabled"
:filter="props.filter"
:editable="props.editable"
:placeholder="props.placeholder"
:filter-message="props.filterMessage"
:selection-message="props.selectionMessage"
:empty-selection-message="props.emptySelectionMessage"
:empty-filter-message="props.emptyFilterMessage"
:empty-message="props.emptyMessage"
:show-clear="props.showClear"
:filter-fields="props.filterFields"
:filter-icon="props.filterIcon"
:filter-locale="props.filterLocale"
:filter-match-mode="props.filterMatchMode"
:filter-placeholder="props.filterPlaceholder"
:scroll-height="props.scrollHeight"
:data-key="props.dataKey"
:loading="props.loading"
v-bind="$attrs"
v-model:model-value="localModelValue"
@update:model-value="emit('update:modelValue', $event)"
@change="emit('change', $event)"
@blur="emit('blur', $event)"
@focus="emit('focus', $event)"
@value-change="emit('value-change', $event)"
@before-hide="emit('before-hide', $event)"
@before-show="emit('before-show', $event)"
@show="emit('show', $event)"
@hide="emit('hide', $event)"
@filter="emit('filter', $event)"
class="p-select"
:class="[
styles['text-body-MD-standard-text-Regular'],
{
'disabled': props.disabled,
'error': props.errorMessage && !props.successMessage && !props.disabled,
'success': !props.errorMessage && props.successMessage && !props.disabled,
}
]"
>
<template v-if="props.optionTemplate" #option="{option, selected, index}">
<slot name="option" :option="option" :selected="selected" :index="index"/>
</template>
</Select>
<div
:id="`select-${messageState}-desc-${messageState}`"
role="alert"
aria-live="assertive"
>
<VHint :title="message" :type="typeMessage" icon/>
</div>
</div>
</template>
<style lang="css" scoped>
.main-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: start;
gap: 0.5rem;
}
.p-select{
width: 100%;
height: 2.5rem;
}
.p-select.disabled{
--p-select-border-color: var(--border-disabled-grey);
--p-select-dropdown-color: var(--text-disabled-grey);
}
.p-select.error{
--p-select-border-color: var(--border-plain-error);
--p-select-hover-border-color: var(--border-plain-error);
--p-select-focus-border-color: var(--border-plain-error);
}
.p-select.success{
--p-select-border-color: var(--border-plain-success);
--p-select-hover-border-color: var(--border-plain-success);
--p-select-focus-border-color: var(--border-plain-success);
}
</style>

57
test/VProgressBar.spec.ts Normal file
View File

@ -0,0 +1,57 @@
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import VProgressBar from '../src/components/progressbar/VProgressBar.vue';
describe('VProgressBar', () => {
it('renders with default props', () => {
const wrapper = mount(VProgressBar);
const progressBar = wrapper.findComponent({ name: 'ProgressBar' });
expect(progressBar.exists()).toBe(true);
expect(progressBar.props('value')).toBe(0);
expect(progressBar.props('mode')).toBe('determinate');
expect(progressBar.props('showValue')).toBe(false);
});
it('renders with custom value', () => {
const wrapper = mount(VProgressBar, {
props: { value: 75 }
});
expect(wrapper.findComponent({ name: 'ProgressBar' }).props('value')).toBe(75);
});
it('renders in indeterminate mode', () => {
const wrapper = mount(VProgressBar, {
props: { indeterminate: true, value: 75 }
});
expect(wrapper.findComponent({ name: 'ProgressBar' }).props('mode')).toBe('indeterminate');
});
it('hides value when small is true even if showValue is true', () => {
const wrapper = mount(VProgressBar, {
props: { showValue: true, small: true, value: 25 }
});
expect(wrapper.findComponent({ name: 'ProgressBar' }).props('showValue')).toBe(false);
});
it('applies small class when small is true', () => {
const wrapper = mount(VProgressBar, {
props: { small: true, value: 15 }
});
expect(wrapper.find('.p-progressbar').classes()).toContain('small');
});
it('renders slot content', () => {
const wrapper = mount(VProgressBar, {
props: { value: 43, showValue: true },
slots: {
default: 'Valeur: 43/100'
}
});
expect(wrapper.html()).toContain('Valeur: 43/100');
});
});

101
test/VSelect.spec.ts Normal file
View File

@ -0,0 +1,101 @@
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import VSelect from '../src/components/select/VSelect.vue';
import Select from 'primevue/select';
// Mock global matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
const globalConfig = {
global: {
components: {
Checkbox: Select
},
mocks: {
$primevue: {
config: {}
}
},
stubs: {
VLabel: false,
VHint: true,
}
}
}
describe('VSelect.vue', () => {
const options = [
{ value: 'apple', text: 'Apple' },
{ value: 'banana', text: 'Banana' },
{ value: 'cherry', text: 'Cherry' }
];
it('renders with basic props', () => {
const wrapper = mount(VSelect, {
props: {
label: 'Fruits',
options,
modelValue: null
},
global: globalConfig.global
});
const label = wrapper.findComponent({ name: 'VLabel' });
expect(label.exists()).toBe(true);
expect(label.props('label')).toBe('Fruits');
});
it('emits update:modelValue and change when value is selected', async () => {
const wrapper = mount(VSelect, {
props: {
options,
modelValue: null
},
global: globalConfig.global
});
// Simule la sélection d'une option
wrapper.vm.$emit('update:modelValue', 'banana');
wrapper.vm.$emit('change', { value: 'banana' });
const emittedUpdate = wrapper.emitted('update:modelValue');
const emittedChange = wrapper.emitted('change');
expect(emittedUpdate).toBeTruthy();
expect(emittedUpdate![0]).toEqual(['banana']);
expect(emittedChange).toBeTruthy();
expect(emittedChange![0]).toEqual([{ value: 'banana' }]);
});
it('reacts to external modelValue changes', async () => {
const wrapper = mount(VSelect, {
props: {
options,
modelValue: 'apple'
},
global: globalConfig.global
});
await wrapper.setProps({ modelValue: 'cherry' });
expect(wrapper.emitted('update:modelValue')).toBeFalsy();
const select = wrapper.findComponent({ name: 'Select' });
expect(select.props('modelValue')).toBe('cherry');
});
});