Skip to content

Commit f00ebdb

Browse files
committed
feat: ✨ remanie DsfrTabs pour pouvoir utiliser v-model
BREAKING CHANGE: Gros changements dans `DsfrTabs`. Se reporter à la documentation
1 parent 02003b3 commit f00ebdb

File tree

12 files changed

+275
-221
lines changed

12 files changed

+275
-221
lines changed

demo-app/views/AppTabs.vue

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,24 @@ const tabTitles = [
1111
{ title: 'Onglet avec accordéon', icon: 'ri-checkbox-circle-line', tabId: 'tab-0', panelId: 'tab-content-0' },
1212
{ title: 'Titre 2', icon: 'ri-checkbox-circle-line', tabId: 'tab-1', panelId: 'tab-content-1' },
1313
]
14-
const selectedTabIndex = ref(0)
15-
const asc = ref(true)
14+
const selectedTabIndex = ref(1)
1615
const initialSelectedIndex = 0
1716
const title1 = 'Un titre d’accordéon 1'
1817
const title2 = 'Un titre d’accordéon 2'
1918
const title3 = 'Un titre d’accordéon 3'
2019
const expandedId = ref(undefined)
21-
22-
function selectTab (idx) {
23-
asc.value = selectedTabIndex.value < idx
24-
selectedTabIndex.value = idx
25-
}
2620
</script>
2721

2822
<template>
2923
<DsfrTabs
24+
v-model="selectedTabIndex"
3025
:tab-list-name="tabListName"
3126
:tab-titles="tabTitles"
3227
:initial-selected-index="initialSelectedIndex"
33-
@select-tab="selectTab"
3428
>
3529
<DsfrTabContent
3630
panel-id="tab-content-0"
3731
tab-id="tab-0"
38-
:selected="selectedTabIndex === 0"
39-
:asc="asc"
4032
>
4133
<DsfrAccordionsGroup>
4234
<li>
@@ -74,14 +66,11 @@ function selectTab (idx) {
7466
<DsfrTabContent
7567
panel-id="tab-content-1"
7668
tab-id="tab-1"
77-
:selected="selectedTabIndex === 1"
78-
:asc="asc"
7969
>
8070
<DsfrCard
81-
detail="detail"
82-
description="description"
83-
img-src="https://loremflickr.com/300/200/cat"
84-
title="title"
71+
detail="Détails de la carte"
72+
description="Description de la carte : lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore dignissimos deleniti, velit ut cupiditate rem odit nobis iste eos qui itaque necessitatibus ab nostrum quibusdam veniam accusamus deserunt earum perspiciatis."
73+
title="Titre de la carte"
8574
/>
8675
</DsfrTabContent>
8776
</DsfrTabs>

src/components/DsfrTabs/DsfrTabContent.stories.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import DsfrTabs from './DsfrTabs.vue'
12
import DsfrTabContent from './DsfrTabContent.vue'
23

34
export default {
@@ -29,6 +30,7 @@ export default {
2930

3031
export const ContenuDOnglet = (args) => ({
3132
components: {
33+
DsfrTabs,
3234
DsfrTabContent,
3335
},
3436

@@ -37,21 +39,26 @@ export const ContenuDOnglet = (args) => ({
3739
},
3840

3941
template: `
40-
<div class="fr-tabs" style="overflow: visible">
42+
<DsfrTabs
43+
v-model="selectedTabIndex"
44+
:tab-list-name="tabListName"
45+
:tab-titles="tabTitles"
46+
>
4147
<DsfrTabContent
42-
panel-id="tab-content-3"
43-
tab-id="tab-3"
44-
:selected="selected"
45-
:asc="asc"
48+
panel-id="tab-content-0"
49+
tab-id="tab-0"
4650
>
4751
<div>Contenu personnalisé</div>
4852
</DsfrTabContent>
49-
</div>
53+
</DsfrTabs>
5054
`,
5155
})
5256
ContenuDOnglet.args = {
5357
panelId: 'tab-content-0',
5458
tabId: 'tab-0',
55-
selected: true,
56-
asc: true,
59+
selectedTabIndex: 0,
60+
tabListName: 'Liste d’onglet',
61+
tabTitles: [
62+
{ title: 'Titre 1', icon: 'ri-checkbox-circle-line', tabId: 'tab-0', panelId: 'tab-content-0' },
63+
],
5764
}

src/components/DsfrTabs/DsfrTabContent.vue

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
<script setup lang="ts">
2-
import { computed } from 'vue'
2+
import { computed, inject, toRef } from 'vue'
33
44
import type { DsfrTabContentProps } from './DsfrTabs.types'
5+
import { registerTabKey } from './injection-key'
56
67
export type { DsfrTabContentProps }
78
89
const props = defineProps<DsfrTabContentProps>()
910
1011
const values = { true: '100%', false: '-100%' }
12+
const useTab = inject(registerTabKey)!
13+
const { isVisible, asc } = useTab(toRef(() => props.tabId))
1114
// @ts-expect-error this will be fine
12-
const translateValueFrom = computed(() => values[String(props.asc)])
15+
const translateValueFrom = computed(() => values[String(asc?.value)])
1316
// @ts-expect-error this will be fine
14-
const translateValueTo = computed(() => values[String(!props.asc)])
17+
const translateValueTo = computed(() => values[String(!asc?.value)])
1518
</script>
1619

1720
<template>
@@ -20,15 +23,15 @@ const translateValueTo = computed(() => values[String(!props.asc)])
2023
mode="in-out"
2124
>
2225
<div
23-
v-show="selected"
26+
v-show="isVisible"
2427
:id="panelId"
2528
class="fr-tabs__panel"
2629
:class="{
27-
'fr-tabs__panel--selected': selected,
30+
'fr-tabs__panel--selected': isVisible,
2831
}"
2932
role="tabpanel"
3033
:aria-labelledby="tabId"
31-
:tabindex="selected ? 0 : -1"
34+
:tabindex="isVisible ? 0 : -1"
3235
>
3336
<!-- @slot Slot par défaut pour le contenu de l’onglet. Sera dans `<div class="fr-tabs__panel">` -->
3437
<slot />

src/components/DsfrTabs/DsfrTabItem.vue

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<script lang="ts" setup>
2-
import { ref, watch } from 'vue'
2+
import { inject, ref, toRef, watch } from 'vue'
33
import VIcon from '../VIcon/VIcon.vue'
44
55
import type { DsfrTabItemProps } from './DsfrTabs.types'
6+
import { registerTabKey } from './injection-key'
67
78
export type { DsfrTabItemProps }
89
@@ -11,7 +12,7 @@ const props = withDefaults(defineProps<DsfrTabItemProps>(), {
1112
})
1213
1314
const emit = defineEmits<{
14-
(e: 'click', payload: MouseEvent): void
15+
(e: 'click', tabId: string): void
1516
(e: 'next'): void
1617
(e: 'previous'): void
1718
(e: 'first'): void
@@ -43,6 +44,9 @@ function onKeyDown (event: KeyboardEvent) {
4344
emit(eventToEmit)
4445
}
4546
}
47+
48+
const useTab = inject(registerTabKey)!
49+
const { isVisible } = useTab(toRef(() => props.tabId))
4650
</script>
4751

4852
<template>
@@ -54,12 +58,12 @@ function onKeyDown (event: KeyboardEvent) {
5458
ref="button"
5559
:data-testid="`test-${tabId}`"
5660
class="fr-tabs__tab"
57-
:tabindex="selected ? 0 : -1"
61+
:tabindex="isVisible ? 0 : -1"
5862
role="tab"
5963
type="button"
60-
:aria-selected="selected"
64+
:aria-selected="isVisible"
6165
:aria-controls="panelId"
62-
@click.prevent="$emit('click', $event)"
66+
@click.prevent="$emit('click', tabId)"
6367
@keydown="onKeyDown($event)"
6468
>
6569
<span

src/components/DsfrTabs/DsfrTabs.md

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,55 +12,112 @@ Bonjour les artistes du code ! Voici `DsfrTabs`, le composant d'onglets Vue qui
1212

1313
| Nom | Type | Défaut | Obligatoire | Description |
1414
|----------------------|---------------------------|--------------|-------------|------------------------------------------------------------|
15-
| `'tabContents'` | `string[]` | `[]` | | Contenus des onglets. |
16-
| `'initialSelectedIndex'` | `number` | `0` | | Index de l'onglet sélectionné au chargement. |
17-
| `'tabTitles'` | `string[]` | `[]` | | Titres des onglets avec les id des panneaux et onglets associés. |
15+
| `tabContents` | `string[]` | `[]` | | Contenus (simples) des onglets. |
16+
| `modelValue` | `number` | `0` | | Index de l'onglet sélectionné au chargement (*existe depuis VueDsfr v6.0.0*). |
17+
| `tabTitles` | `string[]` | `[]` | | Titres des onglets avec les id des panneaux et onglets associés. |
18+
>>>>>>> 8171671 (feat: :sparkles: remanie DsfrTabs pour pouvoir utiliser v-model)
1819
1920
## 📡 Événements
2021

2122
| nom | donnée (*payload*) | détail de la donnée
2223
| ---------------------- | --------- | --- |
23-
| `'select-tab '` | *`string`* | Émis lorsqu'un onglet est sélectionné. Envoyant l'index de l'onglet sélectionné. |
24+
| `'update:modelValue'` | *`number`* | Émis lorsqu'un onglet est sélectionné. Envoyant l'index de l'onglet sélectionné. |
25+
26+
::: warning Important
27+
Depuis la v6, le composant `DsfrTabs` déclarant la prop `modelValue` et émettant l’événement `update:modelValue`, il est recommandé d’utiliser la directive `v-model`. Elle contient l’index (commençant à 0) de l’onglet à afficher. Aussi, plus besoin, depuis la v6, d’utiliser le composable `useTabs()`. Cf. les exemples ci-dessous.
28+
:::
2429

2530
## 🧩 Slots
2631

2732
| Nom | Description |
2833
|--------------|--------------------------------------------------------------------|
29-
| `'tab-items'` | Slot nommé pour insérer des titres d’onglets personnalisés. Si rempli, la prop `tabTitles` n’a aucun effet. |
30-
| `'default'` | Slot par défaut pour le contenu des onglets. |
34+
| `tab-items` | Slot nommé pour insérer des titres d’onglets personnalisés. Si rempli, la prop `tabTitles` n’a aucun effet. |
35+
| `default` | Slot par défaut pour le contenu des onglets. |
3136

3237
## Les méthodes exposées
3338

3439
- `DsfrTabs#renderTabs()`: permet de forcer le recalcul de la hauteur de l’onglet
35-
- `DsfrTabs#selectIndex()`: permet d’indiquer quel onglet doit être sélectionné (commence à 0)
40+
41+
::: warning Important depuis la v6
42+
43+
Méthodes supprimées :
44+
3645
- `DsfrTabs#selectFirst` : permet de sélectionner le premier onglet (raccourci de `selectIndex(0)`)
3746
- `DsfrTabs#selectLast` : permet de sélectionner le dernier onglet (raccourci de `selectIndex(tabs.length - 1)`)
47+
- `DsfrTabs#selectIndex()`: n’existe plus : utiliser directement la *ref* utilisée dans le `v-model` de `DsfrTabs`
3848

39-
## 📝 Exemples
49+
Au lieu de :
4050

41-
1. **Onglets Simples :**
51+
```vue
52+
<script lang="ts">
53+
import { ref } from 'vue'
4254
43-
::: code-group
55+
const tabs = ref<HTMLElement>()
56+
57+
// Quelque part dans le code :
58+
tabs.value.selectFirst()
59+
tabs.value.selectLast()
60+
tabs.value.selectIndex(n)
61+
62+
// (...)
63+
</script>
64+
65+
<template>
66+
<!-- eslint-disable-next-line vue/no-unused-refs -->
67+
<DsfrTabs ref="tabs">
68+
<!-- (...) -->
69+
</DsfrTabs>
70+
</template>
71+
```
72+
73+
Utiliser :
4474

45-
<Story data-title="Démo" min-h="160px">
46-
<DsfrTabsDemoSimple />
47-
</Story>
75+
```vue
76+
<script lang="ts">
77+
import { ref } from 'vue'
4878
49-
<<< docs-demo/DsfrTabsDemoSimple.vue [Code de la démo]
79+
const activeTab = ref(0)
80+
// Quelque part dans le code :
81+
activeTab.value = 0 // active le premier onglet
82+
activeTab.value = n // active l’onglet n
83+
activeTab.value = tabTitles.length - 1 // active le dernier onglet
84+
85+
// (...)
86+
</script>
87+
88+
<template>
89+
<DsfrTabs v-model="activeTab">
90+
<!-- (...) -->
91+
</DsfrTabs>
92+
</template>
93+
```
5094

5195
:::
5296

97+
## 📝 Exemples
98+
99+
1. **Onglets Simples :**
100+
101+
::: code-group
102+
103+
<Story data-title="Démo" min-h="160px">
104+
<DsfrTabsDemoSimple />
105+
</Story>
106+
107+
<<< docs-demo/DsfrTabsDemoSimple.vue [Code de la démo]
108+
109+
:::
53110
2. **Onglets Complexes :**
54111

55-
::: code-group
112+
::: code-group
56113

57-
<Story data-title="Démo" min-h="260px">
58-
<DsfrTabsDemoComplex />
59-
</Story>
114+
<Story data-title="Démo" min-h="260px">
115+
<DsfrTabsDemoComplex />
116+
</Story>
60117

61-
<<< docs-demo/DsfrTabsDemoComplex.vue [Code de la démo]
118+
<<< docs-demo/DsfrTabsDemoComplex.vue [Code de la démo]
62119

63-
:::
120+
:::
64121

65122
## ⚙️ Code source des composants
66123

src/components/DsfrTabs/DsfrTabs.spec.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import VIcon from '../VIcon/VIcon.vue'
22
import { fireEvent } from '@testing-library/dom'
33
import { render } from '@testing-library/vue'
4+
import { ref } from 'vue'
45

56
// import '@gouvfr/dsfr/dist/core/core.module.js'
67

@@ -13,6 +14,7 @@ describe('DsfrTabs', () => {
1314
const title1 = 'Titre 1'
1415
const title2 = 'Titre 2'
1516
const title3 = 'Titre 3'
17+
const modelValue = ref(0)
1618

1719
const tabTitles = [
1820
{ title: title1, tabId: 'tab1' },
@@ -24,7 +26,7 @@ describe('DsfrTabs', () => {
2426
const tabContents = ['Contenu1', 'Contenu2', 'Contenu3', 'Contenu4']
2527

2628
// When
27-
const { getByText, getByTestId, getAllByRole, getByRole } = render(DsfrTabs, {
29+
const { getByText, getByTestId, getAllByRole, getByRole, emitted } = render(DsfrTabs, {
2830
global: {
2931
components: {
3032
VIcon,
@@ -34,6 +36,7 @@ describe('DsfrTabs', () => {
3436
tabListName,
3537
tabTitles,
3638
tabContents,
39+
modelValue: modelValue.value,
3740
},
3841
})
3942

@@ -60,15 +63,9 @@ describe('DsfrTabs', () => {
6063
await fireEvent.click(thirdTabEl)
6164
await fireEvent.click(secondTabEl)
6265

63-
i = 0
64-
for (const tabItemEl of tabItemEls) {
65-
if (i === 1) {
66-
expect(tabItemEl).toHaveAttribute('aria-selected', 'true')
67-
} else {
68-
expect(tabItemEl).toHaveAttribute('aria-selected', 'false')
69-
}
70-
i++
71-
}
66+
expect(emitted()['update:modelValue']).toBeTruthy()
67+
expect(emitted()['update:modelValue'][1]).toEqual([2]) // 2nd tab
68+
expect(emitted()['update:modelValue'][2]).toEqual([1]) // 2nd tab
7269

7370
// Then
7471
expect(tabTitleEls[0]).toContainElement(firstTabEl)

0 commit comments

Comments
 (0)