@bethinkpl/design-system 42.0.0 → 44.0.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/design-system.css +1 -1
- package/dist/design-system.js +17470 -13135
- package/dist/design-system.js.map +1 -1
- package/dist/lib/js/components/Cards/Card/Card.consts.d.ts +14 -0
- package/dist/lib/js/components/Cards/Card/Card.vue.d.ts +10 -5
- package/dist/lib/js/components/Cards/CardExpandable/CardExpandable.vue.d.ts +33 -15
- package/dist/lib/js/components/Form/PinInputField/PinInputField.consts.d.ts +14 -0
- package/dist/lib/js/components/Form/PinInputField/PinInputField.types.d.ts +12 -0
- package/dist/lib/js/components/Form/PinInputField/PinInputField.utils.d.ts +2 -0
- package/dist/lib/js/components/Form/PinInputField/PinInputField.vue.d.ts +91 -0
- package/dist/lib/js/components/Form/PinInputField/index.d.ts +5 -0
- package/dist/lib/js/components/Form/PinInputField/usePinInputFieldWithinForm.d.ts +8 -0
- package/dist/lib/js/components/Form/SelectField/SelectField.types.d.ts +26 -0
- package/dist/lib/js/components/Form/SelectField/SelectField.utils.d.ts +7 -0
- package/dist/lib/js/components/Form/SelectField/SelectField.vue.d.ts +107 -0
- package/dist/lib/js/components/Form/SelectField/SelectFieldOption.vue.d.ts +19 -0
- package/dist/lib/js/components/Form/SelectField/index.d.ts +4 -0
- package/dist/lib/js/components/Form/SelectField/useSelectFieldWithinForm.d.ts +8 -0
- package/dist/lib/js/components/Form/TextAreaField/TextAreaField.types.d.ts +9 -0
- package/dist/lib/js/components/Form/TextAreaField/TextAreaField.vue.d.ts +76 -0
- package/dist/lib/js/components/Form/TextAreaField/TextAreaFieldAutosized.vue.d.ts +6 -0
- package/dist/lib/js/components/Form/TextAreaField/index.d.ts +4 -0
- package/dist/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.vue.d.ts +12 -1
- package/dist/lib/js/components/Pagination/Pagination.vue.d.ts +2 -0
- package/dist/lib/js/components/SelectList/SelectListItem/SelectListItem.vue.d.ts +2 -0
- package/dist/lib/js/components/SelectList/SelectListItemToggle/SelectListItemToggle.vue.d.ts +2 -0
- package/dist/lib/js/components/SurveyQuestions/SurveyQuestionOpenEnded/SurveyQuestionOpenEnded.vue.d.ts +33 -15
- package/dist/lib/js/components/SurveyQuestions/SurveyQuestionScale/SurveyQuestionScale.vue.d.ts +33 -15
- package/dist/lib/js/components/Toast/Toast.consts.d.ts +11 -9
- package/dist/lib/js/components/Toast/Toast.vue.d.ts +51 -415
- package/dist/lib/js/composables/useFormFieldWithinForm.d.ts +3 -3
- package/dist/lib/js/composables/useTextFieldWithinForm.d.ts +8 -0
- package/dist/lib/js/index.d.ts +6 -0
- package/lib/js/components/Cards/Card/Card.consts.ts +18 -0
- package/lib/js/components/Cards/Card/Card.spec.ts +159 -33
- package/lib/js/components/Cards/Card/Card.stories.ts +21 -2
- package/lib/js/components/Cards/Card/Card.vue +90 -23
- package/lib/js/components/Dropdown/Dropdown.vue +6 -10
- package/lib/js/components/Form/CheckboxGroupField/CheckboxGroupField.spec.ts +15 -0
- package/lib/js/components/Form/CheckboxGroupField/CheckboxGroupField.vue +5 -2
- package/lib/js/components/Form/Form.stories.ts +37 -2
- package/lib/js/components/Form/InputField/InputField.spec.ts +11 -1
- package/lib/js/components/Form/InputField/InputField.vue +7 -4
- package/lib/js/components/Form/PinInputField/PinInputField.consts.ts +18 -0
- package/lib/js/components/Form/PinInputField/PinInputField.spec.ts +394 -0
- package/lib/js/components/Form/PinInputField/PinInputField.stories.ts +124 -0
- package/lib/js/components/Form/PinInputField/PinInputField.types.ts +42 -0
- package/lib/js/components/Form/PinInputField/PinInputField.utils.ts +5 -0
- package/lib/js/components/Form/PinInputField/PinInputField.vue +211 -0
- package/lib/js/components/Form/PinInputField/index.ts +6 -0
- package/lib/js/components/Form/PinInputField/usePinInputFieldWithinForm.ts +29 -0
- package/lib/js/components/Form/SelectField/SelectField.spec.ts +495 -0
- package/lib/js/components/Form/SelectField/SelectField.stories.ts +282 -0
- package/lib/js/components/Form/SelectField/SelectField.types.ts +65 -0
- package/lib/js/components/Form/SelectField/SelectField.utils.ts +83 -0
- package/lib/js/components/Form/SelectField/SelectField.vue +342 -0
- package/lib/js/components/Form/SelectField/SelectFieldOption.vue +51 -0
- package/lib/js/components/Form/SelectField/index.ts +5 -0
- package/lib/js/components/Form/SelectField/useSelectFieldWithinForm.ts +27 -0
- package/lib/js/components/Form/TextAreaField/TextAreaField.spec.ts +341 -0
- package/lib/js/components/Form/TextAreaField/TextAreaField.stories.ts +99 -0
- package/lib/js/components/Form/TextAreaField/TextAreaField.types.ts +23 -0
- package/lib/js/components/Form/TextAreaField/TextAreaField.vue +190 -0
- package/lib/js/components/Form/TextAreaField/TextAreaFieldAutosized.vue +19 -0
- package/lib/js/components/Form/TextAreaField/index.ts +5 -0
- package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.spec.ts +26 -0
- package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.stories.ts +16 -3
- package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.vue +5 -0
- package/lib/js/components/LoadingBar/LoadingBar.spec.ts +84 -0
- package/lib/js/components/LoadingBar/LoadingBar.vue +30 -19
- package/lib/js/components/SelectList/SelectListItem/SelectListItem.spec.ts +77 -0
- package/lib/js/components/SelectList/SelectListItem/SelectListItem.vue +4 -1
- package/lib/js/components/Toast/Toast.consts.ts +4 -2
- package/lib/js/components/Toast/Toast.spec.ts +177 -0
- package/lib/js/components/Toast/Toast.stories.ts +65 -31
- package/lib/js/components/Toast/Toast.vue +176 -195
- package/lib/js/composables/useFormFieldWithinForm.ts +4 -4
- package/lib/js/{components/Form/InputField/useInputFieldWithinForm.ts → composables/useTextFieldWithinForm.ts} +7 -2
- package/lib/js/index.ts +6 -0
- package/lib/js/styles/Shadows/Shadows.stories.scss +16 -0
- package/lib/js/styles/Shadows/Shadows.stories.ts +4 -0
- package/lib/styles/mixins/_dropdown-surface.scss +24 -0
- package/lib/styles/settings/_shadows.scss +18 -0
- package/package.json +1 -1
- package/dist/lib/js/components/Form/InputField/useInputFieldWithinForm.d.ts +0 -8
|
@@ -19,6 +19,7 @@ interface createComponentOptions {
|
|
|
19
19
|
size?: LabelValueItemSize;
|
|
20
20
|
isLabelStrong?: boolean;
|
|
21
21
|
valueColor?: LabelValueItemValueColor;
|
|
22
|
+
slots?: Record<string, string>;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
describe('LabelValueItem', () => {
|
|
@@ -29,6 +30,7 @@ describe('LabelValueItem', () => {
|
|
|
29
30
|
size = LABEL_VALUE_ITEM_SIZES.MEDIUM,
|
|
30
31
|
isLabelStrong = false,
|
|
31
32
|
valueColor = LABEL_VALUE_ITEM_VALUE_COLORS.NEUTRAL,
|
|
33
|
+
slots = {},
|
|
32
34
|
}: createComponentOptions = {}) => {
|
|
33
35
|
return mount(LabelValueItem, {
|
|
34
36
|
props: {
|
|
@@ -39,6 +41,7 @@ describe('LabelValueItem', () => {
|
|
|
39
41
|
isLabelStrong,
|
|
40
42
|
valueColor,
|
|
41
43
|
},
|
|
44
|
+
slots,
|
|
42
45
|
});
|
|
43
46
|
};
|
|
44
47
|
|
|
@@ -122,4 +125,27 @@ describe('LabelValueItem', () => {
|
|
|
122
125
|
'-ds-color-primary',
|
|
123
126
|
);
|
|
124
127
|
});
|
|
128
|
+
|
|
129
|
+
it('does not render the accessory slot content when the slot is empty', () => {
|
|
130
|
+
const component = createComponent();
|
|
131
|
+
|
|
132
|
+
expect(component.find('.accessory-content').exists()).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('renders the accessory slot content', () => {
|
|
136
|
+
const component = createComponent({
|
|
137
|
+
slots: { accessory: '<span class="accessory-content">chip</span>' },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(component.find('.accessory-content').text()).toBe('chip');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('renders the accessory slot in the loading state', () => {
|
|
144
|
+
const component = createComponent({
|
|
145
|
+
state: LABEL_VALUE_ITEM_STATES.LOADING,
|
|
146
|
+
slots: { accessory: '<span class="accessory-content">chip</span>' },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(component.find('.accessory-content').exists()).toBe(true);
|
|
150
|
+
});
|
|
125
151
|
});
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import LabelValueItem from './LabelValueItem.vue';
|
|
2
|
+
import SlotPlaceholder, {
|
|
3
|
+
SLOT_PLACEHOLDER_SIZES,
|
|
4
|
+
} from '../../../../../.storybook/SlotPlaceholder/SlotPlaceholder.vue';
|
|
2
5
|
|
|
3
6
|
import { Args, ArgTypes, Meta, StoryFn } from '@storybook/vue3';
|
|
4
7
|
import {
|
|
@@ -6,6 +9,7 @@ import {
|
|
|
6
9
|
LABEL_VALUE_ITEM_STATES,
|
|
7
10
|
LABEL_VALUE_ITEM_VALUE_COLORS,
|
|
8
11
|
} from './LabelValueItem.consts';
|
|
12
|
+
import { toRefs } from 'vue';
|
|
9
13
|
|
|
10
14
|
export default {
|
|
11
15
|
title: 'Components/LabelValue/LabelValueItem',
|
|
@@ -13,9 +17,9 @@ export default {
|
|
|
13
17
|
} as Meta<typeof LabelValueItem>;
|
|
14
18
|
|
|
15
19
|
const StoryTemplate: StoryFn<typeof LabelValueItem> = (args) => ({
|
|
16
|
-
components: { LabelValueItem },
|
|
20
|
+
components: { LabelValueItem, SlotPlaceholder },
|
|
17
21
|
setup() {
|
|
18
|
-
return args;
|
|
22
|
+
return { ...toRefs(args), SLOT_PLACEHOLDER_SIZES };
|
|
19
23
|
},
|
|
20
24
|
template: `<div style="height: 300px; width: 200px;">
|
|
21
25
|
<label-value-item
|
|
@@ -25,7 +29,12 @@ const StoryTemplate: StoryFn<typeof LabelValueItem> = (args) => ({
|
|
|
25
29
|
:size="size"
|
|
26
30
|
:is-label-strong="isLabelStrong"
|
|
27
31
|
:value-color="valueColor"
|
|
28
|
-
|
|
32
|
+
>
|
|
33
|
+
<template #accessory>
|
|
34
|
+
<div v-if="accessorySlot" v-html="accessorySlot" />
|
|
35
|
+
<slot-placeholder v-else :size="SLOT_PLACEHOLDER_SIZES.SMALL" label="accessory" />
|
|
36
|
+
</template>
|
|
37
|
+
</label-value-item>
|
|
29
38
|
</div>`,
|
|
30
39
|
});
|
|
31
40
|
|
|
@@ -38,6 +47,7 @@ const args = {
|
|
|
38
47
|
size: LABEL_VALUE_ITEM_SIZES.MEDIUM,
|
|
39
48
|
isLabelStrong: false,
|
|
40
49
|
valueColor: LABEL_VALUE_ITEM_VALUE_COLORS.NEUTRAL,
|
|
50
|
+
accessorySlot: 'accessory slot',
|
|
41
51
|
} as Args;
|
|
42
52
|
|
|
43
53
|
const argTypes = {
|
|
@@ -56,6 +66,9 @@ const argTypes = {
|
|
|
56
66
|
isLabelStrong: {
|
|
57
67
|
control: 'boolean',
|
|
58
68
|
},
|
|
69
|
+
accessorySlot: {
|
|
70
|
+
control: 'text',
|
|
71
|
+
},
|
|
59
72
|
} as ArgTypes;
|
|
60
73
|
|
|
61
74
|
Interactive.argTypes = argTypes;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<div class="ds-labelValueItem__label" :class="{ '-ds-strong': isLabelStrong }">
|
|
4
4
|
{{ label }}
|
|
5
5
|
</div>
|
|
6
|
+
<slot name="accessory" />
|
|
6
7
|
<ds-icon
|
|
7
8
|
v-if="state === LABEL_VALUE_ITEM_STATES.LOADING"
|
|
8
9
|
:icon="ICONS.FAD_SPINNER_THIRD"
|
|
@@ -98,4 +99,8 @@ const {
|
|
|
98
99
|
isLabelStrong?: boolean;
|
|
99
100
|
valueColor?: LabelValueItemValueColor;
|
|
100
101
|
}>();
|
|
102
|
+
|
|
103
|
+
defineSlots<{
|
|
104
|
+
accessory?: () => any;
|
|
105
|
+
}>();
|
|
101
106
|
</script>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { ComponentProps } from 'vue-component-type-helpers';
|
|
4
|
+
import LoadingBar from './LoadingBar.vue';
|
|
5
|
+
import DsContainerRibbon from '../ContainerRibbon/ContainerRibbon.vue';
|
|
6
|
+
import { CONTAINER_RIBBON_COLORS, CONTAINER_RIBBON_SIZES } from '../ContainerRibbon';
|
|
7
|
+
import { LOADING_BAR_COLORS, LOADING_BAR_SIZES } from './LoadingBar.consts';
|
|
8
|
+
|
|
9
|
+
const setup = (props: ComponentProps<typeof LoadingBar>) => mount(LoadingBar, { props });
|
|
10
|
+
|
|
11
|
+
const progressStyle = (wrapper: ReturnType<typeof setup>) =>
|
|
12
|
+
wrapper.find('.ds-loadingBar__progress').attributes('style') ?? '';
|
|
13
|
+
|
|
14
|
+
// jsdom neither runs animations nor applies the component's `<style scoped>` block, so nothing here
|
|
15
|
+
// observes the fill itself — that needs a real browser. What these do cover is the declarative wiring
|
|
16
|
+
// the animation depends on: the duration reaching the element, `'0'` opting out, and no JS being
|
|
17
|
+
// involved in starting it.
|
|
18
|
+
describe('LoadingBar', () => {
|
|
19
|
+
it('should render the track and the progress element', () => {
|
|
20
|
+
const wrapper = setup({ time: '5' });
|
|
21
|
+
|
|
22
|
+
expect(wrapper.find('.ds-loadingBar').exists()).toBe(true);
|
|
23
|
+
expect(wrapper.find('.ds-loadingBar__progress').exists()).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should set the animation duration from the time prop', () => {
|
|
27
|
+
const wrapper = setup({ time: '5' });
|
|
28
|
+
|
|
29
|
+
expect(progressStyle(wrapper)).toContain('animation-duration: 5s');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should not set a width on the progress element when animating', () => {
|
|
33
|
+
// the keyframes own the width — an inline one would override the animated value
|
|
34
|
+
const wrapper = setup({ time: '5' });
|
|
35
|
+
|
|
36
|
+
expect(progressStyle(wrapper)).not.toContain('width');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should show a full bar with no animation for time 0', () => {
|
|
40
|
+
const wrapper = setup({ time: '0' });
|
|
41
|
+
|
|
42
|
+
expect(progressStyle(wrapper)).toContain('animation: none');
|
|
43
|
+
expect(progressStyle(wrapper)).toContain('width: 100%');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// The fill used to be started from `onMounted`, which raced the browser's first style pass and
|
|
47
|
+
// left the bar snapped to full when it lost. The rendered output must be complete on mount.
|
|
48
|
+
it('should render its final styles synchronously on mount', () => {
|
|
49
|
+
const wrapper = setup({ time: '5' });
|
|
50
|
+
const styleOnMount = progressStyle(wrapper);
|
|
51
|
+
|
|
52
|
+
expect(styleOnMount).toBe(progressStyle(wrapper));
|
|
53
|
+
expect(styleOnMount).toContain('animation-duration: 5s');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it.each([
|
|
57
|
+
{ size: LOADING_BAR_SIZES.SMALL, expected: CONTAINER_RIBBON_SIZES.SMALL },
|
|
58
|
+
{ size: LOADING_BAR_SIZES.MEDIUM, expected: CONTAINER_RIBBON_SIZES.MEDIUM },
|
|
59
|
+
{ size: LOADING_BAR_SIZES.LARGE, expected: CONTAINER_RIBBON_SIZES.LARGE },
|
|
60
|
+
])('should pass ribbon size $expected for size $size', ({ size, expected }) => {
|
|
61
|
+
const wrapper = setup({ size, time: '5' });
|
|
62
|
+
|
|
63
|
+
expect(wrapper.findComponent(DsContainerRibbon).props('size')).toBe(expected);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it.each([
|
|
67
|
+
{
|
|
68
|
+
color: LOADING_BAR_COLORS.NEUTRAL_HEAVY,
|
|
69
|
+
expected: CONTAINER_RIBBON_COLORS.NEUTRAL_HEAVY,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
color: LOADING_BAR_COLORS.NEUTRAL_STRONG,
|
|
73
|
+
expected: CONTAINER_RIBBON_COLORS.NEUTRAL_STRONG,
|
|
74
|
+
},
|
|
75
|
+
{ color: LOADING_BAR_COLORS.SUCCESS, expected: CONTAINER_RIBBON_COLORS.SUCCESS },
|
|
76
|
+
{ color: LOADING_BAR_COLORS.WARNING, expected: CONTAINER_RIBBON_COLORS.WARNING },
|
|
77
|
+
{ color: LOADING_BAR_COLORS.DANGER, expected: CONTAINER_RIBBON_COLORS.DANGER },
|
|
78
|
+
{ color: LOADING_BAR_COLORS.INFO, expected: CONTAINER_RIBBON_COLORS.INFO },
|
|
79
|
+
])('should pass ribbon color $expected for color $color', ({ color, expected }) => {
|
|
80
|
+
const wrapper = setup({ color, time: '5' });
|
|
81
|
+
|
|
82
|
+
expect(wrapper.findComponent(DsContainerRibbon).props('color')).toBe(expected);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -13,12 +13,33 @@
|
|
|
13
13
|
<style scoped lang="scss">
|
|
14
14
|
@import '../../../styles/settings/colors/tokens';
|
|
15
15
|
|
|
16
|
+
// A keyframe animation carries its own start value, so — unlike a transition — it needs no resolved
|
|
17
|
+
// *before-change* style and runs from the first style pass the element gets. That is what makes it
|
|
18
|
+
// the right tool here: the bar is mounted and animated in the same breath, and the previous
|
|
19
|
+
// `width: 0 -> 100%` transition (flipped from JS just after mount) could lose the race against the
|
|
20
|
+
// browser's first style pass. When it did, the freshly inserted node went straight to its final
|
|
21
|
+
// width with nothing to interpolate from, and the bar snapped to full instead of filling.
|
|
22
|
+
// Only the duration is set from JS, inline. The animation *name* has to stay in this block so that
|
|
23
|
+
// `scoped` rewrites it together with the keyframes — Vue does not rewrite inline styles.
|
|
24
|
+
@keyframes ds-loading-bar-fill {
|
|
25
|
+
from {
|
|
26
|
+
width: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
to {
|
|
30
|
+
width: 100%;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
.ds-loadingBar {
|
|
17
35
|
background-color: $color-neutral-background;
|
|
18
36
|
display: flex;
|
|
19
37
|
width: 100%;
|
|
20
38
|
|
|
21
39
|
&__progress {
|
|
40
|
+
animation-fill-mode: forwards;
|
|
41
|
+
animation-name: ds-loading-bar-fill;
|
|
42
|
+
animation-timing-function: linear;
|
|
22
43
|
overflow: hidden;
|
|
23
44
|
width: 0;
|
|
24
45
|
}
|
|
@@ -26,7 +47,7 @@
|
|
|
26
47
|
</style>
|
|
27
48
|
|
|
28
49
|
<script setup lang="ts">
|
|
29
|
-
import { computed
|
|
50
|
+
import { computed } from 'vue';
|
|
30
51
|
import DsContainerRibbon from '../ContainerRibbon/ContainerRibbon.vue';
|
|
31
52
|
import {
|
|
32
53
|
CONTAINER_RIBBON_COLORS,
|
|
@@ -52,8 +73,6 @@ const props = withDefaults(
|
|
|
52
73
|
},
|
|
53
74
|
);
|
|
54
75
|
|
|
55
|
-
const width = ref(0);
|
|
56
|
-
|
|
57
76
|
const ribbonSize = computed(() => {
|
|
58
77
|
const sizeMap = {
|
|
59
78
|
[LOADING_BAR_SIZES.SMALL]: CONTAINER_RIBBON_SIZES.SMALL,
|
|
@@ -75,20 +94,12 @@ const ribbonColor = computed(() => {
|
|
|
75
94
|
return colorMap[props.color] || CONTAINER_RIBBON_COLORS.NEUTRAL_HEAVY;
|
|
76
95
|
});
|
|
77
96
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
// without postponing the width change, the transition won't work,
|
|
89
|
-
// and the loading bar is 100% width right away
|
|
90
|
-
setTimeout(() => {
|
|
91
|
-
width.value = 100;
|
|
92
|
-
}, 0);
|
|
93
|
-
});
|
|
97
|
+
// The fill is driven entirely by the `ds-loading-bar-fill` keyframes in this component's styles, so
|
|
98
|
+
// there is nothing to start from JS — no mounted hook, no timer, no frame juggling. `'0'` opts out
|
|
99
|
+
// of the animation altogether and shows a full bar right away.
|
|
100
|
+
const loadingBarStyles = computed(() =>
|
|
101
|
+
props.time === '0'
|
|
102
|
+
? { animation: 'none', width: '100%' }
|
|
103
|
+
: { animationDuration: `${props.time}s` },
|
|
104
|
+
);
|
|
94
105
|
</script>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { h } from 'vue';
|
|
4
|
+
import { ComponentProps } from 'vue-component-type-helpers';
|
|
5
|
+
import SelectListItem from './SelectListItem.vue';
|
|
6
|
+
import { SELECT_LIST_ITEM_SIZES, SELECT_LIST_ITEM_STATES } from './SelectListItem.consts';
|
|
7
|
+
import Icon, { ICONS } from '../../Icons/Icon';
|
|
8
|
+
|
|
9
|
+
const setup = (props: ComponentProps<typeof SelectListItem>, slots = {}) =>
|
|
10
|
+
mount(SelectListItem, { props, slots });
|
|
11
|
+
|
|
12
|
+
describe('SelectListItem', () => {
|
|
13
|
+
it('should render the label', () => {
|
|
14
|
+
const wrapper = setup({ label: 'Basic listItem' });
|
|
15
|
+
|
|
16
|
+
expect(wrapper.find('.ds-selectListItem').exists()).toBe(true);
|
|
17
|
+
expect(wrapper.find('.ds-selectListItem__text').text()).toBe('Basic listItem');
|
|
18
|
+
expect(wrapper.find('.ds-selectListItem').attributes('title')).toBe('Basic listItem');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should render the eyebrow text when provided', () => {
|
|
22
|
+
const wrapper = setup({ label: 'Label', eyebrowText: 'Eyebrow' });
|
|
23
|
+
|
|
24
|
+
expect(wrapper.find('.ds-selectListItem__eyebrowText').text()).toBe('Eyebrow');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.each(Object.values(SELECT_LIST_ITEM_SIZES))('should render in size: %s', (size) => {
|
|
28
|
+
const wrapper = setup({ label: 'Label', size });
|
|
29
|
+
|
|
30
|
+
expect(wrapper.find('.ds-selectListItem').classes()).toContain(`-ds-${size}`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.each([
|
|
34
|
+
{ state: SELECT_LIST_ITEM_STATES.DISABLED, expectedClass: '-ds-disabled' },
|
|
35
|
+
{ state: SELECT_LIST_ITEM_STATES.LOADING, expectedClass: '-ds-loading' },
|
|
36
|
+
])('should apply $expectedClass for state $state', ({ state, expectedClass }) => {
|
|
37
|
+
const wrapper = setup({ label: 'Label', state });
|
|
38
|
+
|
|
39
|
+
expect(wrapper.find('.ds-selectListItem').classes()).toContain(expectedClass);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should render the check icon when selected', () => {
|
|
43
|
+
const wrapper = setup({ label: 'Label', isSelected: true });
|
|
44
|
+
|
|
45
|
+
expect(
|
|
46
|
+
wrapper.findComponent<typeof Icon>('.ds-selectListItem__iconRight').props().icon,
|
|
47
|
+
).toEqual(ICONS.FA_CHECK_SOLID);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should not render the check icon when not selected', () => {
|
|
51
|
+
const wrapper = setup({ label: 'Label' });
|
|
52
|
+
|
|
53
|
+
expect(wrapper.find('.ds-selectListItem__iconRight').exists()).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('text slot', () => {
|
|
57
|
+
it('should fall back to the label when the slot is not provided', () => {
|
|
58
|
+
const wrapper = setup({ label: 'Fallback label' });
|
|
59
|
+
|
|
60
|
+
expect(wrapper.find('.ds-selectListItem__text').text()).toBe('Fallback label');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should override the rendered label when the slot is provided', () => {
|
|
64
|
+
const wrapper = setup(
|
|
65
|
+
{ label: 'Prop label' },
|
|
66
|
+
{ text: () => h('span', { id: 'custom-text' }, 'Slot label') },
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const text = wrapper.find('.ds-selectListItem__text');
|
|
70
|
+
|
|
71
|
+
expect(text.find('#custom-text').exists()).toBe(true);
|
|
72
|
+
expect(text.text()).toBe('Slot label');
|
|
73
|
+
// `label` is still used for the title attribute and typeahead fallback.
|
|
74
|
+
expect(wrapper.find('.ds-selectListItem').attributes('title')).toBe('Prop label');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
:class="{ '-ds-uppercase': isEyebrowTextUppercase }"
|
|
28
28
|
>{{ eyebrowText }}</span
|
|
29
29
|
>
|
|
30
|
-
<span class="ds-selectListItem__text"
|
|
30
|
+
<span class="ds-selectListItem__text"
|
|
31
|
+
><slot name="text">{{ label }}</slot></span
|
|
32
|
+
>
|
|
31
33
|
</span>
|
|
32
34
|
|
|
33
35
|
<slot name="metadata" />
|
|
@@ -197,6 +199,7 @@ const {
|
|
|
197
199
|
const slots = defineSlots<{
|
|
198
200
|
accessory?: () => any;
|
|
199
201
|
metadata?: () => any;
|
|
202
|
+
text?: () => any;
|
|
200
203
|
}>();
|
|
201
204
|
|
|
202
205
|
const isLoading = computed(() => state === SELECT_LIST_ITEM_STATES.LOADING);
|
|
@@ -3,6 +3,7 @@ import { Value } from '../../utils/type.utils';
|
|
|
3
3
|
export const TOAST_SIZES = {
|
|
4
4
|
SMALL: 'small',
|
|
5
5
|
MEDIUM: 'medium',
|
|
6
|
+
LARGE: 'large',
|
|
6
7
|
} as const;
|
|
7
8
|
|
|
8
9
|
export type ToastSizes = Value<typeof TOAST_SIZES>;
|
|
@@ -14,7 +15,7 @@ export const TOAST_COLORS = {
|
|
|
14
15
|
WARNING: 'warning',
|
|
15
16
|
DANGER: 'danger',
|
|
16
17
|
INFO: 'info',
|
|
17
|
-
};
|
|
18
|
+
} as const;
|
|
18
19
|
|
|
19
20
|
export type ToastColors = Value<typeof TOAST_COLORS>;
|
|
20
21
|
|
|
@@ -22,6 +23,7 @@ export const TOAST_POSITIONS = {
|
|
|
22
23
|
LEFT: 'left',
|
|
23
24
|
CENTER: 'center',
|
|
24
25
|
RIGHT: 'right',
|
|
25
|
-
|
|
26
|
+
NONE: 'none',
|
|
27
|
+
} as const;
|
|
26
28
|
|
|
27
29
|
export type ToastPositions = Value<typeof TOAST_POSITIONS>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { h } from 'vue';
|
|
4
|
+
import Toast from './Toast.vue';
|
|
5
|
+
import { TOAST_COLORS, TOAST_POSITIONS, TOAST_SIZES } from './Toast.consts';
|
|
6
|
+
import DsButton, { BUTTON_SIZES } from '../Buttons/Button';
|
|
7
|
+
import DsIconButton, { ICON_BUTTON_COLORS, ICON_BUTTON_SIZES } from '../Buttons/IconButton';
|
|
8
|
+
import { ICONS } from '../Icons/Icon';
|
|
9
|
+
import { ComponentProps } from 'vue-component-type-helpers';
|
|
10
|
+
|
|
11
|
+
const setup = (props: ComponentProps<typeof Toast> = {}) =>
|
|
12
|
+
mount(Toast, {
|
|
13
|
+
props,
|
|
14
|
+
slots: {
|
|
15
|
+
content: () => h('span', 'Wpłynąłem na suchego przestwór oceanu'),
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('Toast', () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.useRealTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should render the content slot', () => {
|
|
25
|
+
const wrapper = setup();
|
|
26
|
+
|
|
27
|
+
expect(wrapper.find('.ds-toast__content').text()).toBe(
|
|
28
|
+
'Wpłynąłem na suchego przestwór oceanu',
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it.each(Object.values(TOAST_POSITIONS))('should render in position: %s', (position) => {
|
|
33
|
+
const wrapper = setup({ position });
|
|
34
|
+
|
|
35
|
+
expect(wrapper.find('.ds-toast').classes()).toContain(`-ds-position-${position}`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should update the position modifier when the prop changes', async () => {
|
|
39
|
+
const wrapper = setup({ position: TOAST_POSITIONS.CENTER });
|
|
40
|
+
|
|
41
|
+
await wrapper.setProps({ position: TOAST_POSITIONS.NONE });
|
|
42
|
+
|
|
43
|
+
expect(wrapper.find('.ds-toast').classes()).toContain('-ds-position-none');
|
|
44
|
+
expect(wrapper.find('.ds-toast').classes()).not.toContain('-ds-position-center');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it.each(Object.values(TOAST_SIZES))('should render in size: %s', (size) => {
|
|
48
|
+
const wrapper = setup({ size });
|
|
49
|
+
|
|
50
|
+
expect(wrapper.find('.ds-toast').classes()).toContain(`-ds-size-${size}`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.each(Object.values(TOAST_COLORS))('should render in color: %s', (color) => {
|
|
54
|
+
const wrapper = setup({ color });
|
|
55
|
+
|
|
56
|
+
expect(wrapper.find('.ds-toast').classes()).toContain(`-ds-color-${color}`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should render the title when provided', () => {
|
|
60
|
+
const wrapper = setup({ title: 'Tytuł' });
|
|
61
|
+
|
|
62
|
+
expect(wrapper.find('.ds-toast__title').text()).toBe('Tytuł');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should not render the title when not provided', () => {
|
|
66
|
+
const wrapper = setup();
|
|
67
|
+
|
|
68
|
+
expect(wrapper.find('.ds-toast__title').exists()).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it.each([
|
|
72
|
+
{ size: TOAST_SIZES.SMALL, expectedButtonSize: BUTTON_SIZES.SMALL },
|
|
73
|
+
{ size: TOAST_SIZES.MEDIUM, expectedButtonSize: BUTTON_SIZES.MEDIUM },
|
|
74
|
+
{ size: TOAST_SIZES.LARGE, expectedButtonSize: BUTTON_SIZES.MEDIUM },
|
|
75
|
+
])(
|
|
76
|
+
'should render $expectedButtonSize footer buttons in size $size',
|
|
77
|
+
({ size, expectedButtonSize }) => {
|
|
78
|
+
const wrapper = setup({
|
|
79
|
+
size,
|
|
80
|
+
footerPrimaryButtonText: 'primary',
|
|
81
|
+
footerSecondaryButtonText: 'secondary',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const buttonSizes = wrapper
|
|
85
|
+
.findAllComponents(DsButton)
|
|
86
|
+
.map((button) => button.props('size'));
|
|
87
|
+
|
|
88
|
+
expect(buttonSizes).toEqual([expectedButtonSize, expectedButtonSize]);
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
it('should not render the footer without button texts', () => {
|
|
93
|
+
const wrapper = setup();
|
|
94
|
+
|
|
95
|
+
expect(wrapper.find('.ds-toast__footerButtons').exists()).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should emit primary-button-click on the primary button click', async () => {
|
|
99
|
+
const wrapper = setup({ footerPrimaryButtonText: 'primary' });
|
|
100
|
+
|
|
101
|
+
await wrapper.findComponent(DsButton).trigger('click');
|
|
102
|
+
|
|
103
|
+
expect(wrapper.emitted('primary-button-click')).toHaveLength(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should emit secondary-button-click on the secondary button click', async () => {
|
|
107
|
+
const wrapper = setup({ footerSecondaryButtonText: 'secondary' });
|
|
108
|
+
|
|
109
|
+
await wrapper.findComponent(DsButton).trigger('click');
|
|
110
|
+
|
|
111
|
+
expect(wrapper.emitted('secondary-button-click')).toHaveLength(1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should emit close after the disappearing timeout', () => {
|
|
115
|
+
vi.useFakeTimers();
|
|
116
|
+
const wrapper = setup({ isDisappearing: true, disappearingTimeout: '3' });
|
|
117
|
+
|
|
118
|
+
vi.advanceTimersByTime(3 * 1000 + 100);
|
|
119
|
+
|
|
120
|
+
expect(wrapper.emitted('close')).toHaveLength(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should not emit close when it is not disappearing', () => {
|
|
124
|
+
vi.useFakeTimers();
|
|
125
|
+
const wrapper = setup({ isDisappearing: false, disappearingTimeout: '3' });
|
|
126
|
+
|
|
127
|
+
vi.advanceTimersByTime(3 * 1000 + 100);
|
|
128
|
+
|
|
129
|
+
expect(wrapper.emitted('close')).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should not render the close button by default', () => {
|
|
133
|
+
const wrapper = setup();
|
|
134
|
+
|
|
135
|
+
expect(wrapper.find('.ds-toast__close').exists()).toBe(false);
|
|
136
|
+
expect(wrapper.find('.ds-toast').classes()).not.toContain('-ds-closable');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should render the close button when it is closable', () => {
|
|
140
|
+
const wrapper = setup({ isClosable: true });
|
|
141
|
+
|
|
142
|
+
expect(wrapper.find('.ds-toast__close').exists()).toBe(true);
|
|
143
|
+
expect(wrapper.find('.ds-toast').classes()).toContain('-ds-closable');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should render the close button as a small neutral-weak xmark', () => {
|
|
147
|
+
const wrapper = setup({ isClosable: true });
|
|
148
|
+
|
|
149
|
+
const closeButton = wrapper.findComponent(DsIconButton);
|
|
150
|
+
|
|
151
|
+
expect(closeButton.props('icon')).toBe(ICONS.FA_XMARK);
|
|
152
|
+
expect(closeButton.props('size')).toBe(ICON_BUTTON_SIZES.SMALL);
|
|
153
|
+
expect(closeButton.props('color')).toBe(ICON_BUTTON_COLORS.NEUTRAL_WEAK);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should emit close on the close button click', async () => {
|
|
157
|
+
const wrapper = setup({ isClosable: true });
|
|
158
|
+
|
|
159
|
+
await wrapper.find('.ds-toast__close > *').trigger('click');
|
|
160
|
+
|
|
161
|
+
expect(wrapper.emitted('close')).toHaveLength(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should emit close only once when the close button pre-empts the disappearing timeout', async () => {
|
|
165
|
+
vi.useFakeTimers();
|
|
166
|
+
const wrapper = setup({
|
|
167
|
+
isClosable: true,
|
|
168
|
+
isDisappearing: true,
|
|
169
|
+
disappearingTimeout: '3',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await wrapper.find('.ds-toast__close > *').trigger('click');
|
|
173
|
+
vi.advanceTimersByTime(3 * 1000 + 100);
|
|
174
|
+
|
|
175
|
+
expect(wrapper.emitted('close')).toHaveLength(1);
|
|
176
|
+
});
|
|
177
|
+
});
|