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
193 changes: 189 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 } from "@vueuse/core";
import { IconArrowLeft, IconArrowRight } from "../../icons/icons";

const props = withDefaults(
defineProps<{
Expand Down Expand Up @@ -30,12 +33,34 @@ 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;
const { scrollWidth, scrollLeft } = tabbarItemsRef.value;
if (scrollWidth > tabbarWidth.value) {
if (scrollLeft < scrollWidth - tabbarWidth.value) {
show.right = true;
}
if (scrollLeft > 20) {
show.left = true;
}
}
return show;
});

function handleHorizontalWheel(event: WheelEvent) {
if (!tabbarItemsRef.value) {
Expand All @@ -52,23 +77,139 @@ 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.some((item) => !item)) {
itemWidthArr.value = [];
for (const item of tabItemRefs.value) {
itemWidthArr.value.push(item.offsetWidth);
}
}
LIlGG marked this conversation as resolved.
Show resolved Hide resolved
let hiddenNum = 0;
let totalWith = 0;
let overWidth = 0;
AeroWang marked this conversation as resolved.
Show resolved Hide resolved
let scrollByX = 0;
const lastItemWidth = itemWidthArr.value[itemWidthArr.value.length - 1];
if (prev) {
overWidth = scrollLeft;
for (let i = 0; i < itemWidthArr.value.length; i++) {
const w = itemWidthArr.value[i];
totalWith += w;
if (totalWith >= overWidth) {
hiddenNum = i;
break;
}
}
if (hiddenNum === 0) {
scrollByX = -itemWidthArr.value[0];
} else {
scrollByX = -(
itemWidthArr.value[hiddenNum] -
totalWith +
overWidth +
itemWidthArr.value[hiddenNum - 1]
);
}
} else {
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 ||
hiddenNum === itemWidthArr.value.length - 2
) {
scrollByX =
lastItemWidth + itemWidthArr.value[itemWidthArr.value.length - 2];
AeroWang marked this conversation as resolved.
Show resolved Hide resolved
} else {
scrollByX =
itemWidthArr.value[hiddenNum] -
(totalWith - overWidth) +
itemWidthArr.value[hiddenNum + 1];
}
}
tabbarItemsRef.value.scrollBy({
left: scrollByX,
behavior: "smooth",
});
}

const handleScroll = () => {
arrowFlag.value = !arrowFlag.value;
};
AeroWang marked this conversation as resolved.
Show resolved Hide resolved

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,50 @@ 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
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;
};