vueless 1.3.9-beta.8 → 1.4.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.
@@ -1,6 +1,6 @@
1
1
  import { flushPromises, mount } from "@vue/test-utils";
2
2
  import { describe, it, expect, vi } from "vitest";
3
- import { ref } from "vue";
3
+ import { ref, nextTick } from "vue";
4
4
 
5
5
  import UTableRow from "../UTableRow.vue";
6
6
  import UIcon from "../../ui.image-icon/UIcon.vue";
@@ -36,6 +36,10 @@ describe("UTableRow.vue", () => {
36
36
  bodyRowAttrs: ref({ class: "row-base" }),
37
37
  bodyCellStickyLeftAttrs: ref({ class: "sticky-left" }),
38
38
  bodyCellStickyRightAttrs: ref({ class: "sticky-right" }),
39
+ bodyCellSearchMatchAttrs: ref({ class: "search-match" }),
40
+ bodyCellSearchMatchTextAttrs: ref({ class: "search-match-text" }),
41
+ bodyCellSearchMatchActiveAttrs: ref({ class: "search-match-active" }),
42
+ bodyCellSearchMatchTextActiveAttrs: ref({ class: "search-match-active-text" }),
39
43
  };
40
44
 
41
45
  const defaultConfig = {
@@ -64,6 +68,7 @@ describe("UTableRow.vue", () => {
64
68
  config: defaultConfig,
65
69
  isChecked: false,
66
70
  isExpanded: false,
71
+ isVisible: true,
67
72
  columnPositions,
68
73
  ...overrides,
69
74
  };
@@ -260,14 +265,17 @@ describe("UTableRow.vue", () => {
260
265
  }),
261
266
  });
262
267
 
263
- const icon = component.findComponent(UIcon);
268
+ let icon = component.findComponent(UIcon);
264
269
 
265
270
  expect(icon.props("name")).toBe(defaultConfig.defaults.expandIcon);
266
271
 
267
- component.setProps({ isExpanded: true });
268
-
272
+ await component.setProps({ isExpanded: true });
273
+ await nextTick();
269
274
  await flushPromises();
270
275
 
276
+ // Re-query the icon after props update
277
+ icon = component.findComponent(UIcon);
278
+
271
279
  expect(icon.props("name")).toBe(defaultConfig.defaults.collapseIcon);
272
280
  });
273
281
 
@@ -286,106 +294,8 @@ describe("UTableRow.vue", () => {
286
294
  });
287
295
  });
288
296
 
289
- describe("Events", () => {
290
- it("Click emits click event when row is clicked", async () => {
291
- const component = mount(UTableRow, {
292
- props: getDefaultProps(),
293
- });
294
-
295
- await component.get("tr").trigger("click");
296
-
297
- expect(component.emitted("click")).toBeTruthy();
298
- expect(component.emitted("click")![0][0]).toEqual(defaultRow);
299
- });
300
-
301
- it("Double Click – emits dblclick event when row is double-clicked", async () => {
302
- const component = mount(UTableRow, {
303
- props: getDefaultProps(),
304
- });
305
-
306
- await component.get("tr").trigger("dblclick");
307
-
308
- expect(component.emitted("dblclick")).toBeTruthy();
309
- expect(component.emitted("dblclick")![0][0]).toEqual(defaultRow);
310
- });
311
-
312
- it("Click Cell – emits clickCell event when cell is clicked", async () => {
313
- const component = mount(UTableRow, {
314
- props: getDefaultProps(),
315
- });
316
-
317
- const firstCell = component.find("td");
318
-
319
- await firstCell.trigger("click");
320
-
321
- expect(component.emitted("clickCell")).toBeTruthy();
322
- expect(component.emitted("clickCell")![0]).toEqual(["John Doe", defaultRow, "name"]);
323
- });
324
-
325
- it("Toggle Expand – emits toggleExpand event when expand icon is clicked", async () => {
326
- const expandableRow: FlatRow = {
327
- ...defaultRow,
328
- row: [{ id: "2", name: "Child", nestedLeveL: 1 }],
329
- };
330
-
331
- const component = mount(UTableRow, {
332
- props: getDefaultProps({ row: expandableRow }),
333
- });
334
-
335
- const expandIcon = component.find("[data-row-toggle-icon='1']");
336
-
337
- await expandIcon.trigger("click");
338
-
339
- expect(component.emitted("toggleExpand")).toBeTruthy();
340
- expect(component.emitted("toggleExpand")![0][0]).toEqual(expandableRow);
341
- });
342
-
343
- it("Toggle Checkbox – emits toggleCheckbox event when checkbox is changed", async () => {
344
- const component = mount(UTableRow, {
345
- props: getDefaultProps({ selectable: true }),
346
- });
347
-
348
- const checkbox = component.getComponent(UCheckbox);
349
-
350
- await checkbox.vm.$emit("input", defaultRow);
351
-
352
- expect(component.emitted("toggleCheckbox")).toBeTruthy();
353
- expect(component.emitted("toggleCheckbox")![0][0]).toEqual(defaultRow);
354
- });
355
-
356
- it("Checkbox Cell – prevents row click events when checkbox cell is clicked", async () => {
357
- const component = mount(UTableRow, {
358
- props: getDefaultProps({ selectable: true }),
359
- });
360
-
361
- const checkboxCell = component.find("td");
362
-
363
- await checkboxCell.trigger("click");
364
- await checkboxCell.trigger("dblclick");
365
-
366
- expect(component.emitted("click")).toBeFalsy();
367
- expect(component.emitted("dblclick")).toBeFalsy();
368
- });
369
-
370
- it("Expand Icon – prevents row click events when expand icon is clicked", async () => {
371
- const expandableRow: FlatRow = {
372
- ...defaultRow,
373
- row: [{ id: "2", name: "Child", nestedLeveL: 1 }],
374
- };
375
-
376
- const component = mount(UTableRow, {
377
- props: getDefaultProps({ row: expandableRow }),
378
- });
379
-
380
- const expandIcon = component.find("[data-row-toggle-icon='1']");
381
-
382
- await expandIcon.trigger("click");
383
- await expandIcon.trigger("dblclick");
384
-
385
- expect(component.emitted("click")).toBeFalsy();
386
- expect(component.emitted("dblclick")).toBeFalsy();
387
- });
388
- });
297
+ // Events are now handled by UTable via event delegation
298
+ // UTableRow no longer emits click, dblclick, clickCell, toggleExpand, or toggleCheckbox events
389
299
 
390
300
  describe("Slots", () => {
391
301
  it("Cell Slot – renders custom content from cell slot", () => {
@@ -550,5 +460,26 @@ describe("UTableRow.vue", () => {
550
460
 
551
461
  expect(iconWrapper.exists()).toBe(true);
552
462
  });
463
+
464
+ it("isVisible – shows row when isVisible is true", () => {
465
+ const component = mount(UTableRow, {
466
+ props: getDefaultProps({ isVisible: true }),
467
+ });
468
+
469
+ const row = component.find("tr");
470
+ const style = row.attributes("style") || "";
471
+
472
+ expect(style).not.toContain("display: none");
473
+ });
474
+
475
+ it("isVisible – hides row when isVisible is false", () => {
476
+ const component = mount(UTableRow, {
477
+ props: getDefaultProps({ isVisible: false }),
478
+ });
479
+
480
+ const row = component.find("tr");
481
+
482
+ expect(row.attributes("style")).toContain("display: none");
483
+ });
553
484
  });
554
485
  });
@@ -16,6 +16,12 @@ export type RowId = string | number;
16
16
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
17
  export type Cell = CellObject & any;
18
18
 
19
+ export interface SearchMatch {
20
+ rowId: RowId;
21
+ columnKey: string;
22
+ indices: number[];
23
+ }
24
+
19
25
  export interface RowData {
20
26
  [key: string]: Cell;
21
27
  }
@@ -111,6 +117,41 @@ export interface Props {
111
117
  */
112
118
  loading?: boolean;
113
119
 
120
+ /**
121
+ * Enable virtual scrolling for large datasets.
122
+ */
123
+ virtualScroll?: boolean;
124
+
125
+ /**
126
+ * Fixed row height in pixels (used for virtual scroll calculations).
127
+ */
128
+ rowHeight?: number;
129
+
130
+ /**
131
+ * Height of the scroll container (CSS value).
132
+ */
133
+ scrollHeight?: string;
134
+
135
+ /**
136
+ * Number of extra rows to render above/below viewport.
137
+ */
138
+ bufferSize?: number;
139
+
140
+ /**
141
+ * Search string to highlight in table cells.
142
+ */
143
+ search?: string;
144
+
145
+ /**
146
+ * Index of the current search match to highlight (0-based).
147
+ */
148
+ searchMatch?: number;
149
+
150
+ /**
151
+ * Enable text ellipsis for table cells (renders div wrapper for overflow handling).
152
+ */
153
+ textEllipsis?: boolean;
154
+
114
155
  /**
115
156
  * Component config object.
116
157
  */
@@ -135,11 +176,20 @@ export interface UTableRowAttrs {
135
176
  bodyRowAttrs: Ref<UnknownObject>;
136
177
  bodyCellStickyLeftAttrs: Ref<UnknownObject>;
137
178
  bodyCellStickyRightAttrs: Ref<UnknownObject>;
179
+ bodyCellSearchMatchAttrs: Ref<UnknownObject>;
180
+ bodyCellSearchMatchTextAttrs: Ref<UnknownObject>;
181
+ bodyCellSearchMatchActiveAttrs: Ref<UnknownObject>;
182
+ bodyCellSearchMatchTextActiveAttrs: Ref<UnknownObject>;
138
183
  }
139
184
 
140
185
  export interface UTableRowProps {
141
186
  row: FlatRow;
142
187
  columns: ColumnObject[];
188
+ /**
189
+ * Row index in the parent table (used for slot params).
190
+ * Optional to keep UTableRow mountable standalone in tests/internal usage.
191
+ */
192
+ rowIndex?: number;
143
193
  emptyCellLabel?: string;
144
194
  selectable: boolean;
145
195
  nestedLevel: number;
@@ -149,5 +199,12 @@ export interface UTableRowProps {
149
199
  config: Config;
150
200
  isChecked: boolean;
151
201
  isExpanded: boolean;
202
+ isVisible: boolean;
152
203
  columnPositions: Map<string, number>;
204
+ search?: string;
205
+ searchMatchColumns?: Set<string>;
206
+ activeSearchMatchColumn?: string;
207
+ textEllipsis?: boolean;
208
+ onToggleExpand?: (row: Row) => void;
209
+ onToggleCheckbox?: (row: Row) => void;
153
210
  }
@@ -7,24 +7,30 @@ export function normalizeColumns(columns: Column[]): ColumnObject[] {
7
7
  }
8
8
 
9
9
  export function mapRowColumns(row: Row, columns: ColumnObject[]): RowData {
10
- const filteredRow = Object.entries(row).filter((item) => {
11
- return columns.some((column) => column.key === item[0] && column.isShown !== false);
12
- });
10
+ const visibleKeys = new Set(columns.filter((col) => col.isShown !== false).map((col) => col.key));
13
11
 
14
- return Object.fromEntries(filteredRow) as RowData;
12
+ const result: RowData = {};
13
+
14
+ for (const key of Object.keys(row)) {
15
+ if (visibleKeys.has(key)) {
16
+ result[key] = row[key];
17
+ }
18
+ }
19
+
20
+ return result;
15
21
  }
16
22
 
17
23
  export function getFlatRows(tableRows: Row[]) {
18
24
  const rows: FlatRow[] = [];
19
25
 
20
26
  function addRow(row: Row, nestedLevel: number, parentRowId?: string | number) {
21
- if (parentRowId) {
22
- row.parentRowId = parentRowId;
23
- }
24
-
25
- row.nestedLevel = nestedLevel;
27
+ const flatRow: FlatRow = {
28
+ ...row,
29
+ nestedLevel,
30
+ ...(parentRowId !== undefined ? { parentRowId } : {}),
31
+ };
26
32
 
27
- rows.push(row as FlatRow);
33
+ rows.push(flatRow);
28
34
 
29
35
  if (row.row && !Array.isArray(row.row)) {
30
36
  addRow(row.row, nestedLevel + 1, row.id);
@@ -121,10 +121,6 @@ function onChange() {
121
121
  emit("input", newModelValue);
122
122
  }
123
123
 
124
- function onIconClick() {
125
- document.getElementById(elementId)?.click();
126
- }
127
-
128
124
  /**
129
125
  * Get element / nested component attributes for each config token ✨
130
126
  * Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
@@ -185,10 +181,10 @@ const {
185
181
  @change="onChange"
186
182
  />
187
183
 
188
- <div
184
+ <label
189
185
  v-if="isChecked"
190
186
  v-bind="partial ? partiallyCheckedAttrs : checkedAttrs"
191
- @click="onIconClick"
187
+ :for="elementId"
192
188
  >
193
189
  <UIcon
194
190
  v-if="partial"
@@ -198,7 +194,7 @@ const {
198
194
  />
199
195
 
200
196
  <UIcon v-else :name="config.defaults.checkedIcon" color="inherit" v-bind="checkedIconAttrs" />
201
- </div>
197
+ </label>
202
198
 
203
199
  <template #bottom>
204
200
  <!-- @slot Use it to add something below the checkbox. -->
@@ -15,7 +15,7 @@ export default /*tw*/ {
15
15
  bodyWarning: "{>body} bg-radial-[circle_at_0%_50%] from-warning/25 from-2.17% to-transparent",
16
16
  bodyError: "{>body} bg-radial-[circle_at_0%_50%] from-error/25 from-2.17% to-transparent",
17
17
  bodyInfo: "{>body} bg-radial-[circle_at_0%_50%] from-info/25 from-2.17% to-transparent",
18
- content: "w-full flex flex-col max-w-full text-medium text-inverted",
18
+ content: "w-full flex flex-col max-w-full text-medium text-inverted break-all text-wrap",
19
19
  label: "mb-0.5 font-medium",
20
20
  description: "wrap-break-word font-normal",
21
21
  statusIcon: "{UIcon} brightness-125 dark:brightness-75",
@@ -48,6 +48,38 @@ const preparedNumber = computed(() => {
48
48
  );
49
49
  });
50
50
 
51
+ const formattedNumber = computed(() => {
52
+ let result = "";
53
+
54
+ if (props.currencyAlign === "left" && props.currency) {
55
+ result += props.currency;
56
+
57
+ if (props.currencySpace) {
58
+ result += " ";
59
+ }
60
+ }
61
+
62
+ if (props.value) {
63
+ result += mathSign.value;
64
+ }
65
+
66
+ result += preparedNumber.value.integer;
67
+
68
+ if (props.maxFractionDigits > 0) {
69
+ result += preparedNumber.value.decimalSeparator + preparedNumber.value.fraction;
70
+ }
71
+
72
+ if (props.currencyAlign === "right" && props.currency) {
73
+ if (props.currencySpace) {
74
+ result += " ";
75
+ }
76
+
77
+ result += props.currency;
78
+ }
79
+
80
+ return result;
81
+ });
82
+
51
83
  defineExpose({
52
84
  /**
53
85
  * A reference to the component's wrapper element for direct DOM manipulation.
@@ -76,10 +108,12 @@ const {
76
108
  <!-- @slot Use it to add something before the number. -->
77
109
  <slot name="left" />
78
110
 
79
- <div v-bind="numberAttrs" :data-test="getDataTest()">
111
+ <template v-if="raw">{{ formattedNumber }}</template>
112
+
113
+ <div v-else v-bind="numberAttrs" :data-test="getDataTest()">
80
114
  <span v-if="currencyAlign === 'left' && currency" v-bind="currencyAttrs" v-text="currency" />
81
115
 
82
- <span v-if="value" v-bind="mathSignAttrs" v-text="mathSign" />
116
+ <span v-if="value && mathSign" v-bind="mathSignAttrs" v-text="mathSign" />
83
117
 
84
118
  <span v-bind="integerAttrs" v-text="preparedNumber.integer" />
85
119
 
@@ -31,6 +31,7 @@ export default /*tw*/ {
31
31
  align: "left",
32
32
  currencyAlign: "right",
33
33
  currencySpace: false,
34
+ raw: false,
34
35
  minFractionDigits: 0,
35
36
  maxFractionDigits: 2,
36
37
  decimalSeparator: ",",
@@ -92,6 +92,17 @@ Sizes.args = { enum: "size" };
92
92
  export const CurrencyAlign = EnumTemplate.bind({});
93
93
  CurrencyAlign.args = { enum: "currencyAlign", currency: "USD", currencySpace: true };
94
94
 
95
+ export const Raw = DefaultTemplate.bind({});
96
+ Raw.args = { raw: true, currency: "USD", currencySpace: true };
97
+ Raw.parameters = {
98
+ docs: {
99
+ description: {
100
+ story:
101
+ "When `raw` is enabled, the number is displayed without formatting and html tags, showing only the raw value.",
102
+ },
103
+ },
104
+ };
105
+
95
106
  export const LimitFractionDigits: StoryFn<UNumberArgs> = (args: UNumberArgs) => ({
96
107
  components: { UNumber, UCol },
97
108
  setup: () => ({ args }),
@@ -192,6 +192,96 @@ describe("UNumber.vue", () => {
192
192
  });
193
193
  });
194
194
 
195
+ it("Raw – renders formatted number as plain text without HTML elements when raw is true", () => {
196
+ const component = mount(UNumber, {
197
+ props: {
198
+ value,
199
+ raw: true,
200
+ },
201
+ });
202
+
203
+ // Should render plain text without the number div structure
204
+ expect(component.find("[vl-key='number']").exists()).toBe(false);
205
+ expect(component.text()).toContain("1 234,56");
206
+ });
207
+
208
+ it("Raw – renders with currency when raw is true and currency is set", () => {
209
+ const currency = "$";
210
+
211
+ const component = mount(UNumber, {
212
+ props: {
213
+ value,
214
+ currency,
215
+ currencyAlign: "left",
216
+ raw: true,
217
+ },
218
+ });
219
+
220
+ expect(component.text()).toBe("$1 234,56");
221
+ });
222
+
223
+ it("Raw – renders with currency and space when raw is true, currency is set, and currencySpace is true", () => {
224
+ const currency = "$";
225
+
226
+ const component = mount(UNumber, {
227
+ props: {
228
+ value,
229
+ currency,
230
+ currencyAlign: "left",
231
+ currencySpace: true,
232
+ raw: true,
233
+ },
234
+ });
235
+
236
+ expect(component.text()).toBe("$ 1 234,56");
237
+ });
238
+
239
+ it("Raw – renders with currency on right when raw is true and currencyAlign is right", () => {
240
+ const currency = "$";
241
+
242
+ const component = mount(UNumber, {
243
+ props: {
244
+ value,
245
+ currency,
246
+ currencyAlign: "right",
247
+ raw: true,
248
+ },
249
+ });
250
+
251
+ expect(component.text()).toBe("1 234,56$");
252
+ });
253
+
254
+ // eslint-disable-next-line vue/max-len
255
+ it("Raw – renders with currency on right and space when raw is true, currencyAlign is right, and currencySpace is true", () => {
256
+ const currency = "$";
257
+
258
+ const component = mount(UNumber, {
259
+ props: {
260
+ value,
261
+ currency,
262
+ currencyAlign: "right",
263
+ currencySpace: true,
264
+ raw: true,
265
+ },
266
+ });
267
+
268
+ expect(component.text()).toBe("1 234,56 $");
269
+ });
270
+
271
+ it("Raw – renders with sign when raw is true and sign is set", () => {
272
+ const testNegativeValue = -123;
273
+
274
+ const component = mount(UNumber, {
275
+ props: {
276
+ value: testNegativeValue,
277
+ sign: MATH_SIGN_TYPE.auto as Props["sign"],
278
+ raw: true,
279
+ },
280
+ });
281
+
282
+ expect(component.text()).toContain(MATH_SIGN.MINUS);
283
+ });
284
+
195
285
  it("MinFractionDigits – adds zeros to meet the minimum fraction digits requirement", () => {
196
286
  const value = 123;
197
287
  const minFractionDigits = 2;
@@ -48,6 +48,11 @@ export interface Props {
48
48
  */
49
49
  currencySpace?: boolean;
50
50
 
51
+ /**
52
+ * Show formatted number as plain text without HTML elements.
53
+ */
54
+ raw?: boolean;
55
+
51
56
  /**
52
57
  * Minimal number of signs after the decimal separator (fractional part of a number).
53
58
  */
@@ -4,10 +4,10 @@ export function createMergeConfigs(cx: any): ({ defaultConfig, globalConfig, pro
4
4
  propsConfig: any;
5
5
  config?: {} | undefined;
6
6
  isVariants?: boolean | undefined;
7
- }) => {};
7
+ }) => any;
8
8
  export function createGetMergedConfig(cx: any): ({ defaultConfig, globalConfig, propsConfig, unstyled }: {
9
9
  defaultConfig: any;
10
10
  globalConfig: any;
11
11
  propsConfig: any;
12
12
  unstyled: any;
13
- }) => {};
13
+ }) => any;