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,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.0.2-beta.49",
3
+ "version": "1.0.2-beta.50",
4
4
  "license": "MIT",
5
5
  "description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
6
6
  "keywords": [
@@ -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
- const localValue = computed({
78
- get() {
79
- return props.modelValue;
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
- const newRowCount = getNewRowCount();
87
+ watch([() => props.labelAlign, () => props.size], setLabelPosition, { flush: "post" });
103
88
 
104
- if (newRowCount > currentRows.value) {
105
- currentRows.value = newRowCount;
106
- }
89
+ onMounted(() => {
90
+ toggleReadonly(true);
91
+ setLabelPosition();
107
92
 
108
- if (newRowCount === currentRows.value) {
109
- currentRows.value = newRowCount + 1;
93
+ if (props.autoResize) {
94
+ nextTick(autoResizeTextarea);
110
95
  }
111
- }
96
+ });
112
97
 
113
- function onBackspace() {
114
- if (props.readonly) return;
98
+ function autoResizeTextarea() {
99
+ if (!props.autoResize || props.readonly) return;
115
100
 
116
- const newRowCount = getNewRowCount() - 1;
101
+ const textarea = textareaRef.value;
117
102
 
118
- if (newRowCount < currentRows.value) {
119
- currentRows.value = newRowCount;
120
- }
103
+ if (!textarea) return;
121
104
 
122
- if (newRowCount === currentRows.value) {
123
- currentRows.value = newRowCount + 1;
124
- }
125
- }
105
+ textarea.style.height = "auto";
126
106
 
127
- function onChange() {
128
- emit("change");
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
- function onClick(event: MouseEvent) {
132
- toggleReadonly(false);
114
+ // Set height to the larger of scrollHeight or minimum height
115
+ const newHeight = Math.max(textarea.scrollHeight, minHeight);
133
116
 
134
- emit("click", event);
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
- onMounted(() => setLabelPosition());
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
- pt-2 pb-1.5 block w-full bg-transparent border-none font-normal
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: "pt-[1.2rem]" },
52
- { labelAlign: "topInside", label: true, size: "md", class: "pt-[1.4rem]" },
53
- { labelAlign: "topInside", label: true, size: "lg", class: "pt-[1.6rem]" },
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("props", () => {
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, {
@@ -40,6 +40,11 @@ export interface Props {
40
40
  */
41
41
  resizable?: boolean;
42
42
 
43
+ /**
44
+ * Enable auto-resize functionality based on content.
45
+ */
46
+ autoResize?: boolean;
47
+
43
48
  /**
44
49
  * Make textarea read only.
45
50
  */