vueless 1.2.8 → 1.2.10-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/constants.d.ts +4 -0
- package/constants.js +4 -0
- package/icons/storybook/rocket_launch.svg +1 -0
- package/index.d.ts +3 -1
- package/index.ts +3 -1
- package/package.json +9 -5
- package/plugin-vite.js +6 -1
- package/types.ts +14 -2
- package/ui.button/config.ts +4 -4
- package/ui.button/tests/UButton.test.ts +3 -3
- package/ui.button-toggle/config.ts +2 -2
- 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.container-card/config.ts +1 -1
- package/ui.data-table/config.ts +4 -4
- package/ui.dropdown-badge/UDropdownBadge.vue +68 -3
- package/ui.dropdown-badge/config.ts +5 -1
- package/ui.dropdown-badge/storybook/stories.ts +280 -4
- package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +194 -0
- package/ui.dropdown-badge/types.ts +30 -0
- package/ui.dropdown-button/UDropdownButton.vue +69 -6
- package/ui.dropdown-button/config.ts +5 -1
- package/ui.dropdown-button/storybook/stories.ts +288 -3
- package/ui.dropdown-button/tests/UDropdownButton.test.ts +190 -0
- package/ui.dropdown-button/types.ts +30 -0
- package/ui.dropdown-link/UDropdownLink.vue +69 -6
- package/ui.dropdown-link/config.ts +5 -1
- package/ui.dropdown-link/storybook/stories.ts +281 -4
- package/ui.dropdown-link/tests/UDropdownLink.test.ts +194 -0
- package/ui.dropdown-link/types.ts +30 -0
- package/ui.form-calendar/config.ts +4 -2
- package/ui.form-checkbox/config.ts +1 -1
- package/ui.form-checkbox/tests/UCheckbox.test.ts +2 -2
- package/ui.form-checkbox-group/tests/UCheckboxGroup.test.ts +2 -2
- package/ui.form-date-picker-range/config.ts +1 -1
- package/ui.form-input/UInput.vue +4 -2
- package/ui.form-input/config.ts +1 -1
- package/ui.form-input/tests/UInput.test.ts +2 -2
- package/ui.form-input-counter/UInputCounter.vue +25 -1
- package/ui.form-input-counter/config.ts +7 -2
- package/ui.form-input-counter/tests/UInputCounter.test.ts +85 -1
- package/ui.form-input-counter/types.ts +25 -0
- package/ui.form-input-file/tests/UInputFile.test.ts +2 -2
- package/ui.form-input-number/UInputNumber.vue +15 -3
- package/ui.form-input-number/utilFormat.ts +17 -7
- package/ui.form-input-password/UInputPassword.vue +23 -1
- package/ui.form-label/ULabel.vue +10 -4
- package/ui.form-label/tests/ULabel.test.ts +29 -12
- package/ui.form-listbox/UListbox.vue +21 -9
- package/ui.form-listbox/config.ts +1 -1
- package/ui.form-listbox/storybook/stories.ts +188 -1
- package/ui.form-listbox/tests/UListbox.test.ts +36 -0
- package/ui.form-listbox/types.ts +5 -0
- package/ui.form-radio/config.ts +1 -1
- package/ui.form-radio/tests/URadio.test.ts +2 -2
- package/ui.form-radio-group/tests/URadioGroup.test.ts +2 -2
- package/ui.form-select/USelect.vue +20 -2
- package/ui.form-select/config.ts +2 -1
- package/ui.form-select/storybook/stories.ts +31 -4
- package/ui.form-select/tests/USelect.test.ts +143 -0
- package/ui.form-select/types.ts +10 -0
- package/ui.form-textarea/config.ts +1 -1
- package/ui.form-textarea/tests/UTextarea.test.ts +2 -2
- package/ui.text-alert/config.ts +1 -1
- package/ui.text-badge/config.ts +1 -1
- package/utils/helper.ts +4 -0
- package/utils/node/dynamicProps.d.ts +5 -2
- package/utils/node/dynamicProps.js +126 -53
- package/utils/node/helper.d.ts +10 -7
- package/utils/node/helper.js +59 -2
- package/utils/node/tailwindSafelist.js +9 -2
- package/utils/theme.ts +75 -31
- package/utils/ui.ts +32 -3
package/ui.form-input/config.ts
CHANGED
|
@@ -12,7 +12,7 @@ export default /*tw*/ {
|
|
|
12
12
|
wrapper: {
|
|
13
13
|
base: `
|
|
14
14
|
flex gap-3 w-full px-3 relative bg-default transition
|
|
15
|
-
border rounded-medium border-default outline-transparent
|
|
15
|
+
border border-solid rounded-medium border-default outline-transparent
|
|
16
16
|
hover:border-lifted hover:focus-within:border-primary focus-within:border-primary
|
|
17
17
|
focus-within:outline focus-within:outline-small focus-within:outline-primary focus-within:transition
|
|
18
18
|
`,
|
|
@@ -318,7 +318,7 @@ describe("UInput.vue", () => {
|
|
|
318
318
|
});
|
|
319
319
|
|
|
320
320
|
const labelComponent = component.getComponent(ULabel);
|
|
321
|
-
const labelElement = labelComponent.find("label");
|
|
321
|
+
const labelElement = labelComponent.find("[vl-child-key='label']");
|
|
322
322
|
|
|
323
323
|
expect(labelElement.text()).toBe(customLabelContent);
|
|
324
324
|
});
|
|
@@ -336,7 +336,7 @@ describe("UInput.vue", () => {
|
|
|
336
336
|
});
|
|
337
337
|
|
|
338
338
|
const labelComponent = component.getComponent(ULabel);
|
|
339
|
-
const labelElement = labelComponent.find("label");
|
|
339
|
+
const labelElement = labelComponent.find("[vl-child-key='label']");
|
|
340
340
|
|
|
341
341
|
expect(labelElement.text()).toBe(`Modified ${defaultLabel}`);
|
|
342
342
|
});
|
|
@@ -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
|
/>
|
|
@@ -10,7 +10,7 @@ export default /*tw*/ {
|
|
|
10
10
|
},
|
|
11
11
|
},
|
|
12
12
|
counterInput: {
|
|
13
|
-
base: "{UInputNumber} w-
|
|
13
|
+
base: "{UInputNumber} w-inherit",
|
|
14
14
|
numberInput: {
|
|
15
15
|
base: "{UInput}",
|
|
16
16
|
input: "text-center",
|
|
@@ -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
|
*/
|
|
@@ -269,7 +269,7 @@ describe("UInputFile.vue", () => {
|
|
|
269
269
|
});
|
|
270
270
|
|
|
271
271
|
const labelComponent = component.getComponent(ULabel);
|
|
272
|
-
const labelElement = labelComponent.find("label");
|
|
272
|
+
const labelElement = labelComponent.find("[vl-child-key='label']");
|
|
273
273
|
|
|
274
274
|
expect(labelElement.text()).toBe(customLabelContent);
|
|
275
275
|
});
|
|
@@ -287,7 +287,7 @@ describe("UInputFile.vue", () => {
|
|
|
287
287
|
});
|
|
288
288
|
|
|
289
289
|
const labelComponent = component.getComponent(ULabel);
|
|
290
|
-
const labelElement = labelComponent.find("label");
|
|
290
|
+
const labelElement = labelComponent.find("[vl-child-key='label']");
|
|
291
291
|
|
|
292
292
|
expect(labelElement.text()).toBe(`Custom ${defaultLabel}`);
|
|
293
293
|
});
|
|
@@ -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
|
]);
|
|
@@ -97,19 +104,23 @@ watch(
|
|
|
97
104
|
);
|
|
98
105
|
|
|
99
106
|
onMounted(() => {
|
|
100
|
-
if (localValue.value) {
|
|
107
|
+
if (localValue.value || localValue.value === 0) {
|
|
101
108
|
setValue(stringLocalValue.value);
|
|
102
109
|
}
|
|
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
|
<!--
|
package/ui.form-label/ULabel.vue
CHANGED
|
@@ -33,6 +33,10 @@ const isHorizontalPlacement = computed(() => {
|
|
|
33
33
|
return props.align === PLACEMENT.left || props.align === PLACEMENT.right;
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
const tag = computed(() => {
|
|
37
|
+
return props.for ? "label" : "div";
|
|
38
|
+
});
|
|
39
|
+
|
|
36
40
|
const isTopWithDescPlacement = computed(() => {
|
|
37
41
|
return props.align === PLACEMENT.topWithDesc;
|
|
38
42
|
});
|
|
@@ -95,7 +99,8 @@ const { getDataTest, wrapperAttrs, contentAttrs, labelAttrs, descriptionAttrs, e
|
|
|
95
99
|
|
|
96
100
|
<!-- `v-bind` isn't assigned, because the div is system -->
|
|
97
101
|
<div v-if="label || hasSlotContent(slots['label'], { label }) || error || description">
|
|
98
|
-
<
|
|
102
|
+
<component
|
|
103
|
+
:is="tag"
|
|
99
104
|
v-if="label || hasSlotContent(slots['label'], { label })"
|
|
100
105
|
ref="label"
|
|
101
106
|
:for="props.for"
|
|
@@ -110,7 +115,7 @@ const { getDataTest, wrapperAttrs, contentAttrs, labelAttrs, descriptionAttrs, e
|
|
|
110
115
|
<slot name="label" :label="label">
|
|
111
116
|
{{ label }}
|
|
112
117
|
</slot>
|
|
113
|
-
</
|
|
118
|
+
</component>
|
|
114
119
|
|
|
115
120
|
<div
|
|
116
121
|
v-if="isShownError"
|
|
@@ -132,7 +137,8 @@ const { getDataTest, wrapperAttrs, contentAttrs, labelAttrs, descriptionAttrs, e
|
|
|
132
137
|
</div>
|
|
133
138
|
|
|
134
139
|
<div v-else ref="wrapper" v-bind="wrapperAttrs">
|
|
135
|
-
<
|
|
140
|
+
<component
|
|
141
|
+
:is="tag"
|
|
136
142
|
v-if="label || hasSlotContent(slots['label'], { label })"
|
|
137
143
|
v-bind="labelAttrs"
|
|
138
144
|
ref="label"
|
|
@@ -147,7 +153,7 @@ const { getDataTest, wrapperAttrs, contentAttrs, labelAttrs, descriptionAttrs, e
|
|
|
147
153
|
<slot name="label" :label="label">
|
|
148
154
|
{{ label }}
|
|
149
155
|
</slot>
|
|
150
|
-
</
|
|
156
|
+
</component>
|
|
151
157
|
|
|
152
158
|
<div v-bind="contentAttrs" :data-test="getDataTest('content')">
|
|
153
159
|
<!-- @slot Use it to add label content. -->
|
|
@@ -16,7 +16,7 @@ describe("ULabel.vue", () => {
|
|
|
16
16
|
},
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
expect(component.
|
|
19
|
+
expect(component.find("[vl-key='label']").text()).toContain(label);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("For – applied to label element", () => {
|
|
@@ -29,7 +29,7 @@ describe("ULabel.vue", () => {
|
|
|
29
29
|
},
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
expect(component.
|
|
32
|
+
expect(component.find("[vl-key='label']").attributes("for")).toBe(forId);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
it("For – applies interactive classes when true", () => {
|
|
@@ -42,7 +42,7 @@ describe("ULabel.vue", () => {
|
|
|
42
42
|
},
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
expect(component.
|
|
45
|
+
expect(component.find("[vl-key='label']").attributes("class")).toContain(interactiveClasses);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
it("Description – description is rendered with provided text", () => {
|
|
@@ -133,7 +133,7 @@ describe("ULabel.vue", () => {
|
|
|
133
133
|
},
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
expect(component.
|
|
136
|
+
expect(component.find("[vl-key='label']").attributes("class")).toContain(expectedClass);
|
|
137
137
|
});
|
|
138
138
|
});
|
|
139
139
|
|
|
@@ -148,7 +148,7 @@ describe("ULabel.vue", () => {
|
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
expect(component.attributes("class")).toContain(disabledClasses);
|
|
151
|
-
expect(component.
|
|
151
|
+
expect(component.find("[vl-key='label']").attributes("class")).toContain(disabledClasses);
|
|
152
152
|
});
|
|
153
153
|
|
|
154
154
|
it("Disabled – does not apply disabled class when false", () => {
|
|
@@ -162,7 +162,7 @@ describe("ULabel.vue", () => {
|
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
expect(component.attributes("class")).not.toContain(disabledClasses);
|
|
165
|
-
expect(component.
|
|
165
|
+
expect(component.find("[vl-key='label']").attributes("class")).not.toContain(disabledClasses);
|
|
166
166
|
});
|
|
167
167
|
|
|
168
168
|
it("Disabled – interactive class is not applied when disabled true", () => {
|
|
@@ -176,7 +176,9 @@ describe("ULabel.vue", () => {
|
|
|
176
176
|
},
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
expect(component.
|
|
179
|
+
expect(component.find("[vl-key='label']").attributes("class")).not.toContain(
|
|
180
|
+
interactiveClasses,
|
|
181
|
+
);
|
|
180
182
|
});
|
|
181
183
|
|
|
182
184
|
it("Disabled – topInside label doesn't have focus styles disabled", () => {
|
|
@@ -190,7 +192,7 @@ describe("ULabel.vue", () => {
|
|
|
190
192
|
},
|
|
191
193
|
});
|
|
192
194
|
|
|
193
|
-
expect(component.
|
|
195
|
+
expect(component.find("[vl-key='label']").attributes("class")).not.toContain(focusClasses);
|
|
194
196
|
});
|
|
195
197
|
|
|
196
198
|
it("Disabled – error is not rendered when disabled is true", () => {
|
|
@@ -294,7 +296,7 @@ describe("ULabel.vue", () => {
|
|
|
294
296
|
},
|
|
295
297
|
});
|
|
296
298
|
|
|
297
|
-
const labelElement = component.
|
|
299
|
+
const labelElement = component.find("[vl-key='label']");
|
|
298
300
|
|
|
299
301
|
expect(labelElement.text()).toBe(slotContent);
|
|
300
302
|
expect(labelElement.text()).not.toContain(defaultLabel);
|
|
@@ -313,7 +315,7 @@ describe("ULabel.vue", () => {
|
|
|
313
315
|
},
|
|
314
316
|
});
|
|
315
317
|
|
|
316
|
-
expect(component.
|
|
318
|
+
expect(component.find("[vl-key='label']").text()).toBe(slotContent);
|
|
317
319
|
});
|
|
318
320
|
|
|
319
321
|
it("Bottom – renders content from bottom slot", () => {
|
|
@@ -337,7 +339,7 @@ describe("ULabel.vue", () => {
|
|
|
337
339
|
},
|
|
338
340
|
});
|
|
339
341
|
|
|
340
|
-
const labelElement = component.
|
|
342
|
+
const labelElement = component.find("[vl-key='label']");
|
|
341
343
|
|
|
342
344
|
await labelElement.trigger("click");
|
|
343
345
|
|
|
@@ -346,7 +348,7 @@ describe("ULabel.vue", () => {
|
|
|
346
348
|
});
|
|
347
349
|
|
|
348
350
|
describe("Exposed properties", () => {
|
|
349
|
-
it("Exposes label element ref", () => {
|
|
351
|
+
it("Exposes label element ref as a div", () => {
|
|
350
352
|
const defaultLabel = "Label";
|
|
351
353
|
|
|
352
354
|
const component = mount(ULabel, {
|
|
@@ -355,6 +357,21 @@ describe("ULabel.vue", () => {
|
|
|
355
357
|
},
|
|
356
358
|
});
|
|
357
359
|
|
|
360
|
+
expect(component.vm.labelElement).toBeDefined();
|
|
361
|
+
expect(component.vm.labelElement!.tagName).toBe("DIV");
|
|
362
|
+
expect(component.vm.labelElement!.textContent).toBe(defaultLabel);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("Exposes label element ref as a label", () => {
|
|
366
|
+
const defaultLabel = "Label";
|
|
367
|
+
|
|
368
|
+
const component = mount(ULabel, {
|
|
369
|
+
props: {
|
|
370
|
+
label: defaultLabel,
|
|
371
|
+
for: "test-id",
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
|
|
358
375
|
expect(component.vm.labelElement).toBeDefined();
|
|
359
376
|
expect(component.vm.labelElement!.tagName).toBe("LABEL");
|
|
360
377
|
expect(component.vm.labelElement!.textContent).toBe(defaultLabel);
|
|
@@ -49,6 +49,7 @@ const emit = defineEmits([
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Triggers when the search input value changes.
|
|
52
|
+
* @property {string} value
|
|
52
53
|
*/
|
|
53
54
|
"searchChange",
|
|
54
55
|
|
|
@@ -56,6 +57,12 @@ const emit = defineEmits([
|
|
|
56
57
|
* Triggers when the search input loses focus.
|
|
57
58
|
*/
|
|
58
59
|
"searchBlur",
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Triggers when the search v-model updates.
|
|
63
|
+
* @property {string} query
|
|
64
|
+
*/
|
|
65
|
+
"update:search",
|
|
59
66
|
]);
|
|
60
67
|
|
|
61
68
|
const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
|
|
@@ -66,7 +73,7 @@ const addOptionRef = useTemplateRef<HTMLLIElement>("add-option");
|
|
|
66
73
|
|
|
67
74
|
const wrapperMaxHeight = ref("");
|
|
68
75
|
|
|
69
|
-
const
|
|
76
|
+
const localSearch = ref(props.search ?? "");
|
|
70
77
|
|
|
71
78
|
const { pointer, pointerDirty, pointerSet, pointerBackward, pointerForward, pointerReset } =
|
|
72
79
|
usePointer(props.options, optionsRef, wrapperRef);
|
|
@@ -79,6 +86,15 @@ const { localeMessages } = useComponentLocaleMessages<typeof defaultConfig.i18n>
|
|
|
79
86
|
props?.config?.i18n,
|
|
80
87
|
);
|
|
81
88
|
|
|
89
|
+
const searchModel = computed({
|
|
90
|
+
get: () => localSearch.value,
|
|
91
|
+
set: (value: string) => {
|
|
92
|
+
emit("update:search", value);
|
|
93
|
+
|
|
94
|
+
localSearch.value = value ?? "";
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
82
98
|
const selectedValue = computed({
|
|
83
99
|
get: () => {
|
|
84
100
|
if (props.multiple && !Array.isArray(props.modelValue)) {
|
|
@@ -87,11 +103,7 @@ const selectedValue = computed({
|
|
|
87
103
|
|
|
88
104
|
return props.modelValue;
|
|
89
105
|
},
|
|
90
|
-
set: (value) =>
|
|
91
|
-
if (search.value) search.value = "";
|
|
92
|
-
|
|
93
|
-
emit("update:modelValue", value);
|
|
94
|
-
},
|
|
106
|
+
set: (value) => emit("update:modelValue", value),
|
|
95
107
|
});
|
|
96
108
|
|
|
97
109
|
const addOptionKeyCombination = computed(() => {
|
|
@@ -99,7 +111,7 @@ const addOptionKeyCombination = computed(() => {
|
|
|
99
111
|
});
|
|
100
112
|
|
|
101
113
|
const filteredOptions = computed(() => {
|
|
102
|
-
const normalizedSearch =
|
|
114
|
+
const normalizedSearch = searchModel.value.toLowerCase().trim();
|
|
103
115
|
|
|
104
116
|
let options = [...props.options];
|
|
105
117
|
|
|
@@ -117,7 +129,7 @@ const filteredOptions = computed(() => {
|
|
|
117
129
|
});
|
|
118
130
|
|
|
119
131
|
watch(
|
|
120
|
-
() => [props.options, props.size, props.visibleOptions, props.searchable],
|
|
132
|
+
() => [props.options, props.size, props.visibleOptions, props.searchable, searchModel.value],
|
|
121
133
|
() => {
|
|
122
134
|
nextTick(() => {
|
|
123
135
|
const options = [
|
|
@@ -418,7 +430,7 @@ const {
|
|
|
418
430
|
v-if="searchable"
|
|
419
431
|
:id="elementId"
|
|
420
432
|
ref="listbox-input"
|
|
421
|
-
v-model="
|
|
433
|
+
v-model="searchModel"
|
|
422
434
|
:placeholder="localeMessages.search"
|
|
423
435
|
:size="size"
|
|
424
436
|
:debounce="debounce"
|
|
@@ -2,7 +2,7 @@ export default /*tw*/ {
|
|
|
2
2
|
wrapper: {
|
|
3
3
|
base: `
|
|
4
4
|
my-2 p-1 flex flex-col gap-1 w-auto absolute z-50 shadow-sm
|
|
5
|
-
rounded-medium border border-default bg-default
|
|
5
|
+
rounded-medium border border-solid border-default bg-default
|
|
6
6
|
overflow-auto [-webkit-overflow-scrolling:touch]
|
|
7
7
|
focus:outline-hidden
|
|
8
8
|
`,
|