Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tabbar component horizontal-scroll-indicator #4979

Merged
merged 12 commits into from Dec 21, 2023
Merged
194 changes: 190 additions & 4 deletions console/packages/components/src/components/tabs/Tabbar.vue
@@ -1,6 +1,9 @@
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from "vue";
import type { Direction, Type } from "./interface";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import type { ArrowShow, Direction, Type } from "./interface";
import type { ComputedRef } from "vue";
import { useElementSize, useThrottleFn } from "@vueuse/core";
import { IconArrowLeft, IconArrowRight } from "../../icons/icons";

const props = withDefaults(
defineProps<{
Expand Down Expand Up @@ -30,12 +33,35 @@ const classes = computed(() => {
return [`tabbar-${props.type}`, `tabbar-direction-${props.direction}`];
});

const handleChange = (id: number | string) => {
const handleChange = (id: number | string, index: number) => {
handleClickTabItem(index);
emit("update:activeId", id);
emit("change", id);
};

const tabbarItemsRef = ref<HTMLElement | undefined>();
const tabItemRefs = ref<HTMLElement[] | undefined>();
const itemWidthArr = ref<number[]>([]);
const indicatorRef = ref<HTMLElement | undefined>();
const arrowFlag = ref(false);
const { width: tabbarWidth } = useElementSize(tabbarItemsRef);

const arrowShow: ComputedRef<ArrowShow> = computed(() => {
const show: ArrowShow = { left: false, right: false };
if (!tabbarItemsRef.value) return show;
void arrowFlag.value;
void tabbarWidth.value;
const { scrollWidth, scrollLeft, clientWidth } = tabbarItemsRef.value;
if (scrollWidth > clientWidth) {
if (scrollLeft < scrollWidth - clientWidth) {
show.right = true;
}
if (scrollLeft > 20) {
show.left = true;
}
}
return show;
});

function handleHorizontalWheel(event: WheelEvent) {
if (!tabbarItemsRef.value) {
Expand All @@ -52,23 +78,138 @@ function handleHorizontalWheel(event: WheelEvent) {
}
}

function saveItemsWidth() {
if (!tabbarItemsRef.value || !tabItemRefs.value) return;
itemWidthArr.value = [];
for (const item of tabItemRefs.value) {
itemWidthArr.value.push(item.offsetWidth);
}
arrowFlag.value = !arrowFlag.value;
}

function handleClickTabItem(index: number) {
if (!tabbarItemsRef.value || !indicatorRef.value) return;
const { scrollWidth, clientWidth } = tabbarItemsRef.value;
if (scrollWidth <= clientWidth) return;
if (index === 0) {
tabbarItemsRef.value.scrollTo({ left: 0, behavior: "smooth" });
return;
}
if (index === itemWidthArr.value.length - 1) {
tabbarItemsRef.value.scrollTo({
left: scrollWidth - clientWidth,
behavior: "smooth",
});
return;
}
}

function handleClickArrow(prev: boolean) {
if (!tabbarItemsRef.value || !indicatorRef.value || !tabItemRefs.value)
return;
const { scrollWidth, scrollLeft, clientWidth } = tabbarItemsRef.value;
if (scrollWidth <= clientWidth) return;
if (!itemWidthArr.value[0]) {
itemWidthArr.value = [];
for (const item of tabItemRefs.value) {
itemWidthArr.value.push(item.offsetWidth);
}
}
let hiddenNum = 0;
let totalWith = 0;
let scrollByX = 0;
const lastItemWidth = itemWidthArr.value[itemWidthArr.value.length - 1];
if (prev) {
for (let i = 0; i < itemWidthArr.value.length; i++) {
const w = itemWidthArr.value[i];
totalWith += w;
if (totalWith >= scrollLeft) {
hiddenNum = i;
break;
}
}
if (hiddenNum === 0) {
scrollByX = -itemWidthArr.value[0];
} else {
scrollByX = -(
itemWidthArr.value[hiddenNum] -
totalWith +
scrollLeft +
itemWidthArr.value[hiddenNum - 1]
);
}
} else {
const overWidth = scrollWidth - scrollLeft - clientWidth;
for (let i = itemWidthArr.value.length - 1; i >= 0; i--) {
const w = itemWidthArr.value[i];
totalWith += w;
if (totalWith >= overWidth) {
hiddenNum = i;
break;
}
}

if (hiddenNum === itemWidthArr.value.length - 1) {
scrollByX =
lastItemWidth + itemWidthArr.value[itemWidthArr.value.length - 1];
} else {
scrollByX =
itemWidthArr.value[hiddenNum] -
(totalWith - overWidth) +
itemWidthArr.value[hiddenNum + 1];
}
}
tabbarItemsRef.value.scrollBy({
left: scrollByX,
behavior: "smooth",
});
}

const handleScroll = useThrottleFn(
() => {
arrowFlag.value = !arrowFlag.value;
},
100,
true
);

watch(() => tabItemRefs.value?.length, saveItemsWidth);

onMounted(() => {
tabbarItemsRef.value?.addEventListener("wheel", handleHorizontalWheel);
tabbarItemsRef.value?.addEventListener("scroll", handleScroll);
});

onUnmounted(() => {
tabbarItemsRef.value?.removeEventListener("wheel", handleHorizontalWheel);
tabbarItemsRef.value?.removeEventListener("scroll", handleScroll);
});
</script>
<template>
<div :class="classes" class="tabbar-wrapper">
<div
ref="indicatorRef"
:class="['indicator', 'left', arrowShow.left ? 'visible' : 'invisible']"
>
<div title="向前" class="arrow-left" @click="handleClickArrow(true)">
<IconArrowLeft />
</div>
</div>
<div
:class="['indicator', 'right', arrowShow.right ? 'visible' : 'invisible']"
>
<div title="向后" class="arrow-right" @click="handleClickArrow(false)">
<IconArrowRight />
</div>
</div>
<div ref="tabbarItemsRef" class="tabbar-items">
<div
v-for="(item, index) in items"
:key="index"
ref="tabItemRefs"
:class="{ 'tabbar-item-active': item[idKey] === activeId }"
class="tabbar-item"
@click="handleChange(item[idKey])"
@click="handleChange(item[idKey], index)"
>
<div v-if="item.icon" class="tabbar-item-icon">
<component :is="item.icon" />
Expand All @@ -82,6 +223,51 @@ onUnmounted(() => {
</template>
<style lang="scss">
.tabbar-wrapper {
@apply relative;
.indicator {
@apply absolute
top-0
z-10
w-20
AeroWang marked this conversation as resolved.
Show resolved Hide resolved
h-full
flex
items-center
from-transparent
from-10%
via-white/80
via-30%
to-white
to-70%
pt-1
pointer-events-none
pb-1.5;
AeroWang marked this conversation as resolved.
Show resolved Hide resolved

&.left {
@apply left-0
justify-start
bg-gradient-to-l;
}
&.right {
@apply right-0
justify-end
bg-gradient-to-r;
}
.arrow-left,
.arrow-right {
@apply w-10
h-9
flex
justify-center
items-center
pointer-events-auto
cursor-pointer
select-none;
svg {
font-size: 1.5em;
}
}
}

.tabbar-items {
@apply flex
items-center
Expand Down
4 changes: 4 additions & 0 deletions console/packages/components/src/components/tabs/interface.ts
@@ -1,2 +1,6 @@
export type Type = "default" | "pills" | "outline";
export type Direction = "row" | "column";
export type ArrowShow = {
left: boolean;
right: boolean;
};