vueless 1.3.6-beta.6 → 1.3.6-beta.7
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/package.json +2 -2
- package/ui.button/UButton.vue +1 -1
- 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-notify/UNotify.vue +31 -8
- package/ui.text-notify/config.ts +1 -1
- package/ui.text-notify/tests/UNotify.test.ts +22 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vueless",
|
|
3
|
-
"version": "1.3.6-beta.
|
|
3
|
+
"version": "1.3.6-beta.7",
|
|
4
4
|
"description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
|
|
5
5
|
"author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
|
|
6
6
|
"homepage": "https://vueless.com",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@vue/eslint-config-typescript": "^14.6.0",
|
|
58
58
|
"@vue/test-utils": "^2.4.6",
|
|
59
59
|
"@vue/tsconfig": "^0.7.0",
|
|
60
|
-
"@vueless/storybook": "^1.3.
|
|
60
|
+
"@vueless/storybook": "^1.3.14",
|
|
61
61
|
"eslint": "^9.32.0",
|
|
62
62
|
"eslint-plugin-storybook": "^10.0.2",
|
|
63
63
|
"eslint-plugin-vue": "^10.3.0",
|
package/ui.button/UButton.vue
CHANGED
|
@@ -62,7 +62,7 @@ defineExpose({
|
|
|
62
62
|
* Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
|
|
63
63
|
*/
|
|
64
64
|
const mutatedProps = computed(() => ({
|
|
65
|
-
icon: Boolean(props.icon) || hasSlotContent(slots["default"]),
|
|
65
|
+
icon: Boolean(props.icon) || (!props.label && hasSlotContent(slots["default"])),
|
|
66
66
|
leftIcon: Boolean(props.leftIcon) || hasSlotContent(slots["left"]),
|
|
67
67
|
rightIcon: Boolean(props.rightIcon) || hasSlotContent(slots["right"]),
|
|
68
68
|
label: Boolean(props.label),
|
|
@@ -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", () => {
|
|
@@ -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
|
@@ -59,14 +59,22 @@ describe("UNotify.vue", () => {
|
|
|
59
59
|
dispatchNotifyEvent("notifyStart", mockNotification);
|
|
60
60
|
await component.vm.$nextTick();
|
|
61
61
|
|
|
62
|
-
//
|
|
63
|
-
|
|
62
|
+
// Manually trigger setPosition since waitForPageElement won't find the elements in tests
|
|
63
|
+
// @ts-expect-error - Accessing private method for testing
|
|
64
|
+
component.vm.setPosition();
|
|
65
|
+
await component.vm.$nextTick();
|
|
66
|
+
|
|
67
|
+
// Access the internal notifyPositionStyles ref
|
|
68
|
+
// @ts-expect-error - Accessing private property for testing
|
|
69
|
+
const positionStyles = component.vm.notifyPositionStyles;
|
|
64
70
|
|
|
65
71
|
// For center position, we expect a calculated left value
|
|
66
72
|
if (position === "center") {
|
|
67
|
-
expect(
|
|
73
|
+
expect(positionStyles).toHaveProperty("left");
|
|
74
|
+
expect(positionStyles.left).toBeDefined();
|
|
68
75
|
} else {
|
|
69
|
-
expect(
|
|
76
|
+
expect(positionStyles).toHaveProperty(position);
|
|
77
|
+
expect(positionStyles[position]).toBe("0px");
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
});
|
|
@@ -83,10 +91,17 @@ describe("UNotify.vue", () => {
|
|
|
83
91
|
dispatchNotifyEvent("notifyStart", mockNotification);
|
|
84
92
|
await component.vm.$nextTick();
|
|
85
93
|
|
|
86
|
-
//
|
|
87
|
-
|
|
94
|
+
// Manually trigger setPosition since waitForPageElement won't find the elements in tests
|
|
95
|
+
// @ts-expect-error - Accessing private method for testing
|
|
96
|
+
component.vm.setPosition();
|
|
97
|
+
await component.vm.$nextTick();
|
|
98
|
+
|
|
99
|
+
// Access the internal notifyPositionStyles ref
|
|
100
|
+
// @ts-expect-error - Accessing private property for testing
|
|
101
|
+
const positionStyles = component.vm.notifyPositionStyles;
|
|
88
102
|
|
|
89
|
-
expect(
|
|
103
|
+
expect(positionStyles).toHaveProperty(position);
|
|
104
|
+
expect(positionStyles[position]).toBe("0px");
|
|
90
105
|
}
|
|
91
106
|
});
|
|
92
107
|
|