Merge branch 'checkbox' into 'main'

Checkbox

See merge request cellule-financiere-pmo/design-system/visua-vue!6
This commit is contained in:
Paul Valerie GOMA 2025-07-23 10:54:02 +00:00
commit 5534c2ae45
12 changed files with 427 additions and 16 deletions

View File

@ -5,6 +5,14 @@ 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.6] - 2025-07-23
### Added
- Checkbox component
### Fixed
- VLabel `required-tip` slot
- VHint UI style
## [1.0.5] - 2025-07-22
### Added
- Input component

View File

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

4
package-lock.json generated
View File

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

View File

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

View File

@ -3,7 +3,8 @@
// import VButtonGroupView from '../template/VButtonGroupView.vue';
// import VLinkView from '../template/VLinkView.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';
</script>
@ -12,5 +13,6 @@ import VInputView from '../template/VInputView.vue';
<!-- <VButtonGroupView/> -->
<!-- <VLinkView/> -->
<!-- <VAccordionView/> -->
<VInputView/>
<!-- <VInputView/> -->
<VCheckboxView/>
</template>

View File

@ -7,3 +7,4 @@
@import './primevue-style/divider.css';
@import './primevue-style/various.css';
@import './primevue-style/form.css';
@import './primevue-style/checkbox.css';

View File

@ -0,0 +1,32 @@
:root{
/* size */
--p-checkbox-width: 1.5rem;
--p-checkbox-height: 1.5rem;
--p-checkbox-sm-width: 1rem;
--p-checkbox-sm-height: 1rem;
/* border */
--p-checkbox-border-radius: 0px;
--p-checkbox-border-color: var(--border-action-high-blue-france);
--p-checkbox-hover-border-color: var(--border-action-high-blue-france);
/* background */
--p-checkbox-background: var(--background-default-grey);
/* icon */
--p-checkbox-icon-size: 1rem;
--p-checkbox-icon-sm-size: 1rem;
--p-checkbox-icon-color: var(--background-default-grey);
/* focus */
--p-checkbox-checked-focus-border-color: var(--border-action-high-blue-france);
--p-checkbox-focus-border-color: var(--border-action-high-blue-france);
--p-checkbox-focus-ring-color: var(--focus-color);
--p-checkbox-focus-ring-width: var(--focus-width);
--p-checkbox-focus-ring-style: var(--focus-style);
--p-checkbox-focus-ring-offset: var(--focus-offset);
/* checked */
--p-checkbox-checked-border-color: var(--border-action-high-blue-france);
--p-checkbox-checked-background: var(--border-active-blue-france);
--p-checkbox-icon-checked-color: var(--background-default-grey);
/* checked:hover */
--p-checkbox-icon-checked-hover-color: var(--background-default-grey);
--p-checkbox-checked-hover-border-color: var(--border-action-high-blue-france);
--p-checkbox-checked-hover-background: var(--border-active-blue-france);
}

View File

@ -0,0 +1,89 @@
/**
* Interface representing the props for a CheckBox Vue component.
*/
export default interface IVCheckBox {
/**
* The unique identifier for the checkbox element.
*/
id?: string;
/**
* The name attribute for the checkbox input.
*/
name?: string;
/**
* Indicates whether the checkbox is required in a form.
*/
required?: boolean;
/**
* The value associated with the checkbox.
*/
value?: unknown;
/**
* Whether the checkbox is initially checked.
*/
checked?: boolean;
/**
* The bound value of the checkbox, can be a boolean or an array of values.
*/
modelValue: Array<unknown> | boolean;
/**
* If true, renders the checkbox in a smaller size.
*/
small?: boolean;
/**
* If true, displays the checkbox inline with other elements.
*/
inline?: boolean;
/**
* If true, the checkbox is read-only and cannot be changed by the user.
*/
readonly?: boolean;
/**
* Opacity level applied when the checkbox is read-only.
*/
readonlyOpacity?: number;
/**
* The label text displayed next to the checkbox.
*/
label?: string;
/**
* The error message shown when validation fails.
*/
errorMessage?: string;
/**
* The message shown when the checkbox input is valid.
*/
validMessage?: string;
/**
* Additional hint or helper text displayed below the checkbox.
*/
hint?: string;
/**
* If true, disables the checkbox input.
*/
disabled?: boolean;
/**
* Visual style of the checkbox, can be 'normal', 'error', or 'success'.
*/
type?: 'normal' | 'error' | 'success';
/**
* If true, the checkbox operates in binary mode (boolean value).
*/
binary?: boolean;
}

View File

@ -0,0 +1,172 @@
<script setup lang="ts">
import Checkbox from 'primevue/checkbox';
import VLabel from '../label/VLabel.vue';
import VHint from '../hint/VHint.vue';
import type IVCheckBox from './IVCheckbox.type';
import { useId, computed, watch, ref } from 'vue';
const props = withDefaults(defineProps<IVCheckBox>(), {
id: () => useId(),
hint: '',
errorMessage: '',
validMessage: '',
label: '',
small: false,
modelValue: false,
readonlyOpacity: 0.75,
});
const size = computed(() => props.small ? 'small' : undefined)
const message = computed(() => props.errorMessage || props.validMessage)
const typeMessage = computed(() => {
if (props.errorMessage) return 'alert';
else if (props.validMessage) return 'success';
else return undefined
});
const binary = computed(() => {
if (typeof props.binary === 'boolean') return props.binary;
return typeof props.modelValue === 'boolean';
});
const labelState = computed(() => {
if((!!props.errorMessage || props.type === 'error') && !props.disabled) return 'error';
else if((!!props.validMessage || props.type === 'success') && !props.disabled) return 'success';
else return 'default';
})
const emit = defineEmits([
'update:modelValue',
'value-change',
'change',
'focus',
'blur'
])
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);
}
});
</script>
<template>
<div class="checkbox-container">
<div class="header">
<Checkbox
:id="props.id"
:name="name"
:value="value"
:size="size"
:required="required"
:readonly="readonly"
:data-testid="`input-checkbox-${id}`"
:data-test="`input-checkbox-${id}`"
:tabindex="readonly ? -1 : undefined"
v-bind="$attrs"
:disabled="disabled"
:label="props.label"
:binary="binary"
:aria-label="props.label"
:class="['p-checkbox', {
'checked-disabled': props.modelValue && disabled,
'unchecked-disabled': !props.modelValue && disabled,
'error': (!!props.errorMessage || type === 'error') && !disabled,
'success': (!!props.validMessage || type === 'success') && !disabled,
}]"
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)"
/>
<VLabel
:for="props.id"
:label="props.label"
:required="props.required"
:disabled="props.disabled"
:hint="props.hint"
:type="labelState"
class="label-container"
>
<template #required-tip v-if="props.required">
<slot name="required-tip"/>
</template>
</VLabel>
</div>
<div
v-if="message && !disabled"
aria-live="assertive"
role="alert"
>
<VHint :title="message" :type="typeMessage" icon/>
</div>
</div>
</template>
<style scoped>
.container.readonly {
pointer-events: none;
cursor: not-allowed;
opacity: v-bind('readonlyOpacity');
}
.checkbox-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: start;
gap: 0.5rem;
align-self: stretch;
}
.header{
display: flex;
flex-direction: row;
align-items: start;
gap: 0.5rem;
padding: 0px;
}
.p-checkbox{margin-top: 0.5rem;}
.required {color: var(--minor-red-marianne);}
.p-checkbox.checked-disabled {
--p-checkbox-disabled-background: var(--background-disabled-grey);
--p-checkbox-checked-disabled-border-color: var(--border-disabled-grey);
--p-checkbox-icon-disabled-color: var(--text-disabled-grey);
}
.p-checkbox.unchecked-disabled {
--p-checkbox-disabled-background: var(--background-default-grey);
--p-checkbox-checked-disabled-border-color: var(--border-disabled-grey);
}
.p-checkbox.success {
--p-checkbox-border-color: var(--border-plain-success);
--p-checkbox-hover-border-color: var(--border-plain-success);
--p-checkbox-focus-border-color: var(--border-plain-success);
--p-checkbox-checked-border-color: var(--border-plain-success);
--p-checkbox-checked-hover-border-color: var(--border-plain-success);
outline: var(--border-width) solid var(--p-checkbox-border-color);
}
.p-checkbox.error {
--p-checkbox-border-color: var(--border-plain-error);
--p-checkbox-hover-border-color: var(--border-plain-error);
--p-checkbox-focus-border-color: var(--border-plain-error);
--p-checkbox-checked-border-color: var(--border-plain-error);
--p-checkbox-checked-hover-border-color: var(--border-plain-error);
outline: var(--border-width) solid var(--p-checkbox-border-color);
}
</style>

View File

@ -39,21 +39,28 @@ const severity = computed(() => {
<Message
variant="simple"
:severity="severity"
class="p-message"
:class="[styles['text-body-XS-mention-text-Regular'], {'disabled': props.disabled }]"
:icon="props.icon ? iconClass: undefined"
:closable="false"
size="small"
role="alert"
>
{{ props.title }}
<template #icon v-if="props.icon">
<i :class="[iconClass]" style="font-size: var(--text-body-SM-detail-text-Regular-size);"></i>
</template>
</Message>
</template>
<style scoped>
.p-message.disabled {color: var(--text-disabled-grey);}
.p-message-icon{
.p-message {
display: flex;
align-items: center;
flex-direction: row;
align-items: baseline;
padding: 0px;
gap: var(--p-message-content-gap);
height: 100%;
}
</style>

View File

@ -24,12 +24,11 @@ const props = withDefaults(defineProps<IVLabel>(), {
:aria-disabled="props.disabled"
>
{{ props.label }}
<slot name="required-type">
<span
v-if="props.required"
:class="{ 'required': props.required }"
>*</span>
</slot>
<template v-if="props.required">
<span v-if="props.required" :class="{ 'required': !props.disabled}">
<slot name="required-tip">*</slot>
</span>
</template>
<VHint
v-if="props.hint"
:title="props.hint"
@ -43,5 +42,8 @@ const props = withDefaults(defineProps<IVLabel>(), {
.success {color: var(--text-default-success);}
.error {color: var(--text-default-error);}
.disabled {color: var(--text-disabled-grey);}
.required {color: var(--minor-red-marianne);}
.required {
color: var(--minor-red-marianne);
display: inline;
}
</style>

98
test/VCheckbox.spec.ts Normal file
View File

@ -0,0 +1,98 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import VCheckbox from '../src/components/checkbox/VCheckbox.vue'
import PrimeCheckbox from 'primevue/checkbox'
const globalConfig = {
global: {
components: {
Checkbox: PrimeCheckbox
},
mocks: {
$primevue: {
config: {}
}
},
stubs: {
VLabel: true,
VHint: true,
}
}
}
describe('VCheckbox.vue', () => {
it('adds value to modelValue array when checkbox is clicked (multiple mode)', async () => {
const wrapper = mount(VCheckbox, {
props: {
label: 'Fruit',
modelValue: [],
value: 'pomme'
},
global: globalConfig.global
});
const input = wrapper.find('input[type="checkbox"]');
await input.setValue(true);
const emitted = wrapper.emitted('update:modelValue');
expect(emitted).toBeTruthy();
expect(emitted?.[0]?.[0]).toContain('pomme');
});
it('removes value from modelValue array when checkbox is unchecked (multiple mode)', async () => {
const wrapper = mount(VCheckbox, {
props: {
label: 'Fruit',
modelValue: ['pomme'],
value: 'pomme'
},
global: globalConfig.global
});
const input = wrapper.find('input[type="checkbox"]');
await input.setValue(false);
const emitted = wrapper.emitted('update:modelValue');
expect(emitted).toBeTruthy();
expect(emitted?.[0]?.[0]).not.toContain('pomme');
});
it('emits change, focus and blur events', async () => {
const wrapper = mount(VCheckbox, {
props: {
label: 'Test Checkbox',
modelValue: false
},
global: globalConfig.global
});
const input = wrapper.find('input[type="checkbox"]');
await input.trigger('focus');
await input.trigger('blur');
await input.trigger('change');
expect(wrapper.emitted('focus')).toBeTruthy();
expect(wrapper.emitted('blur')).toBeTruthy();
expect(wrapper.emitted('change')).toBeTruthy();
});
it('applies error and success classes based on props', () => {
const errorWrapper = mount(VCheckbox, {
props: {
label: 'Error Checkbox',
modelValue: false,
errorMessage: 'Error occurred'
},
global: globalConfig.global
});
expect(errorWrapper.find('.p-checkbox.error').exists()).toBe(true);
const successWrapper = mount(VCheckbox, {
props: {
label: 'Success Checkbox',
modelValue: true,
validMessage: 'Valid input'
},
global: globalConfig.global
});
expect(successWrapper.find('.p-checkbox.success').exists()).toBe(true);
});
});