vueless 1.3.6-beta.0 → 1.3.6-beta.10
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/components.d.ts +2 -0
- package/components.ts +2 -0
- package/constants.d.ts +2 -0
- package/constants.js +2 -0
- package/index.d.ts +10 -1
- package/index.ts +10 -1
- package/package.json +2 -2
- package/types.ts +2 -0
- package/ui.button/UButton.vue +1 -1
- package/ui.button/storybook/stories.ts +2 -2
- package/ui.container-card/storybook/stories.ts +2 -2
- package/ui.container-drawer/UDrawer.vue +2 -2
- package/ui.container-drawer/storybook/stories.ts +2 -2
- package/ui.container-grid/UGrid.vue +39 -0
- package/ui.container-grid/config.ts +123 -0
- package/ui.container-grid/constants.ts +5 -0
- package/ui.container-grid/storybook/docs.mdx +17 -0
- package/ui.container-grid/storybook/stories.ts +246 -0
- package/ui.container-grid/tests/UGrid.test.ts +297 -0
- package/ui.container-grid/types.ts +91 -0
- package/ui.container-modal/storybook/stories.ts +2 -2
- package/ui.container-modal-confirm/storybook/stories.ts +2 -2
- package/ui.container-page/storybook/stories.ts +3 -3
- package/ui.form-calendar/tests/UCalendar.test.ts +113 -0
- package/ui.form-date-picker-range/UDatePickerRangeInputs.vue +5 -1
- package/ui.form-date-picker-range/tests/UDatePickerRange.test.ts +114 -0
- package/ui.form-date-picker-range/types.ts +1 -0
- package/ui.form-listbox/config.ts +1 -1
- package/ui.image-avatar/UAvatar.vue +55 -28
- package/ui.image-avatar/config.ts +18 -1
- package/ui.image-avatar/storybook/docs.mdx +16 -1
- package/ui.image-avatar/storybook/stories.ts +17 -3
- package/ui.image-avatar/tests/UAvatar.test.ts +35 -7
- package/ui.image-avatar/types.ts +13 -0
- package/ui.image-avatar-group/UAvatarGroup.vue +87 -0
- package/ui.image-avatar-group/config.ts +11 -0
- package/ui.image-avatar-group/constants.ts +5 -0
- package/ui.image-avatar-group/storybook/docs.mdx +16 -0
- package/ui.image-avatar-group/storybook/stories.ts +147 -0
- package/ui.image-avatar-group/tests/UAvatarGroup.test.ts +141 -0
- package/ui.image-avatar-group/types.ts +51 -0
- package/ui.navigation-pagination/storybook/stories.ts +2 -2
- package/ui.navigation-tab/tests/UTab.test.ts +2 -3
- package/ui.navigation-tabs/UTabs.vue +44 -1
- package/ui.navigation-tabs/storybook/stories.ts +33 -0
- package/ui.navigation-tabs/tests/UTabs.test.ts +88 -0
- package/ui.text-block/config.ts +1 -0
- package/ui.text-block/storybook/stories.ts +2 -2
- package/ui.text-notify/UNotify.vue +31 -8
- package/ui.text-notify/config.ts +1 -1
- package/ui.text-notify/tests/UNotify.test.ts +22 -7
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getArgs,
|
|
3
|
+
getArgTypes,
|
|
4
|
+
getSlotNames,
|
|
5
|
+
getSlotsFragment,
|
|
6
|
+
getDocsDescription,
|
|
7
|
+
} from "../../utils/storybook";
|
|
8
|
+
|
|
9
|
+
import UAvatarGroup from "../../ui.image-avatar-group/UAvatarGroup.vue";
|
|
10
|
+
import UAvatar from "../../ui.image-avatar/UAvatar.vue";
|
|
11
|
+
import UCol from "../../ui.container-col/UCol.vue";
|
|
12
|
+
import ULink from "../../ui.button-link/ULink.vue";
|
|
13
|
+
|
|
14
|
+
import type { Meta, StoryFn } from "@storybook/vue3-vite";
|
|
15
|
+
import type { Props } from "../types.ts";
|
|
16
|
+
|
|
17
|
+
interface UAvatarGroupArgs extends Props {
|
|
18
|
+
slotTemplate?: string;
|
|
19
|
+
enum: "size" | "variant" | "rounded";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
id: "6030",
|
|
24
|
+
title: "Images & Icons / Avatar Group",
|
|
25
|
+
component: UAvatarGroup,
|
|
26
|
+
args: {
|
|
27
|
+
avatars: [
|
|
28
|
+
{ src: "https://i.pravatar.cc/300?img=1" },
|
|
29
|
+
{ src: "https://i.pravatar.cc/300?img=2" },
|
|
30
|
+
{ src: "https://i.pravatar.cc/300?img=3" },
|
|
31
|
+
{ src: "https://i.pravatar.cc/300?img=4" },
|
|
32
|
+
],
|
|
33
|
+
rounded: "full",
|
|
34
|
+
},
|
|
35
|
+
argTypes: {
|
|
36
|
+
...getArgTypes(UAvatarGroup.__name),
|
|
37
|
+
},
|
|
38
|
+
parameters: {
|
|
39
|
+
docs: {
|
|
40
|
+
...getDocsDescription(UAvatarGroup.__name),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
} as Meta;
|
|
44
|
+
|
|
45
|
+
const DefaultTemplate: StoryFn<UAvatarGroupArgs> = (args: UAvatarGroupArgs) => ({
|
|
46
|
+
components: { UAvatarGroup, UAvatar, ULink },
|
|
47
|
+
setup: () => ({
|
|
48
|
+
args,
|
|
49
|
+
slots: getSlotNames(UAvatarGroup.__name),
|
|
50
|
+
}),
|
|
51
|
+
template: `
|
|
52
|
+
<UAvatarGroup v-bind="args">
|
|
53
|
+
${args.slotTemplate || getSlotsFragment("")}
|
|
54
|
+
</UAvatarGroup>
|
|
55
|
+
`,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const EnumTemplate: StoryFn<UAvatarGroupArgs> = (args: UAvatarGroupArgs, { argTypes }) => ({
|
|
59
|
+
components: { UCol, UAvatarGroup, UAvatar },
|
|
60
|
+
setup: () => ({ args, argTypes, getArgs }),
|
|
61
|
+
template: `
|
|
62
|
+
<UCol>
|
|
63
|
+
<UAvatarGroup
|
|
64
|
+
v-for="option in argTypes?.[args.enum]?.options"
|
|
65
|
+
v-bind="getArgs(args, option)"
|
|
66
|
+
:key="option"
|
|
67
|
+
>
|
|
68
|
+
${args.slotTemplate}
|
|
69
|
+
</UAvatarGroup>
|
|
70
|
+
</UCol>
|
|
71
|
+
`,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const Default = DefaultTemplate.bind({});
|
|
75
|
+
Default.args = {};
|
|
76
|
+
|
|
77
|
+
export const Max = DefaultTemplate.bind({});
|
|
78
|
+
Max.args = { max: 2 };
|
|
79
|
+
Max.parameters = {
|
|
80
|
+
docs: {
|
|
81
|
+
description: {
|
|
82
|
+
story:
|
|
83
|
+
"When the number of avatars is greater than the max, the remaining count avatar is displayed.",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const Sizes = EnumTemplate.bind({});
|
|
89
|
+
Sizes.args = {
|
|
90
|
+
enum: "size",
|
|
91
|
+
slotTemplate: `
|
|
92
|
+
<template #remaining>
|
|
93
|
+
<UAvatar :label="option" />
|
|
94
|
+
</template>
|
|
95
|
+
`,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const Variants = EnumTemplate.bind({});
|
|
99
|
+
Variants.args = {
|
|
100
|
+
enum: "variant",
|
|
101
|
+
avatars: [{ label: "John Doe" }],
|
|
102
|
+
config: { avatar: "ring-0" },
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const AvatarConfig = DefaultTemplate.bind({});
|
|
106
|
+
AvatarConfig.args = {
|
|
107
|
+
avatars: [
|
|
108
|
+
{ src: "https://i.pravatar.cc/300?img=1", label: "John Doe", chip: { color: "primary" } },
|
|
109
|
+
{ color: "warning", placeholderIcon: "person" },
|
|
110
|
+
{
|
|
111
|
+
src: "https://i.pravatar.cc/300?img=9",
|
|
112
|
+
label: "Jane Smith",
|
|
113
|
+
color: "info",
|
|
114
|
+
chip: { color: "grayscale" },
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
AvatarConfig.parameters = {
|
|
119
|
+
docs: {
|
|
120
|
+
description: {
|
|
121
|
+
story:
|
|
122
|
+
// eslint-disable-next-line vue/max-len
|
|
123
|
+
"You can customize the `label`, `color`, `placeholderIcon` and `chip` of a specific avatar by passing the corresponding props to its object.",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const AvatarSlot = DefaultTemplate.bind({});
|
|
129
|
+
AvatarSlot.args = {
|
|
130
|
+
slotTemplate: `
|
|
131
|
+
<template #avatar-2="{ avatar }">
|
|
132
|
+
<UAvatar
|
|
133
|
+
:src="avatar.src"
|
|
134
|
+
class="ring-3 ring-primary"
|
|
135
|
+
/>
|
|
136
|
+
</template>
|
|
137
|
+
`,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const RemainingSlot = DefaultTemplate.bind({});
|
|
141
|
+
RemainingSlot.args = {
|
|
142
|
+
slotTemplate: `
|
|
143
|
+
<template #remaining="{ remainingCount }">
|
|
144
|
+
<ULink :label="'+' + remainingCount" size="lg" color="info" underlined />
|
|
145
|
+
</template>
|
|
146
|
+
`,
|
|
147
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
|
|
4
|
+
import UAvatarGroup from "../UAvatarGroup.vue";
|
|
5
|
+
import UAvatar from "../../ui.image-avatar/UAvatar.vue";
|
|
6
|
+
|
|
7
|
+
import type { Props } from "../types.ts";
|
|
8
|
+
|
|
9
|
+
describe("UAvatarGroup.vue", () => {
|
|
10
|
+
describe("Props", () => {
|
|
11
|
+
it("Size – applies the correct size to child avatars", async () => {
|
|
12
|
+
const sizes = ["3xs", "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl"];
|
|
13
|
+
|
|
14
|
+
sizes.forEach((size) => {
|
|
15
|
+
const component = mount(UAvatarGroup, {
|
|
16
|
+
props: {
|
|
17
|
+
size: size as Props["size"],
|
|
18
|
+
avatars: [{ label: "John Doe" }, { label: "Jane Smith" }],
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Check if avatars have the correct size (they should inherit from group)
|
|
23
|
+
const avatars = component.findAllComponents(UAvatar);
|
|
24
|
+
|
|
25
|
+
expect(avatars.length).toBeGreaterThan(0);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("Max – limits the number of avatars displayed based on max prop", async () => {
|
|
30
|
+
const component = mount(UAvatarGroup, {
|
|
31
|
+
props: {
|
|
32
|
+
max: 2,
|
|
33
|
+
avatars: [{ label: "John Doe" }, { label: "Jane Smith" }, { label: "Bob Johnson" }],
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Should have 2 visible avatars + 1 remaining avatar
|
|
38
|
+
const avatars = component.findAllComponents(UAvatar);
|
|
39
|
+
|
|
40
|
+
expect(avatars.length).toBe(3);
|
|
41
|
+
|
|
42
|
+
// The last avatar should be the remaining count avatar
|
|
43
|
+
const lastAvatar = avatars[avatars.length - 1];
|
|
44
|
+
|
|
45
|
+
expect(lastAvatar.text()).toBe("+1");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("Variant – applies the correct variant to child avatars", async () => {
|
|
49
|
+
const variants = ["solid", "outlined", "subtle", "soft"];
|
|
50
|
+
|
|
51
|
+
variants.forEach((variant) => {
|
|
52
|
+
const component = mount(UAvatarGroup, {
|
|
53
|
+
props: {
|
|
54
|
+
variant: variant as Props["variant"],
|
|
55
|
+
avatars: [{ label: "John Doe" }, { label: "Jane Smith" }],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const avatars = component.findAllComponents(UAvatar);
|
|
60
|
+
|
|
61
|
+
expect(avatars.length).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("Rounded – applies the correct rounded to child avatars", async () => {
|
|
66
|
+
const roundedValues = ["none", "sm", "md", "lg", "full"];
|
|
67
|
+
|
|
68
|
+
roundedValues.forEach((rounded) => {
|
|
69
|
+
const component = mount(UAvatarGroup, {
|
|
70
|
+
props: {
|
|
71
|
+
rounded: rounded as Props["rounded"],
|
|
72
|
+
avatars: [{ label: "John Doe" }, { label: "Jane Smith" }],
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const avatars = component.findAllComponents(UAvatar);
|
|
77
|
+
|
|
78
|
+
expect(avatars.length).toBeGreaterThan(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("Slots", () => {
|
|
84
|
+
it("Avatars – renders avatars from avatars prop", async () => {
|
|
85
|
+
const component = mount(UAvatarGroup, {
|
|
86
|
+
props: {
|
|
87
|
+
avatars: [{ label: "John Doe" }, { label: "Jane Smith" }],
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const avatars = component.findAllComponents(UAvatar);
|
|
92
|
+
|
|
93
|
+
expect(avatars.length).toBe(2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("Remaining – renders custom remaining slot", async () => {
|
|
97
|
+
const component = mount(UAvatarGroup, {
|
|
98
|
+
props: {
|
|
99
|
+
max: 1,
|
|
100
|
+
avatars: [{ label: "John Doe" }, { label: "Jane Smith" }],
|
|
101
|
+
},
|
|
102
|
+
slots: {
|
|
103
|
+
remaining: `
|
|
104
|
+
<template #remaining="{ remainingCount }">
|
|
105
|
+
<span class="custom-remaining">
|
|
106
|
+
Custom {{ remainingCount }}
|
|
107
|
+
</span>
|
|
108
|
+
</template>
|
|
109
|
+
`,
|
|
110
|
+
},
|
|
111
|
+
global: {
|
|
112
|
+
components: {
|
|
113
|
+
UAvatar,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const avatars = component.findAllComponents(UAvatar);
|
|
119
|
+
|
|
120
|
+
expect(avatars.length).toBe(2);
|
|
121
|
+
|
|
122
|
+
// Check if custom remaining slot content is rendered
|
|
123
|
+
const customRemaining = component.find(".custom-remaining");
|
|
124
|
+
|
|
125
|
+
expect(customRemaining.exists()).toBe(true);
|
|
126
|
+
expect(customRemaining.text()).toContain("Custom");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("Exposed refs", () => {
|
|
131
|
+
it("exposes avatarGroupRef", () => {
|
|
132
|
+
const component = mount(UAvatarGroup, {
|
|
133
|
+
props: {
|
|
134
|
+
avatars: [{ label: "John Doe" }],
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(component.vm.avatarGroupRef).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import defaultConfig from "./config";
|
|
2
|
+
|
|
3
|
+
import type { ComponentConfig } from "../types";
|
|
4
|
+
import type { ChipItem } from "../ui.image-avatar/types";
|
|
5
|
+
|
|
6
|
+
export type Config = typeof defaultConfig;
|
|
7
|
+
|
|
8
|
+
export interface AvatarItem {
|
|
9
|
+
src?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
color?: string;
|
|
12
|
+
placeholderIcon?: string;
|
|
13
|
+
chip?: ChipItem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Props {
|
|
17
|
+
/**
|
|
18
|
+
* Avatar items.
|
|
19
|
+
*/
|
|
20
|
+
avatars?: AvatarItem[];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Avatar group size.
|
|
24
|
+
*/
|
|
25
|
+
size?: "3xs" | "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Maximum number of avatars to display.
|
|
29
|
+
*/
|
|
30
|
+
max?: number;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Avatar variant.
|
|
34
|
+
*/
|
|
35
|
+
variant?: "solid" | "outlined" | "subtle" | "soft";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Avatar corner rounding.
|
|
39
|
+
*/
|
|
40
|
+
rounded?: "none" | "sm" | "md" | "lg" | "full";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Component config object.
|
|
44
|
+
*/
|
|
45
|
+
config?: ComponentConfig<Config>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Data-test attribute for automated testing.
|
|
49
|
+
*/
|
|
50
|
+
dataTest?: string | null;
|
|
51
|
+
}
|
|
@@ -85,8 +85,8 @@ Limit.parameters = {
|
|
|
85
85
|
},
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
-
export const
|
|
89
|
-
|
|
88
|
+
export const Variants = EnumTemplate.bind({});
|
|
89
|
+
Variants.args = { enum: "variant" };
|
|
90
90
|
|
|
91
91
|
export const Sizes = EnumTemplate.bind({});
|
|
92
92
|
Sizes.args = { enum: "size" };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { h } from "vue";
|
|
2
1
|
import { mount } from "@vue/test-utils";
|
|
3
2
|
import { describe, it, expect, vi } from "vitest";
|
|
4
3
|
|
|
@@ -386,7 +385,7 @@ describe("UTab.vue", () => {
|
|
|
386
385
|
value,
|
|
387
386
|
},
|
|
388
387
|
slots: {
|
|
389
|
-
left:
|
|
388
|
+
left: '<div :data-active="params.active" />',
|
|
390
389
|
},
|
|
391
390
|
global: {
|
|
392
391
|
provide: {
|
|
@@ -409,7 +408,7 @@ describe("UTab.vue", () => {
|
|
|
409
408
|
leftIcon,
|
|
410
409
|
},
|
|
411
410
|
slots: {
|
|
412
|
-
left:
|
|
411
|
+
left: '<div :data-icon-name="params.iconName" />',
|
|
413
412
|
},
|
|
414
413
|
global: {
|
|
415
414
|
provide: defaultProvide,
|
|
@@ -128,7 +128,50 @@ const {
|
|
|
128
128
|
:disabled="item.disabled"
|
|
129
129
|
v-bind="tabAttrs"
|
|
130
130
|
:data-test="getDataTest(`item-${index}`)"
|
|
131
|
-
|
|
131
|
+
>
|
|
132
|
+
<template #left="{ iconName, active }">
|
|
133
|
+
<!--
|
|
134
|
+
@slot Use it to add something before the tab label.
|
|
135
|
+
@binding {object} item
|
|
136
|
+
@binding {number} index
|
|
137
|
+
@binding {boolean} active
|
|
138
|
+
@binding {string} icon-name
|
|
139
|
+
-->
|
|
140
|
+
<slot name="left" :item="item" :index="index" :active="active" :icon-name="iconName" />
|
|
141
|
+
</template>
|
|
142
|
+
|
|
143
|
+
<template #label="{ label, iconName, active }">
|
|
144
|
+
<!--
|
|
145
|
+
@slot Use it to add something instead of the tab label.
|
|
146
|
+
@binding {object} item
|
|
147
|
+
@binding {number} index
|
|
148
|
+
@binding {string} label
|
|
149
|
+
@binding {boolean} active
|
|
150
|
+
@binding {string} icon-name
|
|
151
|
+
-->
|
|
152
|
+
<slot
|
|
153
|
+
name="label"
|
|
154
|
+
:item="item"
|
|
155
|
+
:index="index"
|
|
156
|
+
:label="label"
|
|
157
|
+
:active="active"
|
|
158
|
+
:icon-name="iconName"
|
|
159
|
+
>
|
|
160
|
+
{{ label }}
|
|
161
|
+
</slot>
|
|
162
|
+
</template>
|
|
163
|
+
|
|
164
|
+
<template #right="{ iconName, active }">
|
|
165
|
+
<!--
|
|
166
|
+
@slot Use it to add something after the tab label.
|
|
167
|
+
@binding {object} item
|
|
168
|
+
@binding {number} index
|
|
169
|
+
@binding {boolean} active
|
|
170
|
+
@binding {string} icon-name
|
|
171
|
+
-->
|
|
172
|
+
<slot name="right" :item="item" :index="index" :active="active" :icon-name="iconName" />
|
|
173
|
+
</template>
|
|
174
|
+
</UTab>
|
|
132
175
|
</slot>
|
|
133
176
|
</div>
|
|
134
177
|
|
|
@@ -10,6 +10,12 @@ import UTabs from "../../ui.navigation-tabs/UTabs.vue";
|
|
|
10
10
|
import URow from "../../ui.container-row/URow.vue";
|
|
11
11
|
import ULabel from "../../ui.form-label/ULabel.vue";
|
|
12
12
|
import UTab from "../../ui.navigation-tab/UTab.vue";
|
|
13
|
+
import UBadge from "../../ui.text-badge/UBadge.vue";
|
|
14
|
+
import UAvatar from "../../ui.image-avatar/UAvatar.vue";
|
|
15
|
+
import UChip from "../../ui.other-chip/UChip.vue";
|
|
16
|
+
import UText from "../../ui.text-block/UText.vue";
|
|
17
|
+
|
|
18
|
+
import johnDoe from "../../ui.navigation-tab/storybook/assets/john-doe.png";
|
|
13
19
|
|
|
14
20
|
import type { Meta, StoryFn } from "@storybook/vue3-vite";
|
|
15
21
|
import type { Props } from "../types";
|
|
@@ -109,6 +115,33 @@ export const DefaultSlot: StoryFn<UTabsArgs> = (args) => ({
|
|
|
109
115
|
`,
|
|
110
116
|
});
|
|
111
117
|
|
|
118
|
+
export const TabSlots: StoryFn<UTabsArgs> = (args) => ({
|
|
119
|
+
components: { UTabs, UBadge, UAvatar, UChip, UText },
|
|
120
|
+
setup: () => ({ args, johnDoe }),
|
|
121
|
+
template: `
|
|
122
|
+
<UTabs v-model="args.modelValue" v-bind="args">
|
|
123
|
+
<template #left="{ index }">
|
|
124
|
+
<UAvatar
|
|
125
|
+
v-if="index === 0"
|
|
126
|
+
:src="johnDoe"
|
|
127
|
+
size="3xs"
|
|
128
|
+
rounded="full"
|
|
129
|
+
/>
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<template #label="{ label, index }">
|
|
133
|
+
<UChip v-if="index === 1" size="sm">
|
|
134
|
+
<UText :label="label" color="primary" class="mr-1.5" />
|
|
135
|
+
</UChip>
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<template #right="{ index }">
|
|
139
|
+
<UBadge v-if="index === 2" label="New!" size="sm" />
|
|
140
|
+
</template>
|
|
141
|
+
</UTabs>
|
|
142
|
+
`,
|
|
143
|
+
});
|
|
144
|
+
|
|
112
145
|
export const PrevNextSlots: StoryFn<UTabsArgs> = (args) => ({
|
|
113
146
|
components: { UTabs },
|
|
114
147
|
setup: () => ({ args, getOptionsArray }),
|
|
@@ -284,6 +284,94 @@ describe("UTabs.vue", () => {
|
|
|
284
284
|
expect(component.find(`.${slotClass}`).exists()).toBe(true);
|
|
285
285
|
expect(component.find(`.${slotClass}`).text()).toBe(slotContent);
|
|
286
286
|
});
|
|
287
|
+
|
|
288
|
+
it("Left – renders content from left slot for each tab", () => {
|
|
289
|
+
const slotText = "Left";
|
|
290
|
+
const slotClass = "left-content";
|
|
291
|
+
|
|
292
|
+
const component = mount(UTabs, {
|
|
293
|
+
props: {
|
|
294
|
+
options,
|
|
295
|
+
},
|
|
296
|
+
slots: {
|
|
297
|
+
left: `<span class="${slotClass}">${slotText}</span>`,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const leftContents = component.findAll(`.${slotClass}`);
|
|
302
|
+
|
|
303
|
+
expect(leftContents.length).toBe(options.length);
|
|
304
|
+
leftContents.forEach((left) => {
|
|
305
|
+
expect(left.text()).toBe(slotText);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("Label – renders content from label slot instead of default label", () => {
|
|
310
|
+
const testOptions: UTabsOption[] = [{ value: "tab1", label: "Tab 1" }];
|
|
311
|
+
const slotText = "Custom Label";
|
|
312
|
+
const slotClass = "label-content";
|
|
313
|
+
|
|
314
|
+
const component = mount(UTabs, {
|
|
315
|
+
props: {
|
|
316
|
+
options: testOptions,
|
|
317
|
+
},
|
|
318
|
+
slots: {
|
|
319
|
+
label: `<span class="${slotClass}">${slotText}</span>`,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(component.find(`.${slotClass}`).exists()).toBe(true);
|
|
324
|
+
expect(component.find(`.${slotClass}`).text()).toBe(slotText);
|
|
325
|
+
expect(component.text()).not.toContain(testOptions[0].label);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("Right – renders content from right slot", () => {
|
|
329
|
+
const slotText = "Right";
|
|
330
|
+
const slotClass = "right-content";
|
|
331
|
+
|
|
332
|
+
const component = mount(UTabs, {
|
|
333
|
+
props: {
|
|
334
|
+
options,
|
|
335
|
+
},
|
|
336
|
+
slots: {
|
|
337
|
+
right: `<span class="${slotClass}">${slotText}</span>`,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(component.findAll(`.${slotClass}`).length).toBe(options.length);
|
|
342
|
+
expect(component.find(`.${slotClass}`).text()).toBe(slotText);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("Slot – passes item, index and active state to slots", () => {
|
|
346
|
+
const modelValue = "tab2";
|
|
347
|
+
|
|
348
|
+
const component = mount(UTabs, {
|
|
349
|
+
props: {
|
|
350
|
+
options,
|
|
351
|
+
modelValue,
|
|
352
|
+
},
|
|
353
|
+
slots: {
|
|
354
|
+
left: `
|
|
355
|
+
<div
|
|
356
|
+
:data-value="params.item.value"
|
|
357
|
+
:data-index="params.index"
|
|
358
|
+
:data-active="params.active"
|
|
359
|
+
/>
|
|
360
|
+
`,
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
options.forEach((option, index) => {
|
|
365
|
+
const el = component.find(`[data-value="${option.value}"][data-index="${index}"]`);
|
|
366
|
+
|
|
367
|
+
expect(el.exists()).toBe(true);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const activeEl = component.find('[data-active="true"]');
|
|
371
|
+
|
|
372
|
+
expect(activeEl.exists()).toBe(true);
|
|
373
|
+
expect(activeEl.attributes("data-value")).toBe(modelValue);
|
|
374
|
+
});
|
|
287
375
|
});
|
|
288
376
|
|
|
289
377
|
describe("Events", () => {
|
package/ui.text-block/config.ts
CHANGED
|
@@ -90,8 +90,8 @@ Sizes.args = { enum: "size" };
|
|
|
90
90
|
export const Color = EnumTemplate.bind({});
|
|
91
91
|
Color.args = { enum: "color" };
|
|
92
92
|
|
|
93
|
-
export const
|
|
94
|
-
|
|
93
|
+
export const Variants = EnumTemplate.bind({});
|
|
94
|
+
Variants.args = { enum: "variant" };
|
|
95
95
|
|
|
96
96
|
export const Line: StoryFn<UTextArgs> = (args: UTextArgs) => ({
|
|
97
97
|
components: { UText, UCol },
|
|
@@ -44,7 +44,7 @@ onMounted(() => {
|
|
|
44
44
|
window.addEventListener("notifyEnd", onNotifyEnd);
|
|
45
45
|
window.addEventListener("notifyClearAll", onClearAll);
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
waitForPageElement();
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
onBeforeUnmount(() => {
|
|
@@ -86,12 +86,37 @@ function getOffsetWidth(selector: string): number {
|
|
|
86
86
|
return element ? (element as HTMLElement).offsetWidth : 0;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function waitForPageElement() {
|
|
90
|
+
const positionClasses = vuelessConfig.components?.UNotify?.positionClasses;
|
|
91
|
+
const pageClass = positionClasses?.page || config.value?.positionClasses?.page;
|
|
92
|
+
const maxWaitTime = 2000;
|
|
93
|
+
const startTime = Date.now();
|
|
94
|
+
|
|
95
|
+
function checkAndSetPosition() {
|
|
96
|
+
const element = document.querySelector(pageClass);
|
|
97
|
+
|
|
98
|
+
if (element) {
|
|
99
|
+
setPosition();
|
|
100
|
+
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (Date.now() - startTime < maxWaitTime) {
|
|
105
|
+
requestAnimationFrame(checkAndSetPosition);
|
|
106
|
+
} else {
|
|
107
|
+
setPosition();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
checkAndSetPosition();
|
|
112
|
+
}
|
|
113
|
+
|
|
89
114
|
function setPosition() {
|
|
90
115
|
const positionClasses = vuelessConfig.components?.UNotify?.positionClasses;
|
|
91
116
|
const pageClass = positionClasses?.page || config.value?.positionClasses?.page;
|
|
92
117
|
const asideClass = positionClasses?.aside || config.value?.positionClasses?.aside;
|
|
93
|
-
const pageWidth = getOffsetWidth(
|
|
94
|
-
const asideWidth = getOffsetWidth(
|
|
118
|
+
const pageWidth = getOffsetWidth(`.${pageClass}`);
|
|
119
|
+
const asideWidth = getOffsetWidth(`.${asideClass}`);
|
|
95
120
|
const notifyWidth = notificationsWrapperRef.value?.$el.offsetWidth || 0;
|
|
96
121
|
|
|
97
122
|
const styles: Record<string, string> = {
|
|
@@ -104,15 +129,13 @@ function setPosition() {
|
|
|
104
129
|
styles[props.yPosition] = "0px";
|
|
105
130
|
|
|
106
131
|
if (props.xPosition === NotificationPosition.Center) {
|
|
107
|
-
styles.left =
|
|
132
|
+
styles.left = pageWidth
|
|
133
|
+
? `${asideWidth + pageWidth / 2 - notifyWidth / 2}px`
|
|
134
|
+
: `calc(50% - ${notifyWidth / 2}px)`;
|
|
108
135
|
} else {
|
|
109
136
|
styles[props.xPosition] = "0px";
|
|
110
137
|
}
|
|
111
138
|
|
|
112
|
-
if (pageWidth && props.xPosition !== NotificationPosition.Right) {
|
|
113
|
-
styles.left = `${asideWidth + pageWidth / 2 - notifyWidth / 2}px`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
139
|
notifyPositionStyles.value = styles;
|
|
117
140
|
}
|
|
118
141
|
|
package/ui.text-notify/config.ts
CHANGED