Compare commits
8 Commits
v1.1.0
...
88ca968ae3
| Author | SHA1 | Date | |
|---|---|---|---|
| 88ca968ae3 | |||
| 6092b3f2c9 | |||
| 77364daa67 | |||
| f11117ea41 | |||
| 1ab7b2427a | |||
| 82ecc9cfe2 | |||
| 65d9060e26 | |||
| ec4c157226 |
@@ -1,111 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="grid grid-cols-2 gap-6 md:grid-cols-3">
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Simple</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`simple-${option.value}`"
|
|
||||||
v-model="primaryChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="primary-choice"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Preselected</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`preselected-${option.value}`"
|
|
||||||
v-model="preselectedChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="preselected-choice"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Error</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`error-${option.value}`"
|
|
||||||
v-model="errorChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="error-choice"
|
|
||||||
error="Selection required"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Success</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`success-${option.value}`"
|
|
||||||
v-model="successChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="success-choice"
|
|
||||||
success="Selection saved"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Disabled</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`disabled-${option.value}`"
|
|
||||||
v-model="disabledChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="disabled-choice"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Readonly</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`readonly-${option.value}`"
|
|
||||||
v-model="readonlyChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="readonly-choice"
|
|
||||||
readonly
|
|
||||||
hint="Readonly group"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {ref} from 'vue'
|
|
||||||
import MalioRadioButton from '../../../app/components/malio/RadioButton.vue'
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{label: 'Option 1', value: 'option1'},
|
|
||||||
{label: 'Option 2', value: 'option2'},
|
|
||||||
{label: 'Option 3', value: 'option3'},
|
|
||||||
{label: 'Option 4', value: 'option4'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const primaryChoice = ref<string | null>(null)
|
|
||||||
const preselectedChoice = ref<string | null>('option2')
|
|
||||||
const errorChoice = ref<string | null>(null)
|
|
||||||
const successChoice = ref<string | null>('option3')
|
|
||||||
const disabledChoice = ref<string | null>('option2')
|
|
||||||
const readonlyChoice = ref<string | null>('option4')
|
|
||||||
</script>
|
|
||||||
@@ -7,7 +7,6 @@ Liste des évolutions de la librairie Malio layer UI
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
* [#333] Création d'un composant text
|
* [#333] Création d'un composant text
|
||||||
* [#364] Création d'un composant button radio
|
|
||||||
* [#337] Création d'un composant select
|
* [#337] Création d'un composant select
|
||||||
* [#363] Création d'un composant amount
|
* [#363] Création d'un composant amount
|
||||||
* [#363] Création d'un composant checkbox
|
* [#363] Création d'un composant checkbox
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
import {describe, expect, it} from 'vitest'
|
|
||||||
import {mount} from '@vue/test-utils'
|
|
||||||
import type {DefineComponent} from 'vue'
|
|
||||||
import RadioButton from './RadioButton.vue'
|
|
||||||
|
|
||||||
type RadioButtonProps = {
|
|
||||||
id?: string
|
|
||||||
label?: string
|
|
||||||
name?: string
|
|
||||||
modelValue?: string | number | boolean | null | undefined
|
|
||||||
value?: string | number | boolean | null | undefined
|
|
||||||
inputClass?: string
|
|
||||||
labelClass?: string
|
|
||||||
groupClass?: string
|
|
||||||
required?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
readonly?: boolean
|
|
||||||
hint?: string
|
|
||||||
error?: string
|
|
||||||
success?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const RadioButtonForTest = RadioButton as DefineComponent<RadioButtonProps>
|
|
||||||
|
|
||||||
const mountRadioButton = (props: RadioButtonProps = {}) =>
|
|
||||||
mount(RadioButtonForTest, {
|
|
||||||
props,
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('MalioRadioButton', () => {
|
|
||||||
it('renders the label text', () => {
|
|
||||||
const wrapper = mountRadioButton({label: 'Option 1'})
|
|
||||||
|
|
||||||
expect(wrapper.get('.radio-text').text()).toBe('Option 1')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('applies provided id to input and label', () => {
|
|
||||||
const wrapper = mountRadioButton({id: 'radio-id', label: 'Option 1'})
|
|
||||||
|
|
||||||
expect(wrapper.get('input').attributes('id')).toBe('radio-id')
|
|
||||||
expect(wrapper.get('.radio-text').attributes('for')).toBe('radio-id')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('generates an id when missing and reuses it on label', () => {
|
|
||||||
const wrapper = mountRadioButton({label: 'Option 1'})
|
|
||||||
|
|
||||||
const inputId = wrapper.get('input').attributes('id')
|
|
||||||
|
|
||||||
expect(inputId?.startsWith('malio-radio-')).toBe(true)
|
|
||||||
expect(wrapper.get('.radio-text').attributes('for')).toBe(inputId)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('applies the name attribute', () => {
|
|
||||||
const wrapper = mountRadioButton({name: 'choice-group'})
|
|
||||||
|
|
||||||
expect(wrapper.get('input').attributes('name')).toBe('choice-group')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('sets required when true', () => {
|
|
||||||
const wrapper = mountRadioButton({required: true})
|
|
||||||
|
|
||||||
expect(wrapper.get('input').attributes('required')).toBeDefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('sets disabled styles when true', () => {
|
|
||||||
const wrapper = mountRadioButton({disabled: true, label: 'Option 1'})
|
|
||||||
|
|
||||||
expect(wrapper.get('input').attributes('disabled')).toBeDefined()
|
|
||||||
expect(wrapper.get('.radio-control').classes()).toContain('is-disabled')
|
|
||||||
expect(wrapper.get('.radio-text').classes()).toContain('cursor-not-allowed')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('checks the input when modelValue matches value', () => {
|
|
||||||
const wrapper = mountRadioButton({modelValue: 'a', value: 'a'})
|
|
||||||
|
|
||||||
expect((wrapper.get('input').element as HTMLInputElement).checked).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('emits update:modelValue on change', async () => {
|
|
||||||
const wrapper = mountRadioButton({modelValue: 'a', value: 'b'})
|
|
||||||
|
|
||||||
await wrapper.get('input').trigger('change')
|
|
||||||
|
|
||||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['b'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('prevents updates when readonly', async () => {
|
|
||||||
const wrapper = mountRadioButton({modelValue: 'a', value: 'b', readonly: true})
|
|
||||||
const input = wrapper.get('input')
|
|
||||||
|
|
||||||
;(input.element as HTMLInputElement).checked = true
|
|
||||||
await input.trigger('change')
|
|
||||||
|
|
||||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
||||||
expect((input.element as HTMLInputElement).checked).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows hint message and wires aria-describedby', () => {
|
|
||||||
const wrapper = mountRadioButton({hint: 'Helpful hint'})
|
|
||||||
const input = wrapper.get('input')
|
|
||||||
const message = wrapper.get('.radio-message')
|
|
||||||
|
|
||||||
expect(message.text()).toBe('Helpful hint')
|
|
||||||
expect(input.attributes('aria-describedby')).toBe(message.attributes('id'))
|
|
||||||
expect(input.attributes('aria-invalid')).toBe('false')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows error state on control, label and helper text', () => {
|
|
||||||
const wrapper = mountRadioButton({
|
|
||||||
label: 'Option 1',
|
|
||||||
error: 'Selection required',
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.get('.radio-control').classes()).toContain('is-error')
|
|
||||||
expect(wrapper.get('.radio-text').classes()).toContain('text-m-error')
|
|
||||||
expect(wrapper.get('.radio-message').classes()).toContain('text-m-error')
|
|
||||||
expect(wrapper.get('input').attributes('aria-invalid')).toBe('true')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows success state when no error is present', () => {
|
|
||||||
const wrapper = mountRadioButton({
|
|
||||||
label: 'Option 1',
|
|
||||||
success: 'Selection saved',
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.get('.radio-control').classes()).toContain('is-success')
|
|
||||||
expect(wrapper.get('.radio-text').classes()).toContain('text-m-success')
|
|
||||||
expect(wrapper.get('.radio-message').classes()).toContain('text-m-success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('prioritizes error over success', () => {
|
|
||||||
const wrapper = mountRadioButton({
|
|
||||||
error: 'Selection required',
|
|
||||||
success: 'Selection saved',
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.get('.radio-control').classes()).toContain('is-error')
|
|
||||||
expect(wrapper.get('.radio-control').classes()).not.toContain('is-success')
|
|
||||||
expect(wrapper.get('.radio-message').text()).toBe('Selection required')
|
|
||||||
expect(wrapper.get('.radio-message').classes()).toContain('text-m-error')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('merges custom classes on group, input and label', () => {
|
|
||||||
const wrapper = mountRadioButton({
|
|
||||||
label: 'Option 1',
|
|
||||||
groupClass: 'mt-0 custom-group',
|
|
||||||
inputClass: 'border-red-500',
|
|
||||||
labelClass: 'font-bold',
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.get('.radio-item').classes()).toContain('custom-group')
|
|
||||||
expect(wrapper.get('.radio-item').classes()).toContain('mt-0')
|
|
||||||
expect(wrapper.get('input').classes()).toContain('border-red-500')
|
|
||||||
expect(wrapper.get('.radio-text').classes()).toContain('font-bold')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div :class="mergedGroupClass">
|
|
||||||
<div :class="mergedControlClass">
|
|
||||||
<label :for="inputId" class="radio-indicator relative flex cursor-pointer items-center p-3">
|
|
||||||
<input
|
|
||||||
:id="inputId"
|
|
||||||
:name="name"
|
|
||||||
:value="value"
|
|
||||||
:checked="isChecked"
|
|
||||||
:required="required"
|
|
||||||
:disabled="disabled"
|
|
||||||
:aria-invalid="!!error"
|
|
||||||
:aria-describedby="describedBy"
|
|
||||||
:class="mergedInputClass"
|
|
||||||
v-bind="attrs"
|
|
||||||
type="radio"
|
|
||||||
@click="onClick"
|
|
||||||
@change="onChange"
|
|
||||||
>
|
|
||||||
<span class="radio-dot pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-black opacity-0">
|
|
||||||
<svg viewBox="0 0 16 16" fill="currentColor" class="h-[10px]">
|
|
||||||
<circle cx="8" cy="8" r="8" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
v-if="label"
|
|
||||||
:for="inputId"
|
|
||||||
:class="mergedLabelClass"
|
|
||||||
>
|
|
||||||
{{ label }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p
|
|
||||||
v-if="shouldShowMessage"
|
|
||||||
:id="`${inputId}-describedby`"
|
|
||||||
:class="mergedMessageClass"
|
|
||||||
>
|
|
||||||
{{ error || success || hint }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {computed, useAttrs, useId} from 'vue'
|
|
||||||
import {twMerge} from 'tailwind-merge'
|
|
||||||
|
|
||||||
defineOptions({name: 'MalioRadioButton', inheritAttrs: false})
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
id?: string
|
|
||||||
label?: string
|
|
||||||
name?: string
|
|
||||||
modelValue?: string | number | boolean | null | undefined
|
|
||||||
value?: string | number | boolean | null | undefined
|
|
||||||
inputClass?: string
|
|
||||||
labelClass?: string
|
|
||||||
groupClass?: string
|
|
||||||
required?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
readonly?: boolean
|
|
||||||
hint?: string
|
|
||||||
error?: string
|
|
||||||
success?: string
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
id: '',
|
|
||||||
label: '',
|
|
||||||
name: '',
|
|
||||||
modelValue: undefined,
|
|
||||||
value: undefined,
|
|
||||||
inputClass: '',
|
|
||||||
labelClass: '',
|
|
||||||
groupClass: '',
|
|
||||||
required: false,
|
|
||||||
disabled: false,
|
|
||||||
readonly: false,
|
|
||||||
hint: '',
|
|
||||||
error: '',
|
|
||||||
success: '',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const attrs = useAttrs()
|
|
||||||
const generatedId = useId()
|
|
||||||
|
|
||||||
const inputId = computed(() => props.id?.toString() || `malio-radio-${generatedId}`)
|
|
||||||
const isChecked = computed(() => props.modelValue === props.value)
|
|
||||||
const hasError = computed(() => !!props.error)
|
|
||||||
const hasSuccess = computed(() => !!props.success && !hasError.value)
|
|
||||||
const disabled = computed(() => props.disabled)
|
|
||||||
const shouldShowMessage = computed(() => !!(props.hint || hasError.value || hasSuccess.value))
|
|
||||||
|
|
||||||
const describedBy = computed(() => {
|
|
||||||
if (!shouldShowMessage.value) return undefined
|
|
||||||
return `${inputId.value}-describedby`
|
|
||||||
})
|
|
||||||
|
|
||||||
const mergedGroupClass = computed(() =>
|
|
||||||
twMerge(
|
|
||||||
'radio-item mt-4 w-full',
|
|
||||||
props.groupClass,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const mergedControlClass = computed(() =>
|
|
||||||
twMerge(
|
|
||||||
'radio-control flex items-center',
|
|
||||||
hasError.value ? 'is-error' : '',
|
|
||||||
hasSuccess.value ? 'is-success' : '',
|
|
||||||
disabled.value ? 'is-disabled' : '',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const mergedInputClass = computed(() =>
|
|
||||||
twMerge(
|
|
||||||
'h-5 w-5 cursor-pointer appearance-none rounded-full border-2 border-black',
|
|
||||||
props.inputClass,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const mergedLabelClass = computed(() =>
|
|
||||||
twMerge(
|
|
||||||
'radio-text mt-px cursor-pointer text-black',
|
|
||||||
hasError.value ? 'text-m-error' : '',
|
|
||||||
hasSuccess.value ? 'text-m-success' : '',
|
|
||||||
disabled.value ? 'cursor-not-allowed text-black/60' : '',
|
|
||||||
props.labelClass,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const mergedMessageClass = computed(() =>
|
|
||||||
twMerge(
|
|
||||||
'radio-message ml-3 -mt-1 text-xs',
|
|
||||||
hasError.value
|
|
||||||
? 'text-m-error'
|
|
||||||
: hasSuccess.value
|
|
||||||
? 'text-m-success'
|
|
||||||
: 'text-m-muted',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:modelValue', value: string | number | boolean | null | undefined): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const onClick = (event: MouseEvent) => {
|
|
||||||
if (!props.readonly) return
|
|
||||||
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChange = (event: Event) => {
|
|
||||||
if (props.readonly) {
|
|
||||||
const target = event.target as HTMLInputElement
|
|
||||||
target.checked = isChecked.value
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('update:modelValue', props.value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.radio-control input[type='radio']:checked + .radio-dot {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-control.is-error input[type='radio'] {
|
|
||||||
border-color: rgb(var(--m-error) / 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-control.is-error .radio-dot {
|
|
||||||
color: rgb(var(--m-error) / 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-control.is-success input[type='radio'] {
|
|
||||||
border-color: rgb(var(--m-success) / 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-control.is-success .radio-dot {
|
|
||||||
color: rgb(var(--m-success) / 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-control.is-disabled .radio-indicator,
|
|
||||||
.radio-control.is-disabled .radio-text {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-item:has(+ .radio-item) .radio-message {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Story title="Input/Radio">
|
|
||||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Simple</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`simple-${option.value}`"
|
|
||||||
v-model="primaryChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="primary-choice"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Preselected</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`preselected-${option.value}`"
|
|
||||||
v-model="preselectedChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="preselected-choice"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Error</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`error-${option.value}`"
|
|
||||||
v-model="errorChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="error-choice"
|
|
||||||
error="Selection required"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Success</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`success-${option.value}`"
|
|
||||||
v-model="successChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="success-choice"
|
|
||||||
success="Selection saved"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Disabled</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`disabled-${option.value}`"
|
|
||||||
v-model="disabledChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="disabled-choice"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-lg border p-4">
|
|
||||||
<h2 class="mb-4 text-xl font-bold">Readonly</h2>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<MalioRadioButton
|
|
||||||
v-for="option in options"
|
|
||||||
:key="`readonly-${option.value}`"
|
|
||||||
v-model="readonlyChoice"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
name="readonly-choice"
|
|
||||||
readonly
|
|
||||||
hint="Readonly group"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Story>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<docs lang="md">
|
|
||||||
# MalioRadioButton
|
|
||||||
|
|
||||||
Composant radio personnalisé compatible avec `v-model`, les groupes via `name`,
|
|
||||||
et les états visuels de validation.
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## Props détaillées
|
|
||||||
|
|
||||||
### modelValue
|
|
||||||
|
|
||||||
- Type: `string | number | boolean | null | undefined`
|
|
||||||
- Description: Valeur actuellement sélectionnée dans le groupe.
|
|
||||||
- Comportement:
|
|
||||||
- Compatible avec `v-model`.
|
|
||||||
- Le radio est coché quand `modelValue === value`.
|
|
||||||
|
|
||||||
### value
|
|
||||||
|
|
||||||
- Type: `string | number | boolean | null | undefined`
|
|
||||||
- Description: Valeur portée par le radio courant.
|
|
||||||
|
|
||||||
### label
|
|
||||||
|
|
||||||
- Type: `string`
|
|
||||||
- Description: Texte affiché à droite du radio.
|
|
||||||
|
|
||||||
### name
|
|
||||||
|
|
||||||
- Type: `string`
|
|
||||||
- Description: Nom HTML partagé par les radios d’un même groupe.
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## États
|
|
||||||
|
|
||||||
### error
|
|
||||||
|
|
||||||
- Type: `string`
|
|
||||||
- Description: Message et style d’erreur.
|
|
||||||
|
|
||||||
### success
|
|
||||||
|
|
||||||
- Type: `string`
|
|
||||||
- Description: Message et style de succès.
|
|
||||||
- Comportement: ignoré si `error` est présent.
|
|
||||||
|
|
||||||
### disabled
|
|
||||||
|
|
||||||
- Type: `boolean`
|
|
||||||
- Description: Désactive le radio.
|
|
||||||
|
|
||||||
### readonly
|
|
||||||
|
|
||||||
- Type: `boolean`
|
|
||||||
- Description: Empêche le changement de valeur tout en gardant le rendu affiché.
|
|
||||||
|
|
||||||
### hint
|
|
||||||
|
|
||||||
- Type: `string`
|
|
||||||
- Description: Message d’aide sous le groupe.
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## Events
|
|
||||||
|
|
||||||
### update:modelValue
|
|
||||||
|
|
||||||
- Émis à la sélection d’un radio.
|
|
||||||
- Retourne la `value` du radio sélectionné.
|
|
||||||
|
|
||||||
</docs>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {ref} from 'vue'
|
|
||||||
import MalioRadioButton from '../components/malio/RadioButton.vue'
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{label: 'Option 1', value: 'option1'},
|
|
||||||
{label: 'Option 2', value: 'option2'},
|
|
||||||
{label: 'Option 3', value: 'option3'},
|
|
||||||
{label: 'Option 4', value: 'option4'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const primaryChoice = ref<string | null>(null)
|
|
||||||
const preselectedChoice = ref<string | null>('option2')
|
|
||||||
const errorChoice = ref<string | null>(null)
|
|
||||||
const successChoice = ref<string | null>('option3')
|
|
||||||
const disabledChoice = ref<string | null>('option2')
|
|
||||||
const readonlyChoice = ref<string | null>('option4')
|
|
||||||
</script>
|
|
||||||
Reference in New Issue
Block a user