@mozaic-ds/vue 2.10.0 → 2.11.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 +156 -48
- package/dist/mozaic-vue.js +1528 -1200
- 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 +2 -2
- package/src/components/kpiitem/MKpiItem.spec.ts +71 -0
- package/src/components/kpiitem/MKpiItem.stories.ts +69 -0
- package/src/components/kpiitem/MKpiItem.vue +89 -0
- package/src/components/kpiitem/README.md +15 -0
- package/src/components/phonenumber/README.md +2 -0
- package/src/components/starrating/MStarRating.spec.ts +203 -0
- package/src/components/starrating/MStarRating.stories.ts +82 -0
- package/src/components/starrating/MStarRating.vue +187 -0
- package/src/components/starrating/README.md +31 -0
- package/src/components/statusbadge/README.md +1 -1
- package/src/components/statusdot/README.md +1 -1
- package/src/components/statusmessage/MStatusMessage.spec.ts +76 -0
- package/src/components/statusmessage/MStatusMessage.stories.ts +52 -0
- package/src/components/statusmessage/MStatusMessage.vue +70 -0
- package/src/components/statusmessage/README.md +11 -0
- package/src/components/statusnotification/README.md +1 -1
- package/src/components/steppercompact/MStepperCompact.spec.ts +98 -0
- package/src/components/steppercompact/MStepperCompact.stories.ts +43 -0
- package/src/components/steppercompact/MStepperCompact.vue +105 -0
- package/src/components/steppercompact/README.md +13 -0
- package/src/components/tag/MTag.vue +2 -1
- package/src/components/tag/README.md +1 -1
- package/src/components/toaster/README.md +1 -1
- package/src/main.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mozaic-ds/vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Mozaic-Vue is the Vue.js implementation of ADEO Design system",
|
|
6
6
|
"author": "ADEO - ADEO Design system",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"*.d.ts"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@mozaic-ds/styles": "^2.
|
|
44
|
+
"@mozaic-ds/styles": "^2.4.0",
|
|
45
45
|
"@mozaic-ds/web-fonts": "^1.65.0",
|
|
46
46
|
"postcss-scss": "^4.0.9",
|
|
47
47
|
"vue": "^3.5.13"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import KpiItem from './MKpiItem.vue';
|
|
4
|
+
|
|
5
|
+
describe('MKpiItem component', () => {
|
|
6
|
+
it('renders the large size correctly', () => {
|
|
7
|
+
const wrapper = mount(KpiItem, {
|
|
8
|
+
props: {
|
|
9
|
+
value: '85%',
|
|
10
|
+
label: 'Completion Rate',
|
|
11
|
+
trend: 'increasing',
|
|
12
|
+
information: 'Above target',
|
|
13
|
+
size: 'l',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
expect(wrapper.text()).toContain('85%');
|
|
17
|
+
expect(wrapper.text()).toContain('Completion Rate');
|
|
18
|
+
expect(wrapper.text()).toContain('Above target');
|
|
19
|
+
expect(wrapper.find('.mc-kpi__icon').exists()).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders the medium size component correctly', () => {
|
|
23
|
+
const wrapper = mount(KpiItem, {
|
|
24
|
+
props: {
|
|
25
|
+
value: '85%',
|
|
26
|
+
label: 'Completion Rate',
|
|
27
|
+
information: 'Above target',
|
|
28
|
+
trend: 'increasing',
|
|
29
|
+
size: 'm',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
expect(wrapper.text()).toContain('85%');
|
|
33
|
+
expect(wrapper.text()).toContain('Completion Rate');
|
|
34
|
+
expect(wrapper.text()).not.toContain('Above target');
|
|
35
|
+
expect(wrapper.find('.mc-kpi__icon').exists()).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders the small size component correctly', () => {
|
|
39
|
+
const wrapper = mount(KpiItem, {
|
|
40
|
+
props: {
|
|
41
|
+
value: '85%',
|
|
42
|
+
label: 'Completion Rate',
|
|
43
|
+
information: 'Above target',
|
|
44
|
+
trend: 'increasing',
|
|
45
|
+
size: 's',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
expect(wrapper.text()).toContain('85%');
|
|
49
|
+
expect(wrapper.text()).not.toContain('Completion Rate');
|
|
50
|
+
expect(wrapper.text()).not.toContain('Above target');
|
|
51
|
+
expect(wrapper.find('.mc-kpi__icon').exists()).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('trend icon', () => {
|
|
55
|
+
it('does not render the icon when trend prop is not provided', () => {
|
|
56
|
+
const wrapper = mount(KpiItem, {
|
|
57
|
+
props: { value: '123' },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(wrapper.find('.mc-kpi__icon').exists()).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('renders the icon when trend prop is provided', () => {
|
|
64
|
+
const wrapper = mount(KpiItem, {
|
|
65
|
+
props: { value: '123', trend: 'increasing' },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(wrapper.find('.mc-kpi__icon').exists()).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import MKpiItem from './MKpiItem.vue';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof MKpiItem> = {
|
|
5
|
+
title: 'Status/Kpi Item',
|
|
6
|
+
component: MKpiItem,
|
|
7
|
+
parameters: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component:
|
|
11
|
+
'A KPI Item is used to display Key Performance Indicators (KPIs) within an interface, providing a quick and clear visualization of essential data. It often includes contextual elements such as labels, trends, or status indicators to help users interpret the information at a glance. KPI Items are commonly used in dashboards, reports, and analytics tools to highlight critical metrics and facilitate data-driven decision-making.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
value: '99.99%',
|
|
17
|
+
label: 'Label',
|
|
18
|
+
size: 's',
|
|
19
|
+
},
|
|
20
|
+
argTypes: {
|
|
21
|
+
size: {
|
|
22
|
+
control: { type: 'inline-radio' },
|
|
23
|
+
options: ['s', 'm', 'l'],
|
|
24
|
+
},
|
|
25
|
+
trend: {
|
|
26
|
+
control: { type: 'radio' },
|
|
27
|
+
options: ['increasing', 'decreasing', 'stable', undefined],
|
|
28
|
+
},
|
|
29
|
+
status: {
|
|
30
|
+
control: { type: 'radio' },
|
|
31
|
+
options: ['info', 'warning', 'error', 'success', 'neutral'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
render: (args) => ({
|
|
35
|
+
components: { MKpiItem },
|
|
36
|
+
setup() {
|
|
37
|
+
return { args };
|
|
38
|
+
},
|
|
39
|
+
template: `
|
|
40
|
+
<MKpiItem v-bind="args"></MKpiItem>
|
|
41
|
+
`,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default meta;
|
|
46
|
+
|
|
47
|
+
type Story = StoryObj<typeof MKpiItem>;
|
|
48
|
+
|
|
49
|
+
export const LargeWithIconAndInformation: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
trend: 'increasing',
|
|
52
|
+
information: '> 10% expected',
|
|
53
|
+
size: 'l',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const MediumWithIconAndLabel: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
trend: 'increasing',
|
|
60
|
+
size: 'm',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const SmallWithIcon: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
trend: 'increasing',
|
|
67
|
+
size: 's',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mc-kpi" :class="rootClasses">
|
|
3
|
+
<span v-if="isMedium && label" class="mc-kpi__label">
|
|
4
|
+
{{ label }}
|
|
5
|
+
</span>
|
|
6
|
+
<div class="mc-kpi__content">
|
|
7
|
+
<div class="mc-kpi__main">
|
|
8
|
+
<span v-if="isLarge && label" class="mc-kpi__label">
|
|
9
|
+
{{ label }}
|
|
10
|
+
</span>
|
|
11
|
+
<span class="mc-kpi__value">{{ value }}</span>
|
|
12
|
+
</div>
|
|
13
|
+
<div v-if="trend || information" class="mc-kpi__aside">
|
|
14
|
+
<span v-if="isLarge && information" class="mc-kpi__detail">
|
|
15
|
+
{{ information }}
|
|
16
|
+
</span>
|
|
17
|
+
|
|
18
|
+
<component v-if="trend" :is="getIconComponent" class="mc-kpi__icon" />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import { computed, type Component } from 'vue';
|
|
26
|
+
import ArrowBottomRight24 from '@mozaic-ds/icons-vue/src/components/ArrowBottomRight24/ArrowBottomRight24.vue';
|
|
27
|
+
import ArrowTopRight24 from '@mozaic-ds/icons-vue/src/components/ArrowTopRight24/ArrowTopRight24.vue';
|
|
28
|
+
import Less24 from '@mozaic-ds/icons-vue/src/components/Less24/Less24.vue';
|
|
29
|
+
/**
|
|
30
|
+
* A KPI Item is used to display Key Performance Indicators (KPIs) within an interface, providing a quick and clear visualization of essential data. It often includes contextual elements such as labels, trends, or status indicators to help users interpret the information at a glance. KPI Items are commonly used in dashboards, reports, and analytics tools to highlight critical metrics and facilitate data-driven decision-making.
|
|
31
|
+
*/
|
|
32
|
+
const props = withDefaults(
|
|
33
|
+
defineProps<{
|
|
34
|
+
/**
|
|
35
|
+
* The current value of the kpi item.
|
|
36
|
+
*/
|
|
37
|
+
value: string;
|
|
38
|
+
/**
|
|
39
|
+
* Defines the evolution of the kpi.
|
|
40
|
+
*/
|
|
41
|
+
trend?: 'increasing' | 'decreasing' | 'stable';
|
|
42
|
+
/**
|
|
43
|
+
* Label of the kpi item.
|
|
44
|
+
*/
|
|
45
|
+
label?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Allows to define the kpi item status.
|
|
48
|
+
*/
|
|
49
|
+
status?: 'info' | 'warning' | 'error' | 'success' | 'neutral';
|
|
50
|
+
/**
|
|
51
|
+
* The evolution information defining the kpi.
|
|
52
|
+
*/
|
|
53
|
+
information?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Allows to define the kpi item size.
|
|
56
|
+
*/
|
|
57
|
+
size?: 's' | 'm' | 'l';
|
|
58
|
+
}>(),
|
|
59
|
+
{
|
|
60
|
+
size: 's',
|
|
61
|
+
status: 'info',
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const isMedium = computed(() => props.size === 'm');
|
|
66
|
+
const isLarge = computed(() => props.size === 'l');
|
|
67
|
+
|
|
68
|
+
const iconMap: Record<
|
|
69
|
+
NonNullable<Exclude<typeof props.trend, undefined>>,
|
|
70
|
+
Component
|
|
71
|
+
> = {
|
|
72
|
+
increasing: ArrowTopRight24,
|
|
73
|
+
decreasing: ArrowBottomRight24,
|
|
74
|
+
stable: Less24,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const getIconComponent = computed<Component | undefined>(() =>
|
|
78
|
+
props.trend ? iconMap[props.trend] : undefined,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const rootClasses = computed(() => [
|
|
82
|
+
`mc-kpi--${props.size}`,
|
|
83
|
+
`mc-kpi--${props.status}`,
|
|
84
|
+
]);
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<style lang="scss" scoped>
|
|
88
|
+
@use '@mozaic-ds/styles/components/kpi-item';
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# MKpiItem
|
|
2
|
+
|
|
3
|
+
A KPI Item is used to display Key Performance Indicators (KPIs) within an interface, providing a quick and clear visualization of essential data. It often includes contextual elements such as labels, trends, or status indicators to help users interpret the information at a glance. KPI Items are commonly used in dashboards, reports, and analytics tools to highlight critical metrics and facilitate data-driven decision-making.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Props
|
|
7
|
+
|
|
8
|
+
| Name | Description | Type | Default |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
| `value*` | The current value of the kpi item. | `string` | - |
|
|
11
|
+
| `trend` | Defines the evolution of the kpi. | `"increasing"` `"decreasing"` `"stable"` | - |
|
|
12
|
+
| `label` | Label of the kpi item. | `string` | - |
|
|
13
|
+
| `status` | Allows to define the kpi item status. | `"info"` `"warning"` `"error"` `"success"` `"neutral"` | `"info"` |
|
|
14
|
+
| `information` | The evolution information defining the kpi. | `string` | - |
|
|
15
|
+
| `size` | Allows to define the kpi item size. | `"s"` `"m"` `"l"` | `"s"` |
|
|
@@ -17,6 +17,8 @@ A phone number input is a specialized input field designed to capture and valida
|
|
|
17
17
|
| `readonly` | If `true`, the input is read-only (cannot be edited). | `boolean` | - |
|
|
18
18
|
| `prefix` | If `true`, display the country calling code prefix (+33, +1, etc.). | `boolean` | `true` |
|
|
19
19
|
| `flag` | If `true`, display the country flag selector | `boolean` | `true` |
|
|
20
|
+
| `locale` | Locale for displaying country names (e.g., 'fr', 'en', 'es', 'pt'). | `string` | `"en"` |
|
|
21
|
+
| `countryCodes` | List of country codes to display in the selector. If not provided, all countries will be shown. | `CountryCode[]` | - |
|
|
20
22
|
|
|
21
23
|
## Events
|
|
22
24
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { shallowMount, mount } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { nextTick } from 'vue';
|
|
4
|
+
import MStarRating from './MStarRating.vue';
|
|
5
|
+
import StarFilled24 from '@mozaic-ds/icons-vue/src/components/StarFilled24/StarFilled24.vue';
|
|
6
|
+
import StarHalf24 from '@mozaic-ds/icons-vue/src/components/StarHalf24/StarHalf24.vue';
|
|
7
|
+
|
|
8
|
+
function mockRect(el: Element, { left = 0, width = 100 } = {}) {
|
|
9
|
+
Object.defineProperty(el, 'getBoundingClientRect', {
|
|
10
|
+
value: () => ({
|
|
11
|
+
left,
|
|
12
|
+
width,
|
|
13
|
+
top: 0,
|
|
14
|
+
bottom: 0,
|
|
15
|
+
right: left + width,
|
|
16
|
+
height: 0,
|
|
17
|
+
}),
|
|
18
|
+
configurable: true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('MStarRating', () => {
|
|
23
|
+
it('renders 5 stars by default', () => {
|
|
24
|
+
const wrapper = shallowMount(MStarRating, { props: { modelValue: 0 } });
|
|
25
|
+
const stars = wrapper.findAll('.mc-star-rating__icon');
|
|
26
|
+
expect(stars.length).toBe(5);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders 1 star when compact mode is enabled', () => {
|
|
30
|
+
const wrapper = shallowMount(MStarRating, {
|
|
31
|
+
props: { modelValue: 0, compact: true },
|
|
32
|
+
});
|
|
33
|
+
const stars = wrapper.findAll('.mc-star-rating__icon');
|
|
34
|
+
expect(stars.length).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('does not render half star on input mode', async () => {
|
|
38
|
+
const wrapper = shallowMount(MStarRating, {
|
|
39
|
+
props: { modelValue: 0, readonly: false },
|
|
40
|
+
});
|
|
41
|
+
const stars = wrapper.findAll('.mc-star-rating__icon');
|
|
42
|
+
const first = stars[0];
|
|
43
|
+
mockRect(first.element, { left: 0, width: 100 });
|
|
44
|
+
await first.trigger('mousemove', { clientX: 10 });
|
|
45
|
+
expect(wrapper.findComponent(StarHalf24).exists()).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('render half star on readonly mode if possible', () => {
|
|
49
|
+
const wrapper = shallowMount(MStarRating, {
|
|
50
|
+
props: { modelValue: 1.5, readonly: true },
|
|
51
|
+
});
|
|
52
|
+
const filledStars = wrapper.findAllComponents(StarFilled24);
|
|
53
|
+
const halfStars = wrapper.findAllComponents(StarHalf24);
|
|
54
|
+
expect(filledStars.length).toBe(1);
|
|
55
|
+
expect(halfStars.length).toBe(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('emits update:modelValue with the correct value when clicking a star', async () => {
|
|
59
|
+
const wrapper = shallowMount(MStarRating, {
|
|
60
|
+
props: { modelValue: 2, readonly: false },
|
|
61
|
+
});
|
|
62
|
+
const stars = wrapper.findAll('.mc-star-rating__icon');
|
|
63
|
+
const first = stars[0];
|
|
64
|
+
mockRect(first.element, { left: 0, width: 100 });
|
|
65
|
+
await first.trigger('click', { clientX: 10 });
|
|
66
|
+
const emitted = wrapper.emitted('update:modelValue') || [];
|
|
67
|
+
expect(emitted.length).toBe(1);
|
|
68
|
+
expect(emitted[0][0]).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('emits the correct values when pressing ArrowRight', async () => {
|
|
72
|
+
const wrapper = shallowMount(MStarRating, {
|
|
73
|
+
props: { modelValue: 2, readonly: false },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const ratingInput = wrapper.find('.mc-star-rating__wrapper');
|
|
77
|
+
|
|
78
|
+
await ratingInput.trigger('keydown', { key: 'ArrowRight' });
|
|
79
|
+
const emitted = wrapper.emitted('update:modelValue') || [];
|
|
80
|
+
expect(emitted.length).toBe(1);
|
|
81
|
+
expect(emitted[0][0]).toBe(3);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('emits the correct values when pressing ArrowLeft', async () => {
|
|
85
|
+
const wrapper = shallowMount(MStarRating, {
|
|
86
|
+
props: { modelValue: 2, readonly: false },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const ratingInput = wrapper.find('.mc-star-rating__wrapper');
|
|
90
|
+
await ratingInput.trigger('keydown', { key: 'ArrowLeft' });
|
|
91
|
+
const emitted = wrapper.emitted('update:modelValue') || [];
|
|
92
|
+
expect(emitted.length).toBe(1);
|
|
93
|
+
expect(emitted[0][0]).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('does not do anything if the wrong key is pressed', async () => {
|
|
97
|
+
const wrapper = shallowMount(MStarRating, {
|
|
98
|
+
props: { modelValue: 2, readonly: false },
|
|
99
|
+
});
|
|
100
|
+
const ratingInput = wrapper.find('.mc-star-rating__wrapper');
|
|
101
|
+
await ratingInput.trigger('keydown', { key: 'ArrowUp' });
|
|
102
|
+
const emitted = wrapper.emitted('update:modelValue') || [];
|
|
103
|
+
expect(emitted.length).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('resets hover to null on mouseleave (aria-label falls back to modelValue)', async () => {
|
|
107
|
+
const wrapper = shallowMount(MStarRating, {
|
|
108
|
+
props: { modelValue: 2, size: 'm', readonly: false },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const stars = wrapper.findAll('.mc-star-rating__icon');
|
|
112
|
+
const first = stars[0];
|
|
113
|
+
mockRect(first.element, { left: 0, width: 100 });
|
|
114
|
+
|
|
115
|
+
await first.trigger('mousemove', { clientX: 10 });
|
|
116
|
+
await nextTick();
|
|
117
|
+
|
|
118
|
+
// aria-label should reflect hovered value (0.5) not modelValue (2)
|
|
119
|
+
const root = wrapper.find('[role="slider"]');
|
|
120
|
+
expect(root.attributes('aria-label')).toContain('1');
|
|
121
|
+
|
|
122
|
+
// trigger mouseleave on root, hover should be cleared and aria-label revert to modelValue
|
|
123
|
+
await root.trigger('mouseleave');
|
|
124
|
+
await nextTick();
|
|
125
|
+
expect(root.attributes('aria-label')).toContain('2');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('resets hover to null on blur (aria-label falls back to modelValue)', async () => {
|
|
129
|
+
const wrapper = shallowMount(MStarRating, {
|
|
130
|
+
props: { modelValue: 3, size: 'm', readonly: false },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const stars = wrapper.findAll('.mc-star-rating__icon');
|
|
134
|
+
const secondStar = stars[1];
|
|
135
|
+
mockRect(secondStar.element, { left: 0, width: 100 });
|
|
136
|
+
|
|
137
|
+
const root = wrapper.find('[role="slider"]');
|
|
138
|
+
|
|
139
|
+
await root.trigger('focus');
|
|
140
|
+
await secondStar.trigger('mousemove', { clientX: 10 });
|
|
141
|
+
await nextTick();
|
|
142
|
+
expect(root.attributes('aria-label')).toContain('2');
|
|
143
|
+
|
|
144
|
+
// blur should clear hover and revert aria-label to modelValue (3)
|
|
145
|
+
await root.trigger('blur');
|
|
146
|
+
await nextTick();
|
|
147
|
+
expect(root.attributes('aria-label')).toContain('3');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('renders information text when text prop is provided', () => {
|
|
151
|
+
const wrapper = shallowMount(MStarRating, {
|
|
152
|
+
props: { modelValue: 3, text: 'Note publique' },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const info = wrapper.find('.mc-star-rating__info');
|
|
156
|
+
expect(info.exists()).toBe(true);
|
|
157
|
+
expect(info.text()).toBe('Note publique');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('renders href when href prop is provided and text is not', () => {
|
|
161
|
+
const wrapper = shallowMount(MStarRating, {
|
|
162
|
+
props: { modelValue: 2, href: '/voir' },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const info = wrapper.find('.mc-star-rating__info');
|
|
166
|
+
expect(info.exists()).toBe(true);
|
|
167
|
+
expect(info.text()).toBe('/voir');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('does not render info span when neither text nor href is provided', () => {
|
|
171
|
+
const wrapper = shallowMount(MStarRating, {
|
|
172
|
+
props: { modelValue: 1 },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const info = wrapper.find('.mc-star-rating__info');
|
|
176
|
+
expect(info.exists()).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('renders router-link when href and router are provided', () => {
|
|
180
|
+
const wrapper = mount(MStarRating, {
|
|
181
|
+
props: { modelValue: 3, href: '/path', router: true },
|
|
182
|
+
global: { stubs: ['router-link'] },
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(wrapper.element.tagName.toLowerCase()).toBe('router-link-stub');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('renders an anchor when href is provided and router is falsy', () => {
|
|
189
|
+
const wrapper = shallowMount(MStarRating, {
|
|
190
|
+
props: { modelValue: 2, href: '#', router: false },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(wrapper.element.tagName.toLowerCase()).toBe('a');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('renders a div when no href is provided', () => {
|
|
197
|
+
const wrapper = shallowMount(MStarRating, {
|
|
198
|
+
props: { modelValue: 1 },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(wrapper.element.tagName.toLowerCase()).toBe('div');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import MStarRating from './MStarRating.vue';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof MStarRating> = {
|
|
5
|
+
title: 'Indicators/Star rating',
|
|
6
|
+
component: MStarRating,
|
|
7
|
+
parameters: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component:
|
|
11
|
+
'A Star rating visually represents a score or evaluation and can be used to display a rating or allow users to rate an item, such as a product or service. It serves two main purposes: collecting user feedback by enabling individuals to express their experience and providing social proof by displaying ratings from other users to assist decision-making. Rating Stars are commonly found in e-commerce, review systems, and feedback interfaces, offering a quick and intuitive way to assess quality or satisfaction.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
modelValue: 3.5,
|
|
17
|
+
appearance: 'accent',
|
|
18
|
+
readonly: true,
|
|
19
|
+
},
|
|
20
|
+
render: (args) => ({
|
|
21
|
+
components: { MStarRating },
|
|
22
|
+
setup() {
|
|
23
|
+
return { args };
|
|
24
|
+
},
|
|
25
|
+
template: `
|
|
26
|
+
<MStarRating v-model="args.modelValue" v-bind="args"></MStarRating>
|
|
27
|
+
`,
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
export default meta;
|
|
31
|
+
|
|
32
|
+
type Story = StoryObj<typeof MStarRating>;
|
|
33
|
+
|
|
34
|
+
export const Default: Story = {};
|
|
35
|
+
|
|
36
|
+
export const AsInput: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
readonly: false,
|
|
39
|
+
modelValue: 0,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const SizeM: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
size: 'm',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const SizeL: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
size: 'l',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const StandardAppearance: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
appearance: 'standard',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const WithText: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
appearance: 'accent',
|
|
64
|
+
text: 'Additional text',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const WithLink: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
appearance: 'accent',
|
|
71
|
+
text: 'Additional text',
|
|
72
|
+
href: '#',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const CompactModeWithText: Story = {
|
|
77
|
+
args: {
|
|
78
|
+
compact: true,
|
|
79
|
+
appearance: 'accent',
|
|
80
|
+
text: '(35)',
|
|
81
|
+
},
|
|
82
|
+
};
|