vueless 1.3.2 → 1.3.3

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.
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 -960 960 960"><path d="m640.22-454.22 86 77v68.37H514.07v240.28L480-34.5l-34.07-34.07v-240.28H234.02v-68.37l80-77v-327.45h-50v-68.13h426.2v68.13h-50v327.45Zm-313.96 77h301.48l-55.89-51.89v-352.56h-189.7v352.56l-55.89 51.89Zm150.74 0Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 -960 960 960"><path d="M480-338.26 234.26-584 283-632.74l197 197 197-197L725.74-584 480-338.26Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 -960 960 960"><path d="m480-548.26-197 197L234.26-400 480-645.74 725.74-400 677-351.26l-197-197Z"/></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
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",
@@ -40,7 +40,9 @@ import type {
40
40
  Config,
41
41
  DateDivider,
42
42
  FlatRow,
43
+ ColumnObject,
43
44
  } from "./types";
45
+ import { StickySide } from "./types";
44
46
 
45
47
  defineOptions({ inheritAttrs: false });
46
48
 
@@ -166,13 +168,20 @@ const visibleColumns = computed(() => {
166
168
  return normalizedColumns.value.filter((column) => column.isShown !== false);
167
169
  });
168
170
 
171
+ const columnPositions = ref<Map<string, number>>(new Map());
172
+
169
173
  const colsCount = computed(() => {
170
174
  return normalizedColumns.value.length + 1;
171
175
  });
172
176
 
173
- const isShownActionsHeader = computed(
174
- () => hasSlotContent(slots["header-actions"]) && Boolean(localSelectedRows.value.length),
175
- );
177
+ const isShownActionsHeader = computed(() => {
178
+ const hasSelectedRows = Boolean(localSelectedRows.value.length);
179
+ const hasHeaderActions = hasSlotContent(slots["header-actions"], {
180
+ "selected-rows": localSelectedRows.value,
181
+ });
182
+
183
+ return hasSelectedRows && hasHeaderActions;
184
+ });
176
185
 
177
186
  const isHeaderSticky = computed(() => {
178
187
  const positionForFixHeader =
@@ -212,6 +221,8 @@ const tableRowAttrs = computed(() => ({
212
221
  bodyCellNestedIconWrapperAttrs,
213
222
  bodyRowCheckedAttrs,
214
223
  bodyRowAttrs,
224
+ bodyCellStickyLeftAttrs,
225
+ bodyCellStickyRightAttrs,
215
226
  }));
216
227
 
217
228
  watch(localSelectedRows, onChangeLocalSelectedRows, { deep: true });
@@ -223,15 +234,19 @@ watch(isFooterSticky, (newValue) =>
223
234
  newValue ? nextTick(setFooterCellWidth) : setFooterCellWidth(null),
224
235
  );
225
236
 
226
- onMounted(() => {
237
+ onMounted(async () => {
227
238
  document.addEventListener("keyup", onKeyupEsc);
228
239
  document.addEventListener("scroll", onScroll, { passive: true });
229
240
  window.addEventListener("resize", onWindowResize);
241
+
242
+ await nextTick();
243
+ calculateStickyColumnPositions();
230
244
  });
231
245
 
232
246
  onUpdated(() => {
233
247
  tableHeight.value = Number(tableWrapperRef.value?.offsetHeight);
234
248
  tableWidth.value = Number(tableWrapperRef.value?.offsetWidth);
249
+ calculateStickyColumnPositions();
235
250
  });
236
251
 
237
252
  onBeforeUnmount(() => {
@@ -257,6 +272,106 @@ function onWindowResize() {
257
272
 
258
273
  setHeaderCellWidth();
259
274
  setFooterCellWidth();
275
+ calculateStickyColumnPositions();
276
+ }
277
+
278
+ function calculateStickyColumnPositions() {
279
+ if (!headerRowRef.value) return;
280
+
281
+ const headerCells = [...headerRowRef.value.children] as HTMLElement[];
282
+ const positions = new Map<string, number>();
283
+ let leftOffset = 0;
284
+
285
+ if (props.selectable) {
286
+ leftOffset = headerCells[0]?.offsetWidth || 0;
287
+ }
288
+
289
+ visibleColumns.value.forEach((column, index) => {
290
+ const cellIndex = props.selectable ? index + 1 : index;
291
+ const cell = headerCells[cellIndex];
292
+
293
+ if (!cell) return;
294
+
295
+ if (column.sticky === StickySide.Left) {
296
+ positions.set(column.key, leftOffset);
297
+ leftOffset += cell.offsetWidth;
298
+ }
299
+ });
300
+
301
+ let rightOffset = 0;
302
+
303
+ for (let i = visibleColumns.value.length - 1; i >= 0; i--) {
304
+ const column = visibleColumns.value[i];
305
+ const cellIndex = props.selectable ? i + 1 : i;
306
+ const cell = headerCells[cellIndex];
307
+
308
+ if (!cell) continue;
309
+
310
+ if (column.sticky === StickySide.Right) {
311
+ positions.set(column.key, rightOffset);
312
+ rightOffset += cell.offsetWidth;
313
+ }
314
+ }
315
+
316
+ let hasChanged = positions.size !== columnPositions.value.size;
317
+
318
+ if (!hasChanged) {
319
+ for (const [key, value] of positions) {
320
+ if (columnPositions.value.get(key) !== value) {
321
+ hasChanged = true;
322
+ break;
323
+ }
324
+ }
325
+ }
326
+
327
+ if (hasChanged) {
328
+ columnPositions.value = positions;
329
+ }
330
+ }
331
+
332
+ function getStickyColumnStyle(column: ColumnObject) {
333
+ const position = columnPositions.value.get(column.key);
334
+
335
+ if (position === undefined) return {};
336
+
337
+ if (column.sticky === StickySide.Left) {
338
+ return { left: `${position / PX_IN_REM}rem` };
339
+ }
340
+
341
+ if (column.sticky === StickySide.Right) {
342
+ return { right: `${position / PX_IN_REM}rem` };
343
+ }
344
+
345
+ return {};
346
+ }
347
+
348
+ function getStickyColumnClass(column: ColumnObject) {
349
+ if (column.sticky === StickySide.Left) {
350
+ return headerCellStickyLeftAttrs.value.class as string;
351
+ }
352
+
353
+ if (column.sticky === StickySide.Right) {
354
+ return headerCellStickyRightAttrs.value.class as string;
355
+ }
356
+
357
+ return "";
358
+ }
359
+
360
+ function getHeaderCheckboxCellClass() {
361
+ return cx([
362
+ headerCellCheckboxAttrs.value.class as string,
363
+ visibleColumns.value[0]?.sticky === StickySide.Left
364
+ ? (headerCellStickyLeftAttrs.value.class as string)
365
+ : "",
366
+ ]);
367
+ }
368
+
369
+ function getHeaderCellClass(column: ColumnObject) {
370
+ return cx([
371
+ headerCellBaseAttrs.value.class as string,
372
+ column.thClass,
373
+ getStickyColumnClass(column),
374
+ ]);
260
375
  }
261
376
 
262
377
  function getDateDividerData(rowDate: string | Date | undefined) {
@@ -510,6 +625,10 @@ const {
510
625
  bodyCellNestedIconWrapperAttrs,
511
626
  bodyRowCheckedAttrs,
512
627
  bodyRowAttrs,
628
+ headerCellStickyLeftAttrs,
629
+ headerCellStickyRightAttrs,
630
+ bodyCellStickyLeftAttrs,
631
+ bodyCellStickyRightAttrs,
513
632
  } = useUI<Config>(defaultConfig, mutatedProps);
514
633
  </script>
515
634
 
@@ -652,7 +771,12 @@ const {
652
771
  </tr>
653
772
 
654
773
  <tr ref="header-row" v-bind="headerRowAttrs">
655
- <th v-if="selectable" v-bind="headerCellCheckboxAttrs">
774
+ <th
775
+ v-if="selectable"
776
+ v-bind="headerCellCheckboxAttrs"
777
+ :class="getHeaderCheckboxCellClass()"
778
+ :style="visibleColumns[0]?.sticky === StickySide.Left ? { left: '0' } : {}"
779
+ >
656
780
  <UCheckbox
657
781
  v-model="selectAll"
658
782
  size="md"
@@ -676,7 +800,8 @@ const {
676
800
  v-for="(column, index) in visibleColumns"
677
801
  :key="index"
678
802
  v-bind="headerCellBaseAttrs"
679
- :class="cx([(headerCellBaseAttrs as any).class, column.thClass])"
803
+ :class="getHeaderCellClass(column)"
804
+ :style="getStickyColumnStyle(column)"
680
805
  >
681
806
  <!--
682
807
  @slot Use it to customize needed header cell.
@@ -684,7 +809,7 @@ const {
684
809
  @binding {number} index
685
810
  -->
686
811
  <slot
687
- v-if="hasSlotContent($slots[`header-${column.key}`])"
812
+ v-if="hasSlotContent($slots[`header-${column.key}`], { column, index })"
688
813
  :name="`header-${column.key}`"
689
814
  :column="column"
690
815
  :index="index"
@@ -754,6 +879,7 @@ const {
754
879
  :data-test="getDataTest('row')"
755
880
  :is-expanded="localExpandedRows.includes(row.id)"
756
881
  :is-checked="isRowSelected(row)"
882
+ :column-positions="columnPositions"
757
883
  @click="onClickRow"
758
884
  @dblclick="onDoubleClickRow"
759
885
  @click-cell="onClickCell"
@@ -14,7 +14,8 @@ import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
14
14
 
15
15
  import defaultConfig from "./config";
16
16
 
17
- import type { Cell, CellObject, Row, UTableRowProps, Config } from "./types";
17
+ import { StickySide } from "./types";
18
+ import type { Cell, CellObject, Row, UTableRowProps, Config, ColumnObject } from "./types";
18
19
 
19
20
  const NESTED_ROW_SHIFT_REM = 1.5;
20
21
  const LAST_NESTED_ROW_SHIFT_REM = 1;
@@ -166,6 +167,34 @@ function onInputCheckbox(row: Row) {
166
167
  emit("toggleCheckbox", row);
167
168
  }
168
169
 
170
+ function getStickyColumnStyle(column: ColumnObject) {
171
+ const position = props.columnPositions.get(column.key);
172
+
173
+ if (position === undefined) return {};
174
+
175
+ if (column.sticky === StickySide.Left) {
176
+ return { left: `${position / PX_IN_REM}rem` };
177
+ }
178
+
179
+ if (column.sticky === StickySide.Right) {
180
+ return { right: `${position / PX_IN_REM}rem` };
181
+ }
182
+
183
+ return {};
184
+ }
185
+
186
+ function getStickyColumnClass(column: ColumnObject) {
187
+ if (column.sticky === StickySide.Left) {
188
+ return props.attrs.bodyCellStickyLeftAttrs.value.class as string;
189
+ }
190
+
191
+ if (column.sticky === StickySide.Right) {
192
+ return props.attrs.bodyCellStickyRightAttrs.value.class as string;
193
+ }
194
+
195
+ return "";
196
+ }
197
+
169
198
  const { getDataTest } = useUI<Config>(defaultConfig);
170
199
  </script>
171
200
 
@@ -179,8 +208,17 @@ const { getDataTest } = useUI<Config>(defaultConfig);
179
208
  >
180
209
  <td
181
210
  v-if="selectable"
182
- :style="getNestedCheckboxShift()"
211
+ :style="{
212
+ ...getNestedCheckboxShift(),
213
+ ...(columns[0]?.sticky === StickySide.Left ? { left: '0' } : {}),
214
+ }"
183
215
  v-bind="attrs.bodyCellCheckboxAttrs.value"
216
+ :class="
217
+ cx([
218
+ attrs.bodyCellCheckboxAttrs.value.class,
219
+ columns[0]?.sticky === StickySide.Left ? attrs.bodyCellStickyLeftAttrs.value.class : '',
220
+ ])
221
+ "
184
222
  @click.stop
185
223
  @dblclick.stop
186
224
  >
@@ -198,7 +236,14 @@ const { getDataTest } = useUI<Config>(defaultConfig);
198
236
  v-for="(value, key, index) in mapRowColumns(row, columns)"
199
237
  :key="index"
200
238
  v-bind="attrs.bodyCellBaseAttrs.value"
201
- :class="cx([columns[index].tdClass, getCellClasses(row, String(key))])"
239
+ :class="
240
+ cx([
241
+ columns[index].tdClass,
242
+ getCellClasses(row, String(key)),
243
+ getStickyColumnClass(columns[index]),
244
+ ])
245
+ "
246
+ :style="getStickyColumnStyle(columns[index])"
202
247
  @click="onClickCell(value, row, key)"
203
248
  >
204
249
  <div
@@ -30,7 +30,7 @@ export default /*tw*/ {
30
30
  headerActionsCheckbox: "{UCheckbox}",
31
31
  headerActionsCounter: "{>headerCounterBase} -ml-1.5",
32
32
  tableWrapper: "border border-solid border-muted rounded-medium bg-default overflow-x-auto",
33
- table: "min-w-full border-none text-medium w-full table-auto",
33
+ table: "min-w-full border-none text-medium w-auto table-auto",
34
34
  header:
35
35
  "border-b border-muted [&>tr:first-child>*]:first:rounded-tl-medium [&>tr:last-child>*]:last:rounded-tr-medium relative",
36
36
  headerRow: "",
@@ -44,12 +44,15 @@ export default /*tw*/ {
44
44
  },
45
45
  },
46
46
  },
47
+ headerCellSticky: "sticky z-20 bg-default",
48
+ headerCellStickyLeft: "{>headerCellSticky}",
49
+ headerCellStickyRight: "{>headerCellSticky}",
47
50
  headerCellCheckbox: "{>headerCellBase} w-10 pr-2",
48
51
  headerCheckbox: "{UCheckbox}",
49
52
  headerCounter: "{>stickyHeaderCounter} ml-px",
50
53
  headerLoader: "{ULoaderProgress} absolute top-auto bottom-0",
51
54
  body: "group/body divide-none",
52
- bodyRow: "hover:bg-muted",
55
+ bodyRow: "bg-default hover:bg-muted",
53
56
  bodyRowChecked: "bg-lifted transition",
54
57
  beforeBodyRow: "!p-0",
55
58
  beforeBodyRowChecked: "{>bodyRowChecked} !p-0",
@@ -65,6 +68,9 @@ export default /*tw*/ {
65
68
  },
66
69
  },
67
70
  },
71
+ bodyCellSticky: "sticky z-10 bg-inherit",
72
+ bodyCellStickyLeft: "{>bodyCellSticky}",
73
+ bodyCellStickyRight: "{>bodyCellSticky}",
68
74
  bodyCellContent: "text-ellipsis overflow-hidden",
69
75
  bodyCellCheckbox: "{>bodyCellBase} pr-2",
70
76
  bodyCellDateDivider: "",
@@ -100,7 +106,7 @@ export default /*tw*/ {
100
106
  },
101
107
  },
102
108
  stickyFooterRow: `
103
- fixed bottom-0 -ml-px border-b border-solid border-muted bg-default
109
+ fixed bottom-0 -ml-px border border-solid border-muted bg-default
104
110
  collapse group-[*]/footer-fixed:[visibility:inherit]
105
111
  `,
106
112
  i18n: {
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryFn } from "@storybook/vue3-vite";
2
+
2
3
  import {
3
4
  getArgTypes,
4
5
  getSlotNames,
@@ -18,7 +19,9 @@ import URow from "../../ui.container-row/URow.vue";
18
19
  import UIcon from "../../ui.image-icon/UIcon.vue";
19
20
  import ULoader from "../../ui.loader/ULoader.vue";
20
21
 
21
- import type { Row, Props } from "../types";
22
+ import tooltip from "../../v.tooltip/vTooltip";
23
+ import type { Row, Props, ColumnObject } from "../types";
24
+ import { StickySide } from "../types";
22
25
 
23
26
  interface UTableArgs extends Props {
24
27
  slotTemplate?: string;
@@ -476,6 +479,220 @@ DateDividerCustomLabel.parameters = {
476
479
  },
477
480
  };
478
481
 
482
+ export const StickyColumns: StoryFn<UTableArgs> = (args: UTableArgs) => ({
483
+ components: { UTable, URow, UBadge, UButton, UIcon },
484
+ directives: { tooltip },
485
+ setup() {
486
+ function toggleLeft(key: string) {
487
+ const column = args.columns.find((item) => (item as ColumnObject).key === key);
488
+
489
+ if (!column || typeof column === "string") return;
490
+
491
+ column.sticky = column.sticky === StickySide.Left ? undefined : StickySide.Left;
492
+ args.columns = args.columns.slice();
493
+ }
494
+
495
+ function toggleRight(key: string) {
496
+ const column = args.columns.find((item) => (item as ColumnObject).key === key);
497
+
498
+ if (!column || typeof column === "string") return;
499
+
500
+ column.sticky = column.sticky === StickySide.Right ? undefined : StickySide.Right;
501
+ args.columns = args.columns.slice();
502
+ }
503
+
504
+ function isPinned(key: string, side: string) {
505
+ const column = args.columns.find((item) => (item as ColumnObject).key === key);
506
+
507
+ if (!column || typeof column === "string") return;
508
+
509
+ return column?.sticky === side;
510
+ }
511
+
512
+ return { args, toggleLeft, toggleRight, isPinned };
513
+ },
514
+ template: `
515
+ <UTable v-bind="args">
516
+ <template #header-orderId="{ column }">
517
+ <URow gap="2xs" align="center">
518
+ <div>{{ column.label }}</div>
519
+ <UIcon
520
+ name="keep"
521
+ size="xs"
522
+ interactive
523
+ v-tooltip="isPinned('orderId', 'left') ? 'Unpin left' : 'Pin left'"
524
+ :color="isPinned('orderId', 'left') ? 'primary' : 'inherit'"
525
+ @click.stop="toggleLeft('orderId')"
526
+ />
527
+ </URow>
528
+ </template>
529
+
530
+ <template #header-customerName="{ column }">
531
+ <URow gap="2xs" align="center">
532
+ <div>{{ column.label }}</div>
533
+ <UIcon
534
+ name="keep"
535
+ size="xs"
536
+ interactive
537
+ v-tooltip="isPinned('customerName', 'left') ? 'Unpin left' : 'Pin left'"
538
+ :color="isPinned('customerName', 'left') ? 'primary' : 'inherit'"
539
+ @click.stop="toggleLeft('customerName')"
540
+ />
541
+ </URow>
542
+ </template>
543
+
544
+ <template #header-email="{ column }">
545
+ <URow gap="2xs" align="center">
546
+ <div>{{ column.label }}</div>
547
+ <UIcon
548
+ name="keep"
549
+ size="xs"
550
+ interactive
551
+ v-tooltip="isPinned('email', 'left') ? 'Unpin left' : 'Pin left'"
552
+ :color="isPinned('email', 'left') ? 'primary' : 'inherit'"
553
+ @click.stop="toggleLeft('email')"
554
+ />
555
+ </URow>
556
+ </template>
557
+
558
+ <template #header-totalPrice="{ column }">
559
+ <URow gap="2xs" align="center">
560
+ <div>{{ column.label }}</div>
561
+ <UIcon
562
+ name="keep"
563
+ size="xs"
564
+ interactive
565
+ v-tooltip="isPinned('totalPrice', 'right') ? 'Unpin right' : 'Pin right'"
566
+ :color="isPinned('totalPrice', 'right') ? 'primary' : 'inherit'"
567
+ @click.stop="toggleRight('totalPrice')"
568
+ />
569
+ </URow>
570
+ </template>
571
+
572
+ <template #header-action="{ column }">
573
+ <URow gap="2xs" align="center">
574
+ <div>{{ column.label }}</div>
575
+ <UIcon
576
+ name="keep"
577
+ size="xs"
578
+ interactive
579
+ v-tooltip="isPinned('action', 'right') ? 'Unpin right' : 'Pin right'"
580
+ :color="isPinned('action', 'right') ? 'primary' : 'inherit'"
581
+ @click.stop="toggleRight('action')"
582
+ />
583
+ </URow>
584
+ </template>
585
+
586
+ <template #cell-status="{ value }">
587
+ <UBadge
588
+ :label="value"
589
+ variant="soft"
590
+ :color="
591
+ value === 'Delivered' ? 'success' :
592
+ value === 'Cancelled' ? 'error' :
593
+ value === 'Pending' ? 'notice' :
594
+ value === 'Shipped' ? 'info' : ''
595
+ "
596
+ />
597
+ </template>
598
+
599
+ <template #cell-action>
600
+ <UButton label="View" size="xs" variant="soft" />
601
+ </template>
602
+ </UTable>
603
+ `,
604
+ });
605
+ StickyColumns.args = {
606
+ columns: [
607
+ { key: "orderId", label: "Order ID", sticky: "left" },
608
+ { key: "customerName", label: "Customer Name", thClass: "min-w-[200px]" },
609
+ { key: "email", label: "Email", thClass: "min-w-[250px]", sticky: "left" },
610
+ { key: "phone", label: "Phone", thClass: "min-w-[150px]" },
611
+ { key: "address", label: "Address", thClass: "min-w-[300px]" },
612
+ { key: "city", label: "City", thClass: "min-w-[150px]" },
613
+ { key: "country", label: "Country", thClass: "min-w-[150px]" },
614
+ { key: "status", label: "Status", thClass: "min-w-[120px]" },
615
+ { key: "totalPrice", label: "Total Price", thClass: "min-w-[120px]" },
616
+ { key: "action", label: "Actions", sticky: "right", thClass: "min-w-[100px]" },
617
+ ],
618
+ rows: [
619
+ {
620
+ id: "row-1",
621
+ orderId: "ORD-1001",
622
+ customerName: "Alice Johnson",
623
+ email: "alice.johnson@example.com",
624
+ phone: "+1 (555) 123-4567",
625
+ address: "123 Main Street, Apt 4B",
626
+ city: "New York",
627
+ country: "USA",
628
+ status: "Delivered",
629
+ totalPrice: "$245.99",
630
+ action: "View",
631
+ },
632
+ {
633
+ id: "row-2",
634
+ orderId: "ORD-1002",
635
+ customerName: "Michael Smith",
636
+ email: "michael.smith@example.com",
637
+ phone: "+1 (555) 234-5678",
638
+ address: "456 Oak Avenue, Suite 200",
639
+ city: "Los Angeles",
640
+ country: "USA",
641
+ status: "Pending",
642
+ totalPrice: "$189.50",
643
+ action: "View",
644
+ },
645
+ {
646
+ id: "row-3",
647
+ orderId: "ORD-1003",
648
+ customerName: "Emma Brown",
649
+ email: "emma.brown@example.com",
650
+ phone: "+1 (555) 345-6789",
651
+ address: "789 Pine Road, Building C",
652
+ city: "Chicago",
653
+ country: "USA",
654
+ status: "Shipped",
655
+ totalPrice: "$312.75",
656
+ action: "View",
657
+ },
658
+ {
659
+ id: "row-4",
660
+ orderId: "ORD-1004",
661
+ customerName: "James Wilson",
662
+ email: "james.wilson@example.com",
663
+ phone: "+1 (555) 456-7890",
664
+ address: "321 Elm Street, Floor 3",
665
+ city: "Houston",
666
+ country: "USA",
667
+ status: "Cancelled",
668
+ totalPrice: "$156.20",
669
+ action: "View",
670
+ },
671
+ {
672
+ id: "row-5",
673
+ orderId: "ORD-1005",
674
+ customerName: "Sophia Davis",
675
+ email: "sophia.davis@example.com",
676
+ phone: "+1 (555) 567-8901",
677
+ address: "654 Maple Drive, Unit 12",
678
+ city: "Phoenix",
679
+ country: "USA",
680
+ status: "Delivered",
681
+ totalPrice: "$428.90",
682
+ action: "View",
683
+ },
684
+ ],
685
+ };
686
+ StickyColumns.parameters = {
687
+ docs: {
688
+ description: {
689
+ story:
690
+ "Pin columns to the left or right edge of the table when scrolling horizontally. " +
691
+ "Use the header pin icons to toggle pinning for each column.",
692
+ },
693
+ },
694
+ };
695
+
479
696
  export const HeaderCounterSlot = DefaultTemplate.bind({});
480
697
  HeaderCounterSlot.args = {
481
698
  selectable: true,
@@ -491,7 +708,7 @@ export const HeaderKeySlot = DefaultTemplate.bind({});
491
708
  HeaderKeySlot.args = {
492
709
  slotTemplate: `
493
710
  <template #header-status="{ column }">
494
- <UBadge :label="column?.label" />
711
+ <UBadge :label="column.label" />
495
712
  </template>
496
713
  `,
497
714
  };
@@ -620,9 +837,9 @@ ExpandSlot.args = {
620
837
  slotTemplate: `
621
838
  <template #expand="{ expanded }">
622
839
  <UButton
623
- :icon="expanded ? 'remove' : 'add'"
624
- variant="ghost"
625
- size="xs"
840
+ :icon="expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down'"
841
+ variant="soft"
842
+ size="2xs"
626
843
  square
627
844
  />
628
845
  </template>
@@ -738,7 +955,7 @@ EmptyStateSlot.args = {
738
955
  },
739
956
  slotTemplate: `
740
957
  <template #empty-state>
741
- <ULoader loading size="lg" :config="{ loader: 'mx-auto mb-4' }" />
958
+ <ULoader loading :config="{ loader: 'mx-auto mb-4' }" />
742
959
  <p class="text-center">Fetching latest data, please wait...</p>
743
960
  </template>
744
961
  `,
@@ -34,6 +34,8 @@ describe("UTableRow.vue", () => {
34
34
  bodyCellNestedIconWrapperAttrs: ref({ class: "icon-wrapper" }),
35
35
  bodyRowCheckedAttrs: ref({ class: "row-checked" }),
36
36
  bodyRowAttrs: ref({ class: "row-base" }),
37
+ bodyCellStickyLeftAttrs: ref({ class: "sticky-left" }),
38
+ bodyCellStickyRightAttrs: ref({ class: "sticky-right" }),
37
39
  };
38
40
 
39
41
  const defaultConfig = {
@@ -44,6 +46,12 @@ describe("UTableRow.vue", () => {
44
46
  } as Config;
45
47
 
46
48
  function getDefaultProps(overrides = {}) {
49
+ const columnPositions = new Map<string, number>();
50
+
51
+ columnPositions.set("name", 0);
52
+ columnPositions.set("email", 100);
53
+ columnPositions.set("role", 200);
54
+
47
55
  return {
48
56
  row: defaultRow,
49
57
  columns: defaultColumns,
@@ -56,6 +64,7 @@ describe("UTableRow.vue", () => {
56
64
  config: defaultConfig,
57
65
  isChecked: false,
58
66
  isExpanded: false,
67
+ columnPositions,
59
68
  ...overrides,
60
69
  };
61
70
  }
@@ -39,11 +39,16 @@ export interface FlatRow extends Row {
39
39
  parentRowId?: RowId;
40
40
  nestedLevel: number;
41
41
  }
42
+ export enum StickySide {
43
+ Left = "left",
44
+ Right = "right",
45
+ }
42
46
 
43
47
  export interface ColumnObject {
44
48
  key: string;
45
49
  label?: string;
46
50
  isShown?: boolean;
51
+ sticky?: "left" | "right";
47
52
  class?: string | ((value: unknown | string, row: Row) => string);
48
53
  tdClass?: string;
49
54
  thClass?: string;
@@ -129,6 +134,8 @@ export interface UTableRowAttrs {
129
134
  bodyCellNestedIconWrapperAttrs: Ref<UnknownObject>;
130
135
  bodyRowCheckedAttrs: Ref<UnknownObject>;
131
136
  bodyRowAttrs: Ref<UnknownObject>;
137
+ bodyCellStickyLeftAttrs: Ref<UnknownObject>;
138
+ bodyCellStickyRightAttrs: Ref<UnknownObject>;
132
139
  }
133
140
 
134
141
  export interface UTableRowProps {
@@ -143,4 +150,5 @@ export interface UTableRowProps {
143
150
  config: Config;
144
151
  isChecked: boolean;
145
152
  isExpanded: boolean;
153
+ columnPositions: Map<string, number>;
146
154
  }