@fiscozen/tab 0.1.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.
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/package.json +48 -0
- package/src/FzTab.vue +23 -0
- package/src/FzTabs.vue +127 -0
- package/src/__test__/FzTabs.test.ts +97 -0
- package/src/__test__/__snapshots__/FzTabs.test.ts.snap +81 -0
- package/src/common.ts +4 -0
- package/src/components/FzTabName.vue +44 -0
- package/src/components/FzTabPicker.vue +60 -0
- package/src/components/FzTabPickerValue.vue +40 -0
- package/src/index.ts +2 -0
- package/src/types.ts +42 -0
- package/tsconfig.json +4 -0
- package/vite.config.ts +33 -0
- package/vitest.config.ts +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Fiscozen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @fiscozen/tab
|
|
2
|
+
|
|
3
|
+
Tabs are used to display multiple views in a single window. Users can switch between them by clicking on a tab.
|
|
4
|
+
|
|
5
|
+
Internally, FzTabs uses the API provide/inject to communicate with FzTab and other components.
|
|
6
|
+
This choice is made to avoid the use of global variables to manage the state of the tabs and to make the component uncontrolled: all
|
|
7
|
+
the logic is handled by the FzTabs and FzTab components.
|
|
8
|
+
|
|
9
|
+
If necessary, all components inside FzTabs can use the following code
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { inject, Ref } from 'vue';
|
|
13
|
+
|
|
14
|
+
const selectedTab = inject<Ref<string>>('selectedTab');
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
to manage the state of the tabs. This should be done with caution, as it can lead to unexpected behavior.
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fiscozen/tab",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Design System Tab component",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [],
|
|
8
|
+
"author": "Cristian Barraco",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@fiscozen/badge": "^0.1.0",
|
|
11
|
+
"@fiscozen/composables": "^0.1.0"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"tailwindcss": "^3.4.1",
|
|
15
|
+
"vue": "^3.4.13"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@rushstack/eslint-patch": "^1.3.3",
|
|
19
|
+
"@types/jsdom": "^21.1.6",
|
|
20
|
+
"@types/node": "^18.19.3",
|
|
21
|
+
"@vitejs/plugin-vue": "^4.5.2",
|
|
22
|
+
"@vitest/coverage-v8": "^1.2.1",
|
|
23
|
+
"@vue/test-utils": "^2.4.3",
|
|
24
|
+
"@vue/tsconfig": "^0.5.0",
|
|
25
|
+
"vite-plugin-dts": "^3.8.3",
|
|
26
|
+
"eslint": "^8.49.0",
|
|
27
|
+
"jsdom": "^23.0.1",
|
|
28
|
+
"prettier": "^3.0.3",
|
|
29
|
+
"typescript": "~5.3.0",
|
|
30
|
+
"vite": "^5.0.10",
|
|
31
|
+
"vitest": "^1.2.0",
|
|
32
|
+
"vue-tsc": "^1.8.25",
|
|
33
|
+
"@awesome.me/kit-8137893ad3": "^1.0.65",
|
|
34
|
+
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
|
35
|
+
"@fortawesome/vue-fontawesome": "^3.0.6",
|
|
36
|
+
"@fiscozen/eslint-config": "^0.1.0",
|
|
37
|
+
"@fiscozen/prettier-config": "^0.1.0",
|
|
38
|
+
"@fiscozen/tsconfig": "^0.1.0",
|
|
39
|
+
"@fiscozen/icons": "^0.1.1"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"scripts": {
|
|
43
|
+
"coverage": "vitest run --coverage",
|
|
44
|
+
"format": "prettier --write src/",
|
|
45
|
+
"test:unit": "vitest run",
|
|
46
|
+
"build": "vue-tsc && vite build"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/FzTab.vue
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<slot v-if="isActive"></slot>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { inject, onMounted, Ref, computed } from "vue";
|
|
7
|
+
import { FzTabProps } from "./types";
|
|
8
|
+
|
|
9
|
+
const props = defineProps<FzTabProps>();
|
|
10
|
+
|
|
11
|
+
const selectedTab = inject<Ref<string>>("selectedTab");
|
|
12
|
+
const isActive = computed(() => selectedTab?.value === props.title);
|
|
13
|
+
|
|
14
|
+
onMounted(() => {
|
|
15
|
+
if (selectedTab === undefined) {
|
|
16
|
+
console.error(
|
|
17
|
+
"[Fiscozen Design System]: FzTab must be used inside a FzTabs component"
|
|
18
|
+
);
|
|
19
|
+
} else if (props.initialSelected) {
|
|
20
|
+
selectedTab.value = props.title;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
</script>
|
package/src/FzTabs.vue
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="computedClassWrapper">
|
|
3
|
+
<div :class="computedClass" ref="tabContainer" @wheel="onWheel">
|
|
4
|
+
<FzTabPicker
|
|
5
|
+
v-if="!horizontalOverflow && isOverflowing"
|
|
6
|
+
:tabs="tabs"
|
|
7
|
+
:size="size"
|
|
8
|
+
/>
|
|
9
|
+
<FzTabName
|
|
10
|
+
v-else
|
|
11
|
+
v-for="tab in tabs"
|
|
12
|
+
:tab="tab"
|
|
13
|
+
:key="tab.title"
|
|
14
|
+
:size="size"
|
|
15
|
+
/>
|
|
16
|
+
<slot name="tabs-end" />
|
|
17
|
+
</div>
|
|
18
|
+
<slot :selected="selectedTab"></slot>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup lang="ts">
|
|
23
|
+
import { computed, ref, onMounted, provide, useSlots, watch } from "vue";
|
|
24
|
+
import { FzTabsProps, FzTabProps } from "./types";
|
|
25
|
+
import FzTabPicker from "./components/FzTabPicker.vue";
|
|
26
|
+
import FzTabName from "./components/FzTabName.vue";
|
|
27
|
+
import FzTab from "./FzTab.vue";
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<FzTabsProps>(), {
|
|
30
|
+
size: "sm",
|
|
31
|
+
vertical: false,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits(["change"]);
|
|
35
|
+
|
|
36
|
+
const slots = useSlots();
|
|
37
|
+
|
|
38
|
+
const tabContainer = ref<HTMLElement | null>(null);
|
|
39
|
+
const selectedTab = ref("");
|
|
40
|
+
provide("selectedTab", selectedTab);
|
|
41
|
+
|
|
42
|
+
const tabs = computed(() => {
|
|
43
|
+
if (!slots.default) return [];
|
|
44
|
+
|
|
45
|
+
return slots
|
|
46
|
+
.default()
|
|
47
|
+
.filter((tab) => {
|
|
48
|
+
return tab.type === FzTab;
|
|
49
|
+
})
|
|
50
|
+
.map((tab) => tab.props as FzTabProps);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const isOverflowing = computed(() => {
|
|
54
|
+
if (!tabContainer.value) return false;
|
|
55
|
+
|
|
56
|
+
const parent = tabContainer.value.parentElement ?? document.body;
|
|
57
|
+
return tabContainer.value.scrollWidth > parent.clientWidth;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const computedClass = computed(() => [
|
|
61
|
+
"tab-container flex rounded-lg gap-8 p-2 bg-grey-100 w-fit max-w-full overflow-x-auto",
|
|
62
|
+
props.vertical ? "flex-col" : "flex-row",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const computedClassWrapper = computed(() => [
|
|
66
|
+
"flex",
|
|
67
|
+
!props.vertical ? "flex-col" : "flex-row",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
function onWheel(e: WheelEvent) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
if (e.deltaY > 0) tabContainer.value!.scrollLeft += 100;
|
|
74
|
+
else tabContainer.value!.scrollLeft -= 100;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onMounted(() => {
|
|
78
|
+
if (tabs.value.length === 0) {
|
|
79
|
+
console.error(
|
|
80
|
+
"[Fiscozen Design System]: FzTabs must have at least one FzTab child"
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const findSelected = tabs.value.find((tab) => tab.initialSelected);
|
|
86
|
+
if (findSelected) {
|
|
87
|
+
selectedTab.value = findSelected.title;
|
|
88
|
+
} else {
|
|
89
|
+
selectedTab.value = tabs.value[0].title;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const duplicateTitles = tabs.value
|
|
93
|
+
.map((tab) => tab.title)
|
|
94
|
+
.filter((title, index, self) => self.indexOf(title) !== index);
|
|
95
|
+
|
|
96
|
+
if (duplicateTitles.length) {
|
|
97
|
+
console.warn(
|
|
98
|
+
`[Fiscozen Design System]: FzTabs has duplicate titles: ${duplicateTitles.join(", ")}, this may cause unexpected behavior.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
watch(
|
|
104
|
+
() => selectedTab.value,
|
|
105
|
+
() => {
|
|
106
|
+
const selectedTabElement = tabContainer.value!.querySelector(
|
|
107
|
+
`button[title="${selectedTab.value}"]`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (selectedTabElement) {
|
|
111
|
+
selectedTabElement.scrollIntoView({
|
|
112
|
+
behavior: "smooth",
|
|
113
|
+
block: "nearest",
|
|
114
|
+
inline: "center",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
emit("change", selectedTab.value);
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
</script>
|
|
122
|
+
<style scoped>
|
|
123
|
+
.tab-container::-webkit-scrollbar {
|
|
124
|
+
width: 0em;
|
|
125
|
+
height: 0em;
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
3
|
+
import { FzTab, FzTabs } from "..";
|
|
4
|
+
import { h } from "vue";
|
|
5
|
+
import { FzTabProps, FzTabsProps } from "../types";
|
|
6
|
+
|
|
7
|
+
const createWrapper = async (
|
|
8
|
+
props: FzTabsProps,
|
|
9
|
+
tab1Props: FzTabProps,
|
|
10
|
+
tab2Props: FzTabProps,
|
|
11
|
+
) => {
|
|
12
|
+
const content = mount(FzTabs, {
|
|
13
|
+
props,
|
|
14
|
+
slots: {
|
|
15
|
+
default: () => [
|
|
16
|
+
h(FzTab, tab1Props, "Content tab1"),
|
|
17
|
+
h(FzTab, tab2Props, "Content tab2"),
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await content.vm.$nextTick();
|
|
23
|
+
return content;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("FzTabs", () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
globalThis.HTMLElement.prototype.scrollIntoView = () => {};
|
|
29
|
+
});
|
|
30
|
+
it("renders with base case", async () => {
|
|
31
|
+
const wrapper = await createWrapper(
|
|
32
|
+
{
|
|
33
|
+
size: "sm",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
title: "tab1",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
title: "tab2",
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders with md size", async () => {
|
|
47
|
+
const wrapper = await createWrapper(
|
|
48
|
+
{ size: "md" },
|
|
49
|
+
{ title: "tab1" },
|
|
50
|
+
{ title: "tab2" },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("renders with badgeContent on tab1", async () => {
|
|
57
|
+
const wrapper = await createWrapper(
|
|
58
|
+
{ size: "sm" },
|
|
59
|
+
{ title: "tab1", badgeContent: "1" },
|
|
60
|
+
{ title: "tab2" },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("renders with icon on tab1", async () => {
|
|
67
|
+
const wrapper = await createWrapper(
|
|
68
|
+
{ size: "sm" },
|
|
69
|
+
{ title: "tab1", icon: "bell" },
|
|
70
|
+
{ title: "tab2" },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("change tab", async () => {
|
|
77
|
+
const wrapper = await createWrapper(
|
|
78
|
+
{ size: "sm" },
|
|
79
|
+
{ title: "tab1" },
|
|
80
|
+
{ title: "tab2" },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
await wrapper.findAll("button[title='tab2']")[0].trigger("click");
|
|
84
|
+
|
|
85
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("renders with vertical direction", async () => {
|
|
89
|
+
const wrapper = await createWrapper(
|
|
90
|
+
{ size: "sm", vertical: true },
|
|
91
|
+
{ title: "tab1" },
|
|
92
|
+
{ title: "tab2" },
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
96
|
+
})
|
|
97
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`FzTabs > change tab 1`] = `
|
|
4
|
+
"<div data-v-97c498eb="" class="flex flex-col">
|
|
5
|
+
<div data-v-97c498eb="" class="tab-container flex rounded-lg gap-8 p-2 bg-grey-100 w-fit max-w-full overflow-x-auto flex-row"><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-sm h-40 gap-6 py-8 px-12 rounded-md text-grey-500 bg-grey-100 hover:bg-background-alice-blue active:bg-white active:text-blue-500 cursor-pointer" title="tab1">
|
|
6
|
+
<!--v-if--><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab1</span>
|
|
7
|
+
<!--v-if-->
|
|
8
|
+
</button><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-sm h-40 gap-6 py-8 px-12 rounded-md bg-white text-blue-500 cursor-pointer" title="tab2">
|
|
9
|
+
<!--v-if--><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab2</span>
|
|
10
|
+
<!--v-if-->
|
|
11
|
+
</button></div>
|
|
12
|
+
<!--v-if-->Content tab2
|
|
13
|
+
</div>"
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
exports[`FzTabs > renders with badgeContent on tab1 1`] = `
|
|
17
|
+
"<div data-v-97c498eb="" class="flex flex-col">
|
|
18
|
+
<div data-v-97c498eb="" class="tab-container flex rounded-lg gap-8 p-2 bg-grey-100 w-fit max-w-full overflow-x-auto flex-row"><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-sm h-40 gap-6 py-8 px-12 rounded-md bg-white text-blue-500 cursor-pointer" title="tab1" badgecontent="1">
|
|
19
|
+
<!--v-if--><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab1</span>
|
|
20
|
+
<div class="text-xs px-12 rounded-xl w-fit h-20 flex items-center font-medium bg-blue-500 text-core-white" size="sm">1</div>
|
|
21
|
+
</button><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-sm h-40 gap-6 py-8 px-12 rounded-md text-grey-500 bg-grey-100 hover:bg-background-alice-blue active:bg-white active:text-blue-500 cursor-pointer" title="tab2">
|
|
22
|
+
<!--v-if--><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab2</span>
|
|
23
|
+
<!--v-if-->
|
|
24
|
+
</button></div>Content tab1
|
|
25
|
+
<!--v-if-->
|
|
26
|
+
</div>"
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
exports[`FzTabs > renders with base case 1`] = `
|
|
30
|
+
"<div data-v-97c498eb="" class="flex flex-col">
|
|
31
|
+
<div data-v-97c498eb="" class="tab-container flex rounded-lg gap-8 p-2 bg-grey-100 w-fit max-w-full overflow-x-auto flex-row"><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-sm h-40 gap-6 py-8 px-12 rounded-md bg-white text-blue-500 cursor-pointer" title="tab1">
|
|
32
|
+
<!--v-if--><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab1</span>
|
|
33
|
+
<!--v-if-->
|
|
34
|
+
</button><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-sm h-40 gap-6 py-8 px-12 rounded-md text-grey-500 bg-grey-100 hover:bg-background-alice-blue active:bg-white active:text-blue-500 cursor-pointer" title="tab2">
|
|
35
|
+
<!--v-if--><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab2</span>
|
|
36
|
+
<!--v-if-->
|
|
37
|
+
</button></div>Content tab1
|
|
38
|
+
<!--v-if-->
|
|
39
|
+
</div>"
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
exports[`FzTabs > renders with icon on tab1 1`] = `
|
|
43
|
+
"<div data-v-97c498eb="" class="flex flex-col">
|
|
44
|
+
<div data-v-97c498eb="" class="tab-container flex rounded-lg gap-8 p-2 bg-grey-100 w-fit max-w-full overflow-x-auto flex-row"><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-sm h-40 gap-6 py-8 px-12 rounded-md bg-white text-blue-500 cursor-pointer" title="tab1" icon="bell">
|
|
45
|
+
<div class="flex items-center justify-center w-[15px] h-[15px]"><svg class="svg-inline--fa fa-bell fa-sm h-[12px]" aria-hidden="true" focusable="false" data-prefix="far" data-icon="bell" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
46
|
+
<path class="" fill="currentColor" d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416H424c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6C399.5 322.9 384 278.8 384 233.4V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3C98.1 328 112 281.3 112 233.4V208c0-61.9 50.1-112 112-112zm64 352H224 160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7s18.7-28.3 18.7-45.3z"></path>
|
|
47
|
+
</svg></div><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab1</span>
|
|
48
|
+
<!--v-if-->
|
|
49
|
+
</button><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-sm h-40 gap-6 py-8 px-12 rounded-md text-grey-500 bg-grey-100 hover:bg-background-alice-blue active:bg-white active:text-blue-500 cursor-pointer" title="tab2">
|
|
50
|
+
<!--v-if--><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab2</span>
|
|
51
|
+
<!--v-if-->
|
|
52
|
+
</button></div>Content tab1
|
|
53
|
+
<!--v-if-->
|
|
54
|
+
</div>"
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
exports[`FzTabs > renders with md size 1`] = `
|
|
58
|
+
"<div data-v-97c498eb="" class="flex flex-col">
|
|
59
|
+
<div data-v-97c498eb="" class="tab-container flex rounded-lg gap-8 p-2 bg-grey-100 w-fit max-w-full overflow-x-auto flex-row"><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-md h-40 gap-8 py-12 px-14 rounded-lg bg-white text-blue-500 cursor-pointer" title="tab1">
|
|
60
|
+
<!--v-if--><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab1</span>
|
|
61
|
+
<!--v-if-->
|
|
62
|
+
</button><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-md h-40 gap-8 py-12 px-14 rounded-lg text-grey-500 bg-grey-100 hover:bg-background-alice-blue active:bg-white active:text-blue-500 cursor-pointer" title="tab2">
|
|
63
|
+
<!--v-if--><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab2</span>
|
|
64
|
+
<!--v-if-->
|
|
65
|
+
</button></div>Content tab1
|
|
66
|
+
<!--v-if-->
|
|
67
|
+
</div>"
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
exports[`FzTabs > renders with vertical direction 1`] = `
|
|
71
|
+
"<div data-v-97c498eb="" class="flex flex-row">
|
|
72
|
+
<div data-v-97c498eb="" class="tab-container flex rounded-lg gap-8 p-2 bg-grey-100 w-fit max-w-full overflow-x-auto flex-col"><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-sm h-40 gap-6 py-8 px-12 rounded-md bg-white text-blue-500 cursor-pointer" title="tab1">
|
|
73
|
+
<!--v-if--><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab1</span>
|
|
74
|
+
<!--v-if-->
|
|
75
|
+
</button><button data-v-97c498eb="" class="w-auto flex font-medium items-center max-w-[136px] text-sm h-40 gap-6 py-8 px-12 rounded-md text-grey-500 bg-grey-100 hover:bg-background-alice-blue active:bg-white active:text-blue-500 cursor-pointer" title="tab2">
|
|
76
|
+
<!--v-if--><span class="text-ellipsis whitespace-nowrap overflow-hidden">tab2</span>
|
|
77
|
+
<!--v-if-->
|
|
78
|
+
</button></div>Content tab1
|
|
79
|
+
<!--v-if-->
|
|
80
|
+
</div>"
|
|
81
|
+
`;
|
package/src/common.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button :class="classes" @click="onClickTab" v-bind="tab">
|
|
3
|
+
<FzIcon v-if="tab.icon" :name="tab.icon" :size="size" />
|
|
4
|
+
<span class="text-ellipsis whitespace-nowrap overflow-hidden">{{
|
|
5
|
+
tab.title
|
|
6
|
+
}}</span>
|
|
7
|
+
<FzBadge
|
|
8
|
+
v-if="tab.badgeContent"
|
|
9
|
+
:color="selectedTab === tab.title ? 'blue' : 'black'"
|
|
10
|
+
:size="size"
|
|
11
|
+
>
|
|
12
|
+
{{ tab.badgeContent }}
|
|
13
|
+
</FzBadge>
|
|
14
|
+
</button>
|
|
15
|
+
</template>
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { inject, computed, Ref } from "vue";
|
|
18
|
+
import { FzBadge } from "@fiscozen/badge";
|
|
19
|
+
import { FzIcon } from "@fiscozen/icons";
|
|
20
|
+
import { FzTabProps } from "../types";
|
|
21
|
+
import { mapSizeToClasses } from "../common";
|
|
22
|
+
|
|
23
|
+
const props = defineProps<{
|
|
24
|
+
tab: FzTabProps;
|
|
25
|
+
size: "sm" | "md";
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
const selectedTab = inject<Ref<string>>("selectedTab");
|
|
29
|
+
|
|
30
|
+
const classes = computed(() => [
|
|
31
|
+
"w-auto flex font-medium items-center max-w-[136px]",
|
|
32
|
+
mapSizeToClasses[props.size],
|
|
33
|
+
selectedTab?.value === props.tab.title
|
|
34
|
+
? "bg-white text-blue-500"
|
|
35
|
+
: "text-grey-500 bg-grey-100 hover:bg-background-alice-blue active:bg-white active:text-blue-500",
|
|
36
|
+
props.tab.disabled ? "cursor-not-allowed" : "cursor-pointer",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const onClickTab = () => {
|
|
40
|
+
if (!props.tab.disabled) {
|
|
41
|
+
selectedTab!.value = props.tab.title;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FzFloating position="bottom" :isOpen class="w-full overflow-hidden">
|
|
3
|
+
<template #opener>
|
|
4
|
+
<button
|
|
5
|
+
@click="isOpen = !isOpen"
|
|
6
|
+
:size="size"
|
|
7
|
+
:class="computedClasses"
|
|
8
|
+
ref="opener"
|
|
9
|
+
>
|
|
10
|
+
<span class="overflow-hidden text-ellipsis whitespace-nowrap">
|
|
11
|
+
{{ selectedTab }}
|
|
12
|
+
</span>
|
|
13
|
+
<FzIcon :name="isOpen ? 'chevron-up' : 'chevron-down'" :size="size" />
|
|
14
|
+
</button>
|
|
15
|
+
</template>
|
|
16
|
+
<div
|
|
17
|
+
class="flex flex-col p-4 rounded-[4px] shadow overflow-hidden"
|
|
18
|
+
:style="{ width: containerWidth }"
|
|
19
|
+
>
|
|
20
|
+
<FzTabPickerValue
|
|
21
|
+
v-for="tab in tabs"
|
|
22
|
+
:tab="tab"
|
|
23
|
+
:size="size"
|
|
24
|
+
@click="closePicker"
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
</FzFloating>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { ref, inject, computed } from "vue";
|
|
32
|
+
import { FzFloating } from "@fiscozen/composables";
|
|
33
|
+
import { FzIcon } from "@fiscozen/icons";
|
|
34
|
+
import { FzTabProps } from "../types";
|
|
35
|
+
import { mapSizeToClasses } from "../common";
|
|
36
|
+
import FzTabPickerValue from "./FzTabPickerValue.vue";
|
|
37
|
+
|
|
38
|
+
const isOpen = ref(false);
|
|
39
|
+
const props = defineProps<{
|
|
40
|
+
tabs: FzTabProps[];
|
|
41
|
+
size: "sm" | "md";
|
|
42
|
+
}>();
|
|
43
|
+
const opener = ref<HTMLElement>();
|
|
44
|
+
|
|
45
|
+
const selectedTab = inject("selectedTab");
|
|
46
|
+
|
|
47
|
+
const computedClasses = computed(() => [
|
|
48
|
+
"flex items-center text-left max-w-[136px] h-auto bg-white text-blue-500 font-medium cursor-pointer capitalize ",
|
|
49
|
+
mapSizeToClasses[props.size],
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const containerWidth = computed(() => {
|
|
53
|
+
if (!opener.value) return "auto";
|
|
54
|
+
return `${opener.value.offsetWidth}px`;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const closePicker = () => {
|
|
58
|
+
isOpen.value = false;
|
|
59
|
+
};
|
|
60
|
+
</script>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
:key="tab.title"
|
|
4
|
+
@click="onPickerValueClick"
|
|
5
|
+
:class="computedClasses"
|
|
6
|
+
:disabled="tab.disabled"
|
|
7
|
+
>
|
|
8
|
+
{{ tab.title }}
|
|
9
|
+
</button>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { Ref, inject, computed } from "vue";
|
|
14
|
+
import { FzTabProps } from "../types";
|
|
15
|
+
import { mapSizeToClasses } from "../common";
|
|
16
|
+
|
|
17
|
+
const props = defineProps<{
|
|
18
|
+
tab: FzTabProps;
|
|
19
|
+
size: "sm" | "md";
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits(["click"]);
|
|
23
|
+
|
|
24
|
+
const selectedTab = inject<Ref<string>>("selectedTab");
|
|
25
|
+
const computedClasses = computed(() => [
|
|
26
|
+
"flex items-center text-left max-w-[136px] h-auto bg-white text-blue-500 font-medium cursor-pointer capitalize ",
|
|
27
|
+
mapSizeToClasses[props.size],
|
|
28
|
+
selectedTab?.value === props.tab.title
|
|
29
|
+
? "!bg-background-alice-blue"
|
|
30
|
+
: "hover:!bg-background-alice-blue !text-black hover:!text-blue-500",
|
|
31
|
+
props.tab.disabled ? "cursor-not-allowed" : "cursor-pointer",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const onPickerValueClick = () => {
|
|
35
|
+
if (!props.tab.disabled) {
|
|
36
|
+
selectedTab!.value = props.tab.title;
|
|
37
|
+
emit("click");
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
</script>
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type FzTabsProps = {
|
|
2
|
+
/**
|
|
3
|
+
* Size variant
|
|
4
|
+
*/
|
|
5
|
+
size: "sm" | "md";
|
|
6
|
+
/**
|
|
7
|
+
* Enable horizontal overflow
|
|
8
|
+
*/
|
|
9
|
+
horizontalOverflow?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Enable vertical direction
|
|
12
|
+
*/
|
|
13
|
+
vertical?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type FzTabProps = {
|
|
17
|
+
/**
|
|
18
|
+
* Title of the tab
|
|
19
|
+
*/
|
|
20
|
+
title: string;
|
|
21
|
+
/**
|
|
22
|
+
* Icon to display on the tab
|
|
23
|
+
*/
|
|
24
|
+
icon?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Badge text to display on the tab
|
|
27
|
+
*/
|
|
28
|
+
badgeContent?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Disable the tab
|
|
31
|
+
*/
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Show the tab content
|
|
35
|
+
*/
|
|
36
|
+
initialSelected?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* custom property not handled by the component but maybe useful for the user (e.g. aria-label, class, etc.)
|
|
40
|
+
*/
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
};
|
package/tsconfig.json
ADDED
package/vite.config.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { fileURLToPath, URL } from 'node:url'
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { defineConfig } from 'vite'
|
|
4
|
+
import vue from '@vitejs/plugin-vue'
|
|
5
|
+
import dts from 'vite-plugin-dts'
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [
|
|
9
|
+
vue(),
|
|
10
|
+
dts({
|
|
11
|
+
insertTypesEntry: true,
|
|
12
|
+
})
|
|
13
|
+
],
|
|
14
|
+
resolve: {
|
|
15
|
+
alias: {
|
|
16
|
+
'@': fileURLToPath(new URL('./src', import.meta.url))
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
build: {
|
|
20
|
+
lib: {
|
|
21
|
+
entry: resolve(__dirname, './src/index.ts'),
|
|
22
|
+
name: 'FzTabs',
|
|
23
|
+
},
|
|
24
|
+
rollupOptions: {
|
|
25
|
+
external: ['vue', "@fiscozen/icons", "@fiscozen/badge", "@fiscozen/composables"],
|
|
26
|
+
output: {
|
|
27
|
+
globals: {
|
|
28
|
+
vue: 'Vue',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
})
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
|
3
|
+
import viteConfig from './vite.config'
|
|
4
|
+
|
|
5
|
+
export default mergeConfig(
|
|
6
|
+
viteConfig,
|
|
7
|
+
defineConfig({
|
|
8
|
+
test: {
|
|
9
|
+
environment: 'jsdom',
|
|
10
|
+
exclude: [...configDefaults.exclude, 'e2e/*'],
|
|
11
|
+
root: fileURLToPath(new URL('./', import.meta.url)),
|
|
12
|
+
coverage: {
|
|
13
|
+
provider: 'v8',
|
|
14
|
+
include: ['**/src/**'],
|
|
15
|
+
exclude: ['**/index.ts']
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
)
|