@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.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/dist/design-system-v3.js +957 -4826
  3. package/dist/design-system-v3.umd.cjs +1 -2
  4. package/dist/style.css +1 -1
  5. package/package.json +29 -29
  6. package/src/components/Alert/Alert.vue +8 -8
  7. package/src/components/FranceConnectBtn/FranceConnectBtn.vue +2 -2
  8. package/src/components/HeaderBar/HeaderBar.mdx +137 -0
  9. package/src/components/HeaderBar/HeaderBar.stories.ts +159 -0
  10. package/src/components/HeaderBar/HeaderBar.vue +238 -0
  11. package/src/components/HeaderBar/HeaderComplexMenu/HeaderComplexMenu.stories.ts +272 -0
  12. package/src/components/HeaderBar/HeaderComplexMenu/HeaderComplexMenu.vue +205 -0
  13. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/HeaderMenuItem.stories.ts +49 -0
  14. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/HeaderMenuItem.vue +51 -0
  15. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/tests/HeaderMenuItem.spec.ts +16 -0
  16. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/tests/__snapshots__/HeaderMenuItem.spec.ts.snap +3 -0
  17. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/HeaderMenuSection.stories.ts +56 -0
  18. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/HeaderMenuSection.vue +51 -0
  19. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/tests/HeaderMenuSection.spec.ts +33 -0
  20. package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/HeaderSubMenu.stories.ts +137 -0
  21. package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/HeaderSubMenu.vue +180 -0
  22. package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/tests/HeaderSubMenu.spec.ts +63 -0
  23. package/src/components/HeaderBar/HeaderComplexMenu/conts.ts +1 -0
  24. package/src/components/HeaderBar/HeaderComplexMenu/locals.ts +4 -0
  25. package/src/components/HeaderBar/HeaderComplexMenu/tests/HeaderComplexMenu.spec.ts +129 -0
  26. package/src/components/HeaderBar/HeaderComplexMenu/tests/__snapshots__/HeaderComplexMenu.spec.ts.snap +18 -0
  27. package/src/components/HeaderBar/HeaderComplexMenu/tests/useHandleSubMenus.spec.ts +158 -0
  28. package/src/components/HeaderBar/HeaderComplexMenu/useHandleSubMenus.ts +49 -0
  29. package/src/components/HeaderBar/HeaderLogo/HeaderLogo.vue +106 -0
  30. package/src/components/HeaderBar/HeaderLogo/locales.ts +3 -0
  31. package/src/components/HeaderBar/HeaderLogo/logos/Logo-mobile.vue +117 -0
  32. package/src/components/HeaderBar/HeaderLogo/logos/Logo.vue +279 -0
  33. package/src/components/HeaderBar/HeaderLogo/tests/HeaderLogo.spec.ts +71 -0
  34. package/src/components/HeaderBar/HeaderMenuBtn/HeaderMenuBtn.vue +88 -0
  35. package/src/components/HeaderBar/HeaderMenuBtn/locals.ts +4 -0
  36. package/src/components/HeaderBar/consts.scss +7 -0
  37. package/src/components/HeaderBar/consts.ts +2 -0
  38. package/src/components/HeaderBar/locales.ts +3 -0
  39. package/src/components/HeaderBar/tests/HeaderBar.spec.ts +210 -0
  40. package/src/components/HeaderBar/tests/__snapshots__/HeaderBar.spec.ts.snap +50 -0
  41. package/src/components/HeaderBar/tests/useHeaderResponsiveMode.spec.ts +26 -0
  42. package/src/components/HeaderBar/tests/useScrollDirection.spec.ts +34 -0
  43. package/src/components/HeaderBar/useHeaderResponsiveMode.ts +25 -0
  44. package/src/components/HeaderBar/useScrollDirection.ts +26 -0
  45. package/src/components/NotificationBar/NotificationBar.vue +5 -7
  46. package/src/components/PageContainer/PageContainer.vue +0 -1
@@ -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,4 @@
1
+ export default {
2
+ mainMenu: 'Menu principal',
3
+ publicMenu: 'Menu public',
4
+ }
@@ -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
+ `;
@@ -0,0 +1,158 @@
1
+ /* eslint-disable vue/one-component-per-file */
2
+ import { vuetify } from '@tests/unit/setup'
3
+ import { mount } from '@vue/test-utils'
4
+ import { describe, expect, it } from 'vitest'
5
+ import { computed, defineComponent, inject, readonly, ref, type DeepReadonly, type Ref } from 'vue'
6
+ import { registerSubMenuKey } from '../conts'
7
+ import useHandleSubMenus from '../useHandleSubMenus'
8
+
9
+ describe('useHandleSubMenus', () => {
10
+ const TestParentComponent = defineComponent({
11
+ setup() {
12
+ const openStatus = ref(false)
13
+ const { haveOpenSubMenu } = useHandleSubMenus(readonly(openStatus))
14
+ const rootClasses = computed(() => ({
15
+ 'parent-open': openStatus.value,
16
+ 'has-open-submenu': haveOpenSubMenu.value,
17
+ }))
18
+
19
+ return { rootClasses, openStatus }
20
+ },
21
+ template: `
22
+ <button @click="openStatus = !openStatus" class="parent-menu-btn">Toggle</button>
23
+ <div :class="rootClasses"><slot/></div>
24
+ `,
25
+ })
26
+
27
+ const TestChildrenComponent = defineComponent({
28
+ setup() {
29
+ const openStatus = ref(false)
30
+ const registerSubMenu = inject<((r: DeepReadonly<Ref<boolean>>, c: () => void) => void) | undefined>(registerSubMenuKey)!
31
+
32
+ registerSubMenu(readonly(openStatus), () => {
33
+ openStatus.value = false
34
+ })
35
+
36
+ const rootClasses = computed(() => ({
37
+ 'children-open': openStatus.value,
38
+ }))
39
+
40
+ return { rootClasses, openStatus }
41
+ },
42
+ template: `
43
+ <button @click="openStatus = !openStatus" class="child-menu-btn">Toggle</button>
44
+ <div :class="rootClasses"></div>
45
+ `,
46
+ })
47
+
48
+ it('if close the submenu if the parent menu is closed', async () => {
49
+ const wrapper = mount({
50
+ components: {
51
+ TestParentComponent,
52
+ TestChildrenComponent,
53
+ },
54
+ template: `
55
+ <TestParentComponent>
56
+ <TestChildrenComponent />
57
+ </TestParentComponent>
58
+ `,
59
+ }, {
60
+ global: {
61
+ plugins: [vuetify],
62
+ },
63
+ })
64
+
65
+ expect(wrapper.find('.parent-open').exists()).toBe(false)
66
+ expect(wrapper.find('.children-open').exists()).toBe(false)
67
+
68
+ const parentBtn = wrapper.find('.parent-menu-btn')
69
+ await parentBtn.trigger('click')
70
+ expect(wrapper.find('.parent-open').exists()).toBe(true)
71
+ expect(wrapper.find('.children-open').exists()).toBe(false)
72
+
73
+ const childBtn = wrapper.find('.child-menu-btn')
74
+ await childBtn.trigger('click')
75
+ expect(wrapper.find('.parent-open').exists()).toBe(true)
76
+ expect(wrapper.find('.children-open').exists()).toBe(true)
77
+
78
+ await parentBtn.trigger('click')
79
+ expect(wrapper.find('.parent-open').exists()).toBe(false)
80
+ expect(wrapper.find('.children-open').exists()).toBe(false)
81
+
82
+ await parentBtn.trigger('click')
83
+ expect(wrapper.find('.parent-open').exists()).toBe(true)
84
+ expect(wrapper.find('.children-open').exists()).toBe(false)
85
+ })
86
+
87
+ it('if close the submenu if another submenu is opened', async () => {
88
+ const wrapper = mount({
89
+ components: {
90
+ TestParentComponent,
91
+ TestChildrenComponent,
92
+ },
93
+ template: `
94
+ <TestParentComponent>
95
+ <TestChildrenComponent />
96
+ <TestChildrenComponent />
97
+ </TestParentComponent>
98
+ `,
99
+ }, {
100
+ global: {
101
+ plugins: [vuetify],
102
+ },
103
+ })
104
+
105
+ expect(wrapper.find('.parent-open').exists()).toBe(false)
106
+ expect(wrapper.findAll('.children-open').length).toBe(0)
107
+
108
+ const parentBtn = wrapper.find('.parent-menu-btn')
109
+ await parentBtn.trigger('click')
110
+ expect(wrapper.find('.parent-open').exists()).toBe(true)
111
+ expect(wrapper.findAll('.children-open').length).toBe(0)
112
+
113
+ const childBtns = wrapper.findAll('.child-menu-btn')
114
+ await childBtns[0].trigger('click')
115
+ expect(wrapper.find('.parent-open').exists()).toBe(true)
116
+ expect(wrapper.findAll('.children-open').length).toBe(1)
117
+
118
+ await childBtns[1].trigger('click')
119
+ expect(wrapper.find('.parent-open').exists()).toBe(true)
120
+ expect(wrapper.findAll('.children-open').length).toBe(1)
121
+ })
122
+
123
+ it('return haveOpenSubMenu and update it when the component have a submenu open', async () => {
124
+ const wrapper = mount({
125
+ components: {
126
+ TestParentComponent,
127
+ TestChildrenComponent,
128
+ },
129
+ template: `
130
+ <TestParentComponent>
131
+ <TestChildrenComponent />
132
+ <TestChildrenComponent />
133
+ </TestParentComponent>
134
+ `,
135
+ }, {
136
+ global: {
137
+ plugins: [vuetify],
138
+ },
139
+ })
140
+
141
+ const parentBtn = wrapper.find('.parent-menu-btn')
142
+ await parentBtn.trigger('click')
143
+ const parentMenu = wrapper.find('.parent-open')
144
+
145
+ expect(parentMenu!.classes()).not.toContain('has-open-submenu')
146
+
147
+ const childBtns = wrapper.findAll('.child-menu-btn')
148
+
149
+ await childBtns[0].trigger('click')
150
+ expect(parentMenu!.classes()).toContain('has-open-submenu')
151
+
152
+ await childBtns[0].trigger('click')
153
+ expect(parentMenu!.classes()).not.toContain('has-open-submenu')
154
+
155
+ await childBtns[1].trigger('click')
156
+ expect(parentMenu!.classes()).toContain('has-open-submenu')
157
+ })
158
+ })
@@ -0,0 +1,49 @@
1
+ import { computed, provide, ref, watch, type DeepReadonly, type Ref } from 'vue'
2
+ import { registerSubMenuKey } from './conts'
3
+
4
+ export default function useHandleSubMenus(openStatus: DeepReadonly<Ref<boolean>>) {
5
+ type SubMenu = { id: string, status: Ref<boolean>, close: () => void }
6
+ const subMenus: Ref<SubMenu[]> = ref([])
7
+
8
+ function registerSubMenu(status: Ref<boolean>, close: () => void) {
9
+ const id = String(subMenus.value.length)
10
+ const newSubMenu = { id, status, close }
11
+
12
+ // Register the new submenu
13
+ subMenus.value.push(newSubMenu)
14
+
15
+ // Watch for changes to the status of this specific submenu
16
+ watch(status, (newStatus) => {
17
+ if (newStatus) {
18
+ closeOtherSubMenus(newSubMenu)
19
+ }
20
+ })
21
+ }
22
+
23
+ function closeOtherSubMenus(subMenu: SubMenu) {
24
+ subMenus.value.forEach((otherSubMenu) => {
25
+ if (otherSubMenu.id !== subMenu.id && otherSubMenu.status) {
26
+ otherSubMenu.close()
27
+ }
28
+ })
29
+ }
30
+
31
+ // When the current menu is closed, close its submenus
32
+ watch(openStatus, (newOpenStatus) => {
33
+ if (!newOpenStatus) {
34
+ subMenus.value.forEach((subMenu) => {
35
+ if (subMenu.status) {
36
+ subMenu.close()
37
+ }
38
+ })
39
+ }
40
+ })
41
+
42
+ const haveOpenSubMenu = computed(() => subMenus.value.some(subMenu => subMenu.status))
43
+
44
+ provide(registerSubMenuKey, registerSubMenu)
45
+
46
+ return {
47
+ haveOpenSubMenu,
48
+ }
49
+ }