@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.
Files changed (96) hide show
  1. package/README.md +72 -2
  2. package/dist/design-system-v3.d.ts +234 -2
  3. package/dist/design-system-v3.js +1413 -4404
  4. package/dist/design-system-v3.umd.cjs +1 -2
  5. package/dist/style.css +1 -1
  6. package/package.json +37 -36
  7. package/src/components/Alert/Alert.vue +8 -8
  8. package/src/components/CollapsibleList/CollapsibleList.mdx +47 -0
  9. package/src/components/CollapsibleList/CollapsibleList.stories.ts +52 -0
  10. package/src/components/CollapsibleList/CollapsibleList.vue +157 -0
  11. package/src/components/CollapsibleList/tests/CollapsibleList.spec.ts +60 -0
  12. package/src/components/CollapsibleList/types.d.ts +5 -0
  13. package/src/components/Customs/CustomInputSelect/CustomInputSelect.mdx +42 -0
  14. package/src/components/Customs/CustomInputSelect/CustomInputSelect.stories.ts +154 -0
  15. package/src/components/Customs/CustomInputSelect/CustomInputSelect.vue +185 -0
  16. package/src/components/Customs/CustomInputSelect/tests/CustomInputSelect.spec.ts +216 -0
  17. package/src/components/Customs/CustomSelect/CustomSelect.mdx +47 -0
  18. package/src/components/Customs/CustomSelect/CustomSelect.stories.ts +182 -0
  19. package/src/components/Customs/CustomSelect/CustomSelect.vue +188 -0
  20. package/src/components/Customs/CustomSelect/tests/CustomSelect.spec.ts +236 -0
  21. package/src/components/FooterBar/A11yCompliance.ts +9 -0
  22. package/src/components/FooterBar/FooterBar.mdx +115 -0
  23. package/src/components/FooterBar/FooterBar.stories.ts +632 -0
  24. package/src/components/FooterBar/FooterBar.vue +330 -0
  25. package/src/components/FooterBar/config.ts +20 -0
  26. package/src/components/FooterBar/defaultSocialMediaLinks.ts +21 -0
  27. package/src/components/FooterBar/locales.ts +16 -0
  28. package/src/components/FooterBar/tests/FooterBar.spec.ts +167 -0
  29. package/src/components/FooterBar/tests/FooterBarConfig.spec.ts +36 -0
  30. package/src/components/FooterBar/tests/__snapshots__/FooterBar.spec.ts.snap +27 -0
  31. package/src/components/FooterBar/types.d.ts +10 -0
  32. package/src/components/FranceConnectBtn/FranceConnectBtn.vue +2 -2
  33. package/src/components/HeaderBar/HeaderBar.mdx +137 -0
  34. package/src/components/HeaderBar/HeaderBar.stories.ts +159 -0
  35. package/src/components/HeaderBar/HeaderBar.vue +238 -0
  36. package/src/components/HeaderBar/HeaderComplexMenu/HeaderComplexMenu.stories.ts +272 -0
  37. package/src/components/HeaderBar/HeaderComplexMenu/HeaderComplexMenu.vue +205 -0
  38. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/HeaderMenuItem.stories.ts +49 -0
  39. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/HeaderMenuItem.vue +51 -0
  40. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/tests/HeaderMenuItem.spec.ts +16 -0
  41. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/tests/__snapshots__/HeaderMenuItem.spec.ts.snap +3 -0
  42. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/HeaderMenuSection.stories.ts +56 -0
  43. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/HeaderMenuSection.vue +51 -0
  44. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/tests/HeaderMenuSection.spec.ts +33 -0
  45. package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/HeaderSubMenu.stories.ts +137 -0
  46. package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/HeaderSubMenu.vue +180 -0
  47. package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/tests/HeaderSubMenu.spec.ts +63 -0
  48. package/src/components/HeaderBar/HeaderComplexMenu/conts.ts +1 -0
  49. package/src/components/HeaderBar/HeaderComplexMenu/locals.ts +4 -0
  50. package/src/components/HeaderBar/HeaderComplexMenu/tests/HeaderComplexMenu.spec.ts +129 -0
  51. package/src/components/HeaderBar/HeaderComplexMenu/tests/__snapshots__/HeaderComplexMenu.spec.ts.snap +18 -0
  52. package/src/components/HeaderBar/HeaderComplexMenu/tests/useHandleSubMenus.spec.ts +158 -0
  53. package/src/components/HeaderBar/HeaderComplexMenu/useHandleSubMenus.ts +49 -0
  54. package/src/components/HeaderBar/HeaderLogo/HeaderLogo.vue +106 -0
  55. package/src/components/HeaderBar/HeaderLogo/locales.ts +3 -0
  56. package/src/components/HeaderBar/HeaderLogo/logos/Logo-mobile.vue +117 -0
  57. package/src/components/HeaderBar/HeaderLogo/logos/Logo.vue +279 -0
  58. package/src/components/HeaderBar/HeaderLogo/tests/HeaderLogo.spec.ts +71 -0
  59. package/src/components/HeaderBar/HeaderMenuBtn/HeaderMenuBtn.vue +88 -0
  60. package/src/components/HeaderBar/HeaderMenuBtn/locals.ts +4 -0
  61. package/src/components/HeaderBar/consts.scss +7 -0
  62. package/src/components/HeaderBar/consts.ts +2 -0
  63. package/src/components/HeaderBar/locales.ts +3 -0
  64. package/src/components/HeaderBar/tests/HeaderBar.spec.ts +210 -0
  65. package/src/components/HeaderBar/tests/__snapshots__/HeaderBar.spec.ts.snap +50 -0
  66. package/src/components/HeaderBar/tests/useHeaderResponsiveMode.spec.ts +26 -0
  67. package/src/components/HeaderBar/tests/useScrollDirection.spec.ts +34 -0
  68. package/src/components/HeaderBar/useHeaderResponsiveMode.ts +25 -0
  69. package/src/components/HeaderBar/useScrollDirection.ts +26 -0
  70. package/src/components/LangBtn/LangBtn.mdx +2 -1
  71. package/src/components/LangBtn/LangBtn.vue +3 -3
  72. package/src/components/Logo/Logo.mdx +26 -0
  73. package/src/components/Logo/Logo.stories.ts +217 -0
  74. package/src/components/Logo/Logo.vue +397 -0
  75. package/src/components/Logo/LogoSize.ts +7 -0
  76. package/src/components/Logo/locales.ts +6 -0
  77. package/src/components/Logo/logoDimensionsMapping.ts +16 -0
  78. package/src/components/Logo/tests/Logo.spec.ts +75 -0
  79. package/src/components/Logo/types.d.ts +8 -0
  80. package/src/components/NotificationBar/NotificationBar.vue +5 -7
  81. package/src/components/PageContainer/PageContainer.vue +0 -1
  82. package/src/components/SocialMediaLinks/DefaultSocialMediaLinks.ts +21 -0
  83. package/src/components/SocialMediaLinks/SocialMediaLinks.mdx +15 -0
  84. package/src/components/SocialMediaLinks/SocialMediaLinks.stories.ts +72 -0
  85. package/src/components/SocialMediaLinks/SocialMediaLinks.vue +92 -0
  86. package/src/components/SocialMediaLinks/locales.ts +3 -0
  87. package/src/components/SocialMediaLinks/tests/DefaultSocialMediaLinks.spec.ts +21 -0
  88. package/src/components/SocialMediaLinks/tests/SocialMediaLinks.spec.ts +89 -0
  89. package/src/components/SocialMediaLinks/tests/__snapshots__/SocialMediaLinks.spec.ts.snap +24 -0
  90. package/src/components/SocialMediaLinks/types.d.ts +5 -0
  91. package/src/components/index.ts +6 -0
  92. package/src/directives/clickOutside.ts +24 -0
  93. package/src/temp/TestDTComponent.vue +6 -10
  94. package/src/temp/gridsTests.vue +0 -4
  95. package/src/utils/propValidator/index.ts +20 -0
  96. package/src/utils/propValidator/tests/propValidator.spec.ts +40 -0
@@ -0,0 +1,272 @@
1
+ import HeaderMenu from './HeaderComplexMenu.vue'
2
+ import HeaderBar from '../HeaderBar.vue'
3
+ import type { Meta, StoryObj } from '@storybook/vue3'
4
+ import HeaderMenuSection from './HeaderMenuSection/HeaderMenuSection.vue'
5
+ import HeaderSubMenu from './HeaderSubMenu/HeaderSubMenu.vue'
6
+ import HeaderMenuItem from './HeaderMenuItem/HeaderMenuItem.vue'
7
+ import { VBtn } from 'vuetify/components'
8
+ import { mdiMapMarker } from '@mdi/js'
9
+
10
+ const meta = {
11
+ title: 'Components/HeaderBar/HeaderComplexMenu',
12
+ component: HeaderMenu,
13
+ parameters: {
14
+ layout: 'fullscreen',
15
+ },
16
+ } satisfies Meta<typeof HeaderMenu>
17
+
18
+ export default meta
19
+
20
+ type Story = StoryObj<typeof meta>
21
+
22
+ export const Default: Story = {
23
+ args: {},
24
+ render: (args) => {
25
+ return {
26
+ components: { HeaderMenu, HeaderBar },
27
+ setup() {
28
+ return { args }
29
+ },
30
+ template: `
31
+ <HeaderBar>
32
+ <template #menu>
33
+ <HeaderMenu>
34
+ <p>lorem ipsum</p>
35
+ </HeaderMenu>
36
+ </template>
37
+ </HeaderBar>
38
+ `,
39
+ }
40
+ },
41
+ }
42
+
43
+ export const Populated: Story = {
44
+ args: {},
45
+ render: (args) => {
46
+ return {
47
+ components: { HeaderMenuItem, HeaderMenu, HeaderBar, HeaderSubMenu, HeaderMenuSection, VBtn },
48
+ setup() {
49
+ return { args }
50
+ },
51
+ template: `
52
+ <HeaderBar>
53
+ <template #menu>
54
+ <HeaderMenu>
55
+ <HeaderMenuSection title="section 1">
56
+ <HeaderMenuItem>
57
+ <a>Item 1</a>
58
+ </HeaderMenuItem>
59
+ <HeaderMenuItem>
60
+ <a>Item 2</a>
61
+ </HeaderMenuItem>
62
+ <headerMenuItem>
63
+ <HeaderSubMenu>
64
+ <template #title>
65
+ Menu de premier niveau 1
66
+ </template>
67
+ <HeaderMenuSection title="Section">
68
+ <HeaderMenuItem>
69
+ <a>Item</a>
70
+ </HeaderMenuItem>
71
+ <HeaderSubMenu>
72
+ <template #title>
73
+ Menu de deuxième niveau 1
74
+ </template>
75
+ <HeaderMenuItem>
76
+ <a>Item</a>
77
+ </HeaderMenuItem>
78
+ </HeaderSubMenu>
79
+ </HeaderMenuSection>
80
+ </HeaderSubMenu>
81
+ </headerMenuItem>
82
+ </HeaderMenuSection>
83
+ <HeaderMenuSection title="section 2">
84
+ <headerMenuItem>
85
+ <HeaderSubMenu>
86
+ <template #title>
87
+ Menu de premier niveau 2
88
+ </template>
89
+ <HeaderMenuItem>
90
+ <a>Item 1</a>
91
+ </HeaderMenuItem>
92
+ <HeaderMenuItem>
93
+ <HeaderSubMenu>
94
+ <template #title>
95
+ Menu de deuxième niveau 2
96
+ </template>
97
+ <HeaderMenuItem>
98
+ <a>Item 1</a>
99
+ </HeaderMenuItem>
100
+ </HeaderSubMenu>
101
+ </HeaderMenuItem>
102
+ <HeaderMenuItem>
103
+ <HeaderSubMenu>
104
+ <template #title>
105
+ Menu de deuxième niveau 3
106
+ </template>
107
+ <HeaderMenuSection title="section 1">
108
+ <HeaderMenuItem>
109
+ <a>Item 1</a>
110
+ </HeaderMenuItem>
111
+ </HeaderMenuSection>
112
+ </HeaderSubMenu>
113
+ </HeaderMenuItem>
114
+ </HeaderSubMenu>
115
+ </headerMenuItem>
116
+ <HeaderMenuItem>
117
+ <a>Item 3</a>
118
+ </HeaderMenuItem>
119
+ </HeaderMenuSection>
120
+ <div class="pa-4">
121
+ <p class="font-weight-bold">Veillez vous connecter</p>
122
+ <VBtn variant="tonal" class="mt-4 font-weight-medium" color="primary">Je me connecte</VBtn>
123
+ </div>
124
+ </HeaderMenu>
125
+ </template>
126
+ </HeaderBar>
127
+ `,
128
+ }
129
+ },
130
+ }
131
+
132
+ export const Generated: Story = {
133
+ args: {
134
+ },
135
+ render: (args) => {
136
+ return {
137
+ components: { HeaderMenu, HeaderBar, HeaderMenuSection, HeaderSubMenu, HeaderMenuItem },
138
+ setup() {
139
+ const menu = [
140
+ {
141
+ title: 'Vous informer',
142
+ items: [
143
+ { title: 'Actualités', href: 'https://www.ameli.fr/assure/actualites' },
144
+ {
145
+ subMenuTitle: 'Droits et démarches',
146
+ subMenuSubtitle: 'selon votre situation',
147
+ sections: [
148
+ {
149
+ title: undefined,
150
+ items: [
151
+ { title: 'Les essentiels de l’assuré', href: 'https://www.ameli.fr/assure/droits-demarches/principes' },
152
+ { title: 'Parentalité, couple', href: 'https://www.ameli.fr/assure/droits-demarches/famille' },
153
+ { title: 'Fin de vie, deuil', href: 'https://www.ameli.fr/assure/droits-demarches/fin-de-vie-deuil' },
154
+ { title: 'Etudes et stages', href: 'https://www.ameli.fr/assure/droits-demarches/etudes-stages' },
155
+ { title: 'Vie professionnelle, retraite', href: 'https://www.ameli.fr/assure/droits-demarches/vie-professionnelle-retraite' },
156
+ { title: 'Difficultés d\'accès aux droits et aux soins', href: 'https://www.ameli.fr/assure/droits-demarches/difficultes-acces-droits-soins' },
157
+ { title: 'Maladie, accident, hospitalisation', href: 'https://www.ameli.fr/assure/droits-demarches/maladie-accident-hospitalisation' },
158
+ { title: 'invalidité, handicap', href: 'https://www.ameli.fr/assure/droits-demarches/invalidite-handicap' },
159
+ { title: 'situations particumlières', href: 'https://www.ameli.fr/assure/droits-demarches/situations-particulieres' },
160
+ { title: 'réclamation, médiation, recours', href: 'https://www.ameli.fr/assure/droits-demarches/reclamation-mediation-voies-de-recours' },
161
+ { title: 'Europe, international', href: 'https://www.ameli.fr/assure/droits-demarches/europe-international' },
162
+ ],
163
+ },
164
+ ],
165
+ },
166
+ {
167
+ subMenuTitle: 'Remboursements',
168
+ subMenuSubtitle: 'prestations et aides',
169
+ sections: [
170
+ {
171
+ title: undefined,
172
+ items: [
173
+ { title: 'Ce qui est remboursé', href: 'https://www.ameli.fr/assure/remboursements/rembourse' },
174
+ { title: 'ce qui reste à votre charge', href: 'https://www.ameli.fr/assure/remboursements/reste-charge' },
175
+ { title: 'Être bien remboursé', href: 'https://www.ameli.fr/assure/remboursements/etre-bien-rembourse' },
176
+ { title: 'Indemnités journalières maladie, maternité, paternité', href: 'https://www.ameli.fr/assure/remboursements/indemnites-journalieres-maladie-maternite-paternite' },
177
+ { title: 'Accident du travail : prise en charge et indemnités journalières', href: 'https://www.ameli.fr/assure/remboursements/accident-travail' },
178
+ { title: 'Maladie professionnelle : prise en charge et indemnités journalières', href: 'https://www.ameli.fr/assure/remboursements/maladie-professionnelle' },
179
+ { title: 'Pensions, allocations et rentes', href: 'https://www.ameli.fr/assure/remboursements/pensions-allocations-rentes' },
180
+ { title: 'Incapacité permanente', href: 'https://www.ameli.fr/assure/remboursements/incapacite-permanente' },
181
+ { title: 'Complémentaire santé solidaire : vous n\'avez rien à payer dans la plupart des cas ', href: 'https://www.ameli.fr/assure/remboursements/cmu-aides-financieres/complementaire-sante-solidaire' },
182
+ { title: 'Aide médicale de l\'État et soins urgents', href: 'https://www.ameli.fr/assure/remboursements/aide-medicale-etat-soins-urgents' },
183
+ { title: 'Compte ameli, mode d\'emploi', href: 'https://www.ameli.fr/assure/remboursements/suivre-remboursements' },
184
+ ],
185
+ },
186
+ ],
187
+ },
188
+ {
189
+ subMenuTitle: 'Maladies et prévention',
190
+ sections: [
191
+ {
192
+ title: undefined,
193
+ items: [
194
+ { title: 'Tous les thèmes de santé', href: 'https://www.ameli.fr/assure/sante/themes' },
195
+ { title: 'L\'Assurance Maladie vous accompagne', href: 'https://www.ameli.fr/assure/remboursements/reste-charge' },
196
+ { title: 'Mon espace santé', href: 'https://www.ameli.fr/assure/sante/mon-espace-sante' },
197
+ { title: 'Mon bilan prévention', href: 'https://www.ameli.fr/assure/sante/mon-bilan-prevention' },
198
+ { title: 'Réagir en cas \'urgence ', href: 'https://www.ameli.fr/assure/sante/urgence' },
199
+ { title: 'Accomplir les bons gestes ', href: 'https://www.ameli.fr/assure/sante/bons-gestes' },
200
+ { title: 'Médicaments et vaccins', href: 'https://www.ameli.fr/assure/sante/medicaments' },
201
+ { title: 'Déroulement d\'un examen', href: 'https://www.ameli.fr/assure/sante/examen' },
202
+ { title: 'Certificat médical : dans quels cas et pour qui est-il obligatoire ?', href: 'https://www.ameli.fr/assure/sante/certificat-medical-quand-et-pour-qui' },
203
+ { title: 'Devenir parent', href: 'https://www.ameli.fr/assure/sante/devenir-parent' },
204
+ { title: 'Enfants', href: 'https://www.ameli.fr/assure/sante/enfants' },
205
+ { title: 'Jeunes 16-25 ans', href: 'https://www.ameli.fr/assure/sante/jeunes-16-25-ans' },
206
+ { title: 'Seniors', href: 'https://www.ameli.fr/assure/sante/seniors' },
207
+ { title: 'Télésanté, la santé à distance', href: 'https://www.ameli.fr/assure/sante/telesante' },
208
+ ],
209
+ },
210
+ ],
211
+ },
212
+ ],
213
+ },
214
+ {
215
+ title: 'Besoin d\'aide',
216
+ items: [
217
+ {
218
+ title: 'Contacter l\'Assurance Maladie',
219
+ subtitle: 'obtenir une attestation, envoyer une feuille de soins, contacter sa caisse, etc.',
220
+ href: 'https://www.ameli.fr/assure/adresses-contacts',
221
+ },
222
+ {
223
+ title: 'Trouver un professionnel de santé',
224
+ subtitle: 'médecins, infirmiers...',
225
+ href: 'https://www.ameli.fr/assure/adresses-contacts' },
226
+ { title: 'Télécharger un formulaire (ex: cerfa)', href: 'https://www.ameli.fr/assure/droits-demarches/formulaires' },
227
+ { title: 'Consulter le forum', href: 'https://forum-assures.ameli.fr/' },
228
+ { title: 'Sourds et malentendants', href: 'https://elioz.fr/elioz-connect/annuaire/assurance-maladie-annuaire/' },
229
+ ],
230
+ },
231
+ ]
232
+ return { args, menu, marker: mdiMapMarker }
233
+ },
234
+ template: `
235
+ <HeaderBar>
236
+ <template #menu>
237
+ <HeaderMenu>
238
+ <HeaderMenuSection v-for="section in menu" :key="section.title" :title="section.title">
239
+ <HeaderMenuItem v-for="item in section.items" :key="item.title">
240
+ <HeaderSubMenu v-if="item.subMenuTitle">
241
+ <template #title>
242
+ {{ item.subMenuTitle }}
243
+ <em v-if="item.subMenuSubtitle" style="font-style: normal; color: #757777;">{{ item.subMenuSubtitle }}</em>
244
+ </template>
245
+ <HeaderMenuSection v-for="subSection in item.sections" :key="subSection.title" :title="subSection.title">
246
+ <HeaderMenuItem v-for="subItem in subSection.items" :key="subItem.title">
247
+ <a :href="subItem.href">{{ subItem.title }}</a>
248
+ </HeaderMenuItem>
249
+ </HeaderMenuSection>
250
+ </HeaderSubMenu>
251
+ <a v-else :href="item.href">
252
+ {{ item.title }}
253
+ <em v-if="item.subtitle" style="font-style: normal; color: #757777;">{{ item.subtitle }}</em>
254
+ </a>
255
+ </HeaderMenuItem>
256
+ </HeaderMenuSection>
257
+ <h2 class="border-b-sm mb-2" style="font-size: 1.1rem; padding: 40px 16px 8px 20px;">Votre caisse</h2>
258
+ <div style="padding: 16px 50px 16px 20px;">
259
+ <div class="d-flex align-center ga-2 font-weight-bold">
260
+ <VIcon aria-label="Localisation" role="img" aria-hidden="false" color="primary">{{marker}}</VIcon>
261
+ <p>Vous n'avez pas sélectionné votre caisse</p>
262
+ </div>
263
+ <VBtn class="mt-3 mb-4 font-weight-bold text-capitalize" base-color="primary" density="comfortable" flat height="37">Sélectionner</VBtn>
264
+ <p class="mb-8">Les pages d'ameli seront alors enrichies des informations locales de votre caisse (contacts, événements régionaux, etc.) </p>
265
+ </div>
266
+ </HeaderMenu>
267
+ </template>
268
+ </HeaderBar>
269
+ `,
270
+ }
271
+ },
272
+ }
@@ -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>
@@ -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
+ })
@@ -0,0 +1,3 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`HeaderMenuItem > should render the component 1`] = `"<li data-v-8ba695ac="" class="header-menu-item"><a>Test</a></li>"`;
@@ -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
+ }