@cnamts/synapse 0.0.2-alpha → 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 +1 -1
- package/dist/design-system-v3.js +957 -4826
- package/dist/design-system-v3.umd.cjs +1 -2
- package/dist/style.css +1 -1
- package/package.json +29 -29
- package/src/components/Alert/Alert.vue +8 -8
- 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/NotificationBar/NotificationBar.vue +5 -7
- package/src/components/PageContainer/PageContainer.vue +0 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject, nextTick, onMounted, onUnmounted, readonly, ref, watch, type CSSProperties, type Ref } from 'vue'
|
|
3
|
+
import HeaderMenuBtn from '../HeaderMenuBtn/HeaderMenuBtn.vue'
|
|
4
|
+
import { registerHeaderMenuKey } from '../consts'
|
|
5
|
+
import locals from './locals'
|
|
6
|
+
import useHandleSubMenus from './useHandleSubMenus'
|
|
7
|
+
|
|
8
|
+
const menuWrapper = ref<HTMLElement | null>(null)
|
|
9
|
+
const menuBtnWrapper = ref<HTMLDivElement | null>(null)
|
|
10
|
+
const outerBtn = ref<HTMLElement | null>(null)
|
|
11
|
+
const innerBtn = ref<HTMLElement | null>(null)
|
|
12
|
+
const menuLeft = ref(0)
|
|
13
|
+
const menuTop = ref(0)
|
|
14
|
+
|
|
15
|
+
function positionMenu() {
|
|
16
|
+
// todo debounce
|
|
17
|
+
menuLeft.value = menuBtnWrapper.value!.getBoundingClientRect().left
|
|
18
|
+
menuTop.value = menuBtnWrapper.value!.getBoundingClientRect().top
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
onMounted(() => {
|
|
22
|
+
positionMenu()
|
|
23
|
+
window.addEventListener('scroll', positionMenu)
|
|
24
|
+
window.addEventListener('resize', positionMenu)
|
|
25
|
+
window.addEventListener('click', handleClickOutside, { capture: true })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
onUnmounted(() => {
|
|
29
|
+
window.removeEventListener('scroll', positionMenu)
|
|
30
|
+
window.removeEventListener('resize', positionMenu)
|
|
31
|
+
window.removeEventListener('click', handleClickOutside, { capture: true })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const menuOpen = ref(false)
|
|
35
|
+
|
|
36
|
+
watch(menuOpen, async (newVal) => {
|
|
37
|
+
document.documentElement.style.overflow = newVal ? 'hidden' : 'auto'
|
|
38
|
+
document.body.style.overflow = newVal ? 'hidden' : 'auto'
|
|
39
|
+
|
|
40
|
+
if (newVal) {
|
|
41
|
+
await nextTick()
|
|
42
|
+
innerBtn.value!.focus()
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
outerBtn.value!.focus()
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const menuStyle = computed<CSSProperties>(() => ({
|
|
50
|
+
left: `${menuLeft.value}px`,
|
|
51
|
+
top: `${menuTop.value}px`,
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
function handleClickOutside(event: MouseEvent | KeyboardEvent) {
|
|
55
|
+
if (!menuOpen.value) return
|
|
56
|
+
|
|
57
|
+
// do not close menu if click is inside the menu
|
|
58
|
+
let walkElement = event.target as HTMLElement | null
|
|
59
|
+
while (walkElement && walkElement !== document.body) {
|
|
60
|
+
if (walkElement === menuWrapper.value) return
|
|
61
|
+
walkElement = walkElement.parentElement
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
menuOpen.value = false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { haveOpenSubMenu } = useHandleSubMenus(readonly(menuOpen))
|
|
68
|
+
|
|
69
|
+
const registerHeaderMenu = inject<(menuOpen: Ref<boolean>) => void>(registerHeaderMenuKey)
|
|
70
|
+
if (registerHeaderMenu) registerHeaderMenu(menuOpen)
|
|
71
|
+
</script>
|
|
72
|
+
<template>
|
|
73
|
+
<div
|
|
74
|
+
role="dialog"
|
|
75
|
+
aria-modal="true"
|
|
76
|
+
:aria-label="locals.mainMenu"
|
|
77
|
+
>
|
|
78
|
+
<div ref="menuBtnWrapper">
|
|
79
|
+
<HeaderMenuBtn
|
|
80
|
+
ref="outerBtn"
|
|
81
|
+
v-model="menuOpen"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
<Transition name="menu">
|
|
85
|
+
<div
|
|
86
|
+
v-show="menuOpen"
|
|
87
|
+
class="overlay"
|
|
88
|
+
>
|
|
89
|
+
<div
|
|
90
|
+
ref="menuWrapper"
|
|
91
|
+
role="menu"
|
|
92
|
+
class="menu-wrapper"
|
|
93
|
+
:style="menuStyle"
|
|
94
|
+
>
|
|
95
|
+
<HeaderMenuBtn
|
|
96
|
+
ref="innerBtn"
|
|
97
|
+
v-model="menuOpen"
|
|
98
|
+
/>
|
|
99
|
+
<nav
|
|
100
|
+
id="header-menu-wrapper"
|
|
101
|
+
class="header-menu-wrapper"
|
|
102
|
+
:class="{
|
|
103
|
+
'header-menu-wrapper--submenu-open': haveOpenSubMenu,
|
|
104
|
+
}"
|
|
105
|
+
role="navigation"
|
|
106
|
+
:aria-label="locals.publicMenu"
|
|
107
|
+
>
|
|
108
|
+
<div class="header-menu">
|
|
109
|
+
<slot />
|
|
110
|
+
</div>
|
|
111
|
+
</nav>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</Transition>
|
|
115
|
+
</div>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<style lang="scss" scoped>
|
|
119
|
+
@use '@/assets/tokens.scss' as *;
|
|
120
|
+
@use '../consts' as *;
|
|
121
|
+
|
|
122
|
+
.overlay {
|
|
123
|
+
inset: 0;
|
|
124
|
+
position: fixed;
|
|
125
|
+
z-index: 1000;
|
|
126
|
+
background-color: rgba(3, 16, 37, .5);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.menu-wrapper {
|
|
130
|
+
height: 100dvh;
|
|
131
|
+
background-color: $neutral-white;
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.header-menu-wrapper {
|
|
137
|
+
height: calc(100% - $header-height);
|
|
138
|
+
display: grid;
|
|
139
|
+
position: relative;
|
|
140
|
+
overflow: auto;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.header-menu-wrapper--submenu-open {
|
|
144
|
+
overflow: clip;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@media screen and (min-width: $header-breakpoint) {
|
|
148
|
+
.menu-wrapper {
|
|
149
|
+
position: absolute;
|
|
150
|
+
background-color: transparent;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.header-menu-wrapper {
|
|
154
|
+
height: $menu-height;
|
|
155
|
+
width: $menu-width;
|
|
156
|
+
overflow: visible;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.header-menu {
|
|
160
|
+
background-color: $neutral-white;
|
|
161
|
+
overflow-y : auto;
|
|
162
|
+
overflow-x: hidden;
|
|
163
|
+
height: $menu-height;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.menu-enter-active {
|
|
168
|
+
transition: opacity 0.15s ease-in;
|
|
169
|
+
|
|
170
|
+
.header-menu-wrapper {
|
|
171
|
+
transition: transform 0.1s ease-in;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.menu-leave-active {
|
|
176
|
+
transition: opacity 0.15s ease-out;
|
|
177
|
+
|
|
178
|
+
.header-menu-wrapper {
|
|
179
|
+
transition: transform 0.1s ease-out;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.menu-enter-from, .menu-leave-to {
|
|
184
|
+
opacity: 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@media screen and (min-width: $header-breakpoint) {
|
|
188
|
+
.menu-enter-from, .menu-leave-to {
|
|
189
|
+
.header-menu-wrapper {
|
|
190
|
+
transform: translateY(10px);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@media (prefers-reduced-motion: reduce) {
|
|
196
|
+
.menu-enter-active, .menu-leave-active {
|
|
197
|
+
transition: opacity 0s;
|
|
198
|
+
}
|
|
199
|
+
.menu-enter-from, .menu-leave-to {
|
|
200
|
+
.header-menu-wrapper {
|
|
201
|
+
transform: none;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import HeaderMenuItem from './HeaderMenuItem.vue'
|
|
3
|
+
import HeaderMenu from '../HeaderComplexMenu.vue'
|
|
4
|
+
import HeaderBar from '../../HeaderBar.vue'
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Components/HeaderBar/HeaderComplexMenu/HeaderMenuItem',
|
|
8
|
+
component: HeaderMenuItem,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'fullscreen',
|
|
11
|
+
},
|
|
12
|
+
} satisfies Meta<typeof HeaderMenuItem>
|
|
13
|
+
|
|
14
|
+
export default meta
|
|
15
|
+
|
|
16
|
+
type Story = StoryObj<typeof meta>
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
default: 'lorem ipsum',
|
|
21
|
+
},
|
|
22
|
+
render: (args) => {
|
|
23
|
+
return {
|
|
24
|
+
components: { HeaderMenuItem, HeaderMenu, HeaderBar },
|
|
25
|
+
setup() {
|
|
26
|
+
return { args }
|
|
27
|
+
},
|
|
28
|
+
template: `
|
|
29
|
+
<HeaderBar>
|
|
30
|
+
<template #menu>
|
|
31
|
+
<HeaderMenu>
|
|
32
|
+
<HeaderMenuSection>
|
|
33
|
+
<HeaderMenuItem>
|
|
34
|
+
<a>{{ args.default }}</a>
|
|
35
|
+
</HeaderMenuItem>
|
|
36
|
+
</HeaderMenuSection>
|
|
37
|
+
</HeaderMenu>
|
|
38
|
+
</template>
|
|
39
|
+
</HeaderBar>
|
|
40
|
+
`,
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
play: async ({ canvasElement }) => {
|
|
44
|
+
const menuBtn = canvasElement.querySelector('button')
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
menuBtn!.click()
|
|
47
|
+
}, 1000)
|
|
48
|
+
},
|
|
49
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<li class="header-menu-item">
|
|
7
|
+
<slot />
|
|
8
|
+
</li>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<style lang="scss" scoped>
|
|
12
|
+
@use "@/assets/tokens.scss" as *;
|
|
13
|
+
|
|
14
|
+
.header-menu-item {
|
|
15
|
+
color: $primary-base;
|
|
16
|
+
list-style-type: none;
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 0;
|
|
19
|
+
min-height: 44px; // accessibility requirement
|
|
20
|
+
font-weight: 700;
|
|
21
|
+
|
|
22
|
+
> :deep(a) {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
padding: 16px 50px 16px 20px;
|
|
26
|
+
text-decoration: none;
|
|
27
|
+
color: currentColor;
|
|
28
|
+
|
|
29
|
+
&:hover {
|
|
30
|
+
text-decoration: underline;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&:visited {
|
|
34
|
+
color: currentColor;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&::first-letter {
|
|
38
|
+
text-transform: uppercase;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.header-menu-item:hover {
|
|
44
|
+
background-color: $primary-base;
|
|
45
|
+
color: $neutral-white;
|
|
46
|
+
|
|
47
|
+
> :deep(a > *) {
|
|
48
|
+
color: $neutral-white !important;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</style>
|
package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/tests/HeaderMenuItem.spec.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import HeaderMenuItem from '../HeaderMenuItem.vue'
|
|
4
|
+
|
|
5
|
+
describe('HeaderMenuItem', () => {
|
|
6
|
+
it('should render the component', async () => {
|
|
7
|
+
const wrapper = mount(HeaderMenuItem, {
|
|
8
|
+
slots: {
|
|
9
|
+
default: '<a>Test</a>',
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
expect(wrapper.html()).toMatchSnapshot()
|
|
14
|
+
expect(wrapper.find('a').text()).toBe('Test')
|
|
15
|
+
})
|
|
16
|
+
})
|
package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/HeaderMenuSection.stories.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import HeaderMenuSection from './HeaderMenuSection.vue'
|
|
3
|
+
import HeaderMenuItem from '../HeaderMenuItem/HeaderMenuItem.vue'
|
|
4
|
+
import HeaderMenu from '../HeaderComplexMenu.vue'
|
|
5
|
+
import HeaderBar from '../../HeaderBar.vue'
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Components/HeaderBar/HeaderComplexMenu/HeaderMenuSection',
|
|
9
|
+
component: HeaderMenuSection,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'fullscreen',
|
|
12
|
+
},
|
|
13
|
+
} satisfies Meta<typeof HeaderMenuSection>
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
|
|
17
|
+
type Story = StoryObj<typeof meta>
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
title: 'section 1',
|
|
22
|
+
},
|
|
23
|
+
render: (args) => {
|
|
24
|
+
return {
|
|
25
|
+
components: { HeaderMenuItem, HeaderMenu, HeaderBar, HeaderMenuSection },
|
|
26
|
+
setup() {
|
|
27
|
+
return { args }
|
|
28
|
+
},
|
|
29
|
+
template: `
|
|
30
|
+
<HeaderBar>
|
|
31
|
+
<template #menu>
|
|
32
|
+
<HeaderMenu>
|
|
33
|
+
<HeaderMenuSection :title="args.title">
|
|
34
|
+
<HeaderMenuItem>
|
|
35
|
+
<a>lorem ipsum</a>
|
|
36
|
+
</HeaderMenuItem>
|
|
37
|
+
</HeaderMenuSection>
|
|
38
|
+
|
|
39
|
+
<HeaderMenuSection title="section 2">
|
|
40
|
+
<HeaderMenuItem>
|
|
41
|
+
<a>lorem ipsum</a>
|
|
42
|
+
</HeaderMenuItem>
|
|
43
|
+
</HeaderMenuSection>
|
|
44
|
+
</HeaderMenu>
|
|
45
|
+
</template>
|
|
46
|
+
</HeaderBar>
|
|
47
|
+
`,
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
play: async ({ canvasElement }) => {
|
|
51
|
+
const menuBtn = canvasElement.querySelector('button')
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
menuBtn!.click()
|
|
54
|
+
}, 1000)
|
|
55
|
+
},
|
|
56
|
+
}
|
|
@@ -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
|
+
}
|