diff --git a/packages/vuetify/src/components/VTabs/VTab.tsx b/packages/vuetify/src/components/VTabs/VTab.tsx index 98e0bbfab6b..c9a10336156 100644 --- a/packages/vuetify/src/components/VTabs/VTab.tsx +++ b/packages/vuetify/src/components/VTabs/VTab.tsx @@ -21,6 +21,7 @@ export const makeVTabProps = propsFactory({ fixed: Boolean, sliderColor: String, + sliderTransition: String as PropType<'shift' | 'grow' | 'fade'>, hideSlider: Boolean, direction: { @@ -55,6 +56,49 @@ export const VTab = genericComponent()({ const isHorizontal = computed(() => props.direction === 'horizontal') const isSelected = computed(() => rootEl.value?.group?.isSelected.value ?? false) + function fade (nextEl: HTMLElement, prevEl: HTMLElement) { + return { opacity: [0, 1] } + } + + function grow (nextEl: HTMLElement, prevEl: HTMLElement) { + return { + transform: ['scaleX(0)', 'scaleX(1)'], + } + } + + function shift (nextEl: HTMLElement, prevEl: HTMLElement) { + const prevBox = prevEl.getBoundingClientRect() + const nextBox = nextEl.getBoundingClientRect() + + const xy = isHorizontal.value ? 'x' : 'y' + const XY = isHorizontal.value ? 'X' : 'Y' + const rightBottom = isHorizontal.value ? 'right' : 'bottom' + const widthHeight = isHorizontal.value ? 'width' : 'height' + + const prevPos = prevBox[xy] + const nextPos = nextBox[xy] + const delta = prevPos > nextPos + ? prevBox[rightBottom] - nextBox[rightBottom] + : prevBox[xy] - nextBox[xy] + const origin = + Math.sign(delta) > 0 ? (isHorizontal.value ? 'right' : 'bottom') + : Math.sign(delta) < 0 ? (isHorizontal.value ? 'left' : 'top') + : 'center' + const size = Math.abs(delta) + (Math.sign(delta) < 0 ? prevBox[widthHeight] : nextBox[widthHeight]) + const scale = size / Math.max(prevBox[widthHeight], nextBox[widthHeight]) || 0 + const initialScale = prevBox[widthHeight] / nextBox[widthHeight] || 0 + const sigma = 1.5 + + return { + transform: [ + `translate${XY}(${delta}px) scale${XY}(${initialScale})`, + `translate${XY}(${delta / sigma}px) scale${XY}(${(scale - 1) / sigma + 1})`, + 'none', + ], + transformOrigin: Array(3).fill(origin), + } + } + function updateSlider ({ value }: { value: boolean }) { if (value) { const prevEl: HTMLElement | undefined = rootEl.value?.$el.parentElement?.querySelector('.v-tab--selected .v-tab__slider') @@ -64,36 +108,15 @@ export const VTab = genericComponent()({ const color = getComputedStyle(prevEl).color - const prevBox = prevEl.getBoundingClientRect() - const nextBox = nextEl.getBoundingClientRect() - - const xy = isHorizontal.value ? 'x' : 'y' - const XY = isHorizontal.value ? 'X' : 'Y' - const rightBottom = isHorizontal.value ? 'right' : 'bottom' - const widthHeight = isHorizontal.value ? 'width' : 'height' - - const prevPos = prevBox[xy] - const nextPos = nextBox[xy] - const delta = prevPos > nextPos - ? prevBox[rightBottom] - nextBox[rightBottom] - : prevBox[xy] - nextBox[xy] - const origin = - Math.sign(delta) > 0 ? (isHorizontal.value ? 'right' : 'bottom') - : Math.sign(delta) < 0 ? (isHorizontal.value ? 'left' : 'top') - : 'center' - const size = Math.abs(delta) + (Math.sign(delta) < 0 ? prevBox[widthHeight] : nextBox[widthHeight]) - const scale = size / Math.max(prevBox[widthHeight], nextBox[widthHeight]) || 0 - const initialScale = prevBox[widthHeight] / nextBox[widthHeight] || 0 - - const sigma = 1.5 + const keyframes = { + fade, + grow, + shift, + }[props.sliderTransition ?? 'shift'] ?? shift + animate(nextEl, { backgroundColor: [color, 'currentcolor'], - transform: [ - `translate${XY}(${delta}px) scale${XY}(${initialScale})`, - `translate${XY}(${delta / sigma}px) scale${XY}(${(scale - 1) / sigma + 1})`, - 'none', - ], - transformOrigin: Array(3).fill(origin), + ...keyframes(nextEl, prevEl), }, { duration: 225, easing: standardEasing, diff --git a/packages/vuetify/src/components/VTabs/VTabs.tsx b/packages/vuetify/src/components/VTabs/VTabs.tsx index bd6b47665ba..626f680575b 100644 --- a/packages/vuetify/src/components/VTabs/VTabs.tsx +++ b/packages/vuetify/src/components/VTabs/VTabs.tsx @@ -52,6 +52,7 @@ export const makeVTabsProps = propsFactory({ }, hideSlider: Boolean, sliderColor: String, + sliderTransition: String as PropType<'shift' | 'grow' | 'fade'>, ...makeVSlideGroupProps({ mandatory: 'force' as const }), ...makeDensityProps(), @@ -80,6 +81,7 @@ export const VTabs = genericComponent()({ stacked: toRef(props, 'stacked'), fixed: toRef(props, 'fixedTabs'), sliderColor: toRef(props, 'sliderColor'), + sliderTransition: toRef(props, 'sliderTransition'), hideSlider: toRef(props, 'hideSlider'), }, })