Merge branch 'checkbox' into 'main'
Checkbox See merge request cellule-financiere-pmo/design-system/visua-vue!6
This commit is contained in:
commit
5534c2ae45
|
@ -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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# visua-vue
|
||||
|
||||
**Current version: v1.0.5**
|
||||
**Current version: v1.0.6**
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@cellule-financiere-pmo/visua-vue",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
@import './primevue-style/divider.css';
|
||||
@import './primevue-style/various.css';
|
||||
@import './primevue-style/form.css';
|
||||
@import './primevue-style/checkbox.css';
|
||||
|
|
32
src/assets/style/primevue-style/checkbox.css
Normal file
32
src/assets/style/primevue-style/checkbox.css
Normal 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);
|
||||
}
|
89
src/components/checkbox/IVCheckbox.type.ts
Normal file
89
src/components/checkbox/IVCheckbox.type.ts
Normal 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;
|
||||
}
|
172
src/components/checkbox/VCheckbox.vue
Normal file
172
src/components/checkbox/VCheckbox.vue
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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
98
test/VCheckbox.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user