vueless 1.3.7-beta.9 → 1.3.8-beta.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.3.7-beta.9",
3
+ "version": "1.3.8-beta.0",
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",
package/plugin-vite.js CHANGED
@@ -33,7 +33,6 @@ import {
33
33
  JAVASCRIPT_EXT,
34
34
  TYPESCRIPT_EXT,
35
35
  INTERNAL_ENV,
36
- STORYBOOK_ENV,
37
36
  NUXT_MODULE_ENV,
38
37
  VUELESS_LOCAL_DIR,
39
38
  VUELESS_PACKAGE_DIR,
@@ -67,7 +66,6 @@ export const Vueless = function (options = {}) {
67
66
  const { debug, env, include, basePath } = options;
68
67
 
69
68
  const isInternalEnv = env === INTERNAL_ENV;
70
- const isStorybookEnv = env === STORYBOOK_ENV;
71
69
  const isNuxtModuleEnv = env === NUXT_MODULE_ENV;
72
70
 
73
71
  const vuelessSrcDir = isInternalEnv ? VUELESS_LOCAL_DIR : VUELESS_PACKAGE_DIR;
@@ -83,10 +81,9 @@ export const Vueless = function (options = {}) {
83
81
 
84
82
  /* if server stopped by developer (Ctrl+C) */
85
83
  process.on("SIGINT", async () => {
86
- if (isInternalEnv || isStorybookEnv) {
87
- await removeCustomPropTypes(vuelessSrcDir);
88
- await restoreComponents(vuelessSrcDir);
89
- }
84
+ /* remove `.cache` folder in components and restore changes */
85
+ await removeCustomPropTypes(vuelessSrcDir);
86
+ await restoreComponents(vuelessSrcDir);
90
87
 
91
88
  /* remove cached icons */
92
89
  await removeIconsCache(basePath);
@@ -17,7 +17,7 @@ export default /*tw*/ {
17
17
  title: "{UHeader}",
18
18
  description: "mt-0.5 font-normal text-lifted",
19
19
  footer: {
20
- base: "flex justify-between w-full border-t border-muted mt-6 pt-4 md:pt-6",
20
+ base: "flex justify-between w-full border-muted mt-6 pt-4 md:pt-6",
21
21
  variants: {
22
22
  variant: {
23
23
  solid: "border-muted",
@@ -26,11 +26,16 @@ export default /*tw*/ {
26
26
  soft: "border-default/50",
27
27
  inverted: "border-default/50",
28
28
  },
29
+ divided: {
30
+ true: "border-t",
31
+ false: "mt-0",
32
+ },
29
33
  },
30
34
  },
31
35
  footerLeft: "",
32
36
  footerRight: "",
33
37
  defaults: {
34
38
  variant: "outlined",
39
+ divided: true,
35
40
  },
36
41
  };
@@ -128,6 +128,17 @@ Description.args = {
128
128
  export const Variants = EnumTemplate.bind({});
129
129
  Variants.args = { enum: "variant", title: "{enumValue}" };
130
130
 
131
+ export const NoDivided = DefaultTemplate.bind({});
132
+ NoDivided.args = {
133
+ divided: false,
134
+ slotTemplate: `
135
+ <template #footer-left>
136
+ <UButton size="sm" label="Save" variant="subtle" />
137
+ </template>
138
+ ${defaultTemplate}
139
+ `,
140
+ };
141
+
131
142
  export const BeforeTitleSlot = DefaultTemplate.bind({});
132
143
  BeforeTitleSlot.args = {
133
144
  slotTemplate: `
@@ -62,6 +62,39 @@ describe("UCard", () => {
62
62
  });
63
63
  });
64
64
 
65
+ it("Divided – shows divider between content and footer when divided is true", () => {
66
+ const component = mount(UCard, {
67
+ props: {
68
+ divided: true,
69
+ },
70
+ slots: {
71
+ "footer-left": "<div>Footer Content</div>",
72
+ },
73
+ });
74
+
75
+ const footer = component.find("[vl-key='footer']");
76
+
77
+ expect(footer.exists()).toBe(true);
78
+ expect(footer.attributes("class")).toContain("border-t");
79
+ });
80
+
81
+ it("Divided – hides divider between content and footer when divided is false", () => {
82
+ const component = mount(UCard, {
83
+ props: {
84
+ divided: false,
85
+ },
86
+ slots: {
87
+ "footer-left": "<div>Footer Content</div>",
88
+ },
89
+ });
90
+
91
+ const footer = component.find("[vl-key='footer']");
92
+
93
+ expect(footer.exists()).toBe(true);
94
+ expect(footer.attributes("class")).not.toContain("border-t");
95
+ expect(footer.attributes("class")).toContain("mt-0");
96
+ });
97
+
65
98
  it("Data Test – applies data-test attribute", () => {
66
99
  const dataTest = "card-test";
67
100
 
@@ -19,6 +19,11 @@ export interface Props {
19
19
  */
20
20
  variant?: "solid" | "outlined" | "subtle" | "soft" | "inverted";
21
21
 
22
+ /**
23
+ * Show divider between content and footer.
24
+ */
25
+ divided?: boolean;
26
+
22
27
  /**
23
28
  * Component config object.
24
29
  */
@@ -47,6 +47,7 @@ const isDraggingFromHandle = ref(false);
47
47
  const dragStartPosition = ref({ x: 0, y: 0 });
48
48
  const dragCurrentPosition = ref({ x: 0, y: 0 });
49
49
  const minDragDistance = 10;
50
+ const isMouseDownOnOverlay = ref(false);
50
51
 
51
52
  const isShownDrawer = computed({
52
53
  get: () => props.modelValue,
@@ -180,8 +181,15 @@ function toggleEventListeners() {
180
181
  }
181
182
  }
182
183
 
184
+ function onMouseDownOverlay() {
185
+ isMouseDownOnOverlay.value = true;
186
+ }
187
+
183
188
  function onClickOutside() {
184
- props.closeOnOverlay && closeDrawer();
189
+ if (!props.closeOnOverlay || !isMouseDownOnOverlay.value) return;
190
+
191
+ closeDrawer();
192
+ isMouseDownOnOverlay.value = false;
185
193
  }
186
194
 
187
195
  function onKeydownEsc(e: KeyboardEvent) {
@@ -312,7 +320,11 @@ const {
312
320
  :data-test="getDataTest()"
313
321
  @keydown.self.esc="onKeydownEsc"
314
322
  >
315
- <div v-bind="innerWrapperAttrs" @click.self="onClickOutside">
323
+ <div
324
+ v-bind="innerWrapperAttrs"
325
+ @mousedown.self="onMouseDownOverlay"
326
+ @click.self="onClickOutside"
327
+ >
316
328
  <div
317
329
  ref="drawer"
318
330
  :style="{ transform: dragTransform }"
@@ -106,23 +106,23 @@ export default /*tw*/ {
106
106
  base: "flex items-center justify-center bg-inherit cursor-grab active:cursor-grabbing select-none",
107
107
  variants: {
108
108
  position: {
109
- top: "w-full h-11",
110
- bottom: "w-full h-11",
111
- left: "w-11 h-auto",
112
- right: "w-11 h-auto",
109
+ top: "w-full h-6",
110
+ bottom: "w-full h-6",
111
+ left: "w-6 h-auto",
112
+ right: "w-6 h-auto",
113
113
  },
114
114
  },
115
115
  },
116
116
  handle: {
117
117
  base: "rounded-large cursor-grab active:cursor-grabbing bg-lifted hover:bg-accented transition",
118
118
  compoundVariants: [
119
- { position: ["top", "bottom"], class: "w-11 h-1.5" },
120
- { position: ["left", "right"], class: "w-1.5 h-11" },
119
+ { position: ["top", "bottom"], class: "w-10 h-1" },
120
+ { position: ["left", "right"], class: "w-1 h-10" },
121
121
  ],
122
122
  },
123
123
  defaults: {
124
124
  variant: "solid",
125
- position: "left",
125
+ position: "right",
126
126
  inset: false,
127
127
  handle: true,
128
128
  closeOnEsc: true,
@@ -137,27 +137,29 @@ const EnumTemplate: StoryFn<UDrawerArgs> = (args: UDrawerArgs, { argTypes }) =>
137
137
  });
138
138
 
139
139
  export const Default = DefaultTemplate.bind({});
140
- Default.args = {};
140
+ Default.args = { modelValue: true };
141
141
 
142
142
  export const Description = DefaultTemplate.bind({});
143
143
  Description.args = {
144
+ modelValue: true,
144
145
  description: "Enter your email below to get started and create your account.",
145
146
  };
146
147
 
147
148
  export const NoHandle = DefaultTemplate.bind({});
148
- NoHandle.args = { handle: false };
149
+ NoHandle.args = { modelValue: true, handle: false };
149
150
 
150
151
  export const Inset = DefaultTemplate.bind({});
151
- Inset.args = { inset: true };
152
+ Inset.args = { modelValue: true, inset: true };
152
153
 
153
154
  export const NoCloseOnEscAndOverlay = DefaultTemplate.bind({});
154
155
  NoCloseOnEscAndOverlay.args = {
156
+ modelValue: true,
155
157
  closeOnEsc: false,
156
158
  closeOnOverlay: false,
157
159
  };
158
160
 
159
161
  export const NoCloseOnCross = DefaultTemplate.bind({});
160
- NoCloseOnCross.args = { closeOnCross: false };
162
+ NoCloseOnCross.args = { modelValue: true, closeOnCross: false };
161
163
 
162
164
  export const Position = EnumTemplate.bind({});
163
165
  Position.args = { enum: "position", modelValues: {} };
@@ -167,6 +169,7 @@ Variants.args = { enum: "variant", modelValues: {} };
167
169
 
168
170
  export const BeforeTitleSlot = DefaultTemplate.bind({});
169
171
  BeforeTitleSlot.args = {
172
+ modelValue: true,
170
173
  slotTemplate: `
171
174
  <template #before-title>
172
175
  <UIcon name="account_circle" size="sm" color="primary" />
@@ -179,6 +182,7 @@ BeforeTitleSlot.args = {
179
182
 
180
183
  export const TitleSlot = DefaultTemplate.bind({});
181
184
  TitleSlot.args = {
185
+ modelValue: true,
182
186
  slotTemplate: `
183
187
  <template #title="{ title }">
184
188
  <UHeader :label="title" color="primary" />
@@ -191,6 +195,7 @@ TitleSlot.args = {
191
195
 
192
196
  export const AfterTitleSlot = DefaultTemplate.bind({});
193
197
  AfterTitleSlot.args = {
198
+ modelValue: true,
194
199
  slotTemplate: `
195
200
  <template #after-title>
196
201
  <UIcon name="verified" size="sm" color="primary" />
@@ -203,6 +208,7 @@ AfterTitleSlot.args = {
203
208
 
204
209
  export const ActionsSlot = DefaultTemplate.bind({});
205
210
  ActionsSlot.args = {
211
+ modelValue: true,
206
212
  slotTemplate: `
207
213
  <template #actions="{ close }">
208
214
  <UButton
@@ -221,6 +227,7 @@ ActionsSlot.args = {
221
227
 
222
228
  export const HandleSlot = DefaultTemplate.bind({});
223
229
  HandleSlot.args = {
230
+ modelValue: true,
224
231
  slotTemplate: `
225
232
  <template #default>
226
233
  ${defaultTemplate}
@@ -233,6 +240,7 @@ HandleSlot.args = {
233
240
 
234
241
  export const FooterLeftSlot = DefaultTemplate.bind({});
235
242
  FooterLeftSlot.args = {
243
+ modelValue: true,
236
244
  slotTemplate: `
237
245
  <template #default>
238
246
  ${defaultTemplate}
@@ -246,6 +254,7 @@ FooterLeftSlot.args = {
246
254
 
247
255
  export const FooterRightSlot = DefaultTemplate.bind({});
248
256
  FooterRightSlot.args = {
257
+ modelValue: true,
249
258
  slotTemplate: `
250
259
  <template #default>
251
260
  ${defaultTemplate}
@@ -165,6 +165,7 @@ describe("UDrawer", () => {
165
165
 
166
166
  expect(innerWrapper.exists()).toBe(true);
167
167
 
168
+ await innerWrapper.trigger("mousedown");
168
169
  await innerWrapper.trigger("click");
169
170
  await sleep(500);
170
171
 
@@ -537,6 +538,7 @@ describe("UDrawer", () => {
537
538
 
538
539
  const innerWrapper = component.find("[vl-key='innerWrapper']");
539
540
 
541
+ await innerWrapper.trigger("mousedown");
540
542
  await innerWrapper.trigger("click");
541
543
 
542
544
  if (value) {
@@ -550,6 +552,27 @@ describe("UDrawer", () => {
550
552
  });
551
553
  });
552
554
 
555
+ it("CloseOnOverlay – does not close when mousedown on drawer and mouseup on overlay", async () => {
556
+ const component = mount(UDrawer, {
557
+ props: {
558
+ modelValue: true,
559
+ closeOnOverlay: true,
560
+ },
561
+ });
562
+
563
+ const drawer = component.find("[vl-key='drawer']");
564
+ const innerWrapper = component.find("[vl-key='innerWrapper']");
565
+
566
+ // Mousedown on drawer content
567
+ await drawer.trigger("mousedown");
568
+ // Click (mouseup) on overlay
569
+ await innerWrapper.trigger("click");
570
+
571
+ // Drawer should NOT close
572
+ expect(component.emitted("update:modelValue")).toBeFalsy();
573
+ expect(component.emitted("close")).toBeFalsy();
574
+ });
575
+
553
576
  it("CloseOnEsc – emits events when escape key is pressed based on prop", () => {
554
577
  const closeOnEsc = [true, false];
555
578
 
@@ -11,6 +11,8 @@ import { COMPONENT_NAME } from "./constants";
11
11
  import defaultConfig from "./config";
12
12
 
13
13
  import type { Props, Config } from "./types";
14
+ import { hasSlotContent } from "../utils/helper";
15
+ import UText from "../ui.text-block/UText.vue";
14
16
 
15
17
  defineOptions({ inheritAttrs: false });
16
18
 
@@ -47,6 +49,7 @@ const {
47
49
  descriptionAttrs,
48
50
  wrapperAttrs,
49
51
  headerAttrs,
52
+ contentAttrs,
50
53
  footerAttrs,
51
54
  emptyIconWrapperAttrs,
52
55
  emptyIconAttrs,
@@ -73,11 +76,19 @@ const {
73
76
  @binding {string} description
74
77
  -->
75
78
  <slot :title="title" :description="description">
76
- <UHeader v-if="title" :label="title" v-bind="titleAttrs" />
77
- <div v-if="description" v-bind="descriptionAttrs" v-text="description" />
79
+ <div v-bind="contentAttrs">
80
+ <UHeader v-if="title" :label="title" v-bind="titleAttrs" />
81
+ <UText
82
+ v-if="description"
83
+ align="center"
84
+ :size="size"
85
+ :label="description"
86
+ v-bind="descriptionAttrs"
87
+ />
88
+ </div>
78
89
  </slot>
79
90
 
80
- <div v-bind="footerAttrs">
91
+ <div v-if="hasSlotContent($slots['footer'])" v-bind="footerAttrs">
81
92
  <!-- @slot Use it to add something to the footer. -->
82
93
  <slot name="footer" />
83
94
  </div>
@@ -1,15 +1,6 @@
1
1
  export default /*tw*/ {
2
- wrapper: "flex flex-col items-center justify-center size-full",
3
- header: {
4
- base: "flex justify-center",
5
- variants: {
6
- size: {
7
- sm: "mb-4",
8
- md: "mb-5",
9
- lg: "mb-6",
10
- },
11
- },
12
- },
2
+ wrapper: "flex flex-col items-center justify-center gap-4 size-full",
3
+ header: "flex justify-center",
13
4
  emptyIconWrapper: {
14
5
  base: "rounded-full bg-inverted/5",
15
6
  variants: {
@@ -24,12 +15,13 @@ export default /*tw*/ {
24
15
  base: "{UIcon}",
25
16
  defaults: {
26
17
  size: {
27
- sm: "2xl",
18
+ sm: "xl",
28
19
  md: "3xl",
29
- lg: "4xl",
20
+ lg: "5xl",
30
21
  },
31
22
  },
32
23
  },
24
+ content: "flex flex-col items-center justify-center gap-1.5",
33
25
  title: {
34
26
  base: "{UHeader}",
35
27
  defaults: {
@@ -40,17 +32,8 @@ export default /*tw*/ {
40
32
  },
41
33
  },
42
34
  },
43
- description: {
44
- base: "text-center mt-2",
45
- variants: {
46
- size: {
47
- sm: "text-small",
48
- md: "text-medium",
49
- lg: "text-large",
50
- },
51
- },
52
- },
53
- footer: "mt-4 flex justify-center",
35
+ description: "{UText}",
36
+ footer: "flex justify-center",
54
37
  defaults: {
55
38
  size: "md",
56
39
  /* icons */
@@ -30,6 +30,7 @@ export default {
30
30
  component: UEmpty,
31
31
  args: {
32
32
  title: "No contacts",
33
+ description: "There are no contacts in the list.",
33
34
  },
34
35
  argTypes: {
35
36
  ...getArgTypes(UEmpty.__name),
@@ -68,9 +69,6 @@ const EnumTemplate: StoryFn<UEmptyArgs> = (args: UEmptyArgs, { argTypes }) => ({
68
69
  export const Default = DefaultTemplate.bind({});
69
70
  Default.args = {};
70
71
 
71
- export const Description = DefaultTemplate.bind({});
72
- Description.args = { description: "There are no contacts in the list." };
73
-
74
72
  export const Sizes = EnumTemplate.bind({});
75
73
  Sizes.args = { enum: "size" };
76
74
 
@@ -11,9 +11,9 @@ describe("UEmpty.vue", () => {
11
11
  describe("Props", () => {
12
12
  it("Size – applies the correct size class", async () => {
13
13
  const size = {
14
- sm: "2xl",
14
+ sm: "xl",
15
15
  md: "3xl",
16
- lg: "4xl",
16
+ lg: "5xl",
17
17
  };
18
18
 
19
19
  Object.entries(size).forEach(([size, value]) => {
@@ -49,6 +49,7 @@ const slots = useSlots();
49
49
  const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
50
50
 
51
51
  const isWrapperTransitionComplete = ref(false);
52
+ const isMouseDownOnOverlay = ref(false);
52
53
 
53
54
  const isShownModal = computed({
54
55
  get: () => props.modelValue,
@@ -153,8 +154,15 @@ function onClickBackLink() {
153
154
  emit("back");
154
155
  }
155
156
 
157
+ function onMouseDownOverlay() {
158
+ isMouseDownOnOverlay.value = true;
159
+ }
160
+
156
161
  function onClickOutside() {
157
- props.closeOnOverlay && closeModal();
162
+ if (!props.closeOnOverlay || !isMouseDownOnOverlay.value) return;
163
+
164
+ closeModal();
165
+ isMouseDownOnOverlay.value = false;
158
166
  }
159
167
 
160
168
  function onKeydownEsc(e: KeyboardEvent) {
@@ -229,7 +237,11 @@ const {
229
237
  :data-test="getDataTest()"
230
238
  @keydown.self.esc="onKeydownEsc"
231
239
  >
232
- <div v-bind="innerWrapperAttrs" @click.self="onClickOutside">
240
+ <div
241
+ v-bind="innerWrapperAttrs"
242
+ @mousedown.self="onMouseDownOverlay"
243
+ @click.self="onClickOutside"
244
+ >
233
245
  <div v-bind="modalAttrs">
234
246
  <div v-if="isExistHeader" v-bind="headerAttrs">
235
247
  <div v-bind="beforeTitleAttrs">
@@ -18,7 +18,7 @@ export default /*tw*/ {
18
18
  leaveToClass: "opacity-0",
19
19
  },
20
20
  innerWrapper: {
21
- base: "py-12 w-full h-screen scroll-container [scrollbar-gutter:stable]",
21
+ base: "py-12 w-full h-max scroll-container [scrollbar-gutter:stable]",
22
22
  variants: {
23
23
  wrapperTransitionCompleted: {
24
24
  true: "overflow-y-auto",
@@ -200,11 +200,12 @@ describe("UModal", () => {
200
200
  },
201
201
  });
202
202
 
203
- const overlay = component.find('[vl-key="overlay"]');
203
+ const innerWrapper = component.find('[vl-key="innerWrapper"]');
204
204
 
205
- expect(overlay.exists()).toBe(true);
205
+ expect(innerWrapper.exists()).toBe(true);
206
206
 
207
- await overlay.trigger("click");
207
+ await innerWrapper.trigger("mousedown");
208
+ await innerWrapper.trigger("click");
208
209
  await sleep(500);
209
210
 
210
211
  const modal = component.find('[vl-key="modal"]');
@@ -590,6 +591,7 @@ describe("UModal", () => {
590
591
 
591
592
  const innerWrapper = component.find("[vl-key='innerWrapper']");
592
593
 
594
+ await innerWrapper.trigger("mousedown");
593
595
  await innerWrapper.trigger("click");
594
596
 
595
597
  if (value) {
@@ -603,6 +605,27 @@ describe("UModal", () => {
603
605
  });
604
606
  });
605
607
 
608
+ it("does not close when mousedown on modal and mouseup on overlay", async () => {
609
+ const component = mount(UModal, {
610
+ props: {
611
+ modelValue: true,
612
+ closeOnOverlay: true,
613
+ },
614
+ });
615
+
616
+ const modal = component.find("[vl-key='modal']");
617
+ const innerWrapper = component.find("[vl-key='innerWrapper']");
618
+
619
+ // Mousedown on modal content
620
+ await modal.trigger("mousedown");
621
+ // Click (mouseup) on overlay
622
+ await innerWrapper.trigger("click");
623
+
624
+ // Modal should NOT close
625
+ expect(component.emitted("update:modelValue")).toBeFalsy();
626
+ expect(component.emitted("close")).toBeFalsy();
627
+ });
628
+
606
629
  // CloseOnEsc events
607
630
  it("emits events when escape key is pressed based on closeOnEsc prop", () => {
608
631
  const closeOnEsc = [true, false];
@@ -4,7 +4,7 @@ import { cachedIcons } from "virtual:vueless/icons";
4
4
 
5
5
  import { useUI } from "../composables/useUI";
6
6
  import { getDefaults } from "../utils/ui";
7
- import { ICONS_CACHED_DIR, INTERNAL_ICONS_LIBRARY, STORYBOOK_ICONS_LIBRARY } from "../constants";
7
+ import { INTERNAL_ICONS_LIBRARY, STORYBOOK_ICONS_LIBRARY } from "../constants";
8
8
 
9
9
  import { COMPONENT_NAME } from "./constants";
10
10
  import defaultConfig from "./config";
@@ -33,15 +33,15 @@ const dynamicComponent = computed(() => {
33
33
  let userLibrary = config.value.defaults.library;
34
34
 
35
35
  const isInternalIconExists = cachedIcons.find(([path]: [string]) =>
36
- path.includes(`${ICONS_CACHED_DIR}/${INTERNAL_ICONS_LIBRARY}/${props.name}.svg`),
36
+ path.includes(`${INTERNAL_ICONS_LIBRARY}/${props.name}.svg`),
37
37
  );
38
38
 
39
39
  const isStorybookIconExists = cachedIcons.find(([path]: [string]) =>
40
- path.includes(`${ICONS_CACHED_DIR}/${STORYBOOK_ICONS_LIBRARY}/${props.name}.svg`),
40
+ path.includes(`${STORYBOOK_ICONS_LIBRARY}/${props.name}.svg`),
41
41
  );
42
42
 
43
43
  const isExternalIconExists = cachedIcons.find(([path]: [string]) =>
44
- path.includes(`${ICONS_CACHED_DIR}/${userLibrary}/${props.name}.svg`),
44
+ path.includes(`${userLibrary}/${props.name}.svg`),
45
45
  );
46
46
 
47
47
  if (isInternalIconExists && !isExternalIconExists) {
@@ -67,9 +67,7 @@ const dynamicComponent = computed(() => {
67
67
  if (!name) return "";
68
68
 
69
69
  const [, component] =
70
- cachedIcons.find(([path]: [string]) =>
71
- path.includes(`${ICONS_CACHED_DIR}/${userLibrary}/${props.name}.svg`),
72
- ) || [];
70
+ cachedIcons.find(([path]: [string]) => path.includes(`${userLibrary}/${props.name}.svg`)) || [];
73
71
 
74
72
  if (!component) return "";
75
73
 
@@ -27,10 +27,15 @@ const props = withDefaults(defineProps<Props>(), {
27
27
 
28
28
  const emit = defineEmits([
29
29
  /**
30
- * Triggers when current page changes.
30
+ * Triggers when the current page changes.
31
31
  * @property {number} value
32
32
  */
33
33
  "update:modelValue",
34
+ /**
35
+ * Triggers when pagination is changed.
36
+ * @property {number} value
37
+ */
38
+ "change",
34
39
  ]);
35
40
 
36
41
  const { localeMessages } = useComponentLocaleMessages<typeof defaultConfig.i18n>(
@@ -45,6 +50,7 @@ const currentPage = computed({
45
50
  get: () => props.modelValue,
46
51
  set: (value) => {
47
52
  emit("update:modelValue", value);
53
+ emit("change", value);
48
54
  },
49
55
  });
50
56
 
@@ -378,6 +378,29 @@ describe("UPagination.vue", () => {
378
378
  await buttons[buttons.length - 1].trigger("click");
379
379
  expect(component.emitted("update:modelValue")?.[3]).toEqual([10]);
380
380
  });
381
+
382
+ it("Change – emits change event when page is changed", async () => {
383
+ const component = mount(UPagination, {
384
+ props: {
385
+ modelValue: 1,
386
+ total: 100,
387
+ perPage: 10,
388
+ },
389
+ });
390
+
391
+ // Find the second page button and click it
392
+ const pageButtons = component.findAllComponents(UButton).filter((button) => {
393
+ const text = button.text();
394
+
395
+ return text && !isNaN(Number(text));
396
+ });
397
+
398
+ // Second button
399
+ await pageButtons[1].trigger("click");
400
+
401
+ expect(component.emitted("change")).toBeTruthy();
402
+ expect(component.emitted("change")?.[0]).toEqual([2]); // Second button value
403
+ });
381
404
  });
382
405
 
383
406
  describe("Exposed refs", () => {
@@ -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 />