feat: scroll to show the new active tab header (#3013)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
committed by
Andrew Bastin
parent
08f61e7408
commit
5f68356278
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1 h-auto overflow-y-hidden flex-nowrap">
|
<div class="flex flex-col flex-1 h-auto overflow-y-hidden flex-nowrap">
|
||||||
<div
|
<div
|
||||||
class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto tabs bg-primaryLight group-tabs"
|
class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto divide-x divide-dividerLight bg-primaryLight tabs group-tabs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-1 flex-shrink-0 w-0 overflow-x-auto"
|
class="flex flex-1 flex-shrink-0 w-0 overflow-x-auto"
|
||||||
ref="scrollContainer"
|
ref="scrollContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex justify-between divide-x divide-divider"
|
class="flex justify-between divide-x divide-dividerLight"
|
||||||
@wheel.prevent="scroll"
|
@wheel.prevent="scroll"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@@ -23,7 +23,8 @@
|
|||||||
<template #item="{ element: [tabID, tabMeta] }">
|
<template #item="{ element: [tabID, tabMeta] }">
|
||||||
<button
|
<button
|
||||||
:key="`removable-tab-${tabID}`"
|
:key="`removable-tab-${tabID}`"
|
||||||
class="tab group px-2"
|
:id="`removable-tab-${tabID}`"
|
||||||
|
class="px-2 tab group"
|
||||||
:class="[{ active: modelValue === tabID }]"
|
:class="[{ active: modelValue === tabID }]"
|
||||||
:aria-label="tabMeta.label || ''"
|
:aria-label="tabMeta.label || ''"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -39,14 +40,14 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!tabMeta.tabhead"
|
v-if="!tabMeta.tabhead"
|
||||||
class="truncate w-full text-left px-2"
|
class="w-full px-2 text-left truncate"
|
||||||
>
|
>
|
||||||
<span class="truncate">
|
<span class="truncate">
|
||||||
{{ tabMeta.label }}
|
{{ tabMeta.label }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="truncate w-full text-left">
|
<div v-else class="w-full text-left truncate">
|
||||||
<component :is="tabMeta.tabhead" />
|
<component :is="tabMeta.tabhead" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
},
|
},
|
||||||
'close',
|
'close',
|
||||||
]"
|
]"
|
||||||
class="!p-0.25 rounded"
|
class="rounded !p-0.25"
|
||||||
@click.stop="emit('removeTab', tabID)"
|
@click.stop="emit('removeTab', tabID)"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
@@ -80,33 +81,33 @@
|
|||||||
</draggable>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="sticky right-0 flex items-center justify-center flex-shrink-0 overflow-x-auto z-8"
|
class="sticky right-0 flex items-center justify-center flex-shrink-0 overflow-x-auto z-14"
|
||||||
>
|
>
|
||||||
<slot name="actions">
|
<span
|
||||||
<span
|
v-if="canAddNewTab"
|
||||||
v-if="canAddNewTab"
|
class="flex items-center justify-center h-full px-3 bg-primaryLight z-8"
|
||||||
class="flex items-center justify-center px-3 bg-primaryLight z-8 h-full"
|
>
|
||||||
>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
:title="newText ?? t?.('action.new') ?? 'New'"
|
||||||
:title="newText ?? t?.('action.new') ?? 'New'"
|
:icon="IconPlus"
|
||||||
:icon="IconPlus"
|
class="rounded create-new-tab !text-secondaryDark !p-1"
|
||||||
class="rounded !text-secondaryDark !p-1"
|
filled
|
||||||
filled
|
@click="addTab"
|
||||||
@click="addTab"
|
/>
|
||||||
/>
|
</span>
|
||||||
</span>
|
|
||||||
</slot>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<slot name="actions" />
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="1"
|
min="1"
|
||||||
:max="MAX_SCROLL_VALUE"
|
:max="MAX_SCROLL_VALUE"
|
||||||
v-model="thumbPosition"
|
v-model="thumbPosition"
|
||||||
class="slider absolute bottom-0 hidden left-0"
|
class="absolute bottom-0 left-0 hidden slider"
|
||||||
:class="{
|
:class="{
|
||||||
'!block': scrollThumb.show,
|
'!block': scrollThumb.show,
|
||||||
}"
|
}"
|
||||||
@@ -131,7 +132,15 @@ import { pipe } from "fp-ts/function"
|
|||||||
import { not } from "fp-ts/Predicate"
|
import { not } from "fp-ts/Predicate"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import { ref, ComputedRef, computed, provide, inject, watch } from "vue"
|
import {
|
||||||
|
ref,
|
||||||
|
ComputedRef,
|
||||||
|
computed,
|
||||||
|
provide,
|
||||||
|
inject,
|
||||||
|
watch,
|
||||||
|
nextTick,
|
||||||
|
} from "vue"
|
||||||
import { useElementSize } from "@vueuse/core"
|
import { useElementSize } from "@vueuse/core"
|
||||||
import type { Slot } from "vue"
|
import type { Slot } from "vue"
|
||||||
import draggable from "vuedraggable-es"
|
import draggable from "vuedraggable-es"
|
||||||
@@ -186,9 +195,10 @@ const throwError = (message: string): never => {
|
|||||||
throw new Error(message)
|
throw new Error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TAB_WIDTH = 184
|
||||||
const tabEntries = ref<Array<[string, TabMeta]>>([])
|
const tabEntries = ref<Array<[string, TabMeta]>>([])
|
||||||
const tabStyles = computed(() => ({
|
const tabStyles = computed(() => ({
|
||||||
maxWidth: `${tabEntries.value.length * 184}px`,
|
maxWidth: `${tabEntries.value.length * TAB_WIDTH}px`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
minWidth: "0px",
|
minWidth: "0px",
|
||||||
// transition: "max-width 0.2s",
|
// transition: "max-width 0.2s",
|
||||||
@@ -292,6 +302,49 @@ watch(thumbPosition, (newVal) => {
|
|||||||
const maxScroll = scrollWidth - clientWidth
|
const maxScroll = scrollWidth - clientWidth
|
||||||
scrollContainer.value!.scrollLeft = maxScroll * (newVal / MAX_SCROLL_VALUE)
|
scrollContainer.value!.scrollLeft = maxScroll * (newVal / MAX_SCROLL_VALUE)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Watch TabID changes
|
||||||
|
* and scroll to the tab if it's not visible
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(tabID) => {
|
||||||
|
nextTick(() => {
|
||||||
|
const element = document.getElementById(`removable-tab-${tabID}`)
|
||||||
|
|
||||||
|
const changeThumbPosition: IntersectionObserverCallback = (
|
||||||
|
entries,
|
||||||
|
observer
|
||||||
|
) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.target === element && entry.intersectionRatio >= 1.0) {
|
||||||
|
// Element is visible now. Stop listening for intersection changes
|
||||||
|
observer.disconnect()
|
||||||
|
|
||||||
|
// We still need setTimeout here because the element might not be fully in position yet
|
||||||
|
setTimeout(() => {
|
||||||
|
const { scrollWidth, clientWidth, scrollLeft } =
|
||||||
|
scrollContainer.value!
|
||||||
|
const maxScroll = scrollWidth - clientWidth
|
||||||
|
thumbPosition.value = (scrollLeft / maxScroll) * MAX_SCROLL_VALUE
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let observer = new IntersectionObserver(changeThumbPosition, {
|
||||||
|
root: null,
|
||||||
|
rootMargin: "0px",
|
||||||
|
threshold: 1.0,
|
||||||
|
})
|
||||||
|
observer.observe(element!)
|
||||||
|
|
||||||
|
element?.scrollIntoView({ behavior: "smooth", inline: "center" })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -336,6 +389,13 @@ watch(thumbPosition, (newVal) => {
|
|||||||
@apply text-secondaryDark;
|
@apply text-secondaryDark;
|
||||||
@apply bg-primary;
|
@apply bg-primary;
|
||||||
@apply before: bg-accent;
|
@apply before: bg-accent;
|
||||||
|
@apply after: absolute;
|
||||||
|
@apply after: inset-x-0;
|
||||||
|
@apply after: bottom-0;
|
||||||
|
@apply after: bg-primary;
|
||||||
|
@apply after: z-12;
|
||||||
|
@apply after: h-0.25;
|
||||||
|
@apply after: content-DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
@@ -348,6 +408,16 @@ watch(thumbPosition, (newVal) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create-new-tab {
|
||||||
|
@apply after: absolute;
|
||||||
|
@apply after: inset-x-0;
|
||||||
|
@apply after: bottom-0;
|
||||||
|
@apply after: bg-dividerLight;
|
||||||
|
@apply after: z-14;
|
||||||
|
@apply after: h-0.25;
|
||||||
|
@apply after: content-DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
$slider-height: 4px;
|
$slider-height: 4px;
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
|
|||||||
Reference in New Issue
Block a user