@mozaic-ds/vue 2.14.0 → 2.15.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 +922 -429
- package/dist/mozaic-vue.js +3128 -2461
- 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 -3
- package/src/components/DarkMode.mdx +115 -0
- package/src/components/actionlistbox/MActionListbox.spec.ts +6 -10
- package/src/components/actionlistbox/MActionListbox.vue +2 -11
- package/src/components/avatar/MAvatar.stories.ts +1 -1
- package/src/components/breadcrumb/MBreadcrumb.vue +2 -2
- package/src/components/pageheader/MPageHeader.spec.ts +12 -12
- package/src/components/pageheader/MPageHeader.stories.ts +9 -1
- package/src/components/pageheader/MPageHeader.vue +3 -6
- package/src/components/segmentedcontrol/MSegmentedControl.spec.ts +57 -25
- package/src/components/segmentedcontrol/MSegmentedControl.stories.ts +6 -19
- package/src/components/segmentedcontrol/MSegmentedControl.vue +27 -13
- package/src/components/segmentedcontrol/README.md +6 -3
- package/src/components/select/MSelect.vue +4 -3
- package/src/components/sidebar/stories/DefaultCase.stories.vue +2 -2
- package/src/components/sidebar/stories/README.md +8 -0
- package/src/components/sidebar/stories/WithExpandOnly.stories.vue +1 -1
- package/src/components/sidebar/stories/WithProfileInfoOnly.stories.vue +2 -2
- package/src/components/sidebar/stories/WithSingleLevel.stories.vue +2 -2
- package/src/components/stepperinline/MStepperInline.spec.ts +63 -28
- package/src/components/stepperinline/MStepperInline.stories.ts +18 -10
- package/src/components/stepperinline/MStepperInline.vue +24 -10
- package/src/components/stepperinline/README.md +6 -2
- package/src/components/tabs/MTabs.stories.ts +18 -0
- package/src/components/tabs/MTabs.vue +30 -14
- package/src/components/tabs/Mtabs.spec.ts +56 -10
- package/src/components/tabs/README.md +6 -3
- package/src/components/tileclickable/README.md +1 -1
- package/src/main.ts +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mozaic-ds/vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.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",
|
|
@@ -56,17 +56,18 @@
|
|
|
56
56
|
"@storybook/addon-docs": "^10.0.4",
|
|
57
57
|
"@storybook/addon-themes": "^10.0.4",
|
|
58
58
|
"@storybook/vue3-vite": "^10.0.4",
|
|
59
|
-
"@types/jsdom": "^
|
|
59
|
+
"@types/jsdom": "^28.0.0",
|
|
60
60
|
"@vitejs/plugin-vue": "^6.0.1",
|
|
61
61
|
"@vitest/coverage-v8": "^4.0.7",
|
|
62
62
|
"@vitest/eslint-plugin": "^1.1.38",
|
|
63
63
|
"@vue/eslint-config-prettier": "^10.2.0",
|
|
64
64
|
"@vue/eslint-config-typescript": "^14.5.0",
|
|
65
65
|
"@vue/test-utils": "^2.4.6",
|
|
66
|
-
"eslint": "^
|
|
66
|
+
"eslint": "^10.0.2",
|
|
67
67
|
"eslint-plugin-storybook": "^10.0.5",
|
|
68
68
|
"eslint-plugin-vue": "^10.0.0",
|
|
69
69
|
"eslint-plugin-vuejs-accessibility": "^2.4.1",
|
|
70
|
+
"globals": "^17.3.0",
|
|
70
71
|
"husky": "^9.1.7",
|
|
71
72
|
"jsdom": "^28.0.0",
|
|
72
73
|
"libphonenumber-js": "^1.12.23",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Meta, Source } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Dark Mode" />
|
|
4
|
+
|
|
5
|
+
# Dark Mode
|
|
6
|
+
|
|
7
|
+
A concise guide explaining **how dark mode works** with your CSS variables and **how to use it** in Storybook.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What dark mode is (high‑level)
|
|
12
|
+
|
|
13
|
+
Dark mode is implemented with **two sets of CSS variables** (tokens):
|
|
14
|
+
|
|
15
|
+
- **Light** values live under `:root`.
|
|
16
|
+
- **Dark** values override under `:root[data-theme="dark"]`.
|
|
17
|
+
|
|
18
|
+
Components only reference tokens with `var(--token-name)` — switching theme is just toggling the `data-theme` attribute (no component code changes).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Token structure (SCSS → CSS)
|
|
23
|
+
|
|
24
|
+
Your presets export SCSS like this:
|
|
25
|
+
|
|
26
|
+
<Source
|
|
27
|
+
language="scss"
|
|
28
|
+
dark
|
|
29
|
+
code={`
|
|
30
|
+
$root-selector: ':root' !default;
|
|
31
|
+
$dark-selector: '[data-theme="dark"]' !default;
|
|
32
|
+
|
|
33
|
+
#{$root-selector} {
|
|
34
|
+
/_ Light tokens _/
|
|
35
|
+
--color-background-primary: #ffffff;
|
|
36
|
+
--color-text-primary: #000000;
|
|
37
|
+
/_ … all your light variables … _/
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#{$root-selector}#{$dark-selector} {
|
|
41
|
+
/_ Dark tokens _/
|
|
42
|
+
--color-background-primary: #191919;
|
|
43
|
+
--color-text-primary: #d9d9d9;
|
|
44
|
+
/_ … all your dark variables … _/
|
|
45
|
+
}
|
|
46
|
+
`}
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
After compilation, this becomes standard CSS:
|
|
50
|
+
|
|
51
|
+
<Source
|
|
52
|
+
language="css"
|
|
53
|
+
dark
|
|
54
|
+
code={`
|
|
55
|
+
:root {
|
|
56
|
+
/* light tokens */
|
|
57
|
+
}
|
|
58
|
+
:root[data-theme='dark'] {
|
|
59
|
+
/* dark tokens */
|
|
60
|
+
}
|
|
61
|
+
`}
|
|
62
|
+
/>
|
|
63
|
+
|
|
64
|
+
> If you can’t (or don’t want to) target `:root`, you can pass a different `$root-selector` when building your theme and apply `data-theme="dark"` on that container instead.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Using tokens inside components
|
|
69
|
+
|
|
70
|
+
To enable the dark mode you have to ensure to:
|
|
71
|
+
|
|
72
|
+
- Add the `data-theme` attribute in your root element with the value `dark`,
|
|
73
|
+
- Use variables — never hard‑code colors or sizes
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<div class="root" data-theme="dark">…</div>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
<Source
|
|
80
|
+
language="sass"
|
|
81
|
+
dark
|
|
82
|
+
code={`
|
|
83
|
+
@use "@mozaic-ds/tokens" as *;
|
|
84
|
+
|
|
85
|
+
.mc-component: {
|
|
86
|
+
background-color: $--color-background-primary;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
`}
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
When the theme changes, these values update automatically via CSS.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Accessibility & good practices
|
|
97
|
+
|
|
98
|
+
- Aim for **WCAG AA** contrast at minimum; verify text vs. background pairs.
|
|
99
|
+
- Prefer **semantic tokens** (`--button-color-…`, `--color-text-…`) over raw color hexes.
|
|
100
|
+
- Keep all component styles expressed in tokens so the **theme switch has zero component logic**.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Troubleshooting
|
|
105
|
+
|
|
106
|
+
- **Dark toggle does nothing** → Ensure the tokens were imported **before** component styles and that `data-theme="dark"` is set on the same selector the tokens target (usually `:root`).
|
|
107
|
+
- **Weird colors** → Search for hard‑coded values and replace them with tokens.
|
|
108
|
+
- **Variables undefined** → Check your build order and that the SCSS was compiled to CSS and loaded by Storybook.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Summary
|
|
113
|
+
|
|
114
|
+
- Light tokens on `:root`, dark overrides on `:root[data-theme="dark"]`.
|
|
115
|
+
- Components read tokens with `var(--$token-name)` — no runtime branching required.
|
|
@@ -87,10 +87,7 @@ describe('MActionListbox', () => {
|
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
it('applies disabled class when item.disabled is true', () => {
|
|
90
|
-
const disabledItems = [
|
|
91
|
-
...items,
|
|
92
|
-
{ label: 'Disabled', disabled: true },
|
|
93
|
-
];
|
|
90
|
+
const disabledItems = [...items, { label: 'Disabled', disabled: true }];
|
|
94
91
|
|
|
95
92
|
const wrapper = mountComponent({ items: disabledItems });
|
|
96
93
|
|
|
@@ -101,20 +98,19 @@ describe('MActionListbox', () => {
|
|
|
101
98
|
|
|
102
99
|
it('applies mc-listbox--bottom by default', () => {
|
|
103
100
|
const wrapper = mountComponent();
|
|
104
|
-
expect(wrapper.find('.mc-listbox').classes())
|
|
105
|
-
|
|
101
|
+
expect(wrapper.find('.mc-listbox').classes()).toContain(
|
|
102
|
+
'mc-listbox--bottom',
|
|
103
|
+
);
|
|
106
104
|
});
|
|
107
105
|
|
|
108
106
|
it('applies mc-listbox--top when position is "top"', () => {
|
|
109
107
|
const wrapper = mountComponent({ position: 'top' });
|
|
110
|
-
expect(wrapper.find('.mc-listbox').classes())
|
|
111
|
-
.toContain('mc-listbox--top');
|
|
108
|
+
expect(wrapper.find('.mc-listbox').classes()).toContain('mc-listbox--top');
|
|
112
109
|
});
|
|
113
110
|
|
|
114
111
|
it('applies mc-listbox--left when position is "left"', () => {
|
|
115
112
|
const wrapper = mountComponent({ position: 'left' });
|
|
116
|
-
expect(wrapper.find('.mc-listbox').classes())
|
|
117
|
-
.toContain('mc-listbox--left');
|
|
113
|
+
expect(wrapper.find('.mc-listbox').classes()).toContain('mc-listbox--left');
|
|
118
114
|
});
|
|
119
115
|
|
|
120
116
|
it('emits "close" when close button is clicked', async () => {
|
|
@@ -61,12 +61,7 @@
|
|
|
61
61
|
</template>
|
|
62
62
|
|
|
63
63
|
<script setup lang="ts">
|
|
64
|
-
import {
|
|
65
|
-
useId,
|
|
66
|
-
useTemplateRef,
|
|
67
|
-
type Component,
|
|
68
|
-
type VNode,
|
|
69
|
-
} from 'vue';
|
|
64
|
+
import { useId, useTemplateRef, type Component, type VNode } from 'vue';
|
|
70
65
|
import MIconButton from '../iconbutton/MIconButton.vue';
|
|
71
66
|
import MDivider from '../divider/MDivider.vue';
|
|
72
67
|
import { Cross24 } from '@mozaic-ds/icons-vue';
|
|
@@ -84,11 +79,7 @@ const props = withDefaults(
|
|
|
84
79
|
/**
|
|
85
80
|
* Defines the position of the listbox relative to its trigger or container.
|
|
86
81
|
*/
|
|
87
|
-
position?:
|
|
88
|
-
| 'top'
|
|
89
|
-
| 'bottom'
|
|
90
|
-
| 'left'
|
|
91
|
-
| 'right';
|
|
82
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
92
83
|
/**
|
|
93
84
|
* An array of objects that allows you to provide all the data needed to generate the content for each item.
|
|
94
85
|
*/
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
:aria-current="isLastLink(index) ? 'page' : undefined"
|
|
19
19
|
>
|
|
20
20
|
{{ link.label }}
|
|
21
|
-
<template v-if="index !==
|
|
22
|
-
<ChevronRight20/>
|
|
21
|
+
<template v-if="index !== links.length - 1" #icon>
|
|
22
|
+
<ChevronRight20 />
|
|
23
23
|
</template>
|
|
24
24
|
</MLink>
|
|
25
25
|
</li>
|
|
@@ -98,9 +98,9 @@ describe('MPageHeader', () => {
|
|
|
98
98
|
},
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
expect(
|
|
102
|
-
|
|
103
|
-
)
|
|
101
|
+
expect(wrapper.findComponent({ name: 'MStatusBadge' }).exists()).toBe(
|
|
102
|
+
true,
|
|
103
|
+
);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
it('does not render status badge if statusLabel is missing', () => {
|
|
@@ -111,9 +111,9 @@ describe('MPageHeader', () => {
|
|
|
111
111
|
},
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
expect(
|
|
115
|
-
|
|
116
|
-
)
|
|
114
|
+
expect(wrapper.findComponent({ name: 'MStatusBadge' }).exists()).toBe(
|
|
115
|
+
false,
|
|
116
|
+
);
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
it('renders extra info when provided', () => {
|
|
@@ -124,9 +124,9 @@ describe('MPageHeader', () => {
|
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
expect(
|
|
128
|
-
|
|
129
|
-
)
|
|
127
|
+
expect(wrapper.find('.mc-page-header__extra-info').text()).toBe(
|
|
128
|
+
'Details',
|
|
129
|
+
);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
it('renders info wrapper only when status or extraInfo exists', () => {
|
|
@@ -134,9 +134,9 @@ describe('MPageHeader', () => {
|
|
|
134
134
|
props: { title: 'My Page' },
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
expect(
|
|
138
|
-
|
|
139
|
-
)
|
|
137
|
+
expect(wrapper.find('.mc-page-header__info-wrapper').exists()).toBe(
|
|
138
|
+
false,
|
|
139
|
+
);
|
|
140
140
|
});
|
|
141
141
|
});
|
|
142
142
|
});
|
|
@@ -63,7 +63,15 @@ const meta: Meta<typeof MPageHeader> = {
|
|
|
63
63
|
`,
|
|
64
64
|
},
|
|
65
65
|
render: (args) => ({
|
|
66
|
-
components: {
|
|
66
|
+
components: {
|
|
67
|
+
MPageHeader,
|
|
68
|
+
MTabs,
|
|
69
|
+
MIconButton,
|
|
70
|
+
Search24,
|
|
71
|
+
HelpCircle24,
|
|
72
|
+
Notification24,
|
|
73
|
+
MSelect,
|
|
74
|
+
},
|
|
67
75
|
setup() {
|
|
68
76
|
const handleBackButtonClick = action('back');
|
|
69
77
|
const handleMenuClick = action('toggle-menu');
|
|
@@ -23,10 +23,7 @@
|
|
|
23
23
|
{{ title }}
|
|
24
24
|
</span>
|
|
25
25
|
|
|
26
|
-
<div
|
|
27
|
-
v-if="status || extraInfo"
|
|
28
|
-
class="mc-page-header__info-wrapper"
|
|
29
|
-
>
|
|
26
|
+
<div v-if="status || extraInfo" class="mc-page-header__info-wrapper">
|
|
30
27
|
<MStatusBadge
|
|
31
28
|
v-if="statusLabel && status"
|
|
32
29
|
:label="statusLabel"
|
|
@@ -55,13 +52,13 @@
|
|
|
55
52
|
</MIconButton>
|
|
56
53
|
|
|
57
54
|
<div class="mc-page-header__actions-content">
|
|
58
|
-
<slot name="actions"/>
|
|
55
|
+
<slot name="actions" />
|
|
59
56
|
</div>
|
|
60
57
|
</div>
|
|
61
58
|
</div>
|
|
62
59
|
|
|
63
60
|
<div class="mc-page-header__tabs">
|
|
64
|
-
<slot name="tabs"/>
|
|
61
|
+
<slot name="tabs" />
|
|
65
62
|
</div>
|
|
66
63
|
</div>
|
|
67
64
|
</template>
|
|
@@ -4,9 +4,9 @@ import MSegmentedControl from './MSegmentedControl.vue';
|
|
|
4
4
|
|
|
5
5
|
describe('MSegmentedControl.vue', () => {
|
|
6
6
|
const segments = [
|
|
7
|
-
{ label: 'First' },
|
|
8
|
-
{ label: 'Second' },
|
|
9
|
-
{ label: 'Third' },
|
|
7
|
+
{ id: '1', label: 'First' },
|
|
8
|
+
{ id: '2', label: 'Second' },
|
|
9
|
+
{ id: '3', label: 'Third' },
|
|
10
10
|
];
|
|
11
11
|
|
|
12
12
|
it('renders segments with correct labels', () => {
|
|
@@ -23,42 +23,74 @@ describe('MSegmentedControl.vue', () => {
|
|
|
23
23
|
|
|
24
24
|
it('sets default active segment based on modelValue prop', () => {
|
|
25
25
|
const wrapper = mount(MSegmentedControl, {
|
|
26
|
-
props: { segments, modelValue: 1 },
|
|
26
|
+
props: { segments, modelValue: '1' },
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
const buttons = wrapper.findAll('button');
|
|
30
|
-
expect(buttons[
|
|
30
|
+
expect(buttons[0].classes()).toContain(
|
|
31
31
|
'mc-segmented-control__segment--selected',
|
|
32
32
|
);
|
|
33
|
-
expect(buttons[
|
|
33
|
+
expect(buttons[0].attributes('aria-checked')).toBe('true');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
describe('Segment selection - string type model', () => {
|
|
37
|
+
it('emits update:modelValue and changes active segment on click', async () => {
|
|
38
|
+
const wrapper = mount(MSegmentedControl, {
|
|
39
|
+
props: { segments, modelValue: '1' },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const buttons = wrapper.findAll('button');
|
|
43
|
+
await buttons[2].trigger('click');
|
|
44
|
+
|
|
45
|
+
expect(wrapper.emitted('update:modelValue')).toBeTruthy();
|
|
46
|
+
expect(wrapper.emitted('update:modelValue')![0]).toEqual(['3']);
|
|
47
|
+
|
|
48
|
+
expect(buttons[2].classes()).toContain(
|
|
49
|
+
'mc-segmented-control__segment--selected',
|
|
50
|
+
);
|
|
51
|
+
expect(buttons[2].attributes('aria-checked')).toBe('true');
|
|
39
52
|
});
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
it('does not emit update event if clicking already active segment', async () => {
|
|
55
|
+
const wrapper = mount(MSegmentedControl, {
|
|
56
|
+
props: { segments, modelValue: '2' },
|
|
57
|
+
});
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
const buttons = wrapper.findAll('button');
|
|
60
|
+
await buttons[1].trigger('click');
|
|
46
61
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
);
|
|
50
|
-
expect(buttons[2].attributes('aria-checked')).toBe('true');
|
|
62
|
+
expect(wrapper.emitted('update:modelValue')).toBeFalsy();
|
|
63
|
+
});
|
|
51
64
|
});
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
describe('Segment selection - number type model', () => {
|
|
67
|
+
it('emits update:modelValue and changes active segment on click', async () => {
|
|
68
|
+
const wrapper = mount(MSegmentedControl, {
|
|
69
|
+
props: { segments, modelValue: 0 },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const buttons = wrapper.findAll('button');
|
|
73
|
+
await buttons[2].trigger('click');
|
|
74
|
+
|
|
75
|
+
expect(wrapper.emitted('update:modelValue')).toBeTruthy();
|
|
76
|
+
expect(wrapper.emitted('update:modelValue')![0]).toEqual([2]);
|
|
77
|
+
|
|
78
|
+
expect(buttons[2].classes()).toContain(
|
|
79
|
+
'mc-segmented-control__segment--selected',
|
|
80
|
+
);
|
|
81
|
+
expect(buttons[2].attributes('aria-checked')).toBe('true');
|
|
56
82
|
});
|
|
57
83
|
|
|
58
|
-
|
|
59
|
-
|
|
84
|
+
it('does not emit update event if clicking already active segment', async () => {
|
|
85
|
+
const wrapper = mount(MSegmentedControl, {
|
|
86
|
+
props: { segments, modelValue: 1 },
|
|
87
|
+
});
|
|
60
88
|
|
|
61
|
-
|
|
89
|
+
const buttons = wrapper.findAll('button');
|
|
90
|
+
await buttons[1].trigger('click');
|
|
91
|
+
|
|
92
|
+
expect(wrapper.emitted('update:modelValue')).toBeFalsy();
|
|
93
|
+
});
|
|
62
94
|
});
|
|
63
95
|
|
|
64
96
|
it('applies full width class when full prop is true', () => {
|
|
@@ -87,7 +119,7 @@ describe('MSegmentedControl.vue', () => {
|
|
|
87
119
|
|
|
88
120
|
it('updates active segment when modelValue prop changes', async () => {
|
|
89
121
|
const wrapper = mount(MSegmentedControl, {
|
|
90
|
-
props: { segments, modelValue:
|
|
122
|
+
props: { segments, modelValue: '1' },
|
|
91
123
|
});
|
|
92
124
|
|
|
93
125
|
const buttons = wrapper.findAll('button');
|
|
@@ -95,7 +127,7 @@ describe('MSegmentedControl.vue', () => {
|
|
|
95
127
|
'mc-segmented-control__segment--selected',
|
|
96
128
|
);
|
|
97
129
|
|
|
98
|
-
await wrapper.setProps({ modelValue:
|
|
130
|
+
await wrapper.setProps({ modelValue: '3' });
|
|
99
131
|
|
|
100
132
|
expect(buttons[2].classes()).toContain(
|
|
101
133
|
'mc-segmented-control__segment--selected',
|
|
@@ -15,17 +15,22 @@ const meta: Meta<typeof MSegmentedControl> = {
|
|
|
15
15
|
},
|
|
16
16
|
},
|
|
17
17
|
args: {
|
|
18
|
+
modelValue: 'label1',
|
|
18
19
|
segments: [
|
|
19
20
|
{
|
|
21
|
+
id: 'label1',
|
|
20
22
|
label: 'Label',
|
|
21
23
|
},
|
|
22
24
|
{
|
|
25
|
+
id: 'label2',
|
|
23
26
|
label: 'Label',
|
|
24
27
|
},
|
|
25
28
|
{
|
|
29
|
+
id: 'label3',
|
|
26
30
|
label: 'Label',
|
|
27
31
|
},
|
|
28
32
|
{
|
|
33
|
+
id: 'label4',
|
|
29
34
|
label: 'Label',
|
|
30
35
|
},
|
|
31
36
|
],
|
|
@@ -40,6 +45,7 @@ const meta: Meta<typeof MSegmentedControl> = {
|
|
|
40
45
|
template: `
|
|
41
46
|
<MSegmentedControl
|
|
42
47
|
v-bind="args"
|
|
48
|
+
v-model="args.modelValue"
|
|
43
49
|
@update:modelValue="handleUpdate"
|
|
44
50
|
></MSegmentedControl>
|
|
45
51
|
`,
|
|
@@ -50,25 +56,6 @@ type Story = StoryObj<typeof MSegmentedControl>;
|
|
|
50
56
|
|
|
51
57
|
export const Default: Story = {};
|
|
52
58
|
|
|
53
|
-
export const Icons: Story = {
|
|
54
|
-
args: {
|
|
55
|
-
segments: [
|
|
56
|
-
{
|
|
57
|
-
label: 'Label',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
label: 'Label',
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
label: 'Label',
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
label: 'Label',
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
|
|
72
59
|
export const Size: Story = {
|
|
73
60
|
args: { size: 'm' },
|
|
74
61
|
};
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
type="button"
|
|
7
7
|
class="mc-segmented-control__segment"
|
|
8
8
|
:class="{
|
|
9
|
-
'mc-segmented-control__segment--selected': isSegmentSelected(
|
|
9
|
+
'mc-segmented-control__segment--selected': isSegmentSelected(
|
|
10
|
+
index,
|
|
11
|
+
segment.id,
|
|
12
|
+
),
|
|
10
13
|
}"
|
|
11
|
-
:aria-checked="isSegmentSelected(index)"
|
|
14
|
+
:aria-checked="isSegmentSelected(index, segment.id)"
|
|
12
15
|
role="radio"
|
|
13
|
-
@click="onClickSegment(index)"
|
|
16
|
+
@click="onClickSegment(index, segment.id)"
|
|
14
17
|
>
|
|
15
18
|
{{ segment.label }}
|
|
16
19
|
</button>
|
|
@@ -25,9 +28,12 @@ import { computed, ref, watch } from 'vue';
|
|
|
25
28
|
const props = withDefaults(
|
|
26
29
|
defineProps<{
|
|
27
30
|
/**
|
|
28
|
-
*
|
|
31
|
+
* Defines the currently active tab.
|
|
32
|
+
*
|
|
33
|
+
* - If a `number` is provided, it represents the index of the segment.
|
|
34
|
+
* - If a `string` is provided, it must match the `id` of one of the segments.
|
|
29
35
|
*/
|
|
30
|
-
modelValue?: number;
|
|
36
|
+
modelValue?: string | number;
|
|
31
37
|
/**
|
|
32
38
|
* if `true`, the segmented control take the full width.
|
|
33
39
|
*/
|
|
@@ -40,6 +46,10 @@ const props = withDefaults(
|
|
|
40
46
|
* An array of objects that allows you to provide all the data needed to generate the content for each segment.
|
|
41
47
|
*/
|
|
42
48
|
segments: Array<{
|
|
49
|
+
/**
|
|
50
|
+
* Unique identifier for the segment.
|
|
51
|
+
*/
|
|
52
|
+
id?: string;
|
|
43
53
|
/**
|
|
44
54
|
* The label displayed for the segment.
|
|
45
55
|
*/
|
|
@@ -59,7 +69,7 @@ const classObject = computed(() => {
|
|
|
59
69
|
};
|
|
60
70
|
});
|
|
61
71
|
|
|
62
|
-
const modelValue = ref(props.modelValue);
|
|
72
|
+
const modelValue = ref<string | number | undefined>(props.modelValue);
|
|
63
73
|
|
|
64
74
|
watch(
|
|
65
75
|
() => props.modelValue,
|
|
@@ -68,22 +78,26 @@ watch(
|
|
|
68
78
|
},
|
|
69
79
|
);
|
|
70
80
|
|
|
71
|
-
const onClickSegment = (index: number) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
const onClickSegment = (index: number, id?: string) => {
|
|
82
|
+
const value = typeof props.modelValue === 'number' ? index : id;
|
|
83
|
+
|
|
84
|
+
if (value !== modelValue.value) {
|
|
85
|
+
modelValue.value = value;
|
|
86
|
+
emit('update:modelValue', value);
|
|
75
87
|
}
|
|
76
88
|
};
|
|
77
89
|
|
|
78
|
-
const isSegmentSelected = (index: number) => {
|
|
79
|
-
|
|
90
|
+
const isSegmentSelected = (index: number, id?: string) => {
|
|
91
|
+
const value = typeof props.modelValue === 'number' ? index : id;
|
|
92
|
+
|
|
93
|
+
return modelValue.value === value;
|
|
80
94
|
};
|
|
81
95
|
|
|
82
96
|
const emit = defineEmits<{
|
|
83
97
|
/**
|
|
84
98
|
* Emits when the selected segment changes, updating the modelValue prop.
|
|
85
99
|
*/
|
|
86
|
-
(on: 'update:modelValue', value
|
|
100
|
+
(on: 'update:modelValue', value?: string | number): void;
|
|
87
101
|
}>();
|
|
88
102
|
</script>
|
|
89
103
|
|
|
@@ -7,13 +7,16 @@ A Segmented Control allows users to switch between multiple options or views wit
|
|
|
7
7
|
|
|
8
8
|
| Name | Description | Type | Default |
|
|
9
9
|
| --- | --- | --- | --- |
|
|
10
|
-
| `modelValue` |
|
|
10
|
+
| `modelValue` | Defines the currently active tab.
|
|
11
|
+
|
|
12
|
+
- If a `number` is provided, it represents the index of the segment.
|
|
13
|
+
- If a `string` is provided, it must match the `id` of one of the segments. | `string` `number` | `0` |
|
|
11
14
|
| `full` | if `true`, the segmented control take the full width. | `boolean` | - |
|
|
12
15
|
| `size` | Determines the size of the segmented control. | `"s"` `"m"` | `"s"` |
|
|
13
|
-
| `segments*` | An array of objects that allows you to provide all the data needed to generate the content for each segment. | `{ label: string; }[]` | - |
|
|
16
|
+
| `segments*` | An array of objects that allows you to provide all the data needed to generate the content for each segment. | `{ id?: string` `undefined; label: string; }[]` | - |
|
|
14
17
|
|
|
15
18
|
## Events
|
|
16
19
|
|
|
17
20
|
| Name | Description | Type |
|
|
18
21
|
| --- | --- | --- |
|
|
19
|
-
| `update:modelValue` | Emits when the selected segment changes, updating the modelValue prop. | [value
|
|
22
|
+
| `update:modelValue` | Emits when the selected segment changes, updating the modelValue prop. | [value?: string | number] |
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
@close="emit('close')"
|
|
6
6
|
>
|
|
7
7
|
<template #header>
|
|
8
|
-
<MSidebarHeader title="Adeo Design System" logo="/logo.svg" />
|
|
8
|
+
<MSidebarHeader title="Adeo Design System" logo="/mozaic-vue/logo.svg" />
|
|
9
9
|
</template>
|
|
10
10
|
|
|
11
11
|
<template #shortcuts>
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
title="Dieter Rams"
|
|
50
50
|
subtitle="Industrial designer"
|
|
51
51
|
href="#"
|
|
52
|
-
avatar="/images/Avatar.png"
|
|
52
|
+
avatar="/mozaic-vue/images/Avatar.png"
|
|
53
53
|
@log-out="emit('log-out')"
|
|
54
54
|
>
|
|
55
55
|
<MSidebarNavItem
|