vueless 1.2.8-beta.1 → 1.2.8-beta.3
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/icons/storybook/rocket_launch.svg +1 -0
- package/package.json +1 -1
- package/ui.container-accordion/UAccordion.vue +0 -1
- package/ui.container-accordion/config.ts +1 -1
- package/ui.container-accordion/storybook/stories.ts +13 -1
- package/ui.container-accordion-item/UAccordionItem.vue +17 -4
- package/ui.container-accordion-item/config.ts +1 -1
- package/ui.container-accordion-item/storybook/stories.ts +26 -1
- package/ui.container-accordion-item/tests/UAccordionItem.test.ts +186 -0
- package/ui.form-input/UInput.vue +4 -2
- package/ui.form-input-counter/UInputCounter.vue +25 -1
- package/ui.form-input-counter/config.ts +6 -1
- package/ui.form-input-counter/tests/UInputCounter.test.ts +85 -1
- package/ui.form-input-counter/types.ts +25 -0
- package/ui.form-input-number/UInputNumber.vue +14 -2
- package/ui.form-input-number/utilFormat.ts +17 -7
- package/ui.form-input-password/UInputPassword.vue +23 -1
- package/ui.form-listbox/UListbox.vue +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 -960 960 960"><path d="m188.91-556.74 101.7 43.33q17.28-33.85 36.71-67.18 19.42-33.32 40.98-63.89l-76.37-15.28-103.02 103.02Zm156.39 84.11 131.33 131.56q58.44-27.43 107.48-59.59 49.04-32.17 79.8-62.93 80.05-80.28 117.81-163.84Q819.48-711 823.2-819.2q-108.2 3.72-191.77 41.48-83.56 37.76-163.6 117.81-30.76 30.76-62.93 79.8-32.16 49.04-59.6 107.48Zm231.16-99.59q-19.76-19.76-19.76-48.66 0-28.9 19.76-48.66 19.76-19.53 48.54-19.53 28.78 0 48.54 19.53 19.53 19.76 19.53 48.66 0 28.9-19.53 48.66Q653.78-552.7 625-552.7q-28.78 0-48.54-19.52ZM560.5-184.91l103.26-103.02-15.28-76.37q-30.57 21.56-63.89 41.1-33.33 19.55-67.18 36.59l43.09 101.7Zm331.26-702.61q8.52 143.17-35.08 257.09-43.59 113.91-142.11 212.67-1.48 1.24-2.96 2.84-1.48 1.59-2.72 2.83l22 110.48q3.48 17.15-1.74 33.19-5.22 16.03-17.65 28.46L537.87-65.33l-88.83-206.84-172.87-172.87-206.84-88.83L244.2-707.5q12.43-12.43 28.34-17.65 15.92-5.22 33.07-1.74l110.48 22q1.24-1.24 2.71-2.1 1.48-.86 2.96-2.1 98.76-99 212.79-142.98 114.04-43.97 257.21-35.45ZM141.59-324.04q36.91-36.92 89.56-37.3 52.65-.38 89.33 36.3 36.43 36.43 36.05 89.2-.38 52.77-37.05 89.45-27.68 27.67-85.64 45.63-57.97 17.96-168.64 32 14.04-110.67 31.5-169.26 17.45-58.59 44.89-86.02Zm47.98 48.74q-13.77 14.76-23.93 44.84-10.16 30.09-18.16 78.66 48.56-8 78.65-18.41 30.09-10.4 44.61-24.16 18.28-16.04 18.54-40.35.26-24.3-16.78-42.58-18.28-17.05-42.59-16.55-24.3.5-40.34 18.55Z"/></svg>
|
package/package.json
CHANGED
|
@@ -109,7 +109,6 @@ const { getDataTest, accordionItemAttrs, accordionAttrs } = useUI<Config>(defaul
|
|
|
109
109
|
<UAccordionItem
|
|
110
110
|
v-for="(option, index) in options"
|
|
111
111
|
:key="index"
|
|
112
|
-
:model-value="selectedItem"
|
|
113
112
|
:value="option.value"
|
|
114
113
|
:title="option.title"
|
|
115
114
|
:description="option.description"
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../../utils/storybook";
|
|
9
9
|
|
|
10
10
|
import UAccordion from "../../ui.container-accordion/UAccordion.vue";
|
|
11
|
+
import UAccordionItem from "../../ui.container-accordion-item/UAccordionItem.vue";
|
|
11
12
|
import UButton from "../../ui.button/UButton.vue";
|
|
12
13
|
import ULink from "../../ui.button-link/ULink.vue";
|
|
13
14
|
import UCol from "../../ui.container-col/UCol.vue";
|
|
@@ -68,7 +69,7 @@ export default {
|
|
|
68
69
|
} as Meta;
|
|
69
70
|
|
|
70
71
|
const DefaultTemplate: StoryFn<UAccordionArgs> = (args: UAccordionArgs) => ({
|
|
71
|
-
components: { UAccordion, ULink, UButton, UCol, URow, UIcon },
|
|
72
|
+
components: { UAccordion, UAccordionItem, ULink, UButton, UCol, URow, UIcon },
|
|
72
73
|
setup: () => ({ args, slots: getSlotNames(UAccordion.__name) }),
|
|
73
74
|
template: `
|
|
74
75
|
<UAccordion v-bind="args" v-model="args.modelValue">
|
|
@@ -110,3 +111,14 @@ Sizes.args = {
|
|
|
110
111
|
},
|
|
111
112
|
],
|
|
112
113
|
};
|
|
114
|
+
|
|
115
|
+
export const DefaultSlot: StoryFn<UAccordionArgs> = (args: UAccordionArgs, { argTypes }) => ({
|
|
116
|
+
components: { UAccordion, UAccordionItem },
|
|
117
|
+
setup: () => ({ args, argTypes, getArgs }),
|
|
118
|
+
template: `
|
|
119
|
+
<UAccordion v-model="args.modelValue">
|
|
120
|
+
<UAccordionItem title="Custom Accordion Item 1" description="Custom Accordion Item 1" value="1" />
|
|
121
|
+
<UAccordionItem title="Custom Accordion Item 2" description="Custom Accordion Item 2" value="2" />
|
|
122
|
+
</UAccordion>
|
|
123
|
+
`,
|
|
124
|
+
});
|
|
@@ -122,7 +122,13 @@ const {
|
|
|
122
122
|
<div ref="wrapper" v-bind="wrapperAttrs" :data-test="getDataTest()" @click="onClickItem">
|
|
123
123
|
<div v-bind="bodyAttrs">
|
|
124
124
|
<div v-bind="titleAttrs" ref="title">
|
|
125
|
-
|
|
125
|
+
<!--
|
|
126
|
+
@slot Use it to add custom title content.
|
|
127
|
+
@binding {string} title
|
|
128
|
+
-->
|
|
129
|
+
<slot name="title" :title="title">
|
|
130
|
+
{{ title }}
|
|
131
|
+
</slot>
|
|
126
132
|
<!--
|
|
127
133
|
@slot Use it to add something instead of the toggle icon.
|
|
128
134
|
@binding {string} icon-name
|
|
@@ -140,11 +146,18 @@ const {
|
|
|
140
146
|
</div>
|
|
141
147
|
|
|
142
148
|
<div
|
|
143
|
-
v-if="description"
|
|
149
|
+
v-if="description || hasSlotContent(slots['description'])"
|
|
144
150
|
:id="`description-${elementId}`"
|
|
145
151
|
v-bind="descriptionAttrs"
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
>
|
|
153
|
+
<!--
|
|
154
|
+
@slot Use it to add custom description content.
|
|
155
|
+
@binding {string} description
|
|
156
|
+
-->
|
|
157
|
+
<slot name="description" :description="description">
|
|
158
|
+
{{ description }}
|
|
159
|
+
</slot>
|
|
160
|
+
</div>
|
|
148
161
|
|
|
149
162
|
<div v-if="isOpened && hasSlotContent(slots['default'])" v-bind="contentAttrs">
|
|
150
163
|
<!-- @slot Use it to add accordion content. -->
|
|
@@ -58,11 +58,12 @@ const EnumTemplate: StoryFn<UAccordionItemArgs> = (args: UAccordionItemArgs, { a
|
|
|
58
58
|
components: { UAccordionItem, UCol },
|
|
59
59
|
setup: () => ({ args, argTypes, getArgs }),
|
|
60
60
|
template: `
|
|
61
|
-
<UCol gap="
|
|
61
|
+
<UCol gap="lg">
|
|
62
62
|
<UAccordionItem
|
|
63
63
|
v-for="option in argTypes?.[args.enum]?.options"
|
|
64
64
|
v-bind="getArgs(args, option)"
|
|
65
65
|
:key="option"
|
|
66
|
+
class="py-0"
|
|
66
67
|
/>
|
|
67
68
|
</UCol>
|
|
68
69
|
`,
|
|
@@ -112,3 +113,27 @@ ToggleSlot.args = {
|
|
|
112
113
|
</template>
|
|
113
114
|
`,
|
|
114
115
|
};
|
|
116
|
+
|
|
117
|
+
export const TitleSlot = DefaultTemplate.bind({});
|
|
118
|
+
TitleSlot.args = {
|
|
119
|
+
slotTemplate: `
|
|
120
|
+
<template #title="{ title }">
|
|
121
|
+
<URow gap="xs" align="center">
|
|
122
|
+
<UIcon name="rocket_launch" size="xs" color="primary" />
|
|
123
|
+
<span>{{ title }}</span>
|
|
124
|
+
</URow>
|
|
125
|
+
</template>
|
|
126
|
+
`,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const DescriptionSlot = DefaultTemplate.bind({});
|
|
130
|
+
DescriptionSlot.args = {
|
|
131
|
+
slotTemplate: `
|
|
132
|
+
<template #description="{ description }">
|
|
133
|
+
<URow gap="xs" align="start">
|
|
134
|
+
<UIcon name="info" size="xs" color="accented" />
|
|
135
|
+
<span>{{ description }}</span>
|
|
136
|
+
</URow>
|
|
137
|
+
</template>
|
|
138
|
+
`,
|
|
139
|
+
};
|
|
@@ -176,6 +176,104 @@ describe("UAccordionItem", () => {
|
|
|
176
176
|
expect(toggleElement.attributes("data-opened")).toBe("true");
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
// Title slot
|
|
180
|
+
it("renders custom content in title slot", () => {
|
|
181
|
+
const title = "Original Title";
|
|
182
|
+
const slotClass = "custom-title";
|
|
183
|
+
const slotContent = "Custom Title Content";
|
|
184
|
+
|
|
185
|
+
const component = mount(UAccordionItem, {
|
|
186
|
+
props: { title },
|
|
187
|
+
slots: {
|
|
188
|
+
title: `<div class="${slotClass}">${slotContent}</div>`,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(component.find(`.${slotClass}`).exists()).toBe(true);
|
|
193
|
+
expect(component.find(`.${slotClass}`).text()).toBe(slotContent);
|
|
194
|
+
|
|
195
|
+
expect(component.text()).not.toContain(title);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Title slot bindings
|
|
199
|
+
it("provides title binding to title slot", () => {
|
|
200
|
+
const title = "Test Title";
|
|
201
|
+
const slotClass = "custom-title";
|
|
202
|
+
|
|
203
|
+
const component = mount(UAccordionItem, {
|
|
204
|
+
props: { title },
|
|
205
|
+
slots: {
|
|
206
|
+
title: `
|
|
207
|
+
<template #default="{ title }">
|
|
208
|
+
<div class="${slotClass}" :data-title="title"></div>
|
|
209
|
+
</template>
|
|
210
|
+
`,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const titleElement = component.find(`.${slotClass}`);
|
|
215
|
+
|
|
216
|
+
expect(titleElement.exists()).toBe(true);
|
|
217
|
+
expect(titleElement.attributes("data-title")).toBe(title);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Description slot
|
|
221
|
+
it("renders custom content in description slot", () => {
|
|
222
|
+
const description = "Original Description";
|
|
223
|
+
const slotClass = "custom-description";
|
|
224
|
+
const slotContent = "Custom Description Content";
|
|
225
|
+
|
|
226
|
+
const component = mount(UAccordionItem, {
|
|
227
|
+
props: { description },
|
|
228
|
+
slots: {
|
|
229
|
+
description: `<div class="${slotClass}">${slotContent}</div>`,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(component.find(`.${slotClass}`).exists()).toBe(true);
|
|
234
|
+
expect(component.find(`.${slotClass}`).text()).toBe(slotContent);
|
|
235
|
+
|
|
236
|
+
expect(component.text()).not.toContain(description);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Description slot bindings
|
|
240
|
+
it("provides description binding to description slot", () => {
|
|
241
|
+
const description = "Test Description";
|
|
242
|
+
const slotClass = "custom-description";
|
|
243
|
+
|
|
244
|
+
const component = mount(UAccordionItem, {
|
|
245
|
+
props: { description },
|
|
246
|
+
slots: {
|
|
247
|
+
description: `
|
|
248
|
+
<template #default="{ description }">
|
|
249
|
+
<div class="${slotClass}" :data-description="description"></div>
|
|
250
|
+
</template>
|
|
251
|
+
`,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const descriptionElement = component.find(`.${slotClass}`);
|
|
256
|
+
|
|
257
|
+
expect(descriptionElement.exists()).toBe(true);
|
|
258
|
+
expect(descriptionElement.attributes("data-description")).toBe(description);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Description slot with empty prop
|
|
262
|
+
it("renders description slot even when description prop is empty", () => {
|
|
263
|
+
const slotClass = "custom-description";
|
|
264
|
+
const slotContent = "Custom Description Content";
|
|
265
|
+
|
|
266
|
+
const component = mount(UAccordionItem, {
|
|
267
|
+
props: {},
|
|
268
|
+
slots: {
|
|
269
|
+
description: `<div class="${slotClass}">${slotContent}</div>`,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(component.find(`.${slotClass}`).exists()).toBe(true);
|
|
274
|
+
expect(component.find(`.${slotClass}`).text()).toBe(slotContent);
|
|
275
|
+
});
|
|
276
|
+
|
|
179
277
|
// Default slot
|
|
180
278
|
it("renders default slot content when accordion is opened", async () => {
|
|
181
279
|
const slotContent = "Custom accordion content";
|
|
@@ -221,6 +319,94 @@ describe("UAccordionItem", () => {
|
|
|
221
319
|
|
|
222
320
|
expect(component.find("[vl-key='content']").exists()).toBe(false);
|
|
223
321
|
});
|
|
322
|
+
|
|
323
|
+
// Slot interactions with accordion behavior
|
|
324
|
+
it("title slot content is clickable and toggles accordion", async () => {
|
|
325
|
+
const title = "Test Title";
|
|
326
|
+
const slotClass = "custom-title";
|
|
327
|
+
const slotContent = "Custom Title Content";
|
|
328
|
+
|
|
329
|
+
const component = mount(UAccordionItem, {
|
|
330
|
+
props: { title },
|
|
331
|
+
slots: {
|
|
332
|
+
title: `<div class="${slotClass}">${slotContent}</div>`,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const titleElement = component.find(`.${slotClass}`);
|
|
337
|
+
|
|
338
|
+
expect(titleElement.exists()).toBe(true);
|
|
339
|
+
|
|
340
|
+
await titleElement.trigger("click");
|
|
341
|
+
|
|
342
|
+
const emitted = component.emitted("click");
|
|
343
|
+
|
|
344
|
+
expect(emitted).toBeTruthy();
|
|
345
|
+
expect(emitted?.[0]).toEqual([expect.any(String), true]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("description slot content is not clickable and does not toggle accordion", async () => {
|
|
349
|
+
const description = "Test Description";
|
|
350
|
+
const slotClass = "custom-description";
|
|
351
|
+
const slotContent = "Custom Description Content";
|
|
352
|
+
|
|
353
|
+
const component = mount(UAccordionItem, {
|
|
354
|
+
props: { description },
|
|
355
|
+
slots: {
|
|
356
|
+
description: `<div class="${slotClass}">${slotContent}</div>`,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const descriptionElement = component.find(`.${slotClass}`);
|
|
361
|
+
|
|
362
|
+
expect(descriptionElement.exists()).toBe(true);
|
|
363
|
+
|
|
364
|
+
await descriptionElement.trigger("click");
|
|
365
|
+
|
|
366
|
+
const emitted = component.emitted("click");
|
|
367
|
+
|
|
368
|
+
expect(emitted).toBeFalsy();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("title slot preserves accordion toggle functionality", async () => {
|
|
372
|
+
const title = "Test Title";
|
|
373
|
+
const description = "Test Description";
|
|
374
|
+
const slotClass = "custom-title";
|
|
375
|
+
const slotContent = "Custom Title Content";
|
|
376
|
+
|
|
377
|
+
const component = mount(UAccordionItem, {
|
|
378
|
+
props: { title, description },
|
|
379
|
+
slots: {
|
|
380
|
+
title: `<div class="${slotClass}">${slotContent}</div>`,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
expect(component.find("[id^='description-']").classes()).not.toContain("opacity-100");
|
|
385
|
+
|
|
386
|
+
await component.find(`.${slotClass}`).trigger("click");
|
|
387
|
+
expect(component.find("[id^='description-']").classes()).toContain("opacity-100");
|
|
388
|
+
|
|
389
|
+
await component.find(`.${slotClass}`).trigger("click");
|
|
390
|
+
expect(component.find("[id^='description-']").classes()).not.toContain("opacity-100");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("description slot content is always visible when slot is provided", () => {
|
|
394
|
+
const slotClass = "custom-description";
|
|
395
|
+
const slotContent = "Custom Description Content";
|
|
396
|
+
|
|
397
|
+
const component = mount(UAccordionItem, {
|
|
398
|
+
props: {},
|
|
399
|
+
slots: {
|
|
400
|
+
description: `<div class="${slotClass}">${slotContent}</div>`,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Description slot content should be visible even without description prop
|
|
405
|
+
const descriptionElement = component.find(`.${slotClass}`);
|
|
406
|
+
|
|
407
|
+
expect(descriptionElement.exists()).toBe(true);
|
|
408
|
+
expect(descriptionElement.text()).toBe(slotContent);
|
|
409
|
+
});
|
|
224
410
|
});
|
|
225
411
|
|
|
226
412
|
// Events
|
package/ui.form-input/UInput.vue
CHANGED
|
@@ -25,7 +25,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
25
25
|
|
|
26
26
|
const emit = defineEmits([
|
|
27
27
|
/**
|
|
28
|
-
* Triggers when the input value is
|
|
28
|
+
* Triggers when the input value is changed.
|
|
29
29
|
* @property {string} modelValue
|
|
30
30
|
* @property {number} modelValue
|
|
31
31
|
*/
|
|
@@ -43,6 +43,7 @@ const emit = defineEmits([
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Triggers when the input gains focus.
|
|
46
|
+
* @property {FocusEvent} event
|
|
46
47
|
*/
|
|
47
48
|
"focus",
|
|
48
49
|
|
|
@@ -53,11 +54,12 @@ const emit = defineEmits([
|
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
56
|
* Triggers when the input loses focus.
|
|
57
|
+
* @property {FocusEvent} event
|
|
56
58
|
*/
|
|
57
59
|
"blur",
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
|
-
* Triggers when the input value is
|
|
62
|
+
* Triggers when the input value is changed.
|
|
61
63
|
* @property {string} modelValue
|
|
62
64
|
* @property {number} modelValue
|
|
63
65
|
*/
|
|
@@ -27,6 +27,18 @@ const emit = defineEmits([
|
|
|
27
27
|
* @property {number} modelValue
|
|
28
28
|
*/
|
|
29
29
|
"update:modelValue",
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Triggers when the input gains focus.
|
|
33
|
+
* @property {FocusEvent} event
|
|
34
|
+
*/
|
|
35
|
+
"focus",
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Triggers when the input loses focus.
|
|
39
|
+
* @property {FocusEvent} event
|
|
40
|
+
*/
|
|
41
|
+
"blur",
|
|
30
42
|
]);
|
|
31
43
|
|
|
32
44
|
const inputComponentRef = useTemplateRef<InstanceType<typeof UInput>>("inputComponent");
|
|
@@ -120,9 +132,15 @@ function onMouseLeave() {
|
|
|
120
132
|
clearIntervals();
|
|
121
133
|
}
|
|
122
134
|
|
|
123
|
-
function
|
|
135
|
+
function onFocus(event: FocusEvent) {
|
|
136
|
+
emit("focus", event);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function onBlur(event: FocusEvent) {
|
|
124
140
|
if (Number(count.value) > props.max) count.value = props.max;
|
|
125
141
|
if (Number(count.value) < props.min) count.value = props.min;
|
|
142
|
+
|
|
143
|
+
emit("blur", event);
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
function onInput() {
|
|
@@ -187,7 +205,13 @@ const {
|
|
|
187
205
|
:size="size"
|
|
188
206
|
:disabled="disabled"
|
|
189
207
|
:readonly="readonly"
|
|
208
|
+
:min-fraction-digits="minFractionDigits"
|
|
209
|
+
:max-fraction-digits="maxFractionDigits"
|
|
210
|
+
:decimal-separator="decimalSeparator"
|
|
211
|
+
:thousands-separator="thousandsSeparator"
|
|
212
|
+
:prefix="prefix"
|
|
190
213
|
v-bind="counterInputAttrs"
|
|
214
|
+
@focus="onFocus"
|
|
191
215
|
@blur="onBlur"
|
|
192
216
|
@input="onInput"
|
|
193
217
|
/>
|
|
@@ -33,9 +33,14 @@ export default /*tw*/ {
|
|
|
33
33
|
subtractIcon: "{UIcon}",
|
|
34
34
|
defaults: {
|
|
35
35
|
size: "md",
|
|
36
|
+
decimalSeparator: ",",
|
|
37
|
+
thousandsSeparator: " ",
|
|
38
|
+
prefix: "",
|
|
36
39
|
step: 1,
|
|
37
|
-
min:
|
|
40
|
+
min: 0,
|
|
38
41
|
max: 999,
|
|
42
|
+
minFractionDigits: 0,
|
|
43
|
+
maxFractionDigits: 2,
|
|
39
44
|
readonly: false,
|
|
40
45
|
disabled: false,
|
|
41
46
|
/* icons */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mount } from "@vue/test-utils";
|
|
1
|
+
import { flushPromises, mount } from "@vue/test-utils";
|
|
2
2
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
3
|
|
|
4
4
|
import UInputCounter from "../UInputCounter.vue";
|
|
@@ -169,6 +169,90 @@ describe("UInputCounter.vue", () => {
|
|
|
169
169
|
expect(addButton.props("disabled")).toBe(true);
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
it("Min Fraction Digit – set fraction digit when it was not provided", async () => {
|
|
173
|
+
const initialValue = 12345678;
|
|
174
|
+
const initialValueWithFactions = "12 345 678,00";
|
|
175
|
+
|
|
176
|
+
const component = mount(UInputCounter, {
|
|
177
|
+
props: {
|
|
178
|
+
modelValue: initialValue,
|
|
179
|
+
minFractionDigits: 2,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await flushPromises();
|
|
184
|
+
|
|
185
|
+
expect(component.get("input").element.value).toBe(initialValueWithFactions);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("Max Fraction Digit – truncate fraction digit to max value", async () => {
|
|
189
|
+
const initialValue = 12345678.011;
|
|
190
|
+
const initialValueWithFactions = "12 345 678,01";
|
|
191
|
+
|
|
192
|
+
const component = mount(UInputCounter, {
|
|
193
|
+
props: {
|
|
194
|
+
modelValue: initialValue,
|
|
195
|
+
maxFractionDigits: 2,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await flushPromises();
|
|
200
|
+
|
|
201
|
+
expect(component.get("input").element.value).toBe(initialValueWithFactions);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("Decimal Separator – set correct decimal separator char", async () => {
|
|
205
|
+
const initialValue = 12345678.01;
|
|
206
|
+
const expectedDecimalSubString = "/01";
|
|
207
|
+
|
|
208
|
+
const component = mount(UInputCounter, {
|
|
209
|
+
props: {
|
|
210
|
+
modelValue: initialValue,
|
|
211
|
+
decimalSeparator: "/",
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await flushPromises();
|
|
216
|
+
|
|
217
|
+
expect(component.get("input").element.value).toContain(expectedDecimalSubString);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("Thousands Separator – set correct decimal separator char", async () => {
|
|
221
|
+
const initialValue = 12_345_678.01;
|
|
222
|
+
const expectedThousandsSeparator = "/";
|
|
223
|
+
const expectedThousandsSeparatorAmount = 2;
|
|
224
|
+
|
|
225
|
+
const component = mount(UInputCounter, {
|
|
226
|
+
props: {
|
|
227
|
+
modelValue: initialValue,
|
|
228
|
+
thousandsSeparator: expectedThousandsSeparator,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await flushPromises();
|
|
233
|
+
|
|
234
|
+
const inputValue = component.get("input").element.value;
|
|
235
|
+
const separatorCount = inputValue.split(expectedThousandsSeparator).length - 1;
|
|
236
|
+
|
|
237
|
+
expect(separatorCount).toBe(expectedThousandsSeparatorAmount);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("Prefix – displays prefix at the beginning of the value", async () => {
|
|
241
|
+
const initialValue = 12345678;
|
|
242
|
+
const prefix = "pizza";
|
|
243
|
+
|
|
244
|
+
const component = mount(UInputCounter, {
|
|
245
|
+
props: {
|
|
246
|
+
modelValue: initialValue,
|
|
247
|
+
prefix: prefix,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await flushPromises();
|
|
252
|
+
|
|
253
|
+
expect(component.get("input").element.value.startsWith(prefix)).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
172
256
|
it("Readonly – renders text instead of input when readonly set to true", async () => {
|
|
173
257
|
const component = mount(UInputCounter, {
|
|
174
258
|
props: {
|
|
@@ -25,6 +25,31 @@ export interface Props {
|
|
|
25
25
|
*/
|
|
26
26
|
max?: number;
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Minimal number of signs after the decimal separator (fractional part of a number).
|
|
30
|
+
*/
|
|
31
|
+
minFractionDigits?: number;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Maximal number of signs after the decimal separator (fractional part of a number).
|
|
35
|
+
*/
|
|
36
|
+
maxFractionDigits?: number;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A symbol used to separate the integer part from the fractional part of a number.
|
|
40
|
+
*/
|
|
41
|
+
decimalSeparator?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A symbol used to separate the thousand parts of a number.
|
|
45
|
+
*/
|
|
46
|
+
thousandsSeparator?: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Prefix to display before input value.
|
|
50
|
+
*/
|
|
51
|
+
prefix?: string;
|
|
52
|
+
|
|
28
53
|
/**
|
|
29
54
|
* Input size.
|
|
30
55
|
*/
|
|
@@ -11,7 +11,7 @@ import useFormatNumber from "./useFormatNumber";
|
|
|
11
11
|
import { COMPONENT_NAME, RAW_DECIMAL_MARK } from "./constants";
|
|
12
12
|
|
|
13
13
|
import type { Props, Config } from "./types";
|
|
14
|
-
import { getRawValue } from "./utilFormat";
|
|
14
|
+
import { getRawValue, getFixedNumber } from "./utilFormat";
|
|
15
15
|
|
|
16
16
|
defineOptions({ inheritAttrs: false });
|
|
17
17
|
|
|
@@ -41,8 +41,15 @@ const emit = defineEmits([
|
|
|
41
41
|
*/
|
|
42
42
|
"keyup",
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Triggers when the input gains focus.
|
|
46
|
+
* @property {FocusEvent} event
|
|
47
|
+
*/
|
|
48
|
+
"focus",
|
|
49
|
+
|
|
44
50
|
/**
|
|
45
51
|
* Triggers when the input loses focus.
|
|
52
|
+
* @property {FocusEvent} event
|
|
46
53
|
*/
|
|
47
54
|
"blur",
|
|
48
55
|
]);
|
|
@@ -103,13 +110,17 @@ onMounted(() => {
|
|
|
103
110
|
});
|
|
104
111
|
|
|
105
112
|
function onKeyup(event: KeyboardEvent) {
|
|
106
|
-
const numberValue =
|
|
113
|
+
const numberValue = getFixedNumber(parseFloat(rawValue.value), props.maxFractionDigits || 10);
|
|
107
114
|
|
|
108
115
|
localValue.value = props.valueType === "number" ? numberValue : rawValue.value || "";
|
|
109
116
|
|
|
110
117
|
emit("keyup", event);
|
|
111
118
|
}
|
|
112
119
|
|
|
120
|
+
function onFocus(event: FocusEvent) {
|
|
121
|
+
emit("focus", event);
|
|
122
|
+
}
|
|
123
|
+
|
|
113
124
|
function onBlur(event: FocusEvent) {
|
|
114
125
|
emit("blur", event);
|
|
115
126
|
}
|
|
@@ -164,6 +175,7 @@ const { getDataTest, numberInputAttrs } = useUI<Config>(defaultConfig);
|
|
|
164
175
|
v-bind="numberInputAttrs"
|
|
165
176
|
:data-test="getDataTest()"
|
|
166
177
|
@keyup="onKeyup"
|
|
178
|
+
@focus="onFocus"
|
|
167
179
|
@blur="onBlur"
|
|
168
180
|
@input="onInput"
|
|
169
181
|
>
|
|
@@ -2,6 +2,13 @@ import { RAW_DECIMAL_MARK } from "./constants";
|
|
|
2
2
|
|
|
3
3
|
import type { FormatOptions } from "./types";
|
|
4
4
|
|
|
5
|
+
export function getFixedNumber(value: number, maxDecimals: number = 10): number | "" {
|
|
6
|
+
if (!isFinite(value) || isNaN(value)) return "";
|
|
7
|
+
if (maxDecimals < 0) maxDecimals = 10;
|
|
8
|
+
|
|
9
|
+
return parseFloat(value.toFixed(maxDecimals));
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export function getRawValue(
|
|
6
13
|
value: string,
|
|
7
14
|
options: Pick<FormatOptions, "prefix" | "decimalSeparator" | "thousandsSeparator">,
|
|
@@ -26,6 +33,11 @@ export function getFormattedValue(value: string, options: FormatOptions): string
|
|
|
26
33
|
const isValidMinFractionDigits = minFractionDigits <= maxFractionDigits;
|
|
27
34
|
const actualMinFractionDigit = isValidMinFractionDigits ? minFractionDigits : maxFractionDigits;
|
|
28
35
|
|
|
36
|
+
const numericValue = parseFloat(value);
|
|
37
|
+
const fixedValue = getFixedNumber(numericValue, Math.max(maxFractionDigits, 10));
|
|
38
|
+
|
|
39
|
+
if (fixedValue === "") return prefix;
|
|
40
|
+
|
|
29
41
|
const intlNumberOptions: Intl.NumberFormatOptions = {
|
|
30
42
|
minimumFractionDigits: actualMinFractionDigit,
|
|
31
43
|
maximumFractionDigits: maxFractionDigits,
|
|
@@ -38,14 +50,12 @@ export function getFormattedValue(value: string, options: FormatOptions): string
|
|
|
38
50
|
|
|
39
51
|
const intlNumber = new Intl.NumberFormat("en-US", intlNumberOptions);
|
|
40
52
|
|
|
41
|
-
const formattedValue = intlNumber
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
if (part.type === "group") part.value = thousandsSeparator;
|
|
45
|
-
if (part.type === "decimal") part.value = decimalSeparator;
|
|
53
|
+
const formattedValue = intlNumber.formatToParts(fixedValue).map((part) => {
|
|
54
|
+
if (part.type === "group") part.value = thousandsSeparator;
|
|
55
|
+
if (part.type === "decimal") part.value = decimalSeparator;
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
return part;
|
|
58
|
+
});
|
|
49
59
|
|
|
50
60
|
formattedValue.unshift({ value: prefix, type: "minusSign" });
|
|
51
61
|
|
|
@@ -21,11 +21,23 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
21
21
|
|
|
22
22
|
const emit = defineEmits([
|
|
23
23
|
/**
|
|
24
|
-
* Triggers when the input value is
|
|
24
|
+
* Triggers when the input value is changed.
|
|
25
25
|
* @property {string} modelValue
|
|
26
26
|
* @property {number} modelValue
|
|
27
27
|
*/
|
|
28
28
|
"update:modelValue",
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Triggers when the input gains focus.
|
|
32
|
+
* @property {FocusEvent} event
|
|
33
|
+
*/
|
|
34
|
+
"focus",
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Triggers when the input loses focus.
|
|
38
|
+
* @property {FocusEvent} event
|
|
39
|
+
*/
|
|
40
|
+
"blur",
|
|
29
41
|
]);
|
|
30
42
|
|
|
31
43
|
const elementId = props.id || useId();
|
|
@@ -51,6 +63,14 @@ function onClickShowPassword() {
|
|
|
51
63
|
isShownPassword.value = !isShownPassword.value;
|
|
52
64
|
}
|
|
53
65
|
|
|
66
|
+
function onFocus(event: FocusEvent) {
|
|
67
|
+
emit("focus", event);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function onBlur(event: FocusEvent) {
|
|
71
|
+
emit("blur", event);
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
/**
|
|
55
75
|
* Get element / nested component attributes for each config token ✨
|
|
56
76
|
* Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
|
|
@@ -80,6 +100,8 @@ const { getDataTest, config, passwordInputAttrs, passwordIconAttrs, passwordIcon
|
|
|
80
100
|
:disabled="disabled"
|
|
81
101
|
v-bind="passwordInputAttrs"
|
|
82
102
|
:data-test="getDataTest()"
|
|
103
|
+
@focus="onFocus"
|
|
104
|
+
@blur="onBlur"
|
|
83
105
|
>
|
|
84
106
|
<template #left>
|
|
85
107
|
<!--
|