Skip to content

Commit

Permalink
try fixing tab overflow in vue
Browse files Browse the repository at this point in the history
  • Loading branch information
elevatebart committed Jun 23, 2023
1 parent e2a5148 commit 8ad639e
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 22 deletions.
199 changes: 178 additions & 21 deletions components/Tabs/vue/Tabs.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
<script lang="ts" setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import {
computed,
nextTick,
onBeforeMount,
onMounted,
onUnmounted,
ref,
watch,
} from 'vue'
import {
Tab as TabConfig,
variants,
overflowContainerClass,
} from '@cypress-design/constants-tabs'
import useTooltips from '@cypress-design/place-floating-vue'
import { IconMenuDotsVertical } from '@cypress-design/vue-icon'
import Tab from './_Tab.vue'
const props = withDefaults(
Expand All @@ -28,7 +38,10 @@ const props = withDefaults(
)
const $tab = ref<(typeof Tab)[]>()
const $overflowContainer = ref<HTMLDivElement>()
const $overflowContainer = ref<HTMLDivElement | null>(null)
const $overflowButton = ref<HTMLButtonElement | null>(null)
const $overflowMenu = ref<HTMLDivElement | null>(null)
const $arrowRef = ref<HTMLElement | null>(null)
const emit = defineEmits<{
/**
Expand All @@ -49,7 +62,12 @@ watch(
)
const activeMarkerStyle = ref<
{ left?: string; width?: string; transitionProperty?: string } | undefined
| {
left?: string
width?: string
transitionProperty?: string
}
| undefined
>()
const activeTab = computed(() => {
Expand All @@ -59,34 +77,74 @@ const activeTab = computed(() => {
return activeTab
}
})
const activeIndex = computed(() =>
props.tabs.findIndex((tab) => tab.id === activeId.value)
)
const firstVisibleIndex = ref(10000)
function calcFirstVisibleTabIndex() {
if ($tab.value) {
// find the index of the first element
// visible in the overflow container
const containerRect = $overflowContainer.value?.getBoundingClientRect()
const cloneTabReversed = [...$tab.value].reverse()
const reverseIndex = cloneTabReversed.findIndex((tab) => {
return tab.$el.getBoundingClientRect().right < (containerRect?.right ?? 0)
})
firstVisibleIndex.value = props.tabs.length - reverseIndex
}
}
onUnmounted(() => {
window.removeEventListener('resize', calcFirstVisibleTabIndex)
})
onMounted(() => {
calcFirstVisibleTabIndex()
window.addEventListener('resize', calcFirstVisibleTabIndex)
watch(
() => props.tabs,
() => {
calcFirstVisibleTabIndex()
},
{ deep: true }
)
watch(
activeTab,
(activeTab) => {
if (activeTab) {
activeMarkerStyle.value = {
...activeMarkerStyle.value,
left: `${activeTab.offsetLeft}px`,
width: `${activeTab.offsetWidth}px`,
// get active tab index
if (activeIndex.value >= firstVisibleIndex.value) {
// we need to wait for the button to appear
// before knowing its position
nextTick(() => {
const activeButton = $overflowButton.value
if (activeButton) {
activeMarkerStyle.value = {
...activeMarkerStyle.value,
left: `${activeButton.offsetLeft}px`,
width: `${activeButton.offsetWidth}px`,
}
}
})
} else {
activeMarkerStyle.value = {
...activeMarkerStyle.value,
left: `${activeTab.offsetLeft}px`,
width: `${activeTab.offsetWidth}px`,
}
}
}
},
{ immediate: true }
)
if ($overflowContainer.value && activeTab.value) {
// Scroll to active tab if it is not visible
const overflowContainer = $overflowContainer.value
const leftBoundary =
overflowContainer.offsetWidth / 2 - activeTab.value.offsetWidth / 2
if (activeTab.value.offsetLeft > leftBoundary) {
overflowContainer.scrollTo({
left: activeTab.value.offsetLeft - leftBoundary,
})
}
}
// Only start animation after the first render
nextTick(() => {
activeMarkerStyle.value = {
Expand All @@ -96,6 +154,24 @@ onMounted(() => {
})
})
const {
placeTooltip,
show: showOverflowMenu,
arrowStyle,
tooltipStyle,
positionComputed,
} = useTooltips({
reference: $overflowButton,
tooltip: $overflowMenu,
arrowRef: $arrowRef,
props: {
placement: 'bottom-end',
},
})
const visibleTabs = computed(() => props.tabs.slice(0, firstVisibleIndex.value))
const dropdownTabs = computed(() => props.tabs.slice(firstVisibleIndex.value))
function navigate(shift: number) {
const shiftedIndex =
props.tabs.findIndex((tab) => tab.id === activeId.value) + shift
Expand Down Expand Up @@ -124,13 +200,30 @@ const iconProps = computed(() => {
}
return variants.default.icon
})
onBeforeMount(async () => {
await getTarget()
})
async function getTarget() {
let portalTargetLocal = document.getElementById('portal-target')
if (!portalTargetLocal) {
portalTargetLocal = document.createElement('div')
portalTargetLocal.id = 'portal-target'
portalTargetLocal.style.position = 'absolute'
portalTargetLocal.style.top = '0'
portalTargetLocal.style.left = '0'
portalTargetLocal.style.zIndex = '10000'
document.body.appendChild(portalTargetLocal)
}
}
</script>

<template>
<div ref="$overflowContainer" :class="overflowContainerClass">
<div role="tablist" :class="classes.wrapper">
<Tab
v-for="tab in tabs"
v-for="tab in visibleTabs"
:key="tab.id"
ref="$tab"
:iconProps="iconProps"
Expand All @@ -148,6 +241,21 @@ const iconProps = computed(() => {
emit('switch', tab)
}"
/>
<button
v-if="firstVisibleIndex < tabs.length - 1"
ref="$overflowButton"
:class="[
'w-[24px] h-[24px] flex items-center justify-center rounded',
{
[classes.inActive]: activeIndex <= firstVisibleIndex,
[classes.active]: activeIndex > firstVisibleIndex,
},
]"
@click="placeTooltip()"
>
<IconMenuDotsVertical />
</button>

<template v-if="activeMarkerStyle">
<div
:class="[classes.activeMarker, classes.activeMarkerColor]"
Expand All @@ -159,5 +267,54 @@ const iconProps = computed(() => {
/>
</template>
</div>
<teleport v-if="firstVisibleIndex < tabs.length - 1" to="#portal-target">
<div
ref="$overflowMenu"
class="w-[300px] absolute border border-gray-100 rounded bg-white shadow-lg"
:class="{
invisible: !showOverflowMenu && positionComputed,
'-top-[10000px] invisible': !positionComputed,
}"
:style="tooltipStyle"
>
<svg
ref="$arrowRef"
viewBox="0 0 48 24"
width="24"
height="12"
class="absolute z-10 stroke-gray-100 fill-white"
:style="arrowStyle"
fill="none"
>
<rect
x="0"
y="-4"
width="48"
height="8"
stroke-width="0"
stroke-color="red"
/>
<path
d="M 0 3 C 12 3 18 18 24 18 C 30 18 36 3 48 3"
stroke-width="2"
/>
</svg>
<button
v-for="tab in dropdownTabs"
class="block px-[16px] py-[8px] relative"
@click="
(e: MouseEvent) => {
if(e.ctrlKey || e.metaKey) return
e.preventDefault()
activeId = tab.id
emit('switch', tab)
}"
>
{{ tab.label }}

<div v-if="tab.id === activeId" :class="classes.activeMarkerStatic" />
</button>
</div>
</teleport>
</div>
</template>
3 changes: 2 additions & 1 deletion components/Tabs/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"build:types": "yarn vue-tsc --project ./tsconfig.build.json"
},
"dependencies": {
"@cypress-design/constants-tabs": "*"
"@cypress-design/constants-tabs": "*",
"@cypress-design/place-floating-vue": "*"
},
"devDependencies": {
"@cypress-design/rollup-plugin-tailwind-keep": "*"
Expand Down

0 comments on commit 8ad639e

Please sign in to comment.