vueless 1.5.1-beta.0 → 1.5.1-beta.1

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.5.1-beta.0",
3
+ "version": "1.5.1-beta.1",
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",
@@ -851,6 +851,36 @@ function onBodyDoubleClick(event: MouseEvent) {
851
851
  onDoubleClickRow(rowData);
852
852
  }
853
853
 
854
+ // Cells are always clipped (`truncate`), so show the full value in a native tooltip when it is.
855
+ const OVERFLOW_THRESHOLD_PX = 1;
856
+
857
+ function isElementOverflown(element: HTMLElement) {
858
+ return (
859
+ element.scrollWidth - element.clientWidth > OVERFLOW_THRESHOLD_PX ||
860
+ element.scrollHeight - element.clientHeight > OVERFLOW_THRESHOLD_PX
861
+ );
862
+ }
863
+
864
+ /** Content may be clipped by the cell itself or by any wrapper inside it (slot content included). */
865
+ function isCellContentTruncated(cell: HTMLElement) {
866
+ return [cell, ...cell.querySelectorAll<HTMLElement>("*")].some(isElementOverflown);
867
+ }
868
+
869
+ function onBodyPointerOver(event: PointerEvent) {
870
+ const target = event.target as HTMLElement;
871
+ const cell = target.closest("td");
872
+ const previousCell = (event.relatedTarget as HTMLElement | null)?.closest("td");
873
+
874
+ // Moving between elements of the same cell needs no re-measure.
875
+ if (!cell || cell === previousCell) return;
876
+
877
+ if (isCellContentTruncated(cell)) {
878
+ cell.setAttribute("title", cell.textContent?.trim() || "");
879
+ } else {
880
+ cell.removeAttribute("title");
881
+ }
882
+ }
883
+
854
884
  // Detect double-tap on touch devices and dispatch a native `dblclick`,
855
885
  // which bubbles to the tbody and triggers onBodyDoubleClick.
856
886
  const DOUBLE_TAP_DELAY = 300;
@@ -1469,6 +1499,7 @@ const BodyRows = () =>
1469
1499
  @click="onBodyClick"
1470
1500
  @dblclick="onBodyDoubleClick"
1471
1501
  @touchend="onBodyTouchEnd"
1502
+ @pointerover="onBodyPointerOver"
1472
1503
  >
1473
1504
  <tr
1474
1505
  v-if="hasBeforeFirstRowSlot"
@@ -1,18 +1,7 @@
1
1
  <script setup lang="ts">
2
- import {
3
- Comment,
4
- Fragment,
5
- Text,
6
- computed,
7
- h,
8
- onMounted,
9
- useAttrs,
10
- useSlots,
11
- useTemplateRef,
12
- } from "vue";
2
+ import { Comment, Fragment, Text, computed, h, useAttrs, useSlots } from "vue";
13
3
 
14
4
  import { useUI } from "../composables/useUI";
15
- import { useMutationObserver } from "../composables/useMutationObserver";
16
5
 
17
6
  import { isEmptyValue } from "../utils/helper";
18
7
  import { cx } from "../utils/ui";
@@ -39,17 +28,6 @@ const props = defineProps<UTableRowProps>();
39
28
  const slots = useSlots();
40
29
  const attrs = useAttrs();
41
30
 
42
- const cellRef = useTemplateRef<HTMLDivElement>("cell");
43
-
44
- if (props.textEllipsis) {
45
- useMutationObserver(cellRef, setCellTitle, {
46
- subtree: true,
47
- childList: true,
48
- characterData: true,
49
- attributes: false,
50
- });
51
- }
52
-
53
31
  const toggleIconConfig = computed(() => {
54
32
  const nestedRow = props.row?.row;
55
33
  let isShown = false;
@@ -65,12 +43,6 @@ const toggleIconConfig = computed(() => {
65
43
  : props.attrs.bodyCellNestedExpandIconAttrs.value;
66
44
  });
67
45
 
68
- onMounted(() => {
69
- if (cellRef.value) {
70
- setElementTitle(cellRef.value);
71
- }
72
- });
73
-
74
46
  function getToggleIconName() {
75
47
  return props.isExpanded
76
48
  ? props.config?.defaults?.collapseIcon
@@ -110,30 +82,6 @@ function getNestedCheckboxShift() {
110
82
  return { transform: `translateX(${props.nestedLevel * LAST_NESTED_ROW_SHIFT_REM}rem)` };
111
83
  }
112
84
 
113
- function setCellTitle(mutations: MutationRecord[]) {
114
- mutations.forEach((mutation) => {
115
- const { target } = mutation;
116
-
117
- setElementTitle(target as HTMLElement);
118
- });
119
- }
120
-
121
- function isElementOverflown(element: HTMLElement) {
122
- return element.clientWidth < element.scrollWidth || element.clientHeight < element.scrollHeight;
123
- }
124
-
125
- function setElementTitle(element: HTMLElement) {
126
- const isOverflown = isElementOverflown(element);
127
-
128
- if (isOverflown) {
129
- element.setAttribute("title", String(element.textContent));
130
- }
131
-
132
- if (!isOverflown && element.hasAttribute("title")) {
133
- element.removeAttribute("title");
134
- }
135
- }
136
-
137
85
  function getRowClasses(row: Row) {
138
86
  const rowClasses = row?.class || "";
139
87
 
@@ -307,7 +255,6 @@ function renderCellContent(value: Cell, key: string, cellIndex: number): VNode |
307
255
  // Render cell wrapper with highlighted HTML
308
256
  if (shouldRenderCellWrapper(props.row, keyStr)) {
309
257
  return h("div", {
310
- ref: cellRef,
311
258
  ...props.attrs.bodyCellContentAttrs.value,
312
259
  class: cx([
313
260
  props.attrs.bodyCellContentAttrs.value.class,
@@ -380,7 +327,6 @@ function renderNestedFirstCell(value: Cell, key: string, cellIndex: number): VNo
380
327
  if (shouldRenderCellWrapper(props.row, keyStr)) {
381
328
  return [
382
329
  h("div", {
383
- ref: cellRef,
384
330
  ...props.attrs.bodyCellContentAttrs.value,
385
331
  class: cx([
386
332
  props.attrs.bodyCellContentAttrs.value.class,
@@ -84,6 +84,11 @@ describe("UTable.vue", () => {
84
84
  });
85
85
  }
86
86
 
87
+ function setElementSize(element: Element, size: { clientWidth: number; scrollWidth: number }) {
88
+ Object.defineProperty(element, "clientWidth", { value: size.clientWidth, configurable: true });
89
+ Object.defineProperty(element, "scrollWidth", { value: size.scrollWidth, configurable: true });
90
+ }
91
+
87
92
  describe("Props", () => {
88
93
  it("Columns – renders table headers correctly", () => {
89
94
  const component = mountUTable(getDefaultProps());
@@ -1054,6 +1059,48 @@ describe("UTable.vue", () => {
1054
1059
  });
1055
1060
  });
1056
1061
 
1062
+ describe("Cell Title", () => {
1063
+ it("Cell Title – sets title with the full value when cell content is truncated", async () => {
1064
+ const component = mountUTable(getDefaultProps());
1065
+
1066
+ const cell = component.find("tbody td");
1067
+
1068
+ setElementSize(cell.element, { clientWidth: 100, scrollWidth: 200 });
1069
+
1070
+ await cell.trigger("pointerover");
1071
+
1072
+ expect(cell.attributes("title")).toBe("John Doe");
1073
+ });
1074
+
1075
+ it("Cell Title – removes title when cell content is not truncated", async () => {
1076
+ const component = mountUTable(getDefaultProps());
1077
+
1078
+ const cell = component.find("tbody td");
1079
+
1080
+ cell.element.setAttribute("title", "John Doe");
1081
+ setElementSize(cell.element, { clientWidth: 200, scrollWidth: 200 });
1082
+
1083
+ await cell.trigger("pointerover");
1084
+
1085
+ expect(cell.attributes("title")).toBeUndefined();
1086
+ });
1087
+
1088
+ it("Cell Title – skips re-measure when moving within the same cell", async () => {
1089
+ const component = mountUTable(getDefaultProps());
1090
+
1091
+ const cell = component.find("tbody td");
1092
+
1093
+ setElementSize(cell.element, { clientWidth: 100, scrollWidth: 200 });
1094
+
1095
+ cell.element.dispatchEvent(
1096
+ new MouseEvent("pointerover", { bubbles: true, relatedTarget: cell.element }),
1097
+ );
1098
+ await nextTick();
1099
+
1100
+ expect(cell.attributes("title")).toBeUndefined();
1101
+ });
1102
+ });
1103
+
1057
1104
  describe("Virtual Scroll with Search", () => {
1058
1105
  it("Virtual Scroll with Search – renders only visible rows with search", async () => {
1059
1106
  const manyRows = Array.from({ length: 100 }, (_, i) => ({
@@ -383,61 +383,6 @@ describe("UTableRow.vue", () => {
383
383
  });
384
384
 
385
385
  describe("Functionality", () => {
386
- it("Element Title – sets title attribute when element is overflown", async () => {
387
- // Mock element properties to simulate overflow
388
- const mockElement = {
389
- clientWidth: 100,
390
- scrollWidth: 200,
391
- clientHeight: 50,
392
- scrollHeight: 50,
393
- textContent: "Very long text that overflows",
394
- setAttribute: vi.fn(),
395
- removeAttribute: vi.fn(),
396
- hasAttribute: vi.fn().mockReturnValue(false),
397
- };
398
-
399
- const component = mount(UTableRow, {
400
- props: getDefaultProps(),
401
- });
402
-
403
- // Access the component's internal methods
404
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
405
- const vm = component.vm as any;
406
-
407
- vm.setElementTitle(mockElement);
408
-
409
- expect(mockElement.setAttribute).toHaveBeenCalledWith(
410
- "title",
411
- "Very long text that overflows",
412
- );
413
- });
414
-
415
- it("Element Title – removes title attribute when element is not overflown", async () => {
416
- // Mock element properties to simulate no overflow
417
- const mockElement = {
418
- clientWidth: 200,
419
- scrollWidth: 100,
420
- clientHeight: 50,
421
- scrollHeight: 50,
422
- textContent: "Short text",
423
- setAttribute: vi.fn(),
424
- removeAttribute: vi.fn(),
425
- hasAttribute: vi.fn().mockReturnValue(true),
426
- };
427
-
428
- const component = mount(UTableRow, {
429
- props: getDefaultProps(),
430
- });
431
-
432
- // Access the component's internal methods
433
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
434
- const vm = component.vm as any;
435
-
436
- vm.setElementTitle(mockElement);
437
-
438
- expect(mockElement.removeAttribute).toHaveBeenCalledWith("title");
439
- });
440
-
441
386
  it("Icon Width – calculates correct icon width", async () => {
442
387
  const expandableRow: FlatRow = {
443
388
  ...defaultRow,
@@ -7,9 +7,9 @@ export default /*tw*/ {
7
7
  [&_a:not([class]):hover]:no-underline
8
8
  [&_ul]:font-normal [&_ol]:font-normal
9
9
  [&_ul]:leading-normal [&_ol]:leading-normal
10
- [&_ul]:list-inside [&_ol]:list-inside
10
+ [&_ul]:list-outside [&_ol]:list-outside
11
11
  [&_ul]:list-disc [&_ol]:list-decimal
12
- [&_ul]:ml-2 [&_ol]:ml-2
12
+ [&_ul]:ps-4 [&_ol]:ps-4
13
13
  `,
14
14
  variants: {
15
15
  variant: {