Skip to content

Commit 661fa7e

Browse files
committed
feat: ✨ ajoute le composant curseur - range
1 parent a5dc510 commit 661fa7e

File tree

7 files changed

+350
-4
lines changed

7 files changed

+350
-4
lines changed

.storybook/preview.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { Preview } from "@storybook/vue3"
2-
import { setup } from "@storybook/vue3"
1+
import type { Preview } from '@storybook/vue3'
2+
import { setup } from '@storybook/vue3'
33
import { themes } from '@storybook/theming'
4-
import { withThemeByDataAttribute } from "@storybook/addon-styling"
4+
import { withThemeByDataAttribute } from '@storybook/addon-styling'
55
import { FocusTrap } from 'focus-trap-vue'
66
import { defineComponent } from 'vue'
77
import { OhVueIcon as VIcon} from 'oh-vue-icons'
@@ -33,7 +33,7 @@ const preview: Preview = {
3333
docs: {
3434
theme: { ...themes.normal, ...VueDsfrTheme },
3535
},
36-
actions: { argTypesRegex: "^on[A-Z].*" },
36+
actions: { argTypesRegex: '^on[A-Z].*' },
3737
controls: {
3838
matchers: {
3939
color: /(background|color)$/i,

.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ export default defineConfig({
9797
text: 'DsfrBackToTop',
9898
link: '/composants/DsfrBackToTop.md',
9999
},
100+
{
101+
text: 'DsfrRange',
102+
link: '/composants/DsfrRange.md',
103+
},
100104
{
101105
text: 'DsfrNotice',
102106
link: '/composants/DsfrNotice.md',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Curseur - `DsfrRange`
2+
3+
## Introduction
4+
5+
Bienvenue dans la documentation du `DsfrRange`, un composant Vue qui va slider dans votre coeur comme un croissant bien chaud glisse dans votre petit déjeuner. Ce composant est un véritable couteau suisse pour les curseurs, capable de tout faire, de l'affichage simple à la gestion de valeurs doubles. Mettez vos ceintures, on décolle !
6+
7+
Les curseurs sont des entrées numériques qui permettent de voir graphiquement une sélection par rapport a une valeur minimale et maximale. Ils servent à montrer en temps réelle les options choisies et éclairer la prise de décision ("Why so serious?" 🦇🃏).
8+
9+
## Structure du Composant
10+
11+
- Le composant est encapsulé dans une `div` avec la classe `fr-range-group`, qui peut afficher un message d'erreur via `message`.
12+
- Le `label` est affiché en haut, suivi par un texte d'indice (`hint`) si fourni.
13+
- Le curseur (`input type="range"`) est stylisé avec des classes pour gérer la taille et l'état désactivé.
14+
- Les valeurs minimales et maximales sont affichées si `hideIndicators` est `false`.
15+
- Un second curseur est présent si la prop `double` est `true`.
16+
- Les messages d'erreur ou autres sont affichés dans une `div` spécifique.
17+
18+
## Props
19+
20+
| Nom | Type | Défaut | Description |
21+
| --- | --- | --- | --- |
22+
| `id` | `string` | `getRandomId('range')` | Identifiant unique du curseur. Si non fourni, un id est généré aléatoirement. |
23+
| `min` | `number` | `0` | Valeur minimale du curseur. |
24+
| `max` | `number` | `100` | Valeur maximale du curseur. |
25+
| `modelValue` | `number` | `0` | Valeur actuelle du curseur. |
26+
| `label` | `string` | - | Texte de l'étiquette associée au curseur. |
27+
| `hint` | `string` | `undefined` | Texte d'indice optionnel. |
28+
| `message` | `string` | `undefined` | Message à afficher en cas d'erreur. |
29+
| `prefix` | `string` | `undefined` | Texte à afficher avant la valeur. |
30+
| `suffix` | `string` | `undefined` | Texte à afficher après la valeur. |
31+
| `small` | `boolean` | `undefined` | Si `true`, réduit la taille du curseur. |
32+
| `hideIndicators` | `boolean` | `undefined` | Cache les indicateurs de valeur min/max si `true`. |
33+
| `step` | `number` | `undefined` | Pas d'incrément du curseur. |
34+
| `double` | `boolean` | `undefined` | Active un second curseur si `true`. |
35+
| `disabled` | `boolean` | `undefined` | Désactive le curseur si `true`. |
36+
37+
## Événements
38+
39+
- **`update:modelValue`**: Émis lors de la modification de la valeur du curseur. Renvoie la nouvelle valeur.
40+
41+
## Exemple Pratique
42+
43+
::: code-group
44+
45+
<Story data-title="Démo" min-h="500px">
46+
<DsfrRangeDemo />
47+
</Story>
48+
49+
<<< docs-demo/DsfrRangeDemo.vue [Code de la démo]
50+
51+
<<< DsfrRange.vue
52+
<<< DsfrRange.types.ts
53+
:::
54+
55+
<script setup lang="ts">
56+
import DsfrRangeDemo from './docs-demo/DsfrRangeDemo.vue'
57+
</script>
58+
59+
Et voilà ! Notre DsfrRange est prêt à être croqué dans vos interfaces comme une baguette bien croustillante. N'oubliez pas de l'assaisonner avec vos styles et logiques pour qu'il s'intègre parfaitement dans le festin visuel de votre application. Bon codage ! 🥖💻
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Importation du composant.
2+
import DsfrRange from './DsfrRange.vue'
3+
4+
// Story par défaut
5+
export default {
6+
title: 'Composants/DsfrRange',
7+
component: DsfrRange,
8+
argTypes: {
9+
label: { control: 'text' },
10+
min: { control: 'number' },
11+
max: { control: 'number' },
12+
modelValue: { control: 'number' },
13+
hint: { control: 'text' },
14+
message: { control: 'text' },
15+
prefix: { control: 'text' },
16+
suffix: { control: 'text' },
17+
small: { control: 'boolean' },
18+
hideIndicators: { control: 'boolean' },
19+
step: { control: 'number' },
20+
double: { control: 'boolean' },
21+
disabled: { control: 'boolean' },
22+
},
23+
}
24+
25+
// Template de base pour les stories
26+
const Template = (args) => ({
27+
components: { DsfrRange },
28+
setup () {
29+
return {
30+
args,
31+
value: args.modelValue,
32+
}
33+
},
34+
template: '<DsfrRange v-bind="args" v-model="value" />',
35+
})
36+
37+
// Story pour l'utilisation standard
38+
export const Standard = Template.bind({})
39+
Standard.args = {
40+
label: 'Étiquette standard',
41+
min: 0,
42+
max: 100,
43+
modelValue: 50,
44+
// Autres props si nécessaire
45+
}
46+
47+
// Story avec un message d'erreur
48+
export const WithErrorMessage = Template.bind({})
49+
WithErrorMessage.args = {
50+
label: 'Étiquette avec erreur',
51+
message: 'Message d\'erreur',
52+
min: 0,
53+
max: 100,
54+
modelValue: 30,
55+
// Autres props si nécessaire
56+
}
57+
58+
// Story pour une version petite
59+
export const SmallVersion = Template.bind({})
60+
SmallVersion.args = {
61+
label: 'Petite version',
62+
small: true,
63+
min: 0,
64+
max: 100,
65+
modelValue: 70,
66+
// Autres props si nécessaire
67+
}

src/components/DsfrRange/DsfrRange.types.ts

Whitespace-only changes.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<script lang="ts" setup>
2+
import { computed, onMounted, ref, watch } from 'vue'
3+
4+
import { getRandomId } from '../../utils/random-utils'
5+
6+
const props = withDefaults(defineProps<{
7+
id?: string
8+
min?: number
9+
max?: number
10+
modelValue?: number
11+
lowerValue?: number
12+
label: string
13+
hint?: string
14+
message?: string
15+
prefix?: string
16+
suffix?: string
17+
small?: boolean
18+
hideIndicators?: boolean
19+
step?: number
20+
disabled?: boolean
21+
}>(), {
22+
id: () => getRandomId('range'),
23+
min: 0,
24+
max: 100,
25+
modelValue: 0,
26+
lowerValue: undefined,
27+
hint: undefined,
28+
message: undefined,
29+
prefix: undefined,
30+
suffix: undefined,
31+
step: undefined,
32+
})
33+
34+
// eslint-disable-next-line func-call-spacing
35+
const emit = defineEmits<{
36+
(e: 'update:modelValue', payload: string | number): void
37+
(e: 'update:lowerValue', payload: string | number): void
38+
}>()
39+
40+
const input = ref<HTMLInputElement>()
41+
const output = ref<HTMLSpanElement>()
42+
const inputWidth = ref()
43+
44+
const double = computed(() => props.lowerValue !== undefined)
45+
46+
const outputStyle = computed(() => {
47+
if (props.lowerValue === undefined) {
48+
const translateXValue = (props.modelValue - props.min) / (props.max - props.min) * inputWidth.value
49+
return `transform: translateX(${translateXValue}px) translateX(-${props.modelValue}%);`
50+
}
51+
const translateXValue = (props.modelValue + props.lowerValue - props.min) / 2 / (props.max - props.min) * inputWidth.value
52+
return `transform: translateX(${translateXValue}px) translateX(-${props.lowerValue + ((props.modelValue - props.lowerValue) / 2)}%);`
53+
})
54+
55+
const rangeStyle = computed(() => {
56+
const progressRight = (props.modelValue - props.min) / (props.max - props.min) * inputWidth.value - (double.value ? 12 : 0)
57+
const progressLeft = ((props.lowerValue ?? 0) - props.min) / (props.max - props.min) * inputWidth.value
58+
59+
return {
60+
'--progress-right': progressRight + 24 + 'px',
61+
...(double.value ? { '--progress-left': progressLeft + 12 + 'px' } : {}),
62+
}
63+
})
64+
65+
watch([() => props.modelValue, () => props.lowerValue], ([upper, lower]) => {
66+
if (lower === undefined) {
67+
return
68+
}
69+
70+
if (double.value && upper < lower) {
71+
emit('update:lowerValue', upper)
72+
}
73+
if (double.value && lower > upper) {
74+
emit('update:modelValue', lower)
75+
}
76+
})
77+
78+
const outputValue = computed(() => {
79+
return (props.prefix ?? '')
80+
.concat(double.value ? `${props.lowerValue} - ` : '')
81+
.concat('' + props.modelValue)
82+
.concat(props.suffix ?? '')
83+
})
84+
85+
onMounted(() => {
86+
inputWidth.value = input.value?.offsetWidth
87+
})
88+
</script>
89+
90+
<template>
91+
<div
92+
:id="`${id}-group`"
93+
class="fr-range-group"
94+
:class="{ 'fr-range-group--error': message }"
95+
>
96+
<label
97+
:id="`${id}-label`"
98+
class="fr-label"
99+
>
100+
<slot name="label">
101+
{{ label }}
102+
</slot>
103+
<span class="fr-hint-text">
104+
<slot name="hint">
105+
{{ hint }}
106+
</slot>
107+
</span>
108+
</label>
109+
<div
110+
class="fr-range"
111+
data-fr-js-range="true"
112+
:class="{
113+
'fr-range--sm': small,
114+
'fr-range--double': double,
115+
'fr-range-group--disabled': disabled,
116+
}"
117+
:data-fr-prefix="prefix ?? undefined"
118+
:data-fr-suffix="suffix ?? undefined"
119+
:style="rangeStyle"
120+
>
121+
<span
122+
ref="output"
123+
class="fr-range__output"
124+
data-fr-js-range-output="true"
125+
:style="outputStyle"
126+
>{{ outputValue }}</span>
127+
<input
128+
v-if="double"
129+
:id="`${id}-2`"
130+
type="range"
131+
:min="min"
132+
:max="max"
133+
:step="step"
134+
:value="lowerValue"
135+
:disabled="disabled"
136+
:aria-labelledby="`${id}-label`"
137+
:aria-describedby="`${id}-messages`"
138+
@input="emit('update:lowerValue', +$event.target?.value)"
139+
>
140+
<input
141+
:id="id"
142+
ref="input"
143+
type="range"
144+
:min="min"
145+
:max="max"
146+
:step="step"
147+
:value="modelValue"
148+
:disabled="disabled"
149+
:aria-labelledby="`${id}-label`"
150+
:aria-describedby="`${id}-messages`"
151+
@input="emit('update:modelValue', +$event.target?.value)"
152+
>
153+
154+
<span
155+
v-if="!hideIndicators"
156+
class="fr-range__min"
157+
aria-hidden="true"
158+
data-fr-js-range-limit="true"
159+
>{{ min }}</span>
160+
<span
161+
v-if="!hideIndicators"
162+
class="fr-range__max"
163+
aria-hidden="true"
164+
data-fr-js-range-limit="true"
165+
>{{ max }}</span>
166+
</div>
167+
<div
168+
:id="`${id}-messages`"
169+
class="fr-messages-group"
170+
aria-live="polite"
171+
>
172+
<slot name="messages">
173+
<p
174+
v-if="message"
175+
:id="`${id}-message-error`"
176+
class="fr-message fr-message--error"
177+
>
178+
{{ message }}
179+
</p>
180+
</slot>
181+
</div>
182+
</div>
183+
</template>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<script lang="ts" setup>
2+
import { ref } from 'vue'
3+
4+
import DsfrRange from '../DsfrRange.vue'
5+
6+
const value = ref<number>(100)
7+
const value2 = ref<number>(100)
8+
const lowerValue = ref<number>(0)
9+
</script>
10+
11+
<template>
12+
<div class="fr-container fr-py-4w">
13+
<div>
14+
<DsfrRange
15+
v-model="value"
16+
label="Label du curseur"
17+
/>
18+
</div>
19+
<p>
20+
{{ value }}
21+
</p>
22+
<div>
23+
<DsfrRange
24+
v-model="value2"
25+
v-model:lower-value="lowerValue"
26+
label="Label du curseur"
27+
/>
28+
</div>
29+
<p>
30+
{{ lowerValue }} - {{ value2 }}
31+
</p>
32+
</div>
33+
</template>

0 commit comments

Comments
 (0)