Skip to content

Commit 072058b

Browse files
committed
feat: ✨ Nouveau composant Segmented (Contrôle Segmenté)
1 parent d0a1d87 commit 072058b

File tree

10 files changed

+717
-5
lines changed

10 files changed

+717
-5
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"docs:preview": "cross-env VITEPRESS=true vitepress preview"
7474
},
7575
"dependencies": {
76-
"@gouvfr/dsfr": "~1.10.2",
76+
"@gouvfr/dsfr": "~1.11.0",
7777
"focus-trap": "^7.5.4",
7878
"focus-trap-vue": "^4.0.3",
7979
"oh-vue-icons": "1.0.0-rc3",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { OhVueIcon as VIcon } from 'oh-vue-icons'
2+
import { render } from '@testing-library/vue'
3+
4+
import Segmented from './DsfrSegmented.vue'
5+
6+
describe('DsfrSegmented', () => {
7+
it('should render a radio button with label in div', () => {
8+
// Given
9+
const label = 'Segemented label'
10+
const value = 1
11+
const name = 'segmented-name'
12+
13+
// When
14+
const { getByText, getByDisplayValue } = render(Segmented, {
15+
global: {
16+
components: {
17+
VIcon,
18+
},
19+
},
20+
props: {
21+
label,
22+
value,
23+
name,
24+
},
25+
})
26+
27+
const labelEl = getByText(label)
28+
const inputRadio = getByDisplayValue(value)
29+
30+
// Then
31+
expect(labelEl).toHaveClass('fr-label')
32+
expect(inputRadio).toBeInTheDocument()
33+
expect(labelEl.getAttribute('for')).toBe(inputRadio.id)
34+
})
35+
})
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import DsfrSegmented from './DsfrSegmented.vue'
2+
3+
/**
4+
* [Voir quand l’utiliser sur la documentation du DSFR](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/controle-segmente)
5+
*/
6+
export default {
7+
component: DsfrSegmented,
8+
title: 'Composants/DsfrSegmented',
9+
argTypes: {
10+
id: {
11+
control: 'text',
12+
description: '(optionnel) Valeur de l’attribut `id` du contrôle segmenté. Par défaut, un id pseudo-aléatoire sera donné.',
13+
},
14+
options: {
15+
control: 'object',
16+
description: 'Tableau d’objets : chaque objet contient les props à passer à `DsfrSegmented` - *N.B. : Ne fait pas partie du composant',
17+
},
18+
modelValue: {
19+
control: 'text',
20+
description: 'Valeur de la case active',
21+
},
22+
onChange: { action: 'changed' },
23+
'update:modelValue': {
24+
description: 'Événement émis à chaque changement de valeur',
25+
},
26+
},
27+
}
28+
29+
export const Segmented = (args) => ({
30+
components: { DsfrSegmented },
31+
data () {
32+
return args
33+
},
34+
template: `
35+
<div class="fr-form-group">
36+
<fieldset
37+
class="fr-segmented"
38+
>
39+
<div
40+
class="fr-segmented__elements"
41+
>
42+
<DsfrSegmented
43+
v-for="option of options"
44+
:modelValue="modelValue"
45+
v-bind="option"
46+
@update:modelValue="updateCheckedValue($event)"
47+
/>
48+
</div>
49+
</fieldset>
50+
</div>
51+
`,
52+
methods: {
53+
updateCheckedValue (val) {
54+
if (val === this.modelValue) {
55+
return
56+
}
57+
this.onChange(val)
58+
this.modelValue = val
59+
},
60+
},
61+
})
62+
Segmented.args = {
63+
modelValue: '3',
64+
options: [
65+
{
66+
label: 'Valeur 1',
67+
value: '1',
68+
name: 'Choix',
69+
},
70+
{
71+
label: 'Valeur 2',
72+
value: '2',
73+
disabled: true,
74+
name: 'Choix',
75+
},
76+
{
77+
label: 'Valeur 3',
78+
value: '3',
79+
name: 'Choix',
80+
},
81+
],
82+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { OhVueIcon as VIcon } from 'oh-vue-icons'
2+
3+
export type DsfrSegmentedProps = {
4+
id?: string
5+
name?: string
6+
modelValue?: string | number
7+
value: string | number
8+
label: string,
9+
disabled?: boolean,
10+
icon?: string | InstanceType<typeof VIcon>['$props']
11+
}
12+
13+
export type DsfrSegmentedSetProps = {
14+
titleId?: string,
15+
disabled?: boolean,
16+
small?: boolean,
17+
inline?: boolean,
18+
name?: string,
19+
hint?: string;
20+
legend?: string,
21+
modelValue: string | number,
22+
options?: DsfrSegmentedProps[],
23+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<script lang="ts" setup>
2+
import { getRandomId } from '../../utils/random-utils'
3+
4+
import type { DsfrSegmentedProps } from './DsfrSegmented.types'
5+
import { computed } from 'vue'
6+
7+
export type { DsfrSegmentedProps }
8+
9+
const props = withDefaults(defineProps<DsfrSegmentedProps>(), {
10+
id: () => getRandomId('basic', 'checkbox'),
11+
modelValue: '',
12+
label: '',
13+
hint: '',
14+
icon: undefined,
15+
})
16+
17+
defineEmits<{(e: 'update:modelValue', payload: string | number): void}>()
18+
19+
const iconProps = computed(() => typeof props.icon === 'string' ? { name: props.icon } : props.icon)
20+
</script>
21+
22+
<template>
23+
<div
24+
class="fr-segmented__element"
25+
>
26+
<input
27+
:id="id"
28+
type="radio"
29+
:name="name"
30+
:value="value"
31+
:checked="modelValue === value"
32+
:disabled="disabled"
33+
v-bind="$attrs"
34+
@click="$emit('update:modelValue', value)"
35+
>
36+
<label
37+
:for="id"
38+
class="fr-label"
39+
>
40+
<VIcon
41+
v-if="icon"
42+
v-bind="iconProps"
43+
/>
44+
<span v-if="icon">&nbsp;</span>
45+
{{ label }}
46+
</label>
47+
</div>
48+
</template>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import DsfrSegmentedSet from './DsfrSegmentedSet.vue'
2+
import { OhVueIcon as VIcon } from 'oh-vue-icons'
3+
4+
import '../../main.css'
5+
6+
describe('DsfrSegmentedSet', () => {
7+
it('should mount Segemented Set', () => {
8+
cy.mount(DsfrSegmentedSet, {
9+
global: {
10+
components: {
11+
VIcon,
12+
},
13+
},
14+
props: {
15+
legend: 'Légende des champs',
16+
hint: 'Description 1',
17+
selectedValue: 1,
18+
inline: false,
19+
modelValue: '1',
20+
name: 'segmentedset',
21+
options: [
22+
{
23+
label: 'Valeur 1',
24+
value: '1',
25+
},
26+
{
27+
label: 'Valeur 2',
28+
value: '2',
29+
disabled: true,
30+
},
31+
{
32+
label: 'Valeur 3',
33+
value: '3',
34+
},
35+
],
36+
},
37+
})
38+
.get('.fr-segmented__element:first-child input')
39+
.should('not.have.focus')
40+
})
41+
})
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { OhVueIcon as VIcon } from 'oh-vue-icons'
2+
import { fireEvent, render } from '@testing-library/vue'
3+
4+
import SegmentedSet from './DsfrSegmentedSet.vue'
5+
6+
describe('DsfrSegmentedSet', () => {
7+
it('should render a set of segmented with label in div', () => {
8+
// Given
9+
const legend = 'Légende pour l’ensemble des champs'
10+
const selectedValue = 1
11+
12+
// When
13+
const { getByText, getByRole } = render(SegmentedSet, {
14+
global: {
15+
components: {
16+
VIcon,
17+
},
18+
},
19+
props: {
20+
legend,
21+
modelValue: selectedValue,
22+
},
23+
})
24+
25+
getByRole('group')
26+
const legendEl = getByText(legend)
27+
28+
// Then
29+
expect(legendEl).toBeInTheDocument()
30+
})
31+
32+
it('should render a set of segmented with label in div', async () => {
33+
// Given
34+
const legend = 'Légende pour l’ensemble des champs'
35+
const disabledLabel = 'Label 2'
36+
const disabledValue = 2
37+
const selectedLabel = 'Label 1'
38+
const selectedValue = 1
39+
const toClickLabel = 'Label 3'
40+
const options = [
41+
{
42+
label: selectedLabel,
43+
value: selectedValue,
44+
disabled: false,
45+
},
46+
{
47+
label: disabledLabel,
48+
value: disabledValue,
49+
disabled: true,
50+
},
51+
{
52+
label: toClickLabel,
53+
value: 3,
54+
disabled: false,
55+
},
56+
]
57+
58+
// When
59+
const { getByText, getByDisplayValue } = render(SegmentedSet, {
60+
global: {
61+
components: {
62+
VIcon,
63+
},
64+
},
65+
props: {
66+
options,
67+
legend,
68+
modelValue: selectedValue,
69+
},
70+
})
71+
72+
const legendEl = getByText(legend)
73+
const disabledInput = getByDisplayValue(disabledValue)
74+
const selectedSegmented = getByDisplayValue(selectedValue)
75+
const uncheckedSegmented = getByDisplayValue(3)
76+
77+
await fireEvent.click(uncheckedSegmented)
78+
79+
// Then
80+
expect(legendEl).toBeInTheDocument()
81+
expect(disabledInput).toBeDisabled()
82+
expect(selectedSegmented).not.toBeDisabled()
83+
expect(selectedSegmented).not.toBeChecked()
84+
expect(uncheckedSegmented).toBeChecked()
85+
})
86+
})

0 commit comments

Comments
 (0)