diff --git a/.playground/pages/composant/checkbox.vue b/.playground/pages/composant/checkbox.vue new file mode 100644 index 0000000..211328c --- /dev/null +++ b/.playground/pages/composant/checkbox.vue @@ -0,0 +1,101 @@ + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 28cba51..546484e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Liste des évolutions de la librairie Malio layer UI ### Added * [#333] Création d'un composant text * [#364] Création d'un composant button radio +* [#337] Création d'un composant select +* [#363] Création d'un composant amount +* [#363] Création d'un composant checkbox ### Changed diff --git a/app/components/malio/Checkbox.test.ts b/app/components/malio/Checkbox.test.ts new file mode 100644 index 0000000..6fccf55 --- /dev/null +++ b/app/components/malio/Checkbox.test.ts @@ -0,0 +1,142 @@ +import {describe, expect, it} from 'vitest' +import {mount} from '@vue/test-utils' +import type {DefineComponent} from 'vue' +import Checkbox from './Checkbox.vue' + +type CheckboxProps = { + id?: string + label?: string + name?: string + modelValue?: boolean | null + inputClass?: string + labelClass?: string + groupClass?: string + required?: boolean + disabled?: boolean + readonly?: boolean + hint?: string + error?: string + success?: string +} + +const CheckboxForTest = Checkbox as DefineComponent + +const mountCheckbox = (props: CheckboxProps = {}) => + mount(CheckboxForTest, {props}) + +describe('MalioCheckbox', () => { + it('renders a checkbox input', () => { + const wrapper = mountCheckbox() + + expect(wrapper.get('input').attributes('type')).toBe('checkbox') + }) + + it('renders the label text', () => { + const wrapper = mountCheckbox({label: 'Accept terms'}) + + expect(wrapper.get('label').text()).toContain('Accept terms') + }) + + it('uses a provided id on input and label', () => { + const wrapper = mountCheckbox({ + id: 'checkbox-id', + label: 'Accept terms', + }) + + expect(wrapper.get('input').attributes('id')).toBe('checkbox-id') + expect(wrapper.get('label').attributes('for')).toBe('checkbox-id') + }) + + it('generates an id when none is provided', () => { + const wrapper = mountCheckbox({label: 'Accept terms'}) + const inputId = wrapper.get('input').attributes('id') + + expect(inputId?.startsWith('malio-checkbox-')).toBe(true) + expect(wrapper.get('label').attributes('for')).toBe(inputId) + }) + + it('applies the name attribute', () => { + const wrapper = mountCheckbox({name: 'terms'}) + + expect(wrapper.get('input').attributes('name')).toBe('terms') + }) + + it('reflects the checked state from modelValue', () => { + const wrapper = mountCheckbox({modelValue: true}) + + expect((wrapper.get('input').element as HTMLInputElement).checked).toBe(true) + }) + + it('emits update:modelValue when toggled', async () => { + const wrapper = mountCheckbox({modelValue: false}) + const input = wrapper.get('input') + + await input.setValue(true) + + expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([true]) + }) + + it('does not emit when readonly', async () => { + const wrapper = mountCheckbox({ + modelValue: true, + readonly: true, + }) + const input = wrapper.get('input') + + await input.setValue(false) + + expect(wrapper.emitted('update:modelValue')).toBeUndefined() + expect((input.element as HTMLInputElement).checked).toBe(true) + }) + + it('sets disabled and required attributes', () => { + const wrapper = mountCheckbox({ + disabled: true, + required: true, + }) + + expect(wrapper.get('input').attributes('disabled')).toBeDefined() + expect(wrapper.get('input').attributes('required')).toBeDefined() + }) + + it('shows a hint message and links it with aria-describedby', () => { + const wrapper = mountCheckbox({hint: 'Required field'}) + const inputId = wrapper.get('input').attributes('id') + + expect(wrapper.get('p').text()).toBe('Required field') + expect(wrapper.get('input').attributes('aria-describedby')).toBe(`${inputId}-describedby`) + }) + + it('shows an error state and message', () => { + const wrapper = mountCheckbox({ + label: 'Accept terms', + error: 'You must accept', + }) + + expect(wrapper.get('input').attributes('aria-invalid')).toBe('true') + expect(wrapper.get('label').classes()).toContain('text-m-error') + expect(wrapper.get('p').text()).toBe('You must accept') + }) + + it('shows success only when there is no error', () => { + const wrapper = mountCheckbox({ + success: 'Valid', + error: 'Invalid', + }) + + expect(wrapper.get('p').text()).toBe('Invalid') + expect(wrapper.get('p').classes()).toContain('text-m-error') + }) + + it('shows success styles and message when there is no error', () => { + const wrapper = mountCheckbox({ + label: 'Accept terms', + success: 'Valid', + modelValue: true, + }) + + expect(wrapper.get('label').classes()).toContain('text-m-success') + expect(wrapper.get('p').text()).toBe('Valid') + expect(wrapper.get('p').classes()).toContain('text-m-success') + }) +}) diff --git a/app/components/malio/Checkbox.vue b/app/components/malio/Checkbox.vue new file mode 100644 index 0000000..dac4e76 --- /dev/null +++ b/app/components/malio/Checkbox.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/app/story/inputCheckbox.story.vue b/app/story/inputCheckbox.story.vue new file mode 100644 index 0000000..10d3667 --- /dev/null +++ b/app/story/inputCheckbox.story.vue @@ -0,0 +1,114 @@ + + + +# MalioCheckbox + +Composant checkbox custom avec `v-model`, message d'aide, et états visuels +`error` / `success`. + +------------------------------------------------------------------------ + +## Props + +### id + +- Type: `string` +- Description: Identifiant HTML du checkbox. +- Comportement: si absent, un id unique est généré automatiquement. + +### label + +- Type: `string` +- Description: Texte affiche a cote de la case. + +### name + +- Type: `string` +- Description: Attribut `name` du champ. + +### modelValue + +- Type: `boolean | null | undefined` +- Description: État coche du composant. + +### inputClass + +- Type: `string` +- Description: Classes supplémentaires appliquées a l'input natif. + +### labelClass + +- Type: `string` +- Description: Classes supplémentaires appliquées au label. + +### groupClass + +- Type: `string` +- Description: Classes supplémentaires appliquées au conteneur. + +### required + +- Type: `boolean` +- Description: Ajoute l'attribut HTML `required`. + +### disabled + +- Type: `boolean` +- Description: Désactive le composant. + +### readonly + +- Type: `boolean` +- Description: Empêche la mise a jour du `v-model` tout en gardant + l'affichage courant. + +### hint + +- Type: `string` +- Description: Message d'aide affiche sous le checkbox. + +### error + +- Type: `string` +- Description: Message d'erreur. +- Effet: prioritaire sur `success`, applique `aria-invalid` et la couleur + d'erreur au texte et a la case. + +### success + +- Type: `string` +- Description: Message de succès. +- Effet: applique la couleur de succès au texte et a la case si `error` + est absent. + +------------------------------------------------------------------------ + +## Accessibilité + +- `aria-invalid` est active si `error` existe. +- `aria-describedby` pointe vers le message affiche. +- L'input natif reste present pour conserver le comportement formulaire. + +------------------------------------------------------------------------ + +## Event + +### update:modelValue + +- Émis a chaque changement de l'état coche. +- Retourne un booléen `true` ou `false`. + + +