@mozaic-ds/vue 2.13.0 → 2.14.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/dist/mozaic-vue.css +1 -1
- package/dist/mozaic-vue.d.ts +1088 -378
- package/dist/mozaic-vue.js +2662 -1854
- package/dist/mozaic-vue.js.map +1 -1
- package/dist/mozaic-vue.umd.cjs +5 -5
- package/dist/mozaic-vue.umd.cjs.map +1 -1
- package/package.json +4 -4
- package/src/components/actionlistbox/MActionListbox.spec.ts +53 -59
- package/src/components/actionlistbox/MActionListbox.stories.ts +22 -1
- package/src/components/actionlistbox/MActionListbox.vue +91 -28
- package/src/components/actionlistbox/README.md +15 -0
- package/src/components/breadcrumb/MBreadcrumb.vue +5 -0
- package/src/components/button/README.md +4 -0
- package/src/components/checkbox/README.md +2 -0
- package/src/components/divider/README.md +4 -0
- package/src/components/iconbutton/MIconButton.stories.ts +12 -0
- package/src/components/iconbutton/MIconButton.vue +13 -1
- package/src/components/iconbutton/README.md +27 -0
- package/src/components/loader/README.md +2 -0
- package/src/components/navigationindicator/MNavigationIndicator.spec.ts +152 -0
- package/src/components/navigationindicator/MNavigationIndicator.stories.ts +41 -0
- package/src/components/navigationindicator/MNavigationIndicator.vue +132 -0
- package/src/components/navigationindicator/README.md +37 -0
- package/src/components/pageheader/MPageHeader.spec.ts +142 -0
- package/src/components/pageheader/MPageHeader.stories.ts +125 -0
- package/src/components/pageheader/MPageHeader.vue +133 -0
- package/src/components/pageheader/README.md +46 -0
- package/src/components/popover/MPopover.spec.ts +106 -0
- package/src/components/popover/MPopover.stories.ts +126 -0
- package/src/components/popover/MPopover.vue +131 -0
- package/src/components/popover/README.md +42 -0
- package/src/components/radio/README.md +2 -0
- package/src/components/select/MSelect.spec.ts +2 -1
- package/src/components/select/MSelect.vue +30 -25
- package/src/components/sidebar/MSidebar.const.ts +6 -0
- package/src/components/sidebar/MSidebar.spec.ts +110 -0
- package/src/components/sidebar/MSidebar.stories.ts +108 -0
- package/src/components/sidebar/MSidebar.vue +124 -0
- package/src/components/sidebar/README.md +59 -0
- package/src/components/sidebar/stories/DefaultCase.stories.vue +120 -0
- package/src/components/sidebar/stories/README.md +27 -0
- package/src/components/sidebar/stories/WithExpandOnly.stories.vue +112 -0
- package/src/components/sidebar/stories/WithProfileInfoOnly.stories.vue +119 -0
- package/src/components/sidebar/stories/WithSingleLevel.stories.vue +98 -0
- package/src/components/sidebar/use-floating-item.composable.ts +135 -0
- package/src/components/sidebar/use-floating-item.spec.ts +251 -0
- package/src/components/sidebarexpandableitem/MSidebarExpandableItem.spec.ts +151 -0
- package/src/components/sidebarexpandableitem/MSidebarExpandableItem.vue +113 -0
- package/src/components/sidebarexpandableitem/README.md +36 -0
- package/src/components/sidebarfooter/MSidebarFooter.spec.ts +276 -0
- package/src/components/sidebarfooter/MSidebarFooter.vue +201 -0
- package/src/components/sidebarfooter/README.md +52 -0
- package/src/components/sidebarfooter/_MSidebarFooterMenu.vue +64 -0
- package/src/components/sidebarheader/MSidebarHeader.vue +36 -0
- package/src/components/sidebarheader/README.md +31 -0
- package/src/components/sidebarnavitem/MSidebarNavItem.spec.ts +127 -0
- package/src/components/sidebarnavitem/MSidebarNavItem.vue +113 -0
- package/src/components/sidebarnavitem/README.md +56 -0
- package/src/components/sidebarshortcutitem/MSidebarShortcutItem.spec.ts +59 -0
- package/src/components/sidebarshortcutitem/MSidebarShortcutItem.vue +52 -0
- package/src/components/sidebarshortcutitem/README.md +32 -0
- package/src/components/sidebarshortcuts/MSidebarShortcuts.spec.ts +87 -0
- package/src/components/sidebarshortcuts/MSidebarShortcuts.vue +101 -0
- package/src/components/sidebarshortcuts/README.md +36 -0
- package/src/components/statusbadge/README.md +12 -0
- package/src/components/textinput/MTextInput.stories.ts +13 -1
- package/src/components/textinput/MTextInput.vue +12 -0
- package/src/components/textinput/README.md +3 -1
- package/src/components/tile/MTile.spec.ts +61 -0
- package/src/components/tile/MTile.stories.ts +102 -0
- package/src/components/tile/MTile.vue +68 -0
- package/src/components/tile/README.md +19 -0
- package/src/components/tileclickable/MTileClickable.spec.ts +130 -0
- package/src/components/tileclickable/MTileClickable.stories.ts +60 -0
- package/src/components/tileclickable/MTileClickable.vue +106 -0
- package/src/components/tileclickable/README.md +30 -0
- package/src/components/tileexpandable/MTileExpandable.spec.ts +121 -0
- package/src/components/tileexpandable/MTileExpandable.stories.ts +50 -0
- package/src/components/tileexpandable/MTileExpandable.vue +131 -0
- package/src/components/tileexpandable/README.md +36 -0
- package/src/components/tileselectable/MTileSelectable.spec.ts +177 -0
- package/src/components/tileselectable/MTileSelectable.stories.ts +55 -0
- package/src/components/tileselectable/MTileSelectable.vue +142 -0
- package/src/components/tileselectable/README.md +44 -0
- package/src/components/toaster/README.md +1 -1
- package/src/components/tooltip/MTooltip.vue +5 -0
- package/src/components/tooltip/README.md +16 -1
- package/src/main.ts +12 -2
- package/src/utils/use-is-mobile.composable.ts +20 -0
- package/src/utils/use-is-mobile.spec.ts +70 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import MNavigationIndicator from './MNavigationIndicator.vue';
|
|
4
|
+
|
|
5
|
+
const PauseCircle24 = {
|
|
6
|
+
name: 'PauseCircle24',
|
|
7
|
+
template: '<span data-testid="pause-icon"></span>',
|
|
8
|
+
};
|
|
9
|
+
const PlayCircle24 = {
|
|
10
|
+
name: 'PlayCircle24',
|
|
11
|
+
template: '<span data-testid="play-icon"></span>',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const MButton = {
|
|
15
|
+
name: 'MButton',
|
|
16
|
+
props: ['size', 'iconPosition', 'ghost'],
|
|
17
|
+
template: `
|
|
18
|
+
<button data-testid="m-button">
|
|
19
|
+
<slot />
|
|
20
|
+
|
|
21
|
+
<slot name="icon" />
|
|
22
|
+
<slot />
|
|
23
|
+
</button>
|
|
24
|
+
`,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const MIconButton = {
|
|
28
|
+
name: 'MIconButton',
|
|
29
|
+
props: ['size', 'ghost'],
|
|
30
|
+
template: `<button data-testid="m-icon-button"><slot name="icon" /></button>`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('MNavigationIndicator.vue', () => {
|
|
34
|
+
it('renders the correct number of steps', () => {
|
|
35
|
+
const wrapper = mount(MNavigationIndicator, {
|
|
36
|
+
props: { steps: 4, modelValue: 0 },
|
|
37
|
+
global: {
|
|
38
|
+
stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const items = wrapper.findAll('.mc-navigation-indicator__item');
|
|
43
|
+
expect(items).toHaveLength(4);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('applies active class to the current step', () => {
|
|
47
|
+
const wrapper = mount(MNavigationIndicator, {
|
|
48
|
+
props: { steps: 5, modelValue: 2 },
|
|
49
|
+
global: {
|
|
50
|
+
stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const stepButtons = wrapper.findAll(
|
|
55
|
+
'.mc-navigation-indicator__list .mc-navigation-indicator__button',
|
|
56
|
+
);
|
|
57
|
+
expect(stepButtons[2].classes()).toContain(
|
|
58
|
+
'mc-navigation-indicator__button--active',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('emits update:modelValue when a step is clicked', async () => {
|
|
63
|
+
const wrapper = mount(MNavigationIndicator, {
|
|
64
|
+
props: { steps: 3, modelValue: 0 },
|
|
65
|
+
global: {
|
|
66
|
+
stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const stepButtons = wrapper.findAll(
|
|
71
|
+
'.mc-navigation-indicator__list .mc-navigation-indicator__button',
|
|
72
|
+
);
|
|
73
|
+
await stepButtons[1].trigger('click');
|
|
74
|
+
|
|
75
|
+
const emitted = wrapper.emitted('update:modelValue');
|
|
76
|
+
expect(emitted).toBeTruthy();
|
|
77
|
+
expect(emitted![0][0]).toBe(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('emits update:modelValue when Enter is pressed on a step', async () => {
|
|
81
|
+
const wrapper = mount(MNavigationIndicator, {
|
|
82
|
+
props: { steps: 3, modelValue: 0 },
|
|
83
|
+
global: {
|
|
84
|
+
stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const stepButtons = wrapper.findAll(
|
|
89
|
+
'.mc-navigation-indicator__list .mc-navigation-indicator__button',
|
|
90
|
+
);
|
|
91
|
+
await stepButtons[2].trigger('keydown', { key: 'Enter' });
|
|
92
|
+
|
|
93
|
+
const emitted = wrapper.emitted('update:modelValue');
|
|
94
|
+
expect(emitted).toBeTruthy();
|
|
95
|
+
expect(emitted![0][0]).toBe(2);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('does not emit update:modelValue when a wrong key is pressed', async () => {
|
|
99
|
+
const wrapper = mount(MNavigationIndicator, {
|
|
100
|
+
props: { steps: 3, modelValue: 0 },
|
|
101
|
+
global: {
|
|
102
|
+
stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const stepButtons = wrapper.findAll(
|
|
107
|
+
'.mc-navigation-indicator__list .mc-navigation-indicator__button',
|
|
108
|
+
);
|
|
109
|
+
await stepButtons[2].trigger('keydown', { key: 'Esc' });
|
|
110
|
+
|
|
111
|
+
const emitted = wrapper.emitted('update:modelValue');
|
|
112
|
+
expect(emitted).toBeFalsy();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('does not render action button when player is false', () => {
|
|
116
|
+
const wrapper = mount(MNavigationIndicator, {
|
|
117
|
+
props: { steps: 2, modelValue: 0, player: false },
|
|
118
|
+
global: {
|
|
119
|
+
stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(wrapper.find('[data-testid="m-button"]').exists()).toBe(false);
|
|
124
|
+
expect(wrapper.find('[data-testid="m-icon-button"]').exists()).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('renders icon-only button when no label is defined', () => {
|
|
128
|
+
const wrapper = mount(MNavigationIndicator, {
|
|
129
|
+
props: { steps: 2, modelValue: 0, action: 'resume' },
|
|
130
|
+
global: {
|
|
131
|
+
stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(wrapper.find('[data-testid="m-icon-button"]').exists()).toBe(true);
|
|
136
|
+
expect(wrapper.find('[data-testid="play-icon"]').exists()).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('renders labeled button with correct label and pause icon when action is pause', () => {
|
|
140
|
+
const wrapper = mount(MNavigationIndicator, {
|
|
141
|
+
props: { steps: 2, modelValue: 0, label: 'Pause', action: 'pause' },
|
|
142
|
+
global: {
|
|
143
|
+
stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const button = wrapper.find('[data-testid="m-button"]');
|
|
148
|
+
expect(button.exists()).toBe(true);
|
|
149
|
+
expect(button.text()).toContain('Pause');
|
|
150
|
+
expect(wrapper.find('[data-testid="pause-icon"]').exists()).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import { action } from 'storybook/actions';
|
|
3
|
+
import MNavigationIndicator from './MNavigationIndicator.vue';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof MNavigationIndicator> = {
|
|
6
|
+
title: 'Indicators/Navigation indicator',
|
|
7
|
+
component: MNavigationIndicator,
|
|
8
|
+
tags: ['v2'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'A navigation indicator visually represents the current position within a sequence or step-based process, helping users track progress or navigate through a series of items. It is commonly used in carousels, onboarding flows, or media players. Navigation indicators can be interactive, allowing users to jump between steps, or passive, simply showing progress.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
steps: 6,
|
|
19
|
+
modelValue: 1,
|
|
20
|
+
},
|
|
21
|
+
render: (args) => ({
|
|
22
|
+
components: { MNavigationIndicator },
|
|
23
|
+
setup() {
|
|
24
|
+
const handleAction = action('action');
|
|
25
|
+
return { args, handleAction };
|
|
26
|
+
},
|
|
27
|
+
template: `
|
|
28
|
+
<MNavigationIndicator v-bind="args" v-model="args.modelValue" @action="handleAction"></MNavigationIndicator>
|
|
29
|
+
`,
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
export default meta;
|
|
33
|
+
type Story = StoryObj<typeof MNavigationIndicator>;
|
|
34
|
+
|
|
35
|
+
export const Default: Story = {};
|
|
36
|
+
|
|
37
|
+
export const Standalone: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
player: false,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="mc-navigation-indicator"
|
|
4
|
+
role="navigation"
|
|
5
|
+
aria-label="Navigations steps"
|
|
6
|
+
>
|
|
7
|
+
<ul class="mc-navigation-indicator__list">
|
|
8
|
+
<li
|
|
9
|
+
v-for="(_step, index) in steps"
|
|
10
|
+
:key="index"
|
|
11
|
+
class="mc-navigation-indicator__item"
|
|
12
|
+
>
|
|
13
|
+
<button
|
|
14
|
+
:class="{
|
|
15
|
+
'mc-navigation-indicator__button': true,
|
|
16
|
+
'mc-navigation-indicator__button--active': active === index,
|
|
17
|
+
}"
|
|
18
|
+
aria-label="Navigation step button"
|
|
19
|
+
:aria-current="active === index && 'step'"
|
|
20
|
+
@click="setActiveStep(index)"
|
|
21
|
+
@keydown="onKeydown($event, index)"
|
|
22
|
+
></button>
|
|
23
|
+
</li>
|
|
24
|
+
</ul>
|
|
25
|
+
|
|
26
|
+
<template v-if="player">
|
|
27
|
+
<MButton
|
|
28
|
+
v-if="label"
|
|
29
|
+
size="s"
|
|
30
|
+
icon-position="left"
|
|
31
|
+
ghost
|
|
32
|
+
@click="emit('action')"
|
|
33
|
+
>
|
|
34
|
+
{{ label }}
|
|
35
|
+
|
|
36
|
+
<template #icon>
|
|
37
|
+
<component :is="actionIcon" />
|
|
38
|
+
</template>
|
|
39
|
+
</MButton>
|
|
40
|
+
|
|
41
|
+
<MIconButton
|
|
42
|
+
v-else
|
|
43
|
+
size="s"
|
|
44
|
+
ghost
|
|
45
|
+
@click="emit('action')"
|
|
46
|
+
aria-label="Control button"
|
|
47
|
+
>
|
|
48
|
+
<template #icon>
|
|
49
|
+
<component :is="actionIcon" />
|
|
50
|
+
</template>
|
|
51
|
+
</MIconButton>
|
|
52
|
+
</template>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<script setup lang="ts">
|
|
57
|
+
import { computed } from 'vue';
|
|
58
|
+
import MButton from '../button/MButton.vue';
|
|
59
|
+
import MIconButton from '../iconbutton/MIconButton.vue';
|
|
60
|
+
import { PauseCircle24, PlayCircle24 } from '@mozaic-ds/icons-vue';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A navigation indicator visually represents the current position within a sequence or step-based process, helping users track progress or navigate through a series of items. It is commonly used in carousels, onboarding flows, or media players. Navigation indicators can be interactive, allowing users to jump between steps, or passive, simply showing progress.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
const props = withDefaults(
|
|
67
|
+
defineProps<{
|
|
68
|
+
/**
|
|
69
|
+
* The total number of steps to display.
|
|
70
|
+
*/
|
|
71
|
+
steps: number;
|
|
72
|
+
/**
|
|
73
|
+
* The index of the currently active step (zero-based).
|
|
74
|
+
*/
|
|
75
|
+
modelValue: number;
|
|
76
|
+
/**
|
|
77
|
+
* The current action state of the button. Can be "pause" to show the pause icon or "resume" to show the play icon.
|
|
78
|
+
*/
|
|
79
|
+
action?: 'pause' | 'resume';
|
|
80
|
+
/**
|
|
81
|
+
* The text label displayed in the Resume or Pause button.
|
|
82
|
+
*/
|
|
83
|
+
label?: string;
|
|
84
|
+
/**
|
|
85
|
+
* If `true`, hides the Resume or Pause button next to the steps.
|
|
86
|
+
*/
|
|
87
|
+
player?: boolean;
|
|
88
|
+
}>(),
|
|
89
|
+
{
|
|
90
|
+
player: true,
|
|
91
|
+
action: 'resume',
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const emit = defineEmits<{
|
|
96
|
+
/**
|
|
97
|
+
* Emits when the active step changes, updating the modelValue prop.
|
|
98
|
+
*/
|
|
99
|
+
(on: 'update:modelValue', value: number): void;
|
|
100
|
+
/**
|
|
101
|
+
* Emits when the action button is clicked.
|
|
102
|
+
*/
|
|
103
|
+
(on: 'action'): void;
|
|
104
|
+
}>();
|
|
105
|
+
|
|
106
|
+
const active = computed({
|
|
107
|
+
get() {
|
|
108
|
+
return props.modelValue;
|
|
109
|
+
},
|
|
110
|
+
set(value: number) {
|
|
111
|
+
emit('update:modelValue', value);
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const actionIcon = computed(() =>
|
|
116
|
+
props.action === 'pause' ? PauseCircle24 : PlayCircle24,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
function setActiveStep(step: number) {
|
|
120
|
+
active.value = step;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function onKeydown(event: KeyboardEvent, step: number) {
|
|
124
|
+
if (event.key === 'Enter') {
|
|
125
|
+
setActiveStep(step);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<style lang="scss" scoped>
|
|
131
|
+
@use '@mozaic-ds/styles/components/navigation-indicator';
|
|
132
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# MNavigationIndicator
|
|
2
|
+
|
|
3
|
+
A navigation indicator visually represents the current position within a sequence or step-based process, helping users track progress or navigate through a series of items. It is commonly used in carousels, onboarding flows, or media players. Navigation indicators can be interactive, allowing users to jump between steps, or passive, simply showing progress.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Props
|
|
7
|
+
|
|
8
|
+
| Name | Description | Type | Default |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
| `steps*` | The total number of steps to display. | `number` | - |
|
|
11
|
+
| `modelValue*` | The index of the currently active step (zero-based). | `number` | - |
|
|
12
|
+
| `action` | The current action state of the button. Can be "pause" to show the pause icon or "resume" to show the play icon. | `"resume"` `"pause"` | `"resume"` |
|
|
13
|
+
| `label` | The text label displayed in the Resume or Pause button. | `string` | - |
|
|
14
|
+
| `player` | If `true`, hides the Resume or Pause button next to the steps. | `boolean` | `true` |
|
|
15
|
+
|
|
16
|
+
## Events
|
|
17
|
+
|
|
18
|
+
| Name | Description | Type |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `update:modelValue` | Emits when the active step changes, updating the modelValue prop. | [value: number] |
|
|
21
|
+
| `action` | Emits when the action button is clicked. | [] |
|
|
22
|
+
|
|
23
|
+
## Dependencies
|
|
24
|
+
|
|
25
|
+
### Depends on
|
|
26
|
+
|
|
27
|
+
- [MButton](../button)
|
|
28
|
+
- [MIconButton](../iconbutton)
|
|
29
|
+
|
|
30
|
+
### Graph
|
|
31
|
+
|
|
32
|
+
```mermaid
|
|
33
|
+
graph TD;
|
|
34
|
+
MNavigationIndicator --> MButton
|
|
35
|
+
MNavigationIndicator --> MIconButton
|
|
36
|
+
style MNavigationIndicator fill:#008240,stroke:#333,stroke-width:4px
|
|
37
|
+
```
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import MPageHeader from './MPageHeader.vue';
|
|
4
|
+
|
|
5
|
+
describe('MPageHeader', () => {
|
|
6
|
+
describe('Render basics', () => {
|
|
7
|
+
it('renders the title', () => {
|
|
8
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
9
|
+
props: { title: 'My Page' },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(wrapper.text()).toContain('My Page');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('adds shadow class when shadow prop is true', () => {
|
|
16
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
17
|
+
props: { title: 'My Page', shadow: true },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(wrapper.classes()).toContain('mc-page-header--with-shadow');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('renders content slot when provided', () => {
|
|
24
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
25
|
+
props: { title: 'My Page' },
|
|
26
|
+
slots: {
|
|
27
|
+
content: '<div class="slot-content">ContentSlot</div>',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(wrapper.find('.slot-content').exists()).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders actions slot when provided', () => {
|
|
35
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
36
|
+
props: { title: 'My Page' },
|
|
37
|
+
slots: {
|
|
38
|
+
actions: '<button class="action-btn">Action</button>',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(wrapper.find('.action-btn').exists()).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('renders tabs slot when provided', () => {
|
|
46
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
47
|
+
props: { title: 'My Page' },
|
|
48
|
+
slots: {
|
|
49
|
+
tabs: '<div class="tab-item">Tab 1</div>',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(wrapper.find('.tab-item').exists()).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('Buttons and emitted events', () => {
|
|
58
|
+
it('shows back button and emits "back" when clicked', async () => {
|
|
59
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
60
|
+
props: { title: 'My Page', backButton: true },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const backBtn = wrapper.find('[aria-label="Back button"]');
|
|
64
|
+
expect(backBtn.exists()).toBe(true);
|
|
65
|
+
|
|
66
|
+
await backBtn.trigger('click');
|
|
67
|
+
expect(wrapper.emitted()).toHaveProperty('back');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('does not render back button when backButton is false', () => {
|
|
71
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
72
|
+
props: { title: 'My Page', backButton: false },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(wrapper.find('[aria-label="Back button"]').exists()).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('burger menu button emits "toggle-menu" when clicked', async () => {
|
|
79
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
80
|
+
props: { title: 'My Page' },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const burger = wrapper.find('[aria-label="Burger menu"]');
|
|
84
|
+
expect(burger.exists()).toBe(true);
|
|
85
|
+
|
|
86
|
+
await burger.trigger('click');
|
|
87
|
+
expect(wrapper.emitted()).toHaveProperty('toggle-menu');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Status and extra info', () => {
|
|
92
|
+
it('renders status badge when status and statusLabel are provided', () => {
|
|
93
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
94
|
+
props: {
|
|
95
|
+
title: 'My Page',
|
|
96
|
+
status: 'success',
|
|
97
|
+
statusLabel: 'OK',
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
wrapper.findComponent({ name: 'MStatusBadge' }).exists(),
|
|
103
|
+
).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('does not render status badge if statusLabel is missing', () => {
|
|
107
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
108
|
+
props: {
|
|
109
|
+
title: 'My Page',
|
|
110
|
+
status: 'success',
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
wrapper.findComponent({ name: 'MStatusBadge' }).exists(),
|
|
116
|
+
).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('renders extra info when provided', () => {
|
|
120
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
121
|
+
props: {
|
|
122
|
+
title: 'My Page',
|
|
123
|
+
extraInfo: 'Details',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(
|
|
128
|
+
wrapper.find('.mc-page-header__extra-info').text(),
|
|
129
|
+
).toBe('Details');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('renders info wrapper only when status or extraInfo exists', () => {
|
|
133
|
+
const wrapper = shallowMount(MPageHeader, {
|
|
134
|
+
props: { title: 'My Page' },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(
|
|
138
|
+
wrapper.find('.mc-page-header__info-wrapper').exists(),
|
|
139
|
+
).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import { action } from 'storybook/actions';
|
|
3
|
+
import MPageHeader from './MPageHeader.vue';
|
|
4
|
+
import MTabs from '../tabs/MTabs.vue';
|
|
5
|
+
import MIconButton from '../iconbutton/MIconButton.vue';
|
|
6
|
+
import { Search24, HelpCircle24, Notification24 } from '@mozaic-ds/icons-vue';
|
|
7
|
+
import MSelect from '../select/MSelect.vue';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof MPageHeader> = {
|
|
10
|
+
title: 'Structure/Page Header',
|
|
11
|
+
component: MPageHeader,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'fullscreen',
|
|
14
|
+
docs: {
|
|
15
|
+
story: { height: '250px' },
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
'The Page Header is a fundamental component that structures the top part of an interface, serving as a cognitive anchor point for users. It establishes page context, facilitates navigation, and provides the main actions available within the current scope.',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
args: {
|
|
23
|
+
title: 'Page header title',
|
|
24
|
+
backButton: true,
|
|
25
|
+
status: 'info',
|
|
26
|
+
statusLabel: 'Information',
|
|
27
|
+
extraInfo: 'Additional information',
|
|
28
|
+
actions: `
|
|
29
|
+
<MIconButton
|
|
30
|
+
ghost
|
|
31
|
+
aria-label="Search button"
|
|
32
|
+
>
|
|
33
|
+
<template #icon>
|
|
34
|
+
<Search24 />
|
|
35
|
+
</template>
|
|
36
|
+
</MIconButton>
|
|
37
|
+
<MIconButton
|
|
38
|
+
ghost
|
|
39
|
+
aria-label="Help button"
|
|
40
|
+
>
|
|
41
|
+
<template #icon>
|
|
42
|
+
<HelpCircle24 />
|
|
43
|
+
</template>
|
|
44
|
+
</MIconButton>
|
|
45
|
+
<MIconButton
|
|
46
|
+
ghost
|
|
47
|
+
aria-label="Notification button"
|
|
48
|
+
>
|
|
49
|
+
<template #icon>
|
|
50
|
+
<Notification24 />
|
|
51
|
+
</template>
|
|
52
|
+
</MIconButton>
|
|
53
|
+
<MSelect
|
|
54
|
+
id="scope-select"
|
|
55
|
+
aria-label="Scope select"
|
|
56
|
+
size="s"
|
|
57
|
+
placeholder="Choose a scope"
|
|
58
|
+
:options='[{ "text": "Option1", "value": "option1"}, { "text": "Option2", "value": "option2"}]'
|
|
59
|
+
/>
|
|
60
|
+
`,
|
|
61
|
+
tabs: `
|
|
62
|
+
<MTabs :tabs='[{ "label": "Label" }, { "label": "Label" }, { "label": "Label" }, { "label": "Label" }]' />
|
|
63
|
+
`,
|
|
64
|
+
},
|
|
65
|
+
render: (args) => ({
|
|
66
|
+
components: { MPageHeader, MTabs, MIconButton, Search24, HelpCircle24, Notification24, MSelect },
|
|
67
|
+
setup() {
|
|
68
|
+
const handleBackButtonClick = action('back');
|
|
69
|
+
const handleMenuClick = action('toggle-menu');
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
args,
|
|
73
|
+
|
|
74
|
+
handleBackButtonClick,
|
|
75
|
+
handleMenuClick,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
template: `
|
|
79
|
+
<MPageHeader
|
|
80
|
+
v-bind="args"
|
|
81
|
+
@back="handleBackButtonClick"
|
|
82
|
+
@toggle-menu="handleMenuClick"
|
|
83
|
+
>
|
|
84
|
+
<template v-if="${'tabs' in args}" v-slot:tabs>${args.tabs}</template>
|
|
85
|
+
<template v-if="${'actions' in args}" v-slot:actions>${args.actions}</template>
|
|
86
|
+
</MPageHeader>
|
|
87
|
+
`,
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
export default meta;
|
|
91
|
+
type Story = StoryObj<typeof MPageHeader>;
|
|
92
|
+
|
|
93
|
+
export const Default: Story = {};
|
|
94
|
+
|
|
95
|
+
export const Basic: Story = {
|
|
96
|
+
args: {
|
|
97
|
+
backButton: false,
|
|
98
|
+
status: undefined,
|
|
99
|
+
tabs: undefined,
|
|
100
|
+
extraInfo: '',
|
|
101
|
+
},
|
|
102
|
+
render: (args) => ({
|
|
103
|
+
components: { MPageHeader },
|
|
104
|
+
setup() {
|
|
105
|
+
return { args };
|
|
106
|
+
},
|
|
107
|
+
template: `<MPageHeader v-bind="args" />`,
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const Shadow: Story = {
|
|
112
|
+
args: {
|
|
113
|
+
shadow: true,
|
|
114
|
+
backButton: false,
|
|
115
|
+
status: undefined,
|
|
116
|
+
extraInfo: '',
|
|
117
|
+
},
|
|
118
|
+
render: (args) => ({
|
|
119
|
+
components: { MPageHeader },
|
|
120
|
+
setup() {
|
|
121
|
+
return { args };
|
|
122
|
+
},
|
|
123
|
+
template: `<MPageHeader v-bind="args" />`,
|
|
124
|
+
}),
|
|
125
|
+
};
|