@cnamts/synapse 0.0.0-alpha.0 → 0.0.3-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -2
- package/dist/design-system-v3.d.ts +234 -2
- package/dist/design-system-v3.js +1413 -4404
- package/dist/design-system-v3.umd.cjs +1 -2
- package/dist/style.css +1 -1
- package/package.json +37 -36
- package/src/components/Alert/Alert.vue +8 -8
- package/src/components/CollapsibleList/CollapsibleList.mdx +47 -0
- package/src/components/CollapsibleList/CollapsibleList.stories.ts +52 -0
- package/src/components/CollapsibleList/CollapsibleList.vue +157 -0
- package/src/components/CollapsibleList/tests/CollapsibleList.spec.ts +60 -0
- package/src/components/CollapsibleList/types.d.ts +5 -0
- package/src/components/Customs/CustomInputSelect/CustomInputSelect.mdx +42 -0
- package/src/components/Customs/CustomInputSelect/CustomInputSelect.stories.ts +154 -0
- package/src/components/Customs/CustomInputSelect/CustomInputSelect.vue +185 -0
- package/src/components/Customs/CustomInputSelect/tests/CustomInputSelect.spec.ts +216 -0
- package/src/components/Customs/CustomSelect/CustomSelect.mdx +47 -0
- package/src/components/Customs/CustomSelect/CustomSelect.stories.ts +182 -0
- package/src/components/Customs/CustomSelect/CustomSelect.vue +188 -0
- package/src/components/Customs/CustomSelect/tests/CustomSelect.spec.ts +236 -0
- package/src/components/FooterBar/A11yCompliance.ts +9 -0
- package/src/components/FooterBar/FooterBar.mdx +115 -0
- package/src/components/FooterBar/FooterBar.stories.ts +632 -0
- package/src/components/FooterBar/FooterBar.vue +330 -0
- package/src/components/FooterBar/config.ts +20 -0
- package/src/components/FooterBar/defaultSocialMediaLinks.ts +21 -0
- package/src/components/FooterBar/locales.ts +16 -0
- package/src/components/FooterBar/tests/FooterBar.spec.ts +167 -0
- package/src/components/FooterBar/tests/FooterBarConfig.spec.ts +36 -0
- package/src/components/FooterBar/tests/__snapshots__/FooterBar.spec.ts.snap +27 -0
- package/src/components/FooterBar/types.d.ts +10 -0
- package/src/components/FranceConnectBtn/FranceConnectBtn.vue +2 -2
- package/src/components/HeaderBar/HeaderBar.mdx +137 -0
- package/src/components/HeaderBar/HeaderBar.stories.ts +159 -0
- package/src/components/HeaderBar/HeaderBar.vue +238 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderComplexMenu.stories.ts +272 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderComplexMenu.vue +205 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/HeaderMenuItem.stories.ts +49 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/HeaderMenuItem.vue +51 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/tests/HeaderMenuItem.spec.ts +16 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/tests/__snapshots__/HeaderMenuItem.spec.ts.snap +3 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/HeaderMenuSection.stories.ts +56 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/HeaderMenuSection.vue +51 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/tests/HeaderMenuSection.spec.ts +33 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/HeaderSubMenu.stories.ts +137 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/HeaderSubMenu.vue +180 -0
- package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/tests/HeaderSubMenu.spec.ts +63 -0
- package/src/components/HeaderBar/HeaderComplexMenu/conts.ts +1 -0
- package/src/components/HeaderBar/HeaderComplexMenu/locals.ts +4 -0
- package/src/components/HeaderBar/HeaderComplexMenu/tests/HeaderComplexMenu.spec.ts +129 -0
- package/src/components/HeaderBar/HeaderComplexMenu/tests/__snapshots__/HeaderComplexMenu.spec.ts.snap +18 -0
- package/src/components/HeaderBar/HeaderComplexMenu/tests/useHandleSubMenus.spec.ts +158 -0
- package/src/components/HeaderBar/HeaderComplexMenu/useHandleSubMenus.ts +49 -0
- package/src/components/HeaderBar/HeaderLogo/HeaderLogo.vue +106 -0
- package/src/components/HeaderBar/HeaderLogo/locales.ts +3 -0
- package/src/components/HeaderBar/HeaderLogo/logos/Logo-mobile.vue +117 -0
- package/src/components/HeaderBar/HeaderLogo/logos/Logo.vue +279 -0
- package/src/components/HeaderBar/HeaderLogo/tests/HeaderLogo.spec.ts +71 -0
- package/src/components/HeaderBar/HeaderMenuBtn/HeaderMenuBtn.vue +88 -0
- package/src/components/HeaderBar/HeaderMenuBtn/locals.ts +4 -0
- package/src/components/HeaderBar/consts.scss +7 -0
- package/src/components/HeaderBar/consts.ts +2 -0
- package/src/components/HeaderBar/locales.ts +3 -0
- package/src/components/HeaderBar/tests/HeaderBar.spec.ts +210 -0
- package/src/components/HeaderBar/tests/__snapshots__/HeaderBar.spec.ts.snap +50 -0
- package/src/components/HeaderBar/tests/useHeaderResponsiveMode.spec.ts +26 -0
- package/src/components/HeaderBar/tests/useScrollDirection.spec.ts +34 -0
- package/src/components/HeaderBar/useHeaderResponsiveMode.ts +25 -0
- package/src/components/HeaderBar/useScrollDirection.ts +26 -0
- package/src/components/LangBtn/LangBtn.mdx +2 -1
- package/src/components/LangBtn/LangBtn.vue +3 -3
- package/src/components/Logo/Logo.mdx +26 -0
- package/src/components/Logo/Logo.stories.ts +217 -0
- package/src/components/Logo/Logo.vue +397 -0
- package/src/components/Logo/LogoSize.ts +7 -0
- package/src/components/Logo/locales.ts +6 -0
- package/src/components/Logo/logoDimensionsMapping.ts +16 -0
- package/src/components/Logo/tests/Logo.spec.ts +75 -0
- package/src/components/Logo/types.d.ts +8 -0
- package/src/components/NotificationBar/NotificationBar.vue +5 -7
- package/src/components/PageContainer/PageContainer.vue +0 -1
- package/src/components/SocialMediaLinks/DefaultSocialMediaLinks.ts +21 -0
- package/src/components/SocialMediaLinks/SocialMediaLinks.mdx +15 -0
- package/src/components/SocialMediaLinks/SocialMediaLinks.stories.ts +72 -0
- package/src/components/SocialMediaLinks/SocialMediaLinks.vue +92 -0
- package/src/components/SocialMediaLinks/locales.ts +3 -0
- package/src/components/SocialMediaLinks/tests/DefaultSocialMediaLinks.spec.ts +21 -0
- package/src/components/SocialMediaLinks/tests/SocialMediaLinks.spec.ts +89 -0
- package/src/components/SocialMediaLinks/tests/__snapshots__/SocialMediaLinks.spec.ts.snap +24 -0
- package/src/components/SocialMediaLinks/types.d.ts +5 -0
- package/src/components/index.ts +6 -0
- package/src/directives/clickOutside.ts +24 -0
- package/src/temp/TestDTComponent.vue +6 -10
- package/src/temp/gridsTests.vue +0 -4
- package/src/utils/propValidator/index.ts +20 -0
- package/src/utils/propValidator/tests/propValidator.spec.ts +40 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useId } from 'vue'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
title?: string
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
const id = useId()
|
|
9
|
+
const groupId = `${id}-group`
|
|
10
|
+
const titleId = `${id}-group-title`
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div class="header-menu-section">
|
|
15
|
+
<div
|
|
16
|
+
v-if="title"
|
|
17
|
+
:id="titleId"
|
|
18
|
+
class="header-menu-section-title"
|
|
19
|
+
>
|
|
20
|
+
{{ title }}
|
|
21
|
+
</div>
|
|
22
|
+
<ul
|
|
23
|
+
:id="groupId"
|
|
24
|
+
role="group"
|
|
25
|
+
class="header-menu-section-list"
|
|
26
|
+
>
|
|
27
|
+
<slot />
|
|
28
|
+
</ul>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<style lang="scss" scoped>
|
|
33
|
+
@use "@/assets/tokens.scss" as *;
|
|
34
|
+
@use "../../consts.scss" as menu;
|
|
35
|
+
|
|
36
|
+
.header-menu-section {
|
|
37
|
+
list-style-type: none;
|
|
38
|
+
padding: 0;
|
|
39
|
+
margin: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.header-menu-section-title {
|
|
43
|
+
padding: 40px 16px 8px 20px;
|
|
44
|
+
border-bottom: 1px solid menu.$menu-border-color;
|
|
45
|
+
font-size: 1.1rem;
|
|
46
|
+
margin-bottom: 8px;
|
|
47
|
+
color: #212529;
|
|
48
|
+
text-transform: capitalize;
|
|
49
|
+
font-weight: 700;
|
|
50
|
+
}
|
|
51
|
+
</style>
|
package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/tests/HeaderMenuSection.spec.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import HeaderMenuSection from '../HeaderMenuSection.vue'
|
|
4
|
+
|
|
5
|
+
describe('HeaderMenuSection', () => {
|
|
6
|
+
it('should render the component', async () => {
|
|
7
|
+
const wrapper = mount(HeaderMenuSection, {
|
|
8
|
+
props: {
|
|
9
|
+
title: 'Section title',
|
|
10
|
+
},
|
|
11
|
+
slots: {
|
|
12
|
+
default: [
|
|
13
|
+
'<li><a>Test 1</a></li>',
|
|
14
|
+
'<li><a>Test 2</a></li>',
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
expect(wrapper.find('.header-menu-section-title').text()).toBe('Section title')
|
|
20
|
+
expect(wrapper.find('.header-menu-section-list').element.children.length).toBe(2)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should render the component with no title', async () => {
|
|
24
|
+
const wrapper = mount(HeaderMenuSection, {
|
|
25
|
+
slots: {
|
|
26
|
+
default: '<li><a>Test 1</a></li>',
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(wrapper.find('.header-menu-section-title').exists()).toBe(false)
|
|
31
|
+
expect(wrapper.find('.header-menu-section-list').element.children.length).toBe(1)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import HeaderBar from '../../HeaderBar.vue'
|
|
3
|
+
import HeaderMenu from '../HeaderComplexMenu.vue'
|
|
4
|
+
import HeaderMenuItem from '../HeaderMenuItem/HeaderMenuItem.vue'
|
|
5
|
+
import HeaderMenuSection from '../HeaderMenuSection/HeaderMenuSection.vue'
|
|
6
|
+
import HeaderSubMenu from './HeaderSubMenu.vue'
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Components/HeaderBar/HeaderComplexMenu/HeaderSubMenu',
|
|
10
|
+
component: HeaderSubMenu,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'fullscreen',
|
|
13
|
+
},
|
|
14
|
+
} satisfies Meta<typeof HeaderSubMenu>
|
|
15
|
+
|
|
16
|
+
export default meta
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof meta>
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
args: {
|
|
22
|
+
title: 'Menu de premier niveau',
|
|
23
|
+
},
|
|
24
|
+
render: (args) => {
|
|
25
|
+
return {
|
|
26
|
+
components: { HeaderMenuItem, HeaderMenu, HeaderBar, HeaderSubMenu, HeaderMenuSection },
|
|
27
|
+
setup() {
|
|
28
|
+
return { args }
|
|
29
|
+
},
|
|
30
|
+
template: `
|
|
31
|
+
<HeaderBar>
|
|
32
|
+
<template #menu>
|
|
33
|
+
<HeaderMenu>
|
|
34
|
+
<HeaderMenuSection>
|
|
35
|
+
<HeaderMenuItem>
|
|
36
|
+
<a>Item</a>
|
|
37
|
+
</HeaderMenuItem>
|
|
38
|
+
<HeaderMenuItem>
|
|
39
|
+
<a>Item</a>
|
|
40
|
+
</HeaderMenuItem>
|
|
41
|
+
<headerMenuItem>
|
|
42
|
+
<HeaderSubMenu>
|
|
43
|
+
<template #title>
|
|
44
|
+
{{ args.title }}
|
|
45
|
+
</template>
|
|
46
|
+
<HeaderMenuItem>
|
|
47
|
+
<a>Item</a>
|
|
48
|
+
</HeaderMenuItem>
|
|
49
|
+
</HeaderSubMenu>
|
|
50
|
+
</headerMenuItem>
|
|
51
|
+
<HeaderMenuItem>
|
|
52
|
+
<a>Item</a>
|
|
53
|
+
</HeaderMenuItem>
|
|
54
|
+
</HeaderMenuSection>
|
|
55
|
+
</HeaderMenu>
|
|
56
|
+
</template>
|
|
57
|
+
</HeaderBar>
|
|
58
|
+
`,
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
play: async ({ canvasElement }) => {
|
|
62
|
+
const menuBtn = canvasElement.querySelector('button')
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
menuBtn!.click()
|
|
65
|
+
}, 1000)
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const Deep: Story = {
|
|
70
|
+
args: {
|
|
71
|
+
title: 'Menu de deuxième niveau',
|
|
72
|
+
},
|
|
73
|
+
render: (args) => {
|
|
74
|
+
return {
|
|
75
|
+
components: { HeaderMenuItem, HeaderMenu, HeaderBar, HeaderSubMenu, HeaderMenuSection },
|
|
76
|
+
setup() {
|
|
77
|
+
return { args }
|
|
78
|
+
},
|
|
79
|
+
template: `
|
|
80
|
+
<HeaderBar>
|
|
81
|
+
<template #menu>
|
|
82
|
+
<HeaderMenu>
|
|
83
|
+
<HeaderMenuSection>
|
|
84
|
+
<HeaderMenuItem>
|
|
85
|
+
<a>Item 1</a>
|
|
86
|
+
</HeaderMenuItem>
|
|
87
|
+
<HeaderMenuItem>
|
|
88
|
+
<a>Item 2</a>
|
|
89
|
+
</HeaderMenuItem>
|
|
90
|
+
<headerMenuItem>
|
|
91
|
+
<HeaderSubMenu>
|
|
92
|
+
<template #title>
|
|
93
|
+
Menu de premier niveau
|
|
94
|
+
</template>
|
|
95
|
+
<HeaderMenuItem>
|
|
96
|
+
<a>Item 2.1</a>
|
|
97
|
+
</HeaderMenuItem>
|
|
98
|
+
<HeaderMenuItem>
|
|
99
|
+
<a>Item 2.2</a>
|
|
100
|
+
</HeaderMenuItem>
|
|
101
|
+
<HeaderMenuItem>
|
|
102
|
+
<HeaderSubMenu>
|
|
103
|
+
<template #title>
|
|
104
|
+
Menu de deuxième niveau
|
|
105
|
+
</template>
|
|
106
|
+
<HeaderMenuItem>
|
|
107
|
+
<a>Item 3.1</a>
|
|
108
|
+
</HeaderMenuItem>
|
|
109
|
+
<HeaderMenuItem>
|
|
110
|
+
<a>Item 3.2</a>
|
|
111
|
+
</HeaderMenuItem>
|
|
112
|
+
</HeaderSubMenu>
|
|
113
|
+
</HeaderMenuItem>
|
|
114
|
+
</HeaderSubMenu>
|
|
115
|
+
</headerMenuItem>
|
|
116
|
+
<HeaderMenuItem>
|
|
117
|
+
<a>Item 3</a>
|
|
118
|
+
</HeaderMenuItem>
|
|
119
|
+
</HeaderMenuSection>
|
|
120
|
+
</HeaderMenu>
|
|
121
|
+
</template>
|
|
122
|
+
</HeaderBar>
|
|
123
|
+
`,
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
play: async ({ canvasElement }) => {
|
|
127
|
+
const menuBtn = canvasElement.querySelector('button')
|
|
128
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
129
|
+
await menuBtn!.click()
|
|
130
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
131
|
+
const subMenuBtn = canvasElement.querySelector<HTMLElement>('.sub-menu-btn')
|
|
132
|
+
await subMenuBtn!.click()
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
134
|
+
const subMenuBtn2 = canvasElement.querySelector<HTMLElement>('.sub-menu .sub-menu .sub-menu-btn')
|
|
135
|
+
await subMenuBtn2!.click()
|
|
136
|
+
},
|
|
137
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'
|
|
3
|
+
import { inject, readonly, ref, useId, type DeepReadonly, type Ref } from 'vue'
|
|
4
|
+
import { registerSubMenuKey } from '../conts'
|
|
5
|
+
import useHandleSubMenus from '../useHandleSubMenus'
|
|
6
|
+
|
|
7
|
+
const menuOpen = ref(false)
|
|
8
|
+
const submenuId = useId()
|
|
9
|
+
const btnId = `${submenuId}-btn`
|
|
10
|
+
|
|
11
|
+
const registerSubMenu = inject<((r: DeepReadonly<Ref<boolean>>, c: () => void) => void) | undefined>(registerSubMenuKey, undefined)
|
|
12
|
+
if (!registerSubMenu) throw new Error('The HeaderSubMenu component must be used inside a HeaderComplexMenu component')
|
|
13
|
+
registerSubMenu(menuOpen, () => {
|
|
14
|
+
menuOpen.value = false
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const { haveOpenSubMenu } = useHandleSubMenus(readonly(menuOpen))
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div
|
|
22
|
+
class="sub-menu"
|
|
23
|
+
:class="{
|
|
24
|
+
'sub-menu--open': menuOpen,
|
|
25
|
+
'sub-menu--child-open': haveOpenSubMenu,
|
|
26
|
+
}"
|
|
27
|
+
>
|
|
28
|
+
<button
|
|
29
|
+
:id="btnId"
|
|
30
|
+
class="sub-menu-btn"
|
|
31
|
+
type="button"
|
|
32
|
+
:aria-expanded="menuOpen ? 'true' : 'false'"
|
|
33
|
+
:aria-controls="submenuId"
|
|
34
|
+
:title="menuOpen ? 'Close submenu' : 'Open submenu'"
|
|
35
|
+
@click="menuOpen = !menuOpen"
|
|
36
|
+
>
|
|
37
|
+
<slot name="title" />
|
|
38
|
+
|
|
39
|
+
<VIcon
|
|
40
|
+
size="36"
|
|
41
|
+
class="sub-menu-btn__icon"
|
|
42
|
+
>
|
|
43
|
+
{{ menuOpen ? mdiChevronLeft : mdiChevronRight }}
|
|
44
|
+
</VIcon>
|
|
45
|
+
</button>
|
|
46
|
+
<transition name="slide-fade">
|
|
47
|
+
<div
|
|
48
|
+
v-show="menuOpen"
|
|
49
|
+
:id="submenuId"
|
|
50
|
+
class="sub-menu-content-wrapper"
|
|
51
|
+
:aria-labelledby="btnId"
|
|
52
|
+
>
|
|
53
|
+
<div class="sub-menu-content">
|
|
54
|
+
<slot />
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</transition>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<style lang="scss" scoped>
|
|
62
|
+
@use "@/assets/tokens.scss" as *;
|
|
63
|
+
@use '../../consts' as *;
|
|
64
|
+
|
|
65
|
+
.sub-menu-btn {
|
|
66
|
+
display: flex;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
width: 100%;
|
|
70
|
+
padding: 16px 50px 16px 20px;
|
|
71
|
+
text-align: left;
|
|
72
|
+
color: $primary-base;
|
|
73
|
+
|
|
74
|
+
&:hover {
|
|
75
|
+
background-color: $primary-base;
|
|
76
|
+
color: $neutral-white;
|
|
77
|
+
text-decoration: underline;
|
|
78
|
+
|
|
79
|
+
> :deep(*) {
|
|
80
|
+
color: $neutral-white !important;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&::first-letter {
|
|
85
|
+
text-transform: uppercase;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.sub-menu-btn__icon {
|
|
90
|
+
position: absolute;
|
|
91
|
+
right: 20px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media screen and (max-width: ($header-breakpoint - 1)) {
|
|
95
|
+
.sub-menu--open {
|
|
96
|
+
position: absolute;
|
|
97
|
+
left: 0;
|
|
98
|
+
top: 0;
|
|
99
|
+
width: 100%;
|
|
100
|
+
height: 100%;
|
|
101
|
+
overflow-y: auto;
|
|
102
|
+
background-color: $neutral-white;
|
|
103
|
+
padding-top: 40px;
|
|
104
|
+
z-index: 10;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// If a submenu is open, the parent menu should not scroll, the child menu should
|
|
108
|
+
.sub-menu--child-open {
|
|
109
|
+
overflow-y: clip;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.sub-menu--open > .sub-menu-btn {
|
|
113
|
+
padding: 0 16px 8px 40px;
|
|
114
|
+
border-bottom: 1px solid $menu-border-color;
|
|
115
|
+
color: #000;
|
|
116
|
+
background-color: transparent;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.sub-menu--open > .sub-menu-btn > :deep(.sub-menu-btn__icon) {
|
|
120
|
+
left: 10px;
|
|
121
|
+
right: auto;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@media screen and (min-width: $header-breakpoint) {
|
|
126
|
+
.sub-menu-btn {
|
|
127
|
+
position: relative;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.sub-menu--open > .sub-menu-btn {
|
|
131
|
+
background-color: $primary-base;
|
|
132
|
+
color: $neutral-white;
|
|
133
|
+
transition: color 0.15s linear, background-color 0.15s linear;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.sub-menu-content-wrapper {
|
|
137
|
+
position: absolute;
|
|
138
|
+
top: 0;
|
|
139
|
+
left: $menu-width;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.sub-menu-content {
|
|
143
|
+
width: $menu-width + 1px;
|
|
144
|
+
height: $menu-height;
|
|
145
|
+
background: #f9f9f9;
|
|
146
|
+
border-left: 1px solid $menu-border-color;
|
|
147
|
+
overflow-y: auto;
|
|
148
|
+
overflow-x: hidden;
|
|
149
|
+
|
|
150
|
+
> .sub-menu--open .sub-menu-content {
|
|
151
|
+
left: $menu-width * 2;
|
|
152
|
+
|
|
153
|
+
> .sub-menu--open .sub-menu-content {
|
|
154
|
+
left: $menu-width * 3;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Transitions */
|
|
160
|
+
|
|
161
|
+
.slide-fade-enter-active {
|
|
162
|
+
transition: opacity 0.17s ease-out, transform 0.17s ease-out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.slide-fade-leave-active {
|
|
166
|
+
transition: opacity 0.08s ease-in, transform 0.08s ease-in;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.slide-fade-enter-from, .slide-fade-leave-to {
|
|
170
|
+
opacity: 0;
|
|
171
|
+
transform: translateX(-10px);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@media (prefers-reduced-motion) {
|
|
175
|
+
.slide-fade-enter-active, .slide-fade-leave-active {
|
|
176
|
+
transition: none;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { vuetify } from '@tests/unit/setup'
|
|
5
|
+
import { registerSubMenuKey } from '../../conts'
|
|
6
|
+
import HeaderSubMenu from '../HeaderSubMenu.vue'
|
|
7
|
+
|
|
8
|
+
const registerSubMenu = vi.fn()
|
|
9
|
+
describe('HeaderSubMenu', () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.resetAllMocks()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should render the component', async () => {
|
|
15
|
+
const wrapper = mount(HeaderSubMenu, {
|
|
16
|
+
slots: {
|
|
17
|
+
title: '<h2>Sub menu title</h2>',
|
|
18
|
+
default: '<ul><li><a>Test 1</a></li></ul>',
|
|
19
|
+
},
|
|
20
|
+
global: {
|
|
21
|
+
plugins: [vuetify],
|
|
22
|
+
provide: {
|
|
23
|
+
[registerSubMenuKey]: registerSubMenu,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const content = wrapper.find('.sub-menu-content-wrapper')
|
|
29
|
+
|
|
30
|
+
expect(wrapper.find('h2').text()).toBe('Sub menu title')
|
|
31
|
+
expect(wrapper.find('.sub-menu-content').element.children[0].textContent).toBe('Test 1')
|
|
32
|
+
expect(registerSubMenu.mock.calls.length).toBe(1)
|
|
33
|
+
const sharedStatus = registerSubMenu.mock.calls[0][0]
|
|
34
|
+
const sharedClose = registerSubMenu.mock.calls[0][1]
|
|
35
|
+
|
|
36
|
+
expect(sharedStatus.value).toBe(false)
|
|
37
|
+
expect(content.attributes('style')).toContain('display: none;')
|
|
38
|
+
|
|
39
|
+
const btn = wrapper.find('.sub-menu-btn')
|
|
40
|
+
await btn.trigger('click')
|
|
41
|
+
|
|
42
|
+
expect(sharedStatus.value).toBe(true)
|
|
43
|
+
expect(content.attributes('style')).toBeUndefined()
|
|
44
|
+
|
|
45
|
+
await sharedClose()
|
|
46
|
+
expect(sharedStatus.value).toBe(false)
|
|
47
|
+
expect(content.attributes('style')).toContain('display: none;')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('throws an error if no register function is provided', async () => {
|
|
51
|
+
const mountWithoutInject = () => mount(HeaderSubMenu, {
|
|
52
|
+
slots: {
|
|
53
|
+
title: '<h2>Sub menu title</h2>',
|
|
54
|
+
default: '<ul><li><a>Test 1</a></li></ul>',
|
|
55
|
+
},
|
|
56
|
+
global: {
|
|
57
|
+
plugins: [vuetify],
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(mountWithoutInject).toThrowError('The HeaderSubMenu component must be used inside a HeaderComplexMenu component')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const registerSubMenuKey = Symbol('registerSubMenu')
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { vuetify } from '@tests/unit/setup'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { afterEach } from 'node:test'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { registerHeaderMenuKey } from '../../consts'
|
|
6
|
+
import HeaderComplexMenu from '../HeaderComplexMenu.vue'
|
|
7
|
+
|
|
8
|
+
describe('HeaderComplexMenu', () => {
|
|
9
|
+
const BtnTestComponent = {
|
|
10
|
+
setup() {
|
|
11
|
+
const props = defineProps({ modelValue: Boolean })
|
|
12
|
+
return { open: props.modelValue }
|
|
13
|
+
},
|
|
14
|
+
template: `<button @click="$emit('update:modelValue', !open)">Test</button>`,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.resetAllMocks()
|
|
19
|
+
document.body.innerHTML = ''
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should render the component', async () => {
|
|
23
|
+
const wrapper = mount(HeaderComplexMenu, {
|
|
24
|
+
global: {
|
|
25
|
+
plugins: [vuetify],
|
|
26
|
+
provide: {
|
|
27
|
+
[registerHeaderMenuKey]: () => {},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
slots: {
|
|
31
|
+
default: '<div>Default slot</div>',
|
|
32
|
+
},
|
|
33
|
+
stubs: {
|
|
34
|
+
HeaderMenuBtn: BtnTestComponent,
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(wrapper.html()).toMatchSnapshot()
|
|
39
|
+
|
|
40
|
+
const menu = wrapper.find('.overlay')
|
|
41
|
+
expect(menu.attributes('style')).toContain('display: none;')
|
|
42
|
+
|
|
43
|
+
const btn = wrapper.find('.header-menu-btn')
|
|
44
|
+
await btn.trigger('click')
|
|
45
|
+
expect(menu.attributes('style')).toBeUndefined()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should close the menu when clicking outside', async () => {
|
|
49
|
+
const wrapper = mount(HeaderComplexMenu, {
|
|
50
|
+
global: {
|
|
51
|
+
plugins: [vuetify],
|
|
52
|
+
provide: {
|
|
53
|
+
[registerHeaderMenuKey]: () => {},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
stubs: {
|
|
57
|
+
HeaderMenuBtn: BtnTestComponent,
|
|
58
|
+
},
|
|
59
|
+
slots: {
|
|
60
|
+
default: '<div>Default slot</div>',
|
|
61
|
+
},
|
|
62
|
+
attachTo: document.body,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const overlay = wrapper.find('.overlay')
|
|
66
|
+
const btn = wrapper.find('.header-menu-btn')
|
|
67
|
+
|
|
68
|
+
await btn.trigger('click')
|
|
69
|
+
expect(overlay.attributes('style')).toBeUndefined()
|
|
70
|
+
|
|
71
|
+
await overlay.trigger('click')
|
|
72
|
+
expect(overlay.attributes('style')).toContain('display: none;')
|
|
73
|
+
|
|
74
|
+
wrapper.unmount()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should not close the menu when clicking inside', async () => {
|
|
78
|
+
const wrapper = mount(HeaderComplexMenu, {
|
|
79
|
+
global: {
|
|
80
|
+
plugins: [vuetify],
|
|
81
|
+
provide: {
|
|
82
|
+
[registerHeaderMenuKey]: () => {},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
stubs: {
|
|
86
|
+
HeaderMenuBtn: BtnTestComponent,
|
|
87
|
+
},
|
|
88
|
+
slots: {
|
|
89
|
+
default: '<div>Default slot</div>',
|
|
90
|
+
},
|
|
91
|
+
attachTo: document.body,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const menu = wrapper.find('.overlay')
|
|
95
|
+
const btn = wrapper.find('button')
|
|
96
|
+
await btn.trigger('click')
|
|
97
|
+
expect(menu.attributes('style')).toBeUndefined()
|
|
98
|
+
|
|
99
|
+
await wrapper.find('.header-menu').trigger('click')
|
|
100
|
+
expect(menu.attributes('style')).toBeUndefined()
|
|
101
|
+
|
|
102
|
+
wrapper.unmount()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should listen to the button to open and close the menu', async () => {
|
|
106
|
+
const wrapper = mount(HeaderComplexMenu, {
|
|
107
|
+
global: {
|
|
108
|
+
plugins: [vuetify],
|
|
109
|
+
provide: {
|
|
110
|
+
[registerHeaderMenuKey]: () => {},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
stubs: {
|
|
114
|
+
HeaderMenuBtn: BtnTestComponent,
|
|
115
|
+
},
|
|
116
|
+
slots: {
|
|
117
|
+
default: '<div>Default slot</div>',
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const menu = wrapper.find('.overlay')
|
|
122
|
+
const btn = wrapper.find('button')
|
|
123
|
+
await btn.trigger('click')
|
|
124
|
+
expect(menu.attributes('style')).toBeUndefined()
|
|
125
|
+
|
|
126
|
+
await btn.trigger('click')
|
|
127
|
+
expect(menu.attributes('style')).toContain('display: none;')
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`HeaderComplexMenu > should render the component 1`] = `
|
|
4
|
+
"<div data-v-10d9bc74="" role="dialog" aria-modal="true" aria-label="Menu principal">
|
|
5
|
+
<div data-v-10d9bc74=""><button data-v-70557900="" data-v-10d9bc74="" class="header-menu-btn" style="background-color: #1867C0; color: #fff;" type="button" aria-label="Ouvrir le menu" title="Ouvrir le menu"><i data-v-70557900="" class="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z mdi v-icon notranslate v-theme--light" style="font-size: 48px; height: 48px; width: 48px;" aria-hidden="true"></i><span data-v-70557900="" class="header-menu-btn__label">Menu</span></button></div>
|
|
6
|
+
<transition-stub data-v-10d9bc74="" name="menu" appear="false" persisted="true" css="true">
|
|
7
|
+
<div data-v-10d9bc74="" class="overlay" style="display: none;">
|
|
8
|
+
<div data-v-10d9bc74="" role="menu" class="menu-wrapper" style="left: 0px; top: 0px;"><button data-v-70557900="" data-v-10d9bc74="" class="header-menu-btn" style="background-color: #1867C0; color: #fff;" type="button" aria-label="Ouvrir le menu" title="Ouvrir le menu"><i data-v-70557900="" class="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z mdi v-icon notranslate v-theme--light" style="font-size: 48px; height: 48px; width: 48px;" aria-hidden="true"></i><span data-v-70557900="" class="header-menu-btn__label">Menu</span></button>
|
|
9
|
+
<nav data-v-10d9bc74="" id="header-menu-wrapper" class="header-menu-wrapper" role="navigation" aria-label="Menu public">
|
|
10
|
+
<div data-v-10d9bc74="" class="header-menu">
|
|
11
|
+
<div>Default slot</div>
|
|
12
|
+
</div>
|
|
13
|
+
</nav>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</transition-stub>
|
|
17
|
+
</div>"
|
|
18
|
+
`;
|