Skip to content

Commit 901a04f

Browse files
committed
refactor(DsfrRadioButton): ♻️ remanie les stories et ajoute defineSlots
1 parent 86ae1cb commit 901a04f

File tree

2 files changed

+146
-134
lines changed

2 files changed

+146
-134
lines changed
Lines changed: 131 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { expect, fn, within } from 'storybook/test'
1+
import type { Meta, StoryObj } from '@storybook/vue3'
2+
import { expect, fn, within } from '@storybook/test'
3+
import { ref } from 'vue'
24

35
import DsfrRadioButton from './DsfrRadioButton.vue'
46
import DsfrRadioButtonSet from './DsfrRadioButtonSet.vue'
@@ -8,7 +10,7 @@ import DsfrRadioButtonSet from './DsfrRadioButtonSet.vue'
810
*
911
* Nous vous invitons à regarder plutôt la [nouvelle documentation](https://vue-ds.fr/composants/DsfrRadioButton) pour ce composant
1012
*/
11-
export default {
13+
const meta = {
1214
component: DsfrRadioButton,
1315
title: 'Composants/DsfrRadioButton',
1416
tags: ['formulaire'],
@@ -27,15 +29,15 @@ export default {
2729
control: 'boolean',
2830
description: 'Utilise la version réduite du bouton radio',
2931
},
30-
// label: {
31-
// control: 'text',
32-
// description: 'Label du bouton radio',
33-
// },
3432
modelValue: {
3533
control: 'text',
3634
description: 'Valeur de la case cochée',
3735
},
38-
onChange: { action: fn() },
36+
'onUpdate:modelValue': {
37+
action: fn(),
38+
description:
39+
'Événement émis à chaque changement de valeur du groupe de même bouton radio',
40+
},
3941
img: {
4042
control: 'text',
4143
description: 'Permet d\'ajouter une image au composant',
@@ -53,161 +55,159 @@ export default {
5355
control: 'object',
5456
description: 'Permet de définir des attributs pour le SVG.',
5557
},
56-
'update:modelValue': {
57-
description:
58-
'Événement émis à chaque changement de valeur du groupe de même bouton radio',
59-
},
6058
},
61-
}
59+
} satisfies Meta<typeof DsfrRadioButton>
6260

63-
export const BoutonRadio = (args) => ({
64-
components: { DsfrRadioButton },
65-
data () {
66-
return args
67-
},
68-
template: `
69-
<div class="fr-form-group">
70-
<fieldset
71-
class="fr-fieldset"
72-
>
73-
<div
74-
class="fr-fieldset__content"
75-
role="radiogroup"
76-
>
77-
<DsfrRadioButton
78-
v-for="option of options"
79-
:modelValue="modelValue"
80-
v-bind="option"
81-
:small="small"
82-
@update:modelValue="updateCheckedValue($event)"
83-
/>
84-
</div>
85-
</fieldset>
86-
</div>
87-
`,
88-
methods: {
89-
updateCheckedValue (val) {
90-
if (val === this.modelValue) {
91-
return
61+
export default meta
62+
type Story = StoryObj<typeof meta>
63+
64+
export const BoutonRadio: Story = {
65+
render: (args) => ({
66+
components: { DsfrRadioButton },
67+
setup () {
68+
const modelValue = ref(args.modelValue)
69+
return {
70+
...args,
71+
modelValue,
9272
}
93-
this.onChange(val)
94-
this.modelValue = val
9573
},
74+
template: `
75+
<div class="fr-form-group">
76+
<fieldset
77+
class="fr-fieldset"
78+
>
79+
<div
80+
class="fr-fieldset__content"
81+
role="radiogroup"
82+
>
83+
<DsfrRadioButton
84+
v-for="(option, i) of options"
85+
:key="i"
86+
v-model="modelValue"
87+
v-bind="option"
88+
:small="small"
89+
@update:model-value="args['onUpdate:modelValue']"
90+
/>
91+
</div>
92+
</fieldset>
93+
</div>
94+
`,
95+
}),
96+
args: {
97+
modelValue: '3',
98+
small: false,
99+
options: [
100+
{
101+
label: 'Valeur 1',
102+
value: '1',
103+
hint: 'Description 1',
104+
name: 'Choix',
105+
},
106+
{
107+
label: 'Valeur 2',
108+
value: '2',
109+
disabled: true,
110+
hint: 'Description 2',
111+
name: 'Choix',
112+
},
113+
{
114+
label: 'Valeur 3',
115+
value: '3',
116+
name: 'Choix',
117+
},
118+
],
96119
},
97-
})
98-
BoutonRadio.args = {
99-
modelValue: '3',
100-
small: false,
101-
options: [
102-
{
103-
label: 'Valeur 1',
104-
value: '1',
105-
hint: 'Description 1',
106-
name: 'Choix',
107-
},
108-
{
109-
label: 'Valeur 2',
110-
value: '2',
111-
disabled: true,
112-
hint: 'Description 2',
113-
name: 'Choix',
114-
},
115-
{
116-
label: 'Valeur 3',
117-
value: '3',
118-
name: 'Choix',
119-
},
120-
],
121120
}
122-
BoutonRadio.play = async ({ canvasElement }) => {
121+
122+
BoutonRadio.play = async ({ canvasElement, args }) => {
123123
const canvas = within(canvasElement)
124-
const firstInputLabel = canvas.getByText(BoutonRadio.args.options.at(0)!.label)
125-
const initialCheckedInputLabel = canvas.getByText(BoutonRadio.args.options.at(2)!.label)
124+
const firstInputLabel = canvas.getByText(args.options.at(0).label)
125+
const initialCheckedInputLabel = canvas.getByText(args.options.at(2).label)
126126
expect(initialCheckedInputLabel).toHaveClass('fr-label')
127127
expect(firstInputLabel).toHaveClass('fr-label')
128128
const firstInput = canvas.getAllByRole('radio').at(0) as HTMLInputElement
129129
const initialCheckedInput = canvas.getAllByRole('radio').at(2) as HTMLInputElement
130130
expect(initialCheckedInput.parentElement).toHaveClass('fr-radio-group')
131131
expect(firstInput).not.toBeChecked()
132132
expect(initialCheckedInput).toBeChecked()
133-
firstInputLabel.click()
133+
await firstInputLabel.click()
134134
expect(firstInput).toBeChecked()
135135
expect(initialCheckedInput).not.toBeChecked()
136+
expect(args['onUpdate:modelValue']).toHaveBeenCalledWith('1')
136137
}
137138

138-
export const BoutonRadioRiche = (args) => ({
139-
components: { DsfrRadioButton, DsfrRadioButtonSet },
140-
data () {
141-
return args
142-
},
143-
template: `
144-
<DsfrRadioButtonSet>
145-
<DsfrRadioButton
146-
v-for="option of options"
147-
:modelValue="modelValue"
148-
v-bind="option"
149-
:small="small"
150-
@update:modelValue="updateCheckedValue($event)"
151-
/>
152-
</DsfrRadioButtonSet>
153-
`,
154-
methods: {
155-
updateCheckedValue (val) {
156-
if (val === this.modelValue) {
157-
return
139+
export const BoutonRadioRiche: Story = {
140+
render: (args) => ({
141+
components: { DsfrRadioButton, DsfrRadioButtonSet },
142+
setup () {
143+
const modelValue = ref(args.modelValue)
144+
return {
145+
...args,
146+
modelValue,
158147
}
159-
this.onChange(val)
160-
this.modelValue = val
161148
},
149+
template: `
150+
<DsfrRadioButtonSet>
151+
<DsfrRadioButton
152+
v-for="(option, i) of options"
153+
:key="i"
154+
v-model="modelValue"
155+
v-bind="option"
156+
:small="small"
157+
@update:model-value="args['onUpdate:modelValue']"
158+
/>
159+
</DsfrRadioButtonSet>
160+
`,
161+
}),
162+
args: {
163+
modelValue: '3',
164+
small: false,
165+
options: [
166+
{
167+
label: 'Valeur 1',
168+
value: '1',
169+
hint: 'Description 1',
170+
name: 'Choix',
171+
img: 'https://loremflickr.com/150/250/cat',
172+
imgTitle: 'Un 1er chaton',
173+
},
174+
{
175+
label: 'Valeur 2',
176+
value: '2',
177+
disabled: true,
178+
hint: 'Description 2',
179+
name: 'Choix',
180+
img: 'https://loremflickr.com/200/250/cat',
181+
imgTitle: 'Un 2è chaton',
182+
},
183+
{
184+
label: 'Valeur 3',
185+
value: '3',
186+
name: 'Choix',
187+
img: 'https://loremflickr.com/250/350/cat',
188+
imgTitle: 'Un 3è chaton',
189+
},
190+
],
162191
},
163-
})
164-
BoutonRadioRiche.args = {
165-
modelValue: '3',
166-
small: false,
167-
options: [
168-
{
169-
label: 'Valeur 1',
170-
value: '1',
171-
hint: 'Description 1',
172-
name: 'Choix',
173-
img: 'https://loremflickr.com/150/200/cat',
174-
imgTitle: 'Un 1er chaton',
175-
},
176-
{
177-
label: 'Valeur 2',
178-
value: '2',
179-
disabled: true,
180-
hint: 'Description 2',
181-
name: 'Choix',
182-
img: 'https://loremflickr.com/200/250/cat',
183-
imgTitle: 'Un 2è chaton',
184-
},
185-
{
186-
label: 'Valeur 3',
187-
value: '3',
188-
name: 'Choix',
189-
img: 'https://loremflickr.com/250/350/cat',
190-
imgTitle: 'Un 3è chaton',
191-
},
192-
],
193192
}
194-
BoutonRadioRiche.play = async ({ canvasElement }) => {
193+
BoutonRadioRiche.play = async ({ canvasElement, args }) => {
195194
const canvas = within(canvasElement)
196-
const firstInputLabel = canvas.getByText(BoutonRadio.args.options.at(0)!.label)
197-
const initialCheckedInputLabel = canvas.getByText(BoutonRadio.args.options.at(2)!.label)
195+
const firstInputLabel = canvas.getByText(args.options.at(0)!.label)
196+
const initialCheckedInputLabel = canvas.getByText(args.options.at(2)!.label)
198197
const firstInput = canvas.getAllByRole('radio').at(0) as HTMLInputElement
199198
const initialCheckedInput = canvas.getAllByRole('radio').at(2) as HTMLInputElement
200-
const firstInputImg = canvas.getByTitle(BoutonRadioRiche.args.options.at(0)!.imgTitle)
201-
const initialCheckedInputImg = canvas.getByTitle(BoutonRadioRiche.args.options.at(2)!.imgTitle)
199+
const firstInputImg = canvas.getByTitle(args.options.at(0)!.imgTitle)
200+
const initialCheckedInputImg = canvas.getByTitle(args.options.at(2)!.imgTitle)
202201

203-
expect(firstInputImg).toHaveAttribute('src', BoutonRadioRiche.args.options.at(0)!.img)
204-
expect(initialCheckedInputImg).toHaveAttribute('src', BoutonRadioRiche.args.options.at(2)!.img)
202+
expect(firstInputImg).toHaveAttribute('src', args.options.at(0)!.img)
203+
expect(initialCheckedInputImg).toHaveAttribute('src', args.options.at(2)!.img)
205204
expect(initialCheckedInputLabel).toHaveClass('fr-label')
206205
expect(firstInputLabel).toHaveClass('fr-label')
207206
expect(initialCheckedInput.parentElement).toHaveClass('fr-radio-group')
208207
expect(firstInput).not.toBeChecked()
209208
expect(initialCheckedInput).toBeChecked()
210-
firstInputLabel.click()
209+
await firstInputLabel.click()
211210
expect(firstInput).toBeChecked()
212211
expect(initialCheckedInput).not.toBeChecked()
212+
expect(args['onUpdate:modelValue']).toHaveBeenCalledWith('1')
213213
}

src/components/DsfrRadioButton/DsfrRadioButton.vue

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ const props = withDefaults(defineProps<DsfrRadioButtonProps>(), {
2020
2121
defineEmits<{ (e: 'update:modelValue', payload: string | number | boolean): void }>()
2222
23+
defineSlots<{
24+
/**
25+
* Slot pour personnaliser tout le contenu de la balise <label>
26+
*/
27+
label(props: { label: string }): any
28+
/**
29+
* Slot pour indiquer que le champ est obligatoire. Par défaut, met une astérisque si `required` est à true (dans un `<span class="required">`)
30+
*/
31+
'required-tip'(props: Record<string, never>): any
32+
}>()
33+
2334
const defaultSvgAttrs = { viewBox: '0 0 80 80', width: '80px', height: '80px' }
2435
2536
const richComputed = computed(() => props.rich || (!!props.img || !!props.svgPath))
@@ -51,10 +62,11 @@ const richComputed = computed(() => props.rich || (!!props.img || !!props.svgPat
5162
:for="id"
5263
class="fr-label"
5364
>
54-
<!-- @slot Slot pour personnaliser tout le contenu de la balise <label> cf. [DsfrInput](/?path=/story/composants-champ-de-saisie-champ-simple-dsfrinput--champ-avec-label-personnalise). Une **props porte le même nom pour un label simple** (texte sans mise en forme) -->
55-
<slot name="label">
65+
<slot
66+
name="label"
67+
:label="label"
68+
>
5669
{{ label }}
57-
<!-- @slot Slot pour indiquer que le champ est obligatoire. Par défaut, met une astérisque si `required` est à true (dans un `<span class="required">`) -->
5870
<slot name="required-tip">
5971
<span
6072
v-if="$attrs.required"

0 commit comments

Comments
 (0)