@eturnity/eturnity_reusable_components 9.28.3 → 9.31.0
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/package.json
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import NavigationSideMenu from '@/components/navigationSideMenu'
|
|
3
|
+
|
|
4
|
+
jest.mock('@/components/icon/iconCache.mjs', () => ({
|
|
5
|
+
// need to mock this due to how jest handles import.meta
|
|
6
|
+
fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
const menuData = [
|
|
10
|
+
{
|
|
11
|
+
groupName: 'Project',
|
|
12
|
+
children: [
|
|
13
|
+
{ id: 'technologies', name: 'Technologies', dataId: 'nav_technologies' },
|
|
14
|
+
{ id: 'finance', name: 'Finance', dataId: 'nav_finance' },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
groupName: 'Workflow',
|
|
19
|
+
children: [
|
|
20
|
+
{
|
|
21
|
+
id: 'workflow_template',
|
|
22
|
+
name: 'Workflow template',
|
|
23
|
+
dataId: 'nav_workflow_template',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const mountMenu = (props = {}) =>
|
|
30
|
+
mount(NavigationSideMenu, { props: { menuData, ...props } })
|
|
31
|
+
|
|
32
|
+
const item = (wrapper, dataId) => wrapper.find(`[data-id="${dataId}"]`)
|
|
33
|
+
const section = (wrapper, key) => wrapper.find(`[data-id="nav_section_${key}"]`)
|
|
34
|
+
|
|
35
|
+
describe('navigationSideMenu/index.vue', () => {
|
|
36
|
+
describe('default state', () => {
|
|
37
|
+
it('renders a header for each non-empty group', () => {
|
|
38
|
+
const wrapper = mountMenu()
|
|
39
|
+
|
|
40
|
+
expect(section(wrapper, 'Project').exists()).toBe(true)
|
|
41
|
+
expect(section(wrapper, 'Workflow').exists()).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('shows every group item when nothing is collapsed', () => {
|
|
45
|
+
const wrapper = mountMenu()
|
|
46
|
+
|
|
47
|
+
expect(item(wrapper, 'nav_technologies').exists()).toBe(true)
|
|
48
|
+
expect(item(wrapper, 'nav_finance').exists()).toBe(true)
|
|
49
|
+
expect(item(wrapper, 'nav_workflow_template').exists()).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('reports no group as collapsed initially', () => {
|
|
53
|
+
const wrapper = mountMenu()
|
|
54
|
+
|
|
55
|
+
expect(wrapper.vm.isGroupCollapsed('Project')).toBe(false)
|
|
56
|
+
expect(wrapper.vm.isGroupCollapsed('Workflow')).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('toggling a section', () => {
|
|
61
|
+
it("hides that section's items when its header is clicked, and shows them again on a second click", async () => {
|
|
62
|
+
const wrapper = mountMenu()
|
|
63
|
+
|
|
64
|
+
await section(wrapper, 'Project').trigger('click')
|
|
65
|
+
expect(item(wrapper, 'nav_technologies').exists()).toBe(false)
|
|
66
|
+
expect(item(wrapper, 'nav_finance').exists()).toBe(false)
|
|
67
|
+
|
|
68
|
+
await section(wrapper, 'Project').trigger('click')
|
|
69
|
+
expect(item(wrapper, 'nav_technologies').exists()).toBe(true)
|
|
70
|
+
expect(item(wrapper, 'nav_finance').exists()).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('collapses sections independently — collapsing one leaves the others expanded', async () => {
|
|
74
|
+
const wrapper = mountMenu()
|
|
75
|
+
|
|
76
|
+
await section(wrapper, 'Project').trigger('click')
|
|
77
|
+
|
|
78
|
+
expect(item(wrapper, 'nav_technologies').exists()).toBe(false)
|
|
79
|
+
expect(item(wrapper, 'nav_workflow_template').exists()).toBe(true)
|
|
80
|
+
expect(wrapper.vm.isGroupCollapsed('Project')).toBe(true)
|
|
81
|
+
expect(wrapper.vm.isGroupCollapsed('Workflow')).toBe(false)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('emits section-collapse-toggle with the key and post-toggle collapsed state', async () => {
|
|
85
|
+
const wrapper = mountMenu()
|
|
86
|
+
|
|
87
|
+
await section(wrapper, 'Project').trigger('click')
|
|
88
|
+
await section(wrapper, 'Project').trigger('click')
|
|
89
|
+
|
|
90
|
+
const events = wrapper.emitted('section-collapse-toggle')
|
|
91
|
+
expect(events).toHaveLength(2)
|
|
92
|
+
expect(events[0]).toEqual([{ key: 'Project', collapsed: true }])
|
|
93
|
+
expect(events[1]).toEqual([{ key: 'Project', collapsed: false }])
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('groupKey', () => {
|
|
98
|
+
it('prefers an explicit id over the group name', () => {
|
|
99
|
+
const wrapper = mountMenu()
|
|
100
|
+
|
|
101
|
+
expect(wrapper.vm.groupKey({ id: 'a', groupName: 'B' }, 0)).toBe('a')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('falls back to the group name when there is no id', () => {
|
|
105
|
+
const wrapper = mountMenu()
|
|
106
|
+
|
|
107
|
+
expect(wrapper.vm.groupKey({ groupName: 'B' }, 0)).toBe('B')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('falls back to the index (as a string) when neither id nor name is present', () => {
|
|
111
|
+
const wrapper = mountMenu()
|
|
112
|
+
|
|
113
|
+
expect(wrapper.vm.groupKey({}, 3)).toBe('3')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('edge cases', () => {
|
|
118
|
+
it('mounts without error and renders no sections when menuData is empty', () => {
|
|
119
|
+
const wrapper = mount(NavigationSideMenu, { props: { menuData: [] } })
|
|
120
|
+
|
|
121
|
+
expect(wrapper.find('[data-id^="nav_section_"]').exists()).toBe(false)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('treats groups that share a groupName as one section (name is the key)', async () => {
|
|
125
|
+
const dupData = [
|
|
126
|
+
{
|
|
127
|
+
groupName: 'Other',
|
|
128
|
+
children: [{ id: 'a', name: 'A', dataId: 'nav_a' }],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
groupName: 'Other',
|
|
132
|
+
children: [{ id: 'b', name: 'B', dataId: 'nav_b' }],
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
const wrapper = mount(NavigationSideMenu, {
|
|
136
|
+
props: { menuData: dupData },
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
await wrapper.findAll('[data-id="nav_section_Other"]')[0].trigger('click')
|
|
140
|
+
|
|
141
|
+
expect(item(wrapper, 'nav_a').exists()).toBe(false)
|
|
142
|
+
expect(item(wrapper, 'nav_b').exists()).toBe(false)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
})
|
|
@@ -120,8 +120,31 @@
|
|
|
120
120
|
v-if="group.children?.length"
|
|
121
121
|
:is-collapsed="isCollapsed"
|
|
122
122
|
>
|
|
123
|
-
<
|
|
124
|
-
|
|
123
|
+
<GroupHeader
|
|
124
|
+
v-if="!isCollapsed"
|
|
125
|
+
:data-id="`nav_section_${groupKey(group, groupIdx)}`"
|
|
126
|
+
:data-qa-id="`nav_section_${groupKey(group, groupIdx)}`"
|
|
127
|
+
@click="toggleGroup(groupKey(group, groupIdx))"
|
|
128
|
+
>
|
|
129
|
+
<GroupName>{{ group.groupName }}</GroupName>
|
|
130
|
+
<GroupChevron>
|
|
131
|
+
<IconComponent
|
|
132
|
+
:color="theme.semanticColors.grey[700]"
|
|
133
|
+
:hovered-color="theme.semanticColors.purple[500]"
|
|
134
|
+
:name="
|
|
135
|
+
isGroupCollapsed(groupKey(group, groupIdx))
|
|
136
|
+
? 'chevron_down'
|
|
137
|
+
: 'chevron_up'
|
|
138
|
+
"
|
|
139
|
+
size="10px"
|
|
140
|
+
/>
|
|
141
|
+
</GroupChevron>
|
|
142
|
+
</GroupHeader>
|
|
143
|
+
<ItemList
|
|
144
|
+
v-if="
|
|
145
|
+
isCollapsed || !isGroupCollapsed(groupKey(group, groupIdx))
|
|
146
|
+
"
|
|
147
|
+
>
|
|
125
148
|
<template
|
|
126
149
|
v-for="(item, itemIdx) in group.children"
|
|
127
150
|
:key="item.id ?? itemIdx"
|
|
@@ -137,7 +160,7 @@
|
|
|
137
160
|
:is-disabled="item.isDisabled"
|
|
138
161
|
@click="
|
|
139
162
|
!item.isDisabled &&
|
|
140
|
-
|
|
163
|
+
$emit('on-menu-item-click', { item })
|
|
141
164
|
"
|
|
142
165
|
>
|
|
143
166
|
<IconCell :is-collapsed="isCollapsed">
|
|
@@ -174,7 +197,7 @@
|
|
|
174
197
|
type="tertiary"
|
|
175
198
|
@click="
|
|
176
199
|
!item.isDisabled &&
|
|
177
|
-
|
|
200
|
+
$emit('on-menu-item-click', { item })
|
|
178
201
|
"
|
|
179
202
|
/>
|
|
180
203
|
</template>
|
|
@@ -244,7 +267,7 @@
|
|
|
244
267
|
:is-disabled="sub.isDisabled"
|
|
245
268
|
@click="
|
|
246
269
|
!sub.isDisabled &&
|
|
247
|
-
|
|
270
|
+
$emit('on-menu-item-click', { item: sub })
|
|
248
271
|
"
|
|
249
272
|
>
|
|
250
273
|
<span>{{ sub.name }}</span>
|
|
@@ -307,8 +330,8 @@
|
|
|
307
330
|
p.isCollapsed
|
|
308
331
|
? `${NAV_SIDEBAR_COLLAPSED_OUTER_WIDTH_PX}px`
|
|
309
332
|
: p.expandedWidth
|
|
310
|
-
|
|
311
|
-
|
|
333
|
+
? `${p.expandedWidth}px`
|
|
334
|
+
: 'max-content'};
|
|
312
335
|
min-width: ${(p) =>
|
|
313
336
|
p.isCollapsed ? `${NAV_SIDEBAR_COLLAPSED_OUTER_WIDTH_PX}px` : '0'};
|
|
314
337
|
border-right: 1px solid ${(p) => p.theme.semanticColors.grey[300]};
|
|
@@ -418,6 +441,18 @@
|
|
|
418
441
|
padding: ${(p) => (p.isCollapsed ? '10px 8px' : '10px 16px')};
|
|
419
442
|
`
|
|
420
443
|
|
|
444
|
+
const GroupHeader = styled.div`
|
|
445
|
+
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
justify-content: space-between;
|
|
448
|
+
gap: 6px;
|
|
449
|
+
cursor: pointer;
|
|
450
|
+
|
|
451
|
+
&:hover > div:first-child {
|
|
452
|
+
color: ${(p) => p.theme.semanticColors.purple[500]};
|
|
453
|
+
}
|
|
454
|
+
`
|
|
455
|
+
|
|
421
456
|
const GroupName = styled.div`
|
|
422
457
|
font-size: 10px;
|
|
423
458
|
font-weight: 400;
|
|
@@ -426,6 +461,12 @@
|
|
|
426
461
|
line-height: 150%;
|
|
427
462
|
`
|
|
428
463
|
|
|
464
|
+
const GroupChevron = styled.div`
|
|
465
|
+
display: flex;
|
|
466
|
+
align-items: center;
|
|
467
|
+
flex-shrink: 0;
|
|
468
|
+
`
|
|
469
|
+
|
|
429
470
|
const ItemList = styled.div`
|
|
430
471
|
display: flex;
|
|
431
472
|
flex-direction: column;
|
|
@@ -758,7 +799,9 @@
|
|
|
758
799
|
ScrollbarThumb,
|
|
759
800
|
GroupsList,
|
|
760
801
|
GroupSection,
|
|
802
|
+
GroupHeader,
|
|
761
803
|
GroupName,
|
|
804
|
+
GroupChevron,
|
|
762
805
|
ItemList,
|
|
763
806
|
MenuItem,
|
|
764
807
|
IconCell,
|
|
@@ -837,6 +880,7 @@
|
|
|
837
880
|
'on-menu-item-click',
|
|
838
881
|
'on-footer-button-click',
|
|
839
882
|
'sidebar-dimensions',
|
|
883
|
+
'section-collapse-toggle',
|
|
840
884
|
],
|
|
841
885
|
data() {
|
|
842
886
|
return {
|
|
@@ -844,6 +888,7 @@
|
|
|
844
888
|
isCollapsed: false,
|
|
845
889
|
responsibleSelectSurfaceOpen: false,
|
|
846
890
|
openDropdownId: null,
|
|
891
|
+
collapsedGroups: {},
|
|
847
892
|
isWrapperHovered: false,
|
|
848
893
|
expandedWidth: null,
|
|
849
894
|
scrollThumbStyle: {},
|
|
@@ -1120,6 +1165,27 @@
|
|
|
1120
1165
|
this.openDropdownId = this.openDropdownId === id ? null : id
|
|
1121
1166
|
}
|
|
1122
1167
|
},
|
|
1168
|
+
groupKey(group, idx) {
|
|
1169
|
+
return group.id ?? group.groupName ?? String(idx)
|
|
1170
|
+
},
|
|
1171
|
+
isGroupCollapsed(key) {
|
|
1172
|
+
return !!this.collapsedGroups[key]
|
|
1173
|
+
},
|
|
1174
|
+
toggleGroup(key) {
|
|
1175
|
+
if (this.collapsedGroups[key]) {
|
|
1176
|
+
delete this.collapsedGroups[key]
|
|
1177
|
+
} else {
|
|
1178
|
+
this.collapsedGroups[key] = true
|
|
1179
|
+
}
|
|
1180
|
+
this.$emit('section-collapse-toggle', {
|
|
1181
|
+
key,
|
|
1182
|
+
collapsed: this.isGroupCollapsed(key),
|
|
1183
|
+
})
|
|
1184
|
+
this.$nextTick(() => {
|
|
1185
|
+
this.updateScrollThumb()
|
|
1186
|
+
this.updateExpandedWidth()
|
|
1187
|
+
})
|
|
1188
|
+
},
|
|
1123
1189
|
},
|
|
1124
1190
|
}
|
|
1125
1191
|
</script>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { action } from '@storybook/addon-actions'
|
|
2
|
+
import NavigationSideMenu from './index.vue'
|
|
3
|
+
import theme from '@/assets/theme'
|
|
4
|
+
|
|
5
|
+
// Grouped menu mirroring the beta settings navigation: each top-level entry is a
|
|
6
|
+
// section ({ groupName, children }). Click a section header (e.g. "OFFER") to
|
|
7
|
+
// collapse/expand its items — sections collapse independently.
|
|
8
|
+
const settingsMenu = [
|
|
9
|
+
{
|
|
10
|
+
groupName: 'Project',
|
|
11
|
+
children: [
|
|
12
|
+
{
|
|
13
|
+
id: 'technologies',
|
|
14
|
+
name: 'Technologies',
|
|
15
|
+
dataId: 'nav_technologies',
|
|
16
|
+
icon: 'settings',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'finance',
|
|
20
|
+
name: 'Finance',
|
|
21
|
+
dataId: 'nav_finance',
|
|
22
|
+
icon: 'documents',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
groupName: 'Workflow',
|
|
28
|
+
children: [
|
|
29
|
+
{
|
|
30
|
+
id: 'workflow_template',
|
|
31
|
+
name: 'Workflow template',
|
|
32
|
+
dataId: 'nav_workflow_template',
|
|
33
|
+
icon: 'settings',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'activity_type',
|
|
37
|
+
name: 'Activity type definition',
|
|
38
|
+
dataId: 'nav_activity_type',
|
|
39
|
+
icon: 'documents',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
groupName: 'Offer',
|
|
45
|
+
children: [
|
|
46
|
+
{
|
|
47
|
+
id: 'branding',
|
|
48
|
+
name: 'Branding',
|
|
49
|
+
dataId: 'nav_branding',
|
|
50
|
+
icon: 'settings',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'document_templates',
|
|
54
|
+
name: 'Document templates',
|
|
55
|
+
dataId: 'nav_document_templates',
|
|
56
|
+
icon: 'documents',
|
|
57
|
+
// An item-level dropdown (its own children) — orthogonal to section collapse.
|
|
58
|
+
children: [
|
|
59
|
+
{ id: 'quotation', name: 'Quotation', dataId: 'nav_quotation' },
|
|
60
|
+
{ id: 'invoice', name: 'Invoice', dataId: 'nav_invoice' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
const projectManagerOptions = [
|
|
68
|
+
{ id: 1, full_name: 'Firstname Lastname' },
|
|
69
|
+
{ id: 2, full_name: 'Alex Meyer' },
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
export default {
|
|
73
|
+
title: 'Components/Navigation/NavigationSideMenu',
|
|
74
|
+
component: NavigationSideMenu,
|
|
75
|
+
tags: ['autodocs'],
|
|
76
|
+
parameters: {
|
|
77
|
+
layout: 'fullscreen',
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const Template = (args) => ({
|
|
82
|
+
components: { NavigationSideMenu },
|
|
83
|
+
setup() {
|
|
84
|
+
const onSectionToggle = (payload) =>
|
|
85
|
+
action('section-collapse-toggle')(payload)
|
|
86
|
+
const onItemClick = (payload) => action('on-menu-item-click')(payload)
|
|
87
|
+
return { args, onSectionToggle, onItemClick }
|
|
88
|
+
},
|
|
89
|
+
template: `
|
|
90
|
+
<div data-test-id="navigation-side-menu-story" style="display: flex; min-height: 100vh; background: #f5f5f5;">
|
|
91
|
+
<NavigationSideMenu
|
|
92
|
+
v-bind="args"
|
|
93
|
+
@section-collapse-toggle="onSectionToggle"
|
|
94
|
+
@on-menu-item-click="onItemClick"
|
|
95
|
+
/>
|
|
96
|
+
<main style="flex: 1; padding: 24px;">Main content area</main>
|
|
97
|
+
</div>
|
|
98
|
+
`,
|
|
99
|
+
provide: {
|
|
100
|
+
theme,
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Default: all sections expanded. Click a section header to collapse/expand it;
|
|
105
|
+
// multiple sections can be collapsed at once (independent, not accordion).
|
|
106
|
+
export const CollapsibleSections = Template.bind({})
|
|
107
|
+
CollapsibleSections.args = {
|
|
108
|
+
menuData: settingsMenu,
|
|
109
|
+
headerTitle: 'Settings',
|
|
110
|
+
activeItemId: 'technologies',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// An item that has its own children ("Document templates") renders an item-level
|
|
114
|
+
// dropdown, which behaves independently of section collapse.
|
|
115
|
+
export const WithNestedDropdown = Template.bind({})
|
|
116
|
+
WithNestedDropdown.args = {
|
|
117
|
+
menuData: settingsMenu,
|
|
118
|
+
headerTitle: 'Settings',
|
|
119
|
+
activeItemId: 'quotation',
|
|
120
|
+
activeParentId: 'document_templates',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The project-view placement of the same component: project-manager select in the
|
|
124
|
+
// header plus a footer button. Section collapse works identically here.
|
|
125
|
+
export const ProjectPlacement = Template.bind({})
|
|
126
|
+
ProjectPlacement.args = {
|
|
127
|
+
menuData: settingsMenu,
|
|
128
|
+
showProjectManagerSelect: true,
|
|
129
|
+
projectManagerOptions,
|
|
130
|
+
projectManagerId: 1,
|
|
131
|
+
footerButtonText: 'Documents',
|
|
132
|
+
activeItemId: 'finance',
|
|
133
|
+
}
|