vueless 1.0.2-beta.49 → 1.0.2-beta.50
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,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, ref, watch, useSlots, useId, useTemplateRef } from "vue";
|
|
2
|
+
import { computed, nextTick, onMounted, ref, watch, useSlots, useId, useTemplateRef } from "vue";
|
|
3
3
|
|
|
4
4
|
import useUI from "../composables/useUI.ts";
|
|
5
5
|
import { getDefaults } from "../utils/ui.ts";
|
|
@@ -67,6 +67,11 @@ const wrapperRef = useTemplateRef<HTMLLabelElement>("wrapper");
|
|
|
67
67
|
|
|
68
68
|
const currentRows = ref(Number(props.rows));
|
|
69
69
|
|
|
70
|
+
const localValue = computed({
|
|
71
|
+
get: () => props.modelValue,
|
|
72
|
+
set: (value) => emit("update:modelValue", value),
|
|
73
|
+
});
|
|
74
|
+
|
|
70
75
|
watch(
|
|
71
76
|
() => props.rows,
|
|
72
77
|
(newRows) => {
|
|
@@ -74,84 +79,42 @@ watch(
|
|
|
74
79
|
},
|
|
75
80
|
);
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
set(value) {
|
|
82
|
-
emit("update:modelValue", value);
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
onMounted(() => toggleReadonly(true));
|
|
87
|
-
|
|
88
|
-
function getNewRowCount() {
|
|
89
|
-
const textarea = textareaRef.value;
|
|
90
|
-
|
|
91
|
-
if (!textarea) return 0;
|
|
92
|
-
|
|
93
|
-
const content = textarea.value;
|
|
94
|
-
const newlineCount = (content.match(/\n/g) || []).length;
|
|
95
|
-
|
|
96
|
-
return Math.max(Number(props.rows), newlineCount + 2);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function onEnter() {
|
|
100
|
-
if (props.readonly) return;
|
|
82
|
+
watch(
|
|
83
|
+
() => [props.modelValue, props.autoResize],
|
|
84
|
+
() => props.autoResize && nextTick(autoResizeTextarea),
|
|
85
|
+
);
|
|
101
86
|
|
|
102
|
-
|
|
87
|
+
watch([() => props.labelAlign, () => props.size], setLabelPosition, { flush: "post" });
|
|
103
88
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
89
|
+
onMounted(() => {
|
|
90
|
+
toggleReadonly(true);
|
|
91
|
+
setLabelPosition();
|
|
107
92
|
|
|
108
|
-
if (
|
|
109
|
-
|
|
93
|
+
if (props.autoResize) {
|
|
94
|
+
nextTick(autoResizeTextarea);
|
|
110
95
|
}
|
|
111
|
-
}
|
|
96
|
+
});
|
|
112
97
|
|
|
113
|
-
function
|
|
114
|
-
if (props.readonly) return;
|
|
98
|
+
function autoResizeTextarea() {
|
|
99
|
+
if (!props.autoResize || props.readonly) return;
|
|
115
100
|
|
|
116
|
-
const
|
|
101
|
+
const textarea = textareaRef.value;
|
|
117
102
|
|
|
118
|
-
if (
|
|
119
|
-
currentRows.value = newRowCount;
|
|
120
|
-
}
|
|
103
|
+
if (!textarea) return;
|
|
121
104
|
|
|
122
|
-
|
|
123
|
-
currentRows.value = newRowCount + 1;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
105
|
+
textarea.style.height = "auto";
|
|
126
106
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
107
|
+
// Calculate the minimum height based on rows prop
|
|
108
|
+
const computedStyle = getComputedStyle(textarea);
|
|
109
|
+
const lineHeight = parseFloat(computedStyle.lineHeight);
|
|
110
|
+
const paddingTop = parseFloat(computedStyle.paddingTop);
|
|
111
|
+
const paddingBottom = parseFloat(computedStyle.paddingBottom);
|
|
112
|
+
const minHeight = lineHeight * Number(props.rows) + paddingTop + paddingBottom;
|
|
130
113
|
|
|
131
|
-
|
|
132
|
-
|
|
114
|
+
// Set height to the larger of scrollHeight or minimum height
|
|
115
|
+
const newHeight = Math.max(textarea.scrollHeight, minHeight);
|
|
133
116
|
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function onFocus() {
|
|
138
|
-
toggleReadonly(false);
|
|
139
|
-
|
|
140
|
-
emit("focus");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function onBlur() {
|
|
144
|
-
toggleReadonly(true);
|
|
145
|
-
|
|
146
|
-
emit("blur");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function onMouseleave() {
|
|
150
|
-
toggleReadonly(true);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function onMousedown() {
|
|
154
|
-
emit("mousedown");
|
|
117
|
+
textarea.style.height = `${newHeight}px`;
|
|
155
118
|
}
|
|
156
119
|
|
|
157
120
|
function toggleReadonly(hasReadonly: boolean) {
|
|
@@ -172,8 +135,6 @@ useMutationObserver(wrapperRef, (mutations) => mutations.forEach(setLabelPositio
|
|
|
172
135
|
subtree: true,
|
|
173
136
|
});
|
|
174
137
|
|
|
175
|
-
watch([() => props.labelAlign, () => props.size], setLabelPosition, { flush: "post" });
|
|
176
|
-
|
|
177
138
|
function setLabelPosition() {
|
|
178
139
|
if (props.labelAlign === "top" || !hasSlotContent(slots["left"])) {
|
|
179
140
|
if (labelComponentRef.value?.labelElement) {
|
|
@@ -201,7 +162,35 @@ function setLabelPosition() {
|
|
|
201
162
|
}
|
|
202
163
|
}
|
|
203
164
|
|
|
204
|
-
|
|
165
|
+
function onChange() {
|
|
166
|
+
emit("change");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function onClick(event: MouseEvent) {
|
|
170
|
+
toggleReadonly(false);
|
|
171
|
+
|
|
172
|
+
emit("click", event);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function onFocus() {
|
|
176
|
+
toggleReadonly(false);
|
|
177
|
+
|
|
178
|
+
emit("focus");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function onBlur() {
|
|
182
|
+
toggleReadonly(true);
|
|
183
|
+
|
|
184
|
+
emit("blur");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function onMouseleave() {
|
|
188
|
+
toggleReadonly(true);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function onMousedown() {
|
|
192
|
+
emit("mousedown");
|
|
193
|
+
}
|
|
205
194
|
|
|
206
195
|
defineExpose({
|
|
207
196
|
/**
|
|
@@ -286,8 +275,6 @@ const {
|
|
|
286
275
|
@mouseleave="onMouseleave"
|
|
287
276
|
@mousedown="onMousedown"
|
|
288
277
|
@click="onClick"
|
|
289
|
-
@keydown.enter="onEnter"
|
|
290
|
-
@keyup.delete="onBackspace"
|
|
291
278
|
/>
|
|
292
279
|
|
|
293
280
|
<span v-if="hasSlotContent($slots['right'])" :for="elementId" v-bind="rightSlotAttrs">
|
|
@@ -14,7 +14,7 @@ export default /*tw*/ {
|
|
|
14
14
|
rightSlot: "{>slot} rounded-l-none",
|
|
15
15
|
wrapper: {
|
|
16
16
|
base: `
|
|
17
|
-
flex px-3 gap-3 w-full bg-default transition
|
|
17
|
+
flex px-3 py-2 gap-3 w-full bg-default transition
|
|
18
18
|
rounded-medium border border-default outline-transparent
|
|
19
19
|
hover:border-lifted hover:focus-within:border-primary focus-within:border-primary
|
|
20
20
|
focus-within:outline focus-within:outline-small focus-within:outline-primary focus-within:transition
|
|
@@ -30,7 +30,7 @@ export default /*tw*/ {
|
|
|
30
30
|
},
|
|
31
31
|
textarea: {
|
|
32
32
|
base: `
|
|
33
|
-
|
|
33
|
+
block w-full bg-transparent border-none font-normal
|
|
34
34
|
placeholder:text-muted placeholder:font-normal placeholder:leading-none
|
|
35
35
|
focus:outline-none disabled:cursor-not-allowed
|
|
36
36
|
`,
|
|
@@ -43,14 +43,17 @@ export default /*tw*/ {
|
|
|
43
43
|
resizable: {
|
|
44
44
|
false: "resize-none",
|
|
45
45
|
},
|
|
46
|
+
autoResize: {
|
|
47
|
+
true: "resize-none overflow-hidden",
|
|
48
|
+
},
|
|
46
49
|
error: {
|
|
47
50
|
true: "placeholder:text-error/50",
|
|
48
51
|
},
|
|
49
52
|
},
|
|
50
53
|
compoundVariants: [
|
|
51
|
-
{ labelAlign: "topInside", label: true, size: "sm", class: "
|
|
52
|
-
{ labelAlign: "topInside", label: true, size: "md", class: "
|
|
53
|
-
{ labelAlign: "topInside", label: true, size: "lg", class: "
|
|
54
|
+
{ labelAlign: "topInside", label: true, size: "sm", class: "mt-3" },
|
|
55
|
+
{ labelAlign: "topInside", label: true, size: "md", class: "mt-4" },
|
|
56
|
+
{ labelAlign: "topInside", label: true, size: "lg", class: "mt-5" },
|
|
54
57
|
],
|
|
55
58
|
},
|
|
56
59
|
defaults: {
|
|
@@ -59,6 +62,7 @@ export default /*tw*/ {
|
|
|
59
62
|
inputmode: "text",
|
|
60
63
|
labelAlign: "topInside",
|
|
61
64
|
resizable: false,
|
|
65
|
+
autoResize: false,
|
|
62
66
|
disabled: false,
|
|
63
67
|
readonly: false,
|
|
64
68
|
noAutocomplete: false,
|
|
@@ -7,7 +7,7 @@ import ULabel from "../../ui.form-label/ULabel.vue";
|
|
|
7
7
|
import type { Props } from "../types.ts";
|
|
8
8
|
|
|
9
9
|
describe("UTextarea.vue", () => {
|
|
10
|
-
describe("
|
|
10
|
+
describe("Props", () => {
|
|
11
11
|
it("Model Value – sets initial value correctly", () => {
|
|
12
12
|
const initialValue = "Test textarea";
|
|
13
13
|
|
|
@@ -165,6 +165,76 @@ describe("UTextarea.vue", () => {
|
|
|
165
165
|
expect(componentResizable.get("textarea").attributes("class")).toContain("resize-none");
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
+
it("Auto Resize – applies correct classes when autoResize is enabled", () => {
|
|
169
|
+
const autoResize = true;
|
|
170
|
+
const expectedClasses = "resize-none overflow-hidden";
|
|
171
|
+
|
|
172
|
+
const component = mount(UTextarea, {
|
|
173
|
+
props: {
|
|
174
|
+
autoResize,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(component.props("autoResize")).toBe(autoResize);
|
|
179
|
+
expect(component.get("textarea").attributes("class")).toContain(expectedClasses);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("Auto Resize – does not adjust height when disabled", async () => {
|
|
183
|
+
const component = mount(UTextarea, {
|
|
184
|
+
props: {
|
|
185
|
+
autoResize: false,
|
|
186
|
+
rows: 2,
|
|
187
|
+
modelValue: "",
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const textarea = component.get("textarea");
|
|
192
|
+
const textareaElement = textarea.element;
|
|
193
|
+
const initialHeight = textareaElement.style.height;
|
|
194
|
+
|
|
195
|
+
await textarea.setValue("line1\nline2\nline3\nline4\nline5");
|
|
196
|
+
await textarea.trigger("input");
|
|
197
|
+
|
|
198
|
+
expect(textareaElement.style.height).toBe(initialHeight);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("Auto Resize – does not adjust height when readonly", async () => {
|
|
202
|
+
const component = mount(UTextarea, {
|
|
203
|
+
props: {
|
|
204
|
+
autoResize: true,
|
|
205
|
+
readonly: true,
|
|
206
|
+
rows: 2,
|
|
207
|
+
modelValue: "",
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const textarea = component.get("textarea");
|
|
212
|
+
const textareaElement = textarea.element;
|
|
213
|
+
|
|
214
|
+
const initialHeight = textareaElement.style.height;
|
|
215
|
+
|
|
216
|
+
await textarea.setValue("line1\nline2\nline3\nline4\nline5");
|
|
217
|
+
await textarea.trigger("input");
|
|
218
|
+
|
|
219
|
+
expect(textareaElement.style.height).toBe(initialHeight);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("Auto Resize – respects minimum rows", async () => {
|
|
223
|
+
const component = mount(UTextarea, {
|
|
224
|
+
props: {
|
|
225
|
+
autoResize: true,
|
|
226
|
+
rows: 3,
|
|
227
|
+
modelValue: "short",
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const textarea = component.get("textarea");
|
|
232
|
+
|
|
233
|
+
await textarea.trigger("input");
|
|
234
|
+
|
|
235
|
+
expect(textarea.attributes("rows")).toBe("3");
|
|
236
|
+
});
|
|
237
|
+
|
|
168
238
|
it("Readonly – sets textarea to readonly", () => {
|
|
169
239
|
const component = mount(UTextarea, {
|
|
170
240
|
props: {
|
|
@@ -449,104 +519,6 @@ describe("UTextarea.vue", () => {
|
|
|
449
519
|
});
|
|
450
520
|
});
|
|
451
521
|
|
|
452
|
-
describe("Functionality", () => {
|
|
453
|
-
it("Row auto resize – enter key increases rows when content has more lines than current rows", async () => {
|
|
454
|
-
const component = mount(UTextarea, {
|
|
455
|
-
props: {
|
|
456
|
-
rows: 2,
|
|
457
|
-
modelValue: "line1\nline2",
|
|
458
|
-
},
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
const textarea = component.get("textarea");
|
|
462
|
-
|
|
463
|
-
await textarea.setValue("line1\nline2\nline3");
|
|
464
|
-
await textarea.trigger("keydown", { key: "Enter" });
|
|
465
|
-
|
|
466
|
-
expect(Number(component.get("textarea").attributes("rows"))).toBeGreaterThan(2);
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it("Row auto resize – enter key does not increase rows when textarea is readonly", async () => {
|
|
470
|
-
const component = mount(UTextarea, {
|
|
471
|
-
props: {
|
|
472
|
-
rows: 2,
|
|
473
|
-
modelValue: "line1\nline2",
|
|
474
|
-
readonly: true,
|
|
475
|
-
},
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
const textarea = component.get("textarea");
|
|
479
|
-
|
|
480
|
-
await textarea.setValue("line1\nline2\nline3");
|
|
481
|
-
await textarea.trigger("keydown", { key: "Enter" });
|
|
482
|
-
|
|
483
|
-
expect(component.get("textarea").attributes("rows")).toBe("2");
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it("Row auto resize – backspace key decreases rows when content has fewer lines", async () => {
|
|
487
|
-
const component = mount(UTextarea, {
|
|
488
|
-
props: {
|
|
489
|
-
rows: 2,
|
|
490
|
-
modelValue: "line1\nline2\nline3\nline4",
|
|
491
|
-
},
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
const textarea = component.get("textarea");
|
|
495
|
-
|
|
496
|
-
await textarea.trigger("keydown", { key: "Enter" });
|
|
497
|
-
const initialRows = Number(component.get("textarea").attributes("rows"));
|
|
498
|
-
|
|
499
|
-
await textarea.setValue("line1\nline2");
|
|
500
|
-
await textarea.trigger("keyup", { key: "Delete" });
|
|
501
|
-
|
|
502
|
-
const finalRows = Number(component.get("textarea").attributes("rows"));
|
|
503
|
-
|
|
504
|
-
expect(finalRows).toBeLessThan(initialRows);
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
it("Row auto resize – backspace key does not decrease rows when textarea is readonly", async () => {
|
|
508
|
-
const component = mount(UTextarea, {
|
|
509
|
-
props: {
|
|
510
|
-
rows: 2,
|
|
511
|
-
modelValue: "line1\nline2\nline3",
|
|
512
|
-
readonly: true,
|
|
513
|
-
},
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
const textarea = component.get("textarea");
|
|
517
|
-
|
|
518
|
-
await textarea.trigger("keydown", { key: "Enter" });
|
|
519
|
-
const initialRows = component.get("textarea").attributes("rows");
|
|
520
|
-
|
|
521
|
-
await textarea.setValue("line1");
|
|
522
|
-
await textarea.trigger("keyup", { key: "Delete" });
|
|
523
|
-
|
|
524
|
-
expect(component.get("textarea").attributes("rows")).toBe(initialRows);
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
it("Row auto resize – handles rapid key presses correctly", async () => {
|
|
528
|
-
const component = mount(UTextarea, {
|
|
529
|
-
props: {
|
|
530
|
-
rows: 2,
|
|
531
|
-
modelValue: "line1",
|
|
532
|
-
},
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
const textarea = component.get("textarea");
|
|
536
|
-
|
|
537
|
-
await textarea.setValue("line1\nline2");
|
|
538
|
-
await textarea.trigger("keydown", { key: "Enter" });
|
|
539
|
-
|
|
540
|
-
await textarea.setValue("line1\nline2\nline3");
|
|
541
|
-
await textarea.trigger("keydown", { key: "Enter" });
|
|
542
|
-
|
|
543
|
-
await textarea.setValue("line1\nline2\nline3\nline4");
|
|
544
|
-
await textarea.trigger("keydown", { key: "Enter" });
|
|
545
|
-
|
|
546
|
-
expect(Number(component.get("textarea").attributes("rows"))).toBeGreaterThan(2);
|
|
547
|
-
});
|
|
548
|
-
});
|
|
549
|
-
|
|
550
522
|
describe("Exposed Properties", () => {
|
|
551
523
|
it("Wrapper Element – exposes wrapper element ref", () => {
|
|
552
524
|
const component = mount(UTextarea, {
|