vueless 1.3.6-beta.5 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.3.6-beta.5",
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.8",
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",
@@ -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),
@@ -16,7 +16,9 @@ type UInputRef = InstanceType<typeof UInput>;
16
16
 
17
17
  defineOptions({ internal: true });
18
18
 
19
- const props = defineProps<UDatePickerRangeInputsProps>();
19
+ const props = withDefaults(defineProps<UDatePickerRangeInputsProps>(), {
20
+ dataTest: null,
21
+ });
20
22
 
21
23
  const rangeInputStartRef = useTemplateRef<UInputRef>("range-input-start");
22
24
  const rangeInputEndRef = useTemplateRef<UInputRef>("range-input-end");
@@ -140,6 +142,7 @@ defineExpose({
140
142
  v-bind="attrs.rangeInputFirstAttrs.value"
141
143
  :name="rangeInputName"
142
144
  no-autocomplete
145
+ :data-test="`${dataTest}-from`"
143
146
  @blur="updateDateValue(rangeStart, InputRangeType.Start)"
144
147
  @keydown.enter="updateDateValue(rangeStart, InputRangeType.Start)"
145
148
  @input="validateInput($event, InputRangeType.Start)"
@@ -153,6 +156,7 @@ defineExpose({
153
156
  v-bind="attrs.rangeInputLastAttrs.value"
154
157
  :name="rangeInputName"
155
158
  no-autocomplete
159
+ :data-test="`${dataTest}-to`"
156
160
  @blur="updateDateValue(rangeEnd, InputRangeType.End)"
157
161
  @keydown.enter="updateDateValue(rangeEnd, InputRangeType.End)"
158
162
  @input="validateInput($event, InputRangeType.End)"
@@ -74,6 +74,7 @@ export interface UDatePickerRangeInputsProps {
74
74
  minDate: string | Date | undefined;
75
75
  config: Config;
76
76
  attrs: UDatePickerRangeInputsAttrs;
77
+ dataTest?: string | null;
77
78
  }
78
79
 
79
80
  export interface CustomRangeButton {
@@ -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: (props) => h("div", { "data-active": props.active }),
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: (props) => h("div", { "data-icon-name": props.iconName }),
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
- setPosition();
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(`${pageClass}`);
94
- const asideWidth = getOffsetWidth(`${asideClass}`);
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 = `calc(50% - ${notifyWidth / 2}px)`;
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
 
@@ -1,5 +1,5 @@
1
1
  export default /*tw*/ {
2
- wrapper: "absolute overflow-visible md:w-[22rem]",
2
+ wrapper: "mt-3 absolute overflow-visible md:w-[22rem]",
3
3
  transitionGroup: {
4
4
  moveClass: "transition duration-500",
5
5
  enterActiveClass: "transition duration-500",
@@ -59,14 +59,22 @@ describe("UNotify.vue", () => {
59
59
  dispatchNotifyEvent("notifyStart", mockNotification);
60
60
  await component.vm.$nextTick();
61
61
 
62
- // Check the component's style directly
63
- const style = component.attributes("style") || "";
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(style).toContain("left:");
73
+ expect(positionStyles).toHaveProperty("left");
74
+ expect(positionStyles.left).toBeDefined();
68
75
  } else {
69
- expect(style).toContain(`${position}: 0px`);
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
- // Check the component's style directly
87
- const style = component.attributes("style") || "";
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(style).toContain(`${position}: 0px`);
103
+ expect(positionStyles).toHaveProperty(position);
104
+ expect(positionStyles[position]).toBe("0px");
90
105
  }
91
106
  });
92
107