vueless 1.3.6-beta.1 → 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/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
|
@@ -440,6 +440,37 @@ describe("UDatePickerRange.vue", () => {
|
|
|
440
440
|
|
|
441
441
|
expect(component.find("[vl-key='datepickerCalendar']").exists()).toBe(true);
|
|
442
442
|
});
|
|
443
|
+
|
|
444
|
+
it("Menu – allows selecting the same day for from and to in range mode", async () => {
|
|
445
|
+
const component = mount(UDatePickerRange, {
|
|
446
|
+
props: {
|
|
447
|
+
variant: "input",
|
|
448
|
+
modelValue: { from: null, to: null },
|
|
449
|
+
dateFormat: "Y-m-d",
|
|
450
|
+
"onUpdate:modelValue": (value: RangeDate) => {
|
|
451
|
+
component.setProps({ modelValue: value });
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const input = component.findComponent(UInput).get("input");
|
|
457
|
+
|
|
458
|
+
await input.trigger("focus");
|
|
459
|
+
|
|
460
|
+
const days = component.findAll("[vl-key='day']");
|
|
461
|
+
|
|
462
|
+
await days[10].trigger("click");
|
|
463
|
+
await days[10].trigger("click");
|
|
464
|
+
|
|
465
|
+
expect(component.emitted("update:modelValue")).toBeTruthy();
|
|
466
|
+
|
|
467
|
+
const emittedValues = component.emitted("update:modelValue")!;
|
|
468
|
+
const lastEmittedValue = emittedValues[emittedValues.length - 1][0] as RangeDate;
|
|
469
|
+
|
|
470
|
+
expect(lastEmittedValue.from).not.toBeNull();
|
|
471
|
+
expect(lastEmittedValue.to).not.toBeNull();
|
|
472
|
+
expect(lastEmittedValue.from).toBe(lastEmittedValue.to);
|
|
473
|
+
});
|
|
443
474
|
});
|
|
444
475
|
|
|
445
476
|
describe("Range Navigation", () => {
|
|
@@ -554,6 +585,89 @@ describe("UDatePickerRange.vue", () => {
|
|
|
554
585
|
});
|
|
555
586
|
});
|
|
556
587
|
|
|
588
|
+
describe("Events", () => {
|
|
589
|
+
it("ChangeRange – handles change-range event from calendar when dates are selected", async () => {
|
|
590
|
+
const component = mount(UDatePickerRange, {
|
|
591
|
+
props: {
|
|
592
|
+
variant: "input",
|
|
593
|
+
modelValue: { from: null, to: null },
|
|
594
|
+
dateFormat: "Y-m-d",
|
|
595
|
+
"onUpdate:modelValue": (value: RangeDate) => {
|
|
596
|
+
component.setProps({ modelValue: value });
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const input = component.findComponent(UInput).find("input");
|
|
602
|
+
|
|
603
|
+
await input.trigger("focus");
|
|
604
|
+
|
|
605
|
+
const days = component.findAll("[vl-key='day']");
|
|
606
|
+
|
|
607
|
+
expect(days.length).toBeGreaterThan(0);
|
|
608
|
+
|
|
609
|
+
await days[0].trigger("click");
|
|
610
|
+
await days[3].trigger("click");
|
|
611
|
+
|
|
612
|
+
expect(component.emitted("update:modelValue")).toBeTruthy();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("ChangeRange – handles change-range event from calendar when both dates are selected", async () => {
|
|
616
|
+
const component = mount(UDatePickerRange, {
|
|
617
|
+
props: {
|
|
618
|
+
variant: "input",
|
|
619
|
+
modelValue: { from: null, to: null },
|
|
620
|
+
dateFormat: "Y-m-d",
|
|
621
|
+
"onUpdate:modelValue": (value: RangeDate) => {
|
|
622
|
+
component.setProps({ modelValue: value });
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const input = component.findComponent(UInput).find("input");
|
|
628
|
+
|
|
629
|
+
await input.trigger("focus");
|
|
630
|
+
|
|
631
|
+
const days = component.findAll("[vl-key='day']");
|
|
632
|
+
|
|
633
|
+
await days[0].trigger("click");
|
|
634
|
+
await days[3].trigger("click");
|
|
635
|
+
|
|
636
|
+
expect(component.emitted("update:modelValue")).toBeTruthy();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("ChangeRange – handles change-range event from calendar when same date is selected twice", async () => {
|
|
640
|
+
const component = mount(UDatePickerRange, {
|
|
641
|
+
props: {
|
|
642
|
+
variant: "input",
|
|
643
|
+
modelValue: { from: null, to: null },
|
|
644
|
+
dateFormat: "Y-m-d",
|
|
645
|
+
"onUpdate:modelValue": (value: RangeDate) => {
|
|
646
|
+
component.setProps({ modelValue: value });
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const input = component.findComponent(UInput).find("input");
|
|
652
|
+
|
|
653
|
+
await input.trigger("focus");
|
|
654
|
+
|
|
655
|
+
const days = component.findAll("[vl-key='day']");
|
|
656
|
+
|
|
657
|
+
await days[10].trigger("click");
|
|
658
|
+
await days[10].trigger("click");
|
|
659
|
+
|
|
660
|
+
expect(component.emitted("update:modelValue")).toBeTruthy();
|
|
661
|
+
|
|
662
|
+
const emittedValues = component.emitted("update:modelValue")!;
|
|
663
|
+
const lastEmittedValue = emittedValues[emittedValues.length - 1][0] as RangeDate;
|
|
664
|
+
|
|
665
|
+
expect(lastEmittedValue.from).not.toBeNull();
|
|
666
|
+
expect(lastEmittedValue.to).not.toBeNull();
|
|
667
|
+
expect(lastEmittedValue.from).toBe(lastEmittedValue.to);
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
557
671
|
describe("Exposed Properties", () => {
|
|
558
672
|
it("Exposes wrapper element ref", () => {
|
|
559
673
|
const component = mount(UDatePickerRange, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default /*tw*/ {
|
|
2
2
|
wrapper: {
|
|
3
3
|
base: `
|
|
4
|
-
|
|
4
|
+
p-1 flex flex-col gap-1 w-auto absolute z-50 shadow-sm
|
|
5
5
|
rounded-medium border border-solid border-default bg-default
|
|
6
6
|
overflow-auto [-webkit-overflow-scrolling:touch]
|
|
7
7
|
focus:outline-hidden
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, useTemplateRef } from "vue";
|
|
2
|
+
import { computed, inject, useTemplateRef } from "vue";
|
|
3
3
|
|
|
4
4
|
import { useUI } from "../composables/useUI";
|
|
5
5
|
import { getDefaults } from "../utils/ui";
|
|
6
6
|
|
|
7
7
|
import UIcon from "../ui.image-icon/UIcon.vue";
|
|
8
|
+
import UChip from "../ui.other-chip/UChip.vue";
|
|
8
9
|
|
|
9
10
|
import { COMPONENT_NAME } from "./constants";
|
|
10
11
|
import defaultConfig from "./config";
|
|
@@ -16,6 +17,7 @@ defineOptions({ inheritAttrs: false });
|
|
|
16
17
|
const props = withDefaults(defineProps<Props>(), {
|
|
17
18
|
...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
|
|
18
19
|
label: "",
|
|
20
|
+
chip: () => ({}),
|
|
19
21
|
});
|
|
20
22
|
|
|
21
23
|
const emit = defineEmits([
|
|
@@ -25,6 +27,16 @@ const emit = defineEmits([
|
|
|
25
27
|
"click",
|
|
26
28
|
]);
|
|
27
29
|
|
|
30
|
+
const getAvatarGroupSize = inject<() => Props["size"]>("getAvatarGroupSize", () => undefined);
|
|
31
|
+
const getAvatarGroupVariant = inject<() => Props["variant"]>(
|
|
32
|
+
"getAvatarGroupVariant",
|
|
33
|
+
() => undefined,
|
|
34
|
+
);
|
|
35
|
+
const getAvatarGroupRounded = inject<() => Props["rounded"]>(
|
|
36
|
+
"getAvatarGroupRounded",
|
|
37
|
+
() => undefined,
|
|
38
|
+
);
|
|
39
|
+
|
|
28
40
|
const avatarRef = useTemplateRef<HTMLDivElement>("avatar");
|
|
29
41
|
|
|
30
42
|
const labelFirstLetters = computed(() => {
|
|
@@ -42,6 +54,8 @@ const backgroundImage = computed(() => {
|
|
|
42
54
|
return props.src ? `background-image: url(${props.src});` : "";
|
|
43
55
|
});
|
|
44
56
|
|
|
57
|
+
const hasChip = computed(() => props.chip != null && Boolean(Object.keys(props.chip).length));
|
|
58
|
+
|
|
45
59
|
function onClick(event: MouseEvent) {
|
|
46
60
|
emit("click", event);
|
|
47
61
|
}
|
|
@@ -65,38 +79,51 @@ defineExpose({
|
|
|
65
79
|
const mutatedProps = computed(() => ({
|
|
66
80
|
/* component state, not a props */
|
|
67
81
|
src: Boolean(props.src),
|
|
82
|
+
size: getAvatarGroupSize() || props.size,
|
|
83
|
+
variant: getAvatarGroupVariant() || props.variant,
|
|
84
|
+
rounded: getAvatarGroupRounded() || props.rounded,
|
|
68
85
|
}));
|
|
69
86
|
|
|
70
|
-
const { getDataTest, config, avatarAttrs, placeholderIconAttrs } =
|
|
71
|
-
defaultConfig,
|
|
72
|
-
mutatedProps,
|
|
73
|
-
);
|
|
87
|
+
const { getDataTest, config, avatarAttrs, placeholderIconAttrs, chipAttrs, hiddenAttrs } =
|
|
88
|
+
useUI<Config>(defaultConfig, mutatedProps);
|
|
74
89
|
</script>
|
|
75
90
|
|
|
76
91
|
<template>
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
:
|
|
80
|
-
:
|
|
81
|
-
|
|
82
|
-
:
|
|
83
|
-
|
|
92
|
+
<UChip
|
|
93
|
+
:icon="chip.icon"
|
|
94
|
+
:color="chip.color"
|
|
95
|
+
:inset="chip.inset"
|
|
96
|
+
:x-position="chip.xPosition"
|
|
97
|
+
:y-position="chip.yPosition"
|
|
98
|
+
v-bind="chipAttrs"
|
|
84
99
|
>
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
<div
|
|
101
|
+
ref="avatar"
|
|
102
|
+
:title="label"
|
|
103
|
+
:style="backgroundImage"
|
|
104
|
+
v-bind="avatarAttrs"
|
|
105
|
+
:data-test="getDataTest()"
|
|
106
|
+
@click="onClick"
|
|
107
|
+
>
|
|
108
|
+
<template v-if="!backgroundImage">
|
|
109
|
+
<!--
|
|
110
|
+
@slot Use it to add something instead of the avatar image placeholder.
|
|
111
|
+
@binding {string} icon-name
|
|
112
|
+
-->
|
|
113
|
+
<slot name="placeholder" :icon-name="placeholderIconName">
|
|
114
|
+
<template v-if="labelFirstLetters">{{ labelFirstLetters }}</template>
|
|
115
|
+
<UIcon
|
|
116
|
+
v-else
|
|
117
|
+
:size="size"
|
|
118
|
+
color="inherit"
|
|
119
|
+
:name="placeholderIconName"
|
|
120
|
+
v-bind="placeholderIconAttrs"
|
|
121
|
+
/>
|
|
122
|
+
</slot>
|
|
123
|
+
</template>
|
|
124
|
+
</div>
|
|
125
|
+
<template v-if="!hasChip" #chip>
|
|
126
|
+
<span v-bind="hiddenAttrs"> </span>
|
|
100
127
|
</template>
|
|
101
|
-
</
|
|
128
|
+
</UChip>
|
|
102
129
|
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default /*tw*/ {
|
|
2
2
|
avatar: {
|
|
3
3
|
base: `
|
|
4
|
-
flex items-center justify-center shrink-0 border
|
|
4
|
+
flex items-center justify-center shrink-0 border relative
|
|
5
5
|
text-{color} bg-{color}/10 bg-contain bg-center bg-no-repeat
|
|
6
6
|
`,
|
|
7
7
|
variants: {
|
|
@@ -34,6 +34,23 @@ export default /*tw*/ {
|
|
|
34
34
|
},
|
|
35
35
|
},
|
|
36
36
|
},
|
|
37
|
+
chip: {
|
|
38
|
+
base: "{UChip}",
|
|
39
|
+
defaults: {
|
|
40
|
+
size: {
|
|
41
|
+
"3xs": "xs",
|
|
42
|
+
"2xs": "xs",
|
|
43
|
+
xs: "sm",
|
|
44
|
+
sm: "md",
|
|
45
|
+
md: "md",
|
|
46
|
+
lg: "lg",
|
|
47
|
+
xl: "xl",
|
|
48
|
+
"2xl": "xl",
|
|
49
|
+
"3xl": "xl",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
hidden: "hidden",
|
|
37
54
|
placeholderIcon: "{UIcon}",
|
|
38
55
|
defaults: {
|
|
39
56
|
color: "grayscale",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Meta, Title, Subtitle, Description, Primary, Controls, Stories, Source } from "@storybook/addon-docs/blocks";
|
|
1
|
+
import { Meta, Title, Subtitle, Description, Primary, Controls, Stories, Source, Markdown } from "@storybook/addon-docs/blocks";
|
|
2
2
|
import { getSource } from "../../utils/storybook";
|
|
3
3
|
|
|
4
4
|
import * as stories from "./stories";
|
|
@@ -12,5 +12,20 @@ import defaultConfig from "../config?raw"
|
|
|
12
12
|
<Controls of={stories.Default} />
|
|
13
13
|
<Stories of={stories} />
|
|
14
14
|
|
|
15
|
+
## Chip Object Properties
|
|
16
|
+
Keys you may/have to provide to the component in a `chip` object.
|
|
17
|
+
|
|
18
|
+
<Markdown>
|
|
19
|
+
{`
|
|
20
|
+
| Key name | Description | Type |
|
|
21
|
+
| ---------------------- | ----------------------------------------------------- | ----------------------- |
|
|
22
|
+
| icon | Chip icon name | String |
|
|
23
|
+
| color | Chip color | String |
|
|
24
|
+
| xPosition | Chip x-axis position | String |
|
|
25
|
+
| yPosition | Chip y-axis position | String |
|
|
26
|
+
| inset | Display the chip inside the component | Boolean |
|
|
27
|
+
`}
|
|
28
|
+
</Markdown>
|
|
29
|
+
|
|
15
30
|
## Default config
|
|
16
31
|
<Source code={getSource(defaultConfig)} language="jsx" dark />
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
import UAvatar from "../../ui.image-avatar/UAvatar.vue";
|
|
10
10
|
import URow from "../../ui.container-row/URow.vue";
|
|
11
11
|
import ULoader from "../../ui.loader/ULoader.vue";
|
|
12
|
-
import tooltip from "../../v.tooltip/vTooltip";
|
|
13
12
|
|
|
14
13
|
import johnDoeImg from "./assets/john-doe.png";
|
|
15
14
|
|
|
@@ -22,7 +21,7 @@ interface UAvatarArgs extends Props {
|
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
export default {
|
|
25
|
-
id: "
|
|
24
|
+
id: "6020",
|
|
26
25
|
title: "Images & Icons / Avatar",
|
|
27
26
|
component: UAvatar,
|
|
28
27
|
args: {
|
|
@@ -51,7 +50,6 @@ const DefaultTemplate: StoryFn<UAvatarArgs> = (args: UAvatarArgs) => ({
|
|
|
51
50
|
|
|
52
51
|
const EnumTemplate: StoryFn<UAvatarArgs> = (args: UAvatarArgs, { argTypes }) => ({
|
|
53
52
|
components: { URow, UAvatar },
|
|
54
|
-
directives: { tooltip },
|
|
55
53
|
setup: () => ({ args, argTypes, getArgs }),
|
|
56
54
|
template: `
|
|
57
55
|
<URow>
|
|
@@ -106,6 +104,22 @@ PlaceholderIcon.args = {
|
|
|
106
104
|
placeholderIcon: "account_circle",
|
|
107
105
|
};
|
|
108
106
|
|
|
107
|
+
export const Chip = DefaultTemplate.bind({});
|
|
108
|
+
Chip.args = {
|
|
109
|
+
rounded: "full",
|
|
110
|
+
chip: {
|
|
111
|
+
color: "warning",
|
|
112
|
+
inset: true,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
Chip.parameters = {
|
|
116
|
+
docs: {
|
|
117
|
+
description: {
|
|
118
|
+
story: "For the full list of chip object properties, see the table below.",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
109
123
|
export const Variants = EnumTemplate.bind({});
|
|
110
124
|
Variants.args = { enum: "variant" };
|
|
111
125
|
|
|
@@ -49,7 +49,11 @@ describe("UAvatar.vue", () => {
|
|
|
49
49
|
},
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// Use the exposed ref to access the avatar div
|
|
53
|
+
const avatarRef = component.vm.avatarRef;
|
|
54
|
+
|
|
55
|
+
expect(avatarRef).toBeDefined();
|
|
56
|
+
expect(avatarRef?.getAttribute("style")).toContain(expectedStyle);
|
|
53
57
|
// When src is provided, the component should not render any text content
|
|
54
58
|
expect(component.text()).toBe(expectedText);
|
|
55
59
|
});
|
|
@@ -94,7 +98,11 @@ describe("UAvatar.vue", () => {
|
|
|
94
98
|
},
|
|
95
99
|
});
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
// Use the exposed ref to access the avatar div
|
|
102
|
+
const avatarRef = component.vm.avatarRef;
|
|
103
|
+
|
|
104
|
+
expect(avatarRef).toBeDefined();
|
|
105
|
+
expect(avatarRef?.className).toContain(classes);
|
|
98
106
|
});
|
|
99
107
|
});
|
|
100
108
|
|
|
@@ -119,7 +127,11 @@ describe("UAvatar.vue", () => {
|
|
|
119
127
|
},
|
|
120
128
|
});
|
|
121
129
|
|
|
122
|
-
|
|
130
|
+
// Use the exposed ref to access the avatar div
|
|
131
|
+
const avatarRef = component.vm.avatarRef;
|
|
132
|
+
|
|
133
|
+
expect(avatarRef).toBeDefined();
|
|
134
|
+
expect(avatarRef?.className).toContain(classes);
|
|
123
135
|
});
|
|
124
136
|
});
|
|
125
137
|
|
|
@@ -144,7 +156,11 @@ describe("UAvatar.vue", () => {
|
|
|
144
156
|
},
|
|
145
157
|
});
|
|
146
158
|
|
|
147
|
-
|
|
159
|
+
// Use the exposed ref to access the avatar div
|
|
160
|
+
const avatarRef = component.vm.avatarRef;
|
|
161
|
+
|
|
162
|
+
expect(avatarRef).toBeDefined();
|
|
163
|
+
expect(avatarRef?.className).toContain(color);
|
|
148
164
|
});
|
|
149
165
|
});
|
|
150
166
|
|
|
@@ -165,7 +181,11 @@ describe("UAvatar.vue", () => {
|
|
|
165
181
|
},
|
|
166
182
|
});
|
|
167
183
|
|
|
168
|
-
|
|
184
|
+
// Use the exposed ref to access the avatar div
|
|
185
|
+
const avatarRef = component.vm.avatarRef;
|
|
186
|
+
|
|
187
|
+
expect(avatarRef).toBeDefined();
|
|
188
|
+
expect(avatarRef?.className).toContain(classes);
|
|
169
189
|
});
|
|
170
190
|
});
|
|
171
191
|
|
|
@@ -179,7 +199,11 @@ describe("UAvatar.vue", () => {
|
|
|
179
199
|
},
|
|
180
200
|
});
|
|
181
201
|
|
|
182
|
-
|
|
202
|
+
// Use the exposed ref to access the avatar div
|
|
203
|
+
const avatarRef = component.vm.avatarRef;
|
|
204
|
+
|
|
205
|
+
expect(avatarRef).toBeDefined();
|
|
206
|
+
expect(avatarRef?.getAttribute("data-test")).toBe(dataTest);
|
|
183
207
|
});
|
|
184
208
|
});
|
|
185
209
|
|
|
@@ -215,7 +239,11 @@ describe("UAvatar.vue", () => {
|
|
|
215
239
|
},
|
|
216
240
|
});
|
|
217
241
|
|
|
218
|
-
|
|
242
|
+
// Use the exposed ref to access the avatar div and trigger click
|
|
243
|
+
const avatarRef = component.vm.avatarRef;
|
|
244
|
+
|
|
245
|
+
expect(avatarRef).toBeDefined();
|
|
246
|
+
avatarRef?.dispatchEvent(new Event("click"));
|
|
219
247
|
|
|
220
248
|
expect(component.emitted("click")).toBeTruthy();
|
|
221
249
|
expect(component.emitted("click")?.length).toBe(expectedLength);
|
package/ui.image-avatar/types.ts
CHANGED
|
@@ -4,6 +4,14 @@ import type { ComponentConfig } from "../types";
|
|
|
4
4
|
|
|
5
5
|
export type Config = typeof defaultConfig;
|
|
6
6
|
|
|
7
|
+
export interface ChipItem {
|
|
8
|
+
icon?: string;
|
|
9
|
+
color?: Props["color"];
|
|
10
|
+
xPosition?: "left" | "right";
|
|
11
|
+
yPosition?: "top" | "bottom";
|
|
12
|
+
inset?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
export interface Props {
|
|
8
16
|
/**
|
|
9
17
|
* Avatar label (username, nickname, etc.).
|
|
@@ -49,6 +57,11 @@ export interface Props {
|
|
|
49
57
|
*/
|
|
50
58
|
placeholderIcon?: string;
|
|
51
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Avatar chip config.
|
|
62
|
+
*/
|
|
63
|
+
chip?: ChipItem;
|
|
64
|
+
|
|
52
65
|
/**
|
|
53
66
|
* Component config object.
|
|
54
67
|
*/
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, provide, useTemplateRef } from "vue";
|
|
3
|
+
|
|
4
|
+
import { useUI } from "../composables/useUI";
|
|
5
|
+
import { getDefaults } from "../utils/ui";
|
|
6
|
+
|
|
7
|
+
import UAvatar from "../ui.image-avatar/UAvatar.vue";
|
|
8
|
+
|
|
9
|
+
import { COMPONENT_NAME } from "./constants";
|
|
10
|
+
import defaultConfig from "./config";
|
|
11
|
+
|
|
12
|
+
import type { Props, Config } from "./types";
|
|
13
|
+
import type { Props as AvatarProps } from "../ui.image-avatar/types";
|
|
14
|
+
|
|
15
|
+
defineOptions({ inheritAttrs: false });
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
18
|
+
...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
|
|
19
|
+
avatars: () => [],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const avatarGroupRef = useTemplateRef<HTMLDivElement>("avatarGroup");
|
|
23
|
+
|
|
24
|
+
provide("getAvatarGroupSize", () => props.size);
|
|
25
|
+
provide("getAvatarGroupVariant", () => props.variant);
|
|
26
|
+
provide("getAvatarGroupRounded", () => props.rounded);
|
|
27
|
+
|
|
28
|
+
const visibleAvatars = computed(() => {
|
|
29
|
+
return props.avatars.slice(0, props.max);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const remainingCount = computed(() => {
|
|
33
|
+
return props.avatars.length - props.max;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const hasMoreAvatars = computed(() => remainingCount.value > 0);
|
|
37
|
+
|
|
38
|
+
defineExpose({
|
|
39
|
+
/**
|
|
40
|
+
* A reference to the avatar group element for direct DOM manipulation.
|
|
41
|
+
* @property {HTMLDivElement}
|
|
42
|
+
*/
|
|
43
|
+
avatarGroupRef,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get element / nested component attributes for each config token ✨
|
|
48
|
+
* Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
|
|
49
|
+
*/
|
|
50
|
+
const { getDataTest, avatarGroupAttrs, avatarAttrs, remainingAttrs } = useUI<Config>(defaultConfig);
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<div ref="avatarGroup" v-bind="avatarGroupAttrs" :data-test="getDataTest()">
|
|
55
|
+
<template v-for="(avatar, index) in visibleAvatars" :key="index">
|
|
56
|
+
<!--
|
|
57
|
+
@slot Use it to customize a specific avatar.
|
|
58
|
+
@binding {number} index
|
|
59
|
+
@binding {object} avatar
|
|
60
|
+
-->
|
|
61
|
+
<slot :name="`avatar-${index}`" :index="index" :avatar="avatar">
|
|
62
|
+
<UAvatar
|
|
63
|
+
:src="avatar.src"
|
|
64
|
+
:label="avatar.label"
|
|
65
|
+
:color="avatar.color as AvatarProps['color']"
|
|
66
|
+
:placeholder-icon="avatar.placeholderIcon"
|
|
67
|
+
:chip="avatar.chip"
|
|
68
|
+
v-bind="avatarAttrs"
|
|
69
|
+
/>
|
|
70
|
+
</slot>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<div v-if="hasMoreAvatars">
|
|
74
|
+
<UAvatar :size="size" color="neutral" :rounded="rounded" v-bind="remainingAttrs">
|
|
75
|
+
<template #placeholder>
|
|
76
|
+
<!--
|
|
77
|
+
@slot Use it to customize the remaining count avatar.
|
|
78
|
+
@binding {number} remaining-count
|
|
79
|
+
-->
|
|
80
|
+
<slot name="remaining" :remaining-count="remainingCount">
|
|
81
|
+
{{ `+${remainingCount}` }}
|
|
82
|
+
</slot>
|
|
83
|
+
</template>
|
|
84
|
+
</UAvatar>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Meta, Title, Subtitle, Description, Primary, Controls, Stories, Source } from "@storybook/addon-docs/blocks";
|
|
2
|
+
import { getSource } from "../../utils/storybook";
|
|
3
|
+
|
|
4
|
+
import * as stories from "./stories";
|
|
5
|
+
import defaultConfig from "../config?raw"
|
|
6
|
+
|
|
7
|
+
<Meta of={stories} />
|
|
8
|
+
<Title of={stories} />
|
|
9
|
+
<Subtitle of={stories} />
|
|
10
|
+
<Description of={stories} />
|
|
11
|
+
<Primary of={stories} />
|
|
12
|
+
<Controls of={stories.Default} />
|
|
13
|
+
<Stories of={stories} />
|
|
14
|
+
|
|
15
|
+
## Default config
|
|
16
|
+
<Source code={getSource(defaultConfig)} language="jsx" dark />
|