vueless 1.4.12-beta.0 → 1.4.12-beta.10
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/composables/useUI.ts +19 -1
- package/package.json +2 -2
- package/ui.container-drawer/tests/UDrawer.test.ts +10 -17
- package/ui.container-modal/tests/UModal.test.ts +10 -17
- package/ui.data-list/UDataList.vue +3 -0
- package/ui.data-list/config.ts +4 -1
- package/ui.data-list/storybook/stories.ts +16 -0
- package/ui.data-list/tests/UDataList.test.ts +77 -0
- package/ui.data-list/types.ts +15 -0
- package/ui.data-table/UTable.vue +179 -14
- package/ui.data-table/UTableRow.vue +4 -3
- package/ui.data-table/config.ts +10 -1
- package/ui.data-table/storybook/stories.ts +30 -0
- package/ui.data-table/tests/UTable.test.ts +84 -6
- package/ui.data-table/types.ts +15 -1
- package/ui.form-input/UInput.vue +1 -0
- package/ui.form-input/tests/UInput.test.ts +16 -0
- package/ui.navigation-tab/UTab.vue +14 -9
- package/ui.navigation-tab/tests/UTab.test.ts +14 -0
- package/ui.navigation-tabs/UTabs.vue +152 -2
- package/ui.navigation-tabs/config.ts +2 -1
- package/ui.navigation-tabs/constants.ts +2 -0
- package/ui.navigation-tabs/storybook/stories.ts +8 -0
- package/ui.navigation-tabs/tests/UTabs.test.ts +213 -1
- package/ui.navigation-tabs/types.ts +1 -1
|
@@ -400,6 +400,7 @@ function renderNestedFirstCell(value: Cell, key: string, cellIndex: number): VNo
|
|
|
400
400
|
|
|
401
401
|
function renderTableCell(value: Cell, key: string, index: number): VNode {
|
|
402
402
|
const keyStr = String(key);
|
|
403
|
+
const column = props.columns.find((col) => col.key === key) ?? props.columns[index];
|
|
403
404
|
|
|
404
405
|
const nestedCellNode = isNestedFirstCell(index)
|
|
405
406
|
? renderNestedFirstCell(value, key, index)
|
|
@@ -414,12 +415,12 @@ function renderTableCell(value: Cell, key: string, index: number): VNode {
|
|
|
414
415
|
...props.attrs.bodyCellBaseAttrs.value,
|
|
415
416
|
class: cx([
|
|
416
417
|
props.attrs.bodyCellBaseAttrs.value.class,
|
|
417
|
-
|
|
418
|
+
column.tdClass,
|
|
418
419
|
getCellClasses(props.row, keyStr),
|
|
419
|
-
getStickyColumnClass(
|
|
420
|
+
getStickyColumnClass(column),
|
|
420
421
|
getSearchMatchCellClass(keyStr),
|
|
421
422
|
]),
|
|
422
|
-
style: getStickyColumnStyle(
|
|
423
|
+
style: getStickyColumnStyle(column),
|
|
423
424
|
"data-cell-key": key,
|
|
424
425
|
"data-test": getDataTest(`${key}-cell`),
|
|
425
426
|
},
|
package/ui.data-table/config.ts
CHANGED
|
@@ -13,7 +13,11 @@ export default /*tw*/ {
|
|
|
13
13
|
},
|
|
14
14
|
compoundVariants: [
|
|
15
15
|
{ stickedHeader: true, actionsHeader: true, class: "rounded-none" },
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
stickedHeader: true,
|
|
18
|
+
actionsHeader: false,
|
|
19
|
+
class: "border-muted bg-default",
|
|
20
|
+
},
|
|
17
21
|
],
|
|
18
22
|
},
|
|
19
23
|
stickyHeaderCell: "{>headerCellBase} flex-none whitespace-nowrap",
|
|
@@ -111,6 +115,8 @@ export default /*tw*/ {
|
|
|
111
115
|
},
|
|
112
116
|
bodyEmptyState: "{UEmpty} my-8",
|
|
113
117
|
bodyEmptyStateCell: "",
|
|
118
|
+
skeletonCell: "{USkeleton} h-4 rounded-small",
|
|
119
|
+
skeletonCheckbox: "{USkeleton} size-4 rounded-small",
|
|
114
120
|
footer: {
|
|
115
121
|
base: "group/footer border-t border-solid border-muted",
|
|
116
122
|
variants: {
|
|
@@ -147,6 +153,9 @@ export default /*tw*/ {
|
|
|
147
153
|
stickyHeader: false,
|
|
148
154
|
stickyFooter: false,
|
|
149
155
|
loading: false,
|
|
156
|
+
skeletonLoading: false,
|
|
157
|
+
skeletonRows: 5,
|
|
158
|
+
skeletonWidths: ["w-11/12", "w-4/5", "w-3/4", "w-2/3", "w-1/2"],
|
|
150
159
|
textEllipsis: false,
|
|
151
160
|
search: "",
|
|
152
161
|
searchMatch: -1,
|
|
@@ -182,6 +182,36 @@ Loading.parameters = {
|
|
|
182
182
|
},
|
|
183
183
|
};
|
|
184
184
|
|
|
185
|
+
export const SkeletonLoading = DefaultTemplate.bind({});
|
|
186
|
+
SkeletonLoading.args = {
|
|
187
|
+
rows: [],
|
|
188
|
+
skeletonLoading: true,
|
|
189
|
+
};
|
|
190
|
+
SkeletonLoading.parameters = {
|
|
191
|
+
docs: {
|
|
192
|
+
description: {
|
|
193
|
+
story:
|
|
194
|
+
"Use `skeletonLoading` while the first request is pending. The table keeps its structure " +
|
|
195
|
+
"and renders cell skeletons instead of the empty state.",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const SkeletonLoadingConfigured = DefaultTemplate.bind({});
|
|
201
|
+
SkeletonLoadingConfigured.args = {
|
|
202
|
+
rows: [],
|
|
203
|
+
skeletonLoading: true,
|
|
204
|
+
skeletonRows: 3,
|
|
205
|
+
skeletonWidths: ["w-11/12", "w-2/3", "w-1/2"],
|
|
206
|
+
};
|
|
207
|
+
SkeletonLoadingConfigured.parameters = {
|
|
208
|
+
docs: {
|
|
209
|
+
description: {
|
|
210
|
+
story: "Configure skeleton row count and cell widths for a single table with props.",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
185
215
|
export const EmptyCellLabel = DefaultTemplate.bind({});
|
|
186
216
|
EmptyCellLabel.args = {
|
|
187
217
|
emptyCellLabel: "NO DATA",
|
|
@@ -7,6 +7,7 @@ import UTableRow from "../UTableRow.vue";
|
|
|
7
7
|
import UEmpty from "../../ui.container-empty/UEmpty.vue";
|
|
8
8
|
import ULoaderProgress from "../../ui.loader-progress/ULoaderProgress.vue";
|
|
9
9
|
import UDivider from "../../ui.container-divider/UDivider.vue";
|
|
10
|
+
import USkeleton from "../../ui.skeleton/USkeleton.vue";
|
|
10
11
|
import {
|
|
11
12
|
LoaderProgressSymbol,
|
|
12
13
|
createLoaderProgress,
|
|
@@ -149,6 +150,29 @@ describe("UTable.vue", () => {
|
|
|
149
150
|
expect(emptyComponent.exists()).toBe(true);
|
|
150
151
|
});
|
|
151
152
|
|
|
153
|
+
it("Skeleton loading – renders skeleton body instead of empty state", () => {
|
|
154
|
+
const component = mountUTable(getDefaultProps({ rows: [], skeletonLoading: true }));
|
|
155
|
+
|
|
156
|
+
expect(component.findComponent(UEmpty).exists()).toBe(false);
|
|
157
|
+
expect(component.findAllComponents(USkeleton).length).toBeGreaterThan(0);
|
|
158
|
+
expect(component.find("table").exists()).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("Skeleton loading – applies skeleton rows and widths from props", () => {
|
|
162
|
+
const component = mountUTable(
|
|
163
|
+
getDefaultProps({
|
|
164
|
+
rows: [],
|
|
165
|
+
skeletonLoading: true,
|
|
166
|
+
skeletonRows: 2,
|
|
167
|
+
skeletonWidths: ["w-1/3"],
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(component.findAll("tbody tr")).toHaveLength(2);
|
|
172
|
+
expect(component.findAllComponents(USkeleton)).toHaveLength(defaultColumns.length * 2);
|
|
173
|
+
expect(component.findComponent(USkeleton).classes()).toContain("w-1/3");
|
|
174
|
+
});
|
|
175
|
+
|
|
152
176
|
it("Selectable – renders select all checkbox when selectable is true", () => {
|
|
153
177
|
const component = mountUTable(getDefaultProps({ selectable: true }));
|
|
154
178
|
|
|
@@ -221,6 +245,20 @@ describe("UTable.vue", () => {
|
|
|
221
245
|
expect(loader.props("loading")).toBe(false);
|
|
222
246
|
});
|
|
223
247
|
|
|
248
|
+
it("Skeleton loading – hides progress loader while skeleton is shown", () => {
|
|
249
|
+
const component = mountUTable(
|
|
250
|
+
getDefaultProps({
|
|
251
|
+
rows: [],
|
|
252
|
+
loading: true,
|
|
253
|
+
skeletonLoading: true,
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const loader = component.findComponent(ULoaderProgress);
|
|
258
|
+
|
|
259
|
+
expect(loader.props("loading")).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
|
|
224
262
|
it("Data Test – applies correct data-test attributes", () => {
|
|
225
263
|
const dataTest = "test-table";
|
|
226
264
|
|
|
@@ -644,7 +682,14 @@ describe("UTable.vue", () => {
|
|
|
644
682
|
name: "Parent Row",
|
|
645
683
|
email: "parent@example.com",
|
|
646
684
|
role: "Admin",
|
|
647
|
-
row: [
|
|
685
|
+
row: [
|
|
686
|
+
{
|
|
687
|
+
id: "1-1",
|
|
688
|
+
name: "Child",
|
|
689
|
+
email: "child@example.com",
|
|
690
|
+
role: "User",
|
|
691
|
+
},
|
|
692
|
+
],
|
|
648
693
|
},
|
|
649
694
|
];
|
|
650
695
|
|
|
@@ -710,7 +755,14 @@ describe("UTable.vue", () => {
|
|
|
710
755
|
name: "Parent Row",
|
|
711
756
|
email: "parent@example.com",
|
|
712
757
|
role: "Admin",
|
|
713
|
-
row: [
|
|
758
|
+
row: [
|
|
759
|
+
{
|
|
760
|
+
id: "1-1",
|
|
761
|
+
name: "Child",
|
|
762
|
+
email: "child@example.com",
|
|
763
|
+
role: "User",
|
|
764
|
+
},
|
|
765
|
+
],
|
|
714
766
|
},
|
|
715
767
|
];
|
|
716
768
|
|
|
@@ -792,7 +844,14 @@ describe("UTable.vue", () => {
|
|
|
792
844
|
name: "Parent Row",
|
|
793
845
|
email: "parent@example.com",
|
|
794
846
|
role: "Admin",
|
|
795
|
-
row: [
|
|
847
|
+
row: [
|
|
848
|
+
{
|
|
849
|
+
id: "1-1",
|
|
850
|
+
name: "Child",
|
|
851
|
+
email: "child@example.com",
|
|
852
|
+
role: "User",
|
|
853
|
+
},
|
|
854
|
+
],
|
|
796
855
|
};
|
|
797
856
|
|
|
798
857
|
const component = mountUTable(getDefaultProps({ rows: [expandableRow] }));
|
|
@@ -813,7 +872,14 @@ describe("UTable.vue", () => {
|
|
|
813
872
|
name: "Parent Row",
|
|
814
873
|
email: "parent@example.com",
|
|
815
874
|
role: "Admin",
|
|
816
|
-
row: [
|
|
875
|
+
row: [
|
|
876
|
+
{
|
|
877
|
+
id: "1-1",
|
|
878
|
+
name: "Child",
|
|
879
|
+
email: "child@example.com",
|
|
880
|
+
role: "User",
|
|
881
|
+
},
|
|
882
|
+
],
|
|
817
883
|
};
|
|
818
884
|
|
|
819
885
|
const component = mountUTable(
|
|
@@ -879,7 +945,12 @@ describe("UTable.vue", () => {
|
|
|
879
945
|
email: "child1@example.com",
|
|
880
946
|
role: "User",
|
|
881
947
|
row: [
|
|
882
|
-
{
|
|
948
|
+
{
|
|
949
|
+
id: "1-1-1",
|
|
950
|
+
name: "Grandchild",
|
|
951
|
+
email: "grandchild@example.com",
|
|
952
|
+
role: "Guest",
|
|
953
|
+
},
|
|
883
954
|
],
|
|
884
955
|
},
|
|
885
956
|
],
|
|
@@ -902,7 +973,14 @@ describe("UTable.vue", () => {
|
|
|
902
973
|
name: "Parent Row",
|
|
903
974
|
email: "parent@example.com",
|
|
904
975
|
role: "Admin",
|
|
905
|
-
row: [
|
|
976
|
+
row: [
|
|
977
|
+
{
|
|
978
|
+
id: "1-1",
|
|
979
|
+
name: "Child",
|
|
980
|
+
email: "child@example.com",
|
|
981
|
+
role: "User",
|
|
982
|
+
},
|
|
983
|
+
],
|
|
906
984
|
};
|
|
907
985
|
|
|
908
986
|
const component = mountUTable(getDefaultProps({ rows: [expandableRow] }));
|
package/ui.data-table/types.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import defaultConfig from "./config";
|
|
2
|
-
|
|
3
2
|
import type { Ref } from "vue";
|
|
4
3
|
import type { ComponentConfig, UnknownObject } from "../types";
|
|
5
4
|
import type { Config as UDividerConfig } from "../ui.container-divider/types";
|
|
@@ -117,6 +116,21 @@ export interface Props {
|
|
|
117
116
|
*/
|
|
118
117
|
loading?: boolean;
|
|
119
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Show skeleton body for the first table load instead of the regular loader.
|
|
121
|
+
*/
|
|
122
|
+
skeletonLoading?: boolean;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Number of skeleton rows shown during initial loading.
|
|
126
|
+
*/
|
|
127
|
+
skeletonRows?: number;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Skeleton cell width classes used to vary the loading placeholder layout.
|
|
131
|
+
*/
|
|
132
|
+
skeletonWidths?: string[];
|
|
133
|
+
|
|
120
134
|
/**
|
|
121
135
|
* Enable virtual scrolling for large datasets.
|
|
122
136
|
*/
|
package/ui.form-input/UInput.vue
CHANGED
|
@@ -302,6 +302,22 @@ describe("UInput.vue", () => {
|
|
|
302
302
|
|
|
303
303
|
expect(component.get("input").attributes("data-test")).toBe(dataTestValue);
|
|
304
304
|
});
|
|
305
|
+
|
|
306
|
+
it("Data Test – forwards data-test to the label so the error block is targetable", () => {
|
|
307
|
+
const dataTestValue = "test-input";
|
|
308
|
+
|
|
309
|
+
const component = mount(UInput, {
|
|
310
|
+
props: {
|
|
311
|
+
dataTest: dataTestValue,
|
|
312
|
+
error: "This is an error",
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const errorElement = component.find(`[data-test="${dataTestValue}-label-error"]`);
|
|
317
|
+
|
|
318
|
+
expect(errorElement.exists()).toBe(true);
|
|
319
|
+
expect(errorElement.text()).toBe("This is an error");
|
|
320
|
+
});
|
|
305
321
|
});
|
|
306
322
|
|
|
307
323
|
describe("Slots", () => {
|
|
@@ -13,12 +13,13 @@ import type { Props as UTabsProps, SetUTabsSelectedItem } from "../ui.navigation
|
|
|
13
13
|
|
|
14
14
|
defineOptions({ inheritAttrs: false });
|
|
15
15
|
|
|
16
|
-
const setUTabsSelectedItem = inject<SetUTabsSelectedItem>("setUTabsSelectedItem");
|
|
17
|
-
const getUTabsSelectedItem = inject("getUTabsSelectedItem");
|
|
18
|
-
const getUTabsScrollable = inject<UTabsProps["scrollable"]>("getUTabsScrollable");
|
|
19
|
-
const getUTabsSquare = inject<UTabsProps["square"]>("getUTabsSquare");
|
|
20
|
-
const getUTabsBlock = inject<UTabsProps["block"]>("getUTabsBlock");
|
|
16
|
+
const setUTabsSelectedItem = inject<SetUTabsSelectedItem | null>("setUTabsSelectedItem", null);
|
|
17
|
+
const getUTabsSelectedItem = inject("getUTabsSelectedItem", null);
|
|
18
|
+
const getUTabsScrollable = inject<UTabsProps["scrollable"]>("getUTabsScrollable", false);
|
|
19
|
+
const getUTabsSquare = inject<UTabsProps["square"]>("getUTabsSquare", false);
|
|
20
|
+
const getUTabsBlock = inject<UTabsProps["block"]>("getUTabsBlock", false);
|
|
21
21
|
const getUTabsSize = inject<UTabsProps["size"]>("getUTabsSize", "md");
|
|
22
|
+
const getUTabsPreventTabClick = inject<() => boolean>("getUTabsPreventTabClick", () => false);
|
|
22
23
|
|
|
23
24
|
const props = withDefaults(defineProps<Props>(), {
|
|
24
25
|
...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
|
|
@@ -34,12 +35,16 @@ const size = computed(() => toValue(getUTabsSize));
|
|
|
34
35
|
const block = computed(() => toValue(getUTabsBlock));
|
|
35
36
|
const square = computed(() => toValue(getUTabsSquare));
|
|
36
37
|
const scrollable = computed(() => toValue(getUTabsScrollable));
|
|
37
|
-
const isActive = computed(() =>
|
|
38
|
+
const isActive = computed(() => {
|
|
39
|
+
if (!setUTabsSelectedItem) return true;
|
|
40
|
+
|
|
41
|
+
return toValue(getUTabsSelectedItem) === props.value;
|
|
42
|
+
});
|
|
38
43
|
|
|
39
44
|
async function onClickSetValue() {
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
if (toValue(getUTabsPreventTabClick) || props.disabled || !setUTabsSelectedItem) return;
|
|
46
|
+
|
|
47
|
+
setUTabsSelectedItem(props.value ?? "");
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
defineExpose({
|
|
@@ -149,6 +149,20 @@ describe("UTab.vue", () => {
|
|
|
149
149
|
|
|
150
150
|
// Active state tests
|
|
151
151
|
describe("Active state", () => {
|
|
152
|
+
it("Active – applies active classes when rendered without UTabs", () => {
|
|
153
|
+
const expectedClass = "border-primary";
|
|
154
|
+
|
|
155
|
+
const component = mount(UTab, {
|
|
156
|
+
props: {
|
|
157
|
+
label: "Tab Item",
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const button = component.findComponent(UButton);
|
|
162
|
+
|
|
163
|
+
expect(button.attributes("class")).toContain(expectedClass);
|
|
164
|
+
});
|
|
165
|
+
|
|
152
166
|
it("Active – applies active classes when tab is selected", () => {
|
|
153
167
|
const value = "tab1";
|
|
154
168
|
const expectedClass = "border-primary";
|
|
@@ -7,7 +7,7 @@ import { getDefaults } from "../utils/ui";
|
|
|
7
7
|
import UTab from "../ui.navigation-tab/UTab.vue";
|
|
8
8
|
import UButton from "../ui.button/UButton.vue";
|
|
9
9
|
|
|
10
|
-
import { COMPONENT_NAME, SCROLL_OFFSET } from "./constants";
|
|
10
|
+
import { COMPONENT_NAME, SCROLL_OFFSET, DRAG_THRESHOLD, DRAG_CLICK_SUPPRESS_MS } from "./constants";
|
|
11
11
|
import defaultConfig from "./config";
|
|
12
12
|
|
|
13
13
|
import type { Props, Config } from "./types";
|
|
@@ -37,6 +37,15 @@ const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
|
|
|
37
37
|
const scrollContainerRef = useTemplateRef<HTMLDivElement | null>("scroll-container");
|
|
38
38
|
const showLeftArrow = ref(false);
|
|
39
39
|
const showRightArrow = ref(false);
|
|
40
|
+
const isDragging = ref(false);
|
|
41
|
+
const preventTabClick = ref(false);
|
|
42
|
+
|
|
43
|
+
let isPointerDown = false;
|
|
44
|
+
let dragAxis: "x" | "y" | null = null;
|
|
45
|
+
let startX = 0;
|
|
46
|
+
let startY = 0;
|
|
47
|
+
let startScrollLeft = 0;
|
|
48
|
+
let suppressClickTimer: ReturnType<typeof setTimeout> | null = null;
|
|
40
49
|
|
|
41
50
|
function checkScroll() {
|
|
42
51
|
if (!scrollContainerRef.value) return;
|
|
@@ -59,17 +68,148 @@ function scrollNext() {
|
|
|
59
68
|
scrollContainerRef.value.scrollBy({ left: SCROLL_OFFSET, behavior: "smooth" });
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
function isScrollableOverflow() {
|
|
72
|
+
if (!scrollContainerRef.value) return false;
|
|
73
|
+
|
|
74
|
+
return scrollContainerRef.value.scrollWidth > scrollContainerRef.value.clientWidth;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function clearSuppressClickTimer() {
|
|
78
|
+
if (!suppressClickTimer) return;
|
|
79
|
+
|
|
80
|
+
clearTimeout(suppressClickTimer);
|
|
81
|
+
suppressClickTimer = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function stopDragListeners() {
|
|
85
|
+
document.removeEventListener("pointermove", onPointerMove);
|
|
86
|
+
document.removeEventListener("pointerup", onPointerUp);
|
|
87
|
+
document.removeEventListener("pointercancel", onPointerUp);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resetDragState() {
|
|
91
|
+
isPointerDown = false;
|
|
92
|
+
dragAxis = null;
|
|
93
|
+
isDragging.value = false;
|
|
94
|
+
document.body.style.cursor = "";
|
|
95
|
+
document.body.style.userSelect = "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function suppressTabClick() {
|
|
99
|
+
preventTabClick.value = true;
|
|
100
|
+
clearSuppressClickTimer();
|
|
101
|
+
|
|
102
|
+
suppressClickTimer = setTimeout(() => {
|
|
103
|
+
preventTabClick.value = false;
|
|
104
|
+
suppressClickTimer = null;
|
|
105
|
+
}, DRAG_CLICK_SUPPRESS_MS);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function onPointerDown(event: PointerEvent) {
|
|
109
|
+
if (!props.scrollable || event.button > 0 || !isScrollableOverflow()) return;
|
|
110
|
+
|
|
111
|
+
isPointerDown = true;
|
|
112
|
+
dragAxis = null;
|
|
113
|
+
startX = event.clientX;
|
|
114
|
+
startY = event.clientY;
|
|
115
|
+
startScrollLeft = scrollContainerRef.value?.scrollLeft ?? 0;
|
|
116
|
+
|
|
117
|
+
document.addEventListener("pointermove", onPointerMove, { passive: false });
|
|
118
|
+
document.addEventListener("pointerup", onPointerUp);
|
|
119
|
+
document.addEventListener("pointercancel", onPointerUp);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function onPointerMove(event: PointerEvent) {
|
|
123
|
+
if (!isPointerDown || !scrollContainerRef.value) return;
|
|
124
|
+
|
|
125
|
+
if (event.pointerType === "mouse" && event.buttons === 0) {
|
|
126
|
+
onPointerUp();
|
|
127
|
+
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const deltaX = event.clientX - startX;
|
|
132
|
+
const deltaY = event.clientY - startY;
|
|
133
|
+
|
|
134
|
+
if (!dragAxis) {
|
|
135
|
+
if (Math.abs(deltaX) < DRAG_THRESHOLD && Math.abs(deltaY) < DRAG_THRESHOLD) return;
|
|
136
|
+
|
|
137
|
+
dragAxis = Math.abs(deltaX) >= Math.abs(deltaY) ? "x" : "y";
|
|
138
|
+
|
|
139
|
+
if (dragAxis === "y") {
|
|
140
|
+
stopDragListeners();
|
|
141
|
+
resetDragState();
|
|
142
|
+
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
isDragging.value = true;
|
|
147
|
+
document.body.style.cursor = "move";
|
|
148
|
+
document.body.style.userSelect = "none";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (dragAxis !== "x") return;
|
|
152
|
+
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
scrollContainerRef.value.scrollLeft = startScrollLeft - deltaX;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function onPointerUp() {
|
|
158
|
+
const wasDragging = isDragging.value;
|
|
159
|
+
|
|
160
|
+
stopDragListeners();
|
|
161
|
+
resetDragState();
|
|
162
|
+
|
|
163
|
+
if (wasDragging) {
|
|
164
|
+
suppressTabClick();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onClickCapture(event: MouseEvent) {
|
|
169
|
+
if (!preventTabClick.value) return;
|
|
170
|
+
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
event.stopPropagation();
|
|
173
|
+
preventTabClick.value = false;
|
|
174
|
+
clearSuppressClickTimer();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getHorizontalWheelDelta(event: WheelEvent) {
|
|
178
|
+
if (Math.abs(event.deltaX) >= Math.abs(event.deltaY)) {
|
|
179
|
+
return event.deltaX;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return event.shiftKey ? event.deltaY : 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function onWheel(event: WheelEvent) {
|
|
186
|
+
if (!props.scrollable || !scrollContainerRef.value || !isScrollableOverflow()) return;
|
|
187
|
+
|
|
188
|
+
const deltaX = getHorizontalWheelDelta(event);
|
|
189
|
+
|
|
190
|
+
if (!deltaX) return;
|
|
191
|
+
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
scrollContainerRef.value.scrollLeft += deltaX;
|
|
194
|
+
}
|
|
195
|
+
|
|
62
196
|
onMounted(() => {
|
|
63
197
|
if (scrollContainerRef.value) {
|
|
64
198
|
scrollContainerRef.value.addEventListener("scroll", checkScroll, { passive: true });
|
|
199
|
+
scrollContainerRef.value.addEventListener("wheel", onWheel, { passive: false });
|
|
65
200
|
|
|
66
201
|
checkScroll();
|
|
67
202
|
}
|
|
68
203
|
});
|
|
69
204
|
|
|
70
205
|
onUnmounted(() => {
|
|
206
|
+
stopDragListeners();
|
|
207
|
+
resetDragState();
|
|
208
|
+
clearSuppressClickTimer();
|
|
209
|
+
|
|
71
210
|
if (scrollContainerRef.value) {
|
|
72
211
|
scrollContainerRef.value.removeEventListener("scroll", checkScroll);
|
|
212
|
+
scrollContainerRef.value.removeEventListener("wheel", onWheel);
|
|
73
213
|
}
|
|
74
214
|
});
|
|
75
215
|
|
|
@@ -79,6 +219,7 @@ provide("getUTabsSquare", () => props.square);
|
|
|
79
219
|
provide("getUTabsScrollable", () => props.scrollable);
|
|
80
220
|
provide("getUTabsSelectedItem", () => selectedItem.value);
|
|
81
221
|
provide("setUTabsSelectedItem", (value: string) => (selectedItem.value = value));
|
|
222
|
+
provide("getUTabsPreventTabClick", () => preventTabClick.value);
|
|
82
223
|
|
|
83
224
|
defineExpose({
|
|
84
225
|
/**
|
|
@@ -97,6 +238,7 @@ const {
|
|
|
97
238
|
config,
|
|
98
239
|
wrapperAttrs,
|
|
99
240
|
tabsAttrs,
|
|
241
|
+
dragAttrs,
|
|
100
242
|
tabAttrs,
|
|
101
243
|
prevAttrs,
|
|
102
244
|
nextAttrs,
|
|
@@ -117,7 +259,15 @@ const {
|
|
|
117
259
|
</slot>
|
|
118
260
|
</div>
|
|
119
261
|
|
|
120
|
-
<div
|
|
262
|
+
<div
|
|
263
|
+
ref="scroll-container"
|
|
264
|
+
v-bind="tabsAttrs"
|
|
265
|
+
:class="isDragging && dragAttrs.class"
|
|
266
|
+
:data-test="getDataTest()"
|
|
267
|
+
@scroll="checkScroll"
|
|
268
|
+
@pointerdown="onPointerDown"
|
|
269
|
+
@click.capture="onClickCapture"
|
|
270
|
+
>
|
|
121
271
|
<!-- @slot Use it to add the UTab component. -->
|
|
122
272
|
<slot>
|
|
123
273
|
<UTab
|
|
@@ -4,10 +4,11 @@ export default /*tw*/ {
|
|
|
4
4
|
base: "flex border-b border-default w-full",
|
|
5
5
|
variants: {
|
|
6
6
|
scrollable: {
|
|
7
|
-
true: "overflow-hidden flex-nowrap
|
|
7
|
+
true: "overflow-hidden flex-nowrap touch-pan-y select-none",
|
|
8
8
|
},
|
|
9
9
|
},
|
|
10
10
|
},
|
|
11
|
+
drag: "icon-drag cursor-move scroll-auto! *:pointer-events-none",
|
|
11
12
|
tab: "{UTab}",
|
|
12
13
|
prev: "",
|
|
13
14
|
next: "",
|
|
@@ -89,6 +89,14 @@ Scrollable.args = {
|
|
|
89
89
|
options: getOptionsArray(),
|
|
90
90
|
scrollable: true,
|
|
91
91
|
};
|
|
92
|
+
Scrollable.parameters = {
|
|
93
|
+
docs: {
|
|
94
|
+
description: {
|
|
95
|
+
story:
|
|
96
|
+
"Scroll overflowing tabs with the arrow buttons, by dragging the tab list, or with horizontal wheel.",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
92
100
|
|
|
93
101
|
export const Block = DefaultTemplate.bind({});
|
|
94
102
|
Block.args = { block: true };
|