Skip to content

Commit ee4fb38

Browse files
johnleiderKaelWDJ-Sek
authored
feat(VTabs): add new prop, slider transition (#19556)
Co-authored-by: Kael <[email protected]> Co-authored-by: J-Sek <[email protected]> resolves #15798
1 parent a8751b2 commit ee4fb38

File tree

7 files changed

+96
-38
lines changed

7 files changed

+96
-38
lines changed

packages/api-generator/src/locale/en/VTab.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"hideSlider": "Hides the active tab slider component (no exit or enter animation).",
55
"fixed": "Forces component to take up all available space up to their maximum width (300px), and centers it.",
66
"sliderColor": "Applies specified color to the slider when active on that component - supports utility colors (for example `success` or `purple`) or css color (`#033` or `rgba(255, 0, 0, 0.5)`). Find a list of built-in classes on the [colors page](/styles/colors#material-colors).",
7+
"sliderTransition": "Changes slider transition to one of the predefined animation presets.",
8+
"sliderTransitionDuration": "Applies custom slider transition duration. Default duration depends on transition type (fade: 400, grow: 350, shift: 225).",
79
"stacked": "Displays the tab as a flex-column."
810
},
911
"events": {

packages/api-generator/src/locale/en/VTabs.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"showArrows": "Show pagination arrows if the tab items overflow their container. For mobile devices, arrows will only display when using this prop.",
2525
"sliderColor": "Changes the background color of an auto-generated `v-tabs-slider`.",
2626
"sliderSize": "Changes the size of the slider, **height** for horizontal, **width** for vertical.",
27+
"sliderTransition": "Changes slider transition to one of the predefined animation presets.",
28+
"sliderTransitionDuration": "Applies custom slider transition duration. Default duration depends on transition type (fade: 400, grow: 350, shift: 225).",
2729
"stacked": "Apply the stacked prop to all children v-tab components.",
2830
"touchless": "Disable mobile touch functionality.",
2931
"vertical": "Stacks tabs on top of each other vertically."

packages/docs/src/data/new-in.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,16 @@
272272
"VTab": {
273273
"props": {
274274
"text": "3.2.0",
275-
"spaced": "3.10.0"
275+
"spaced": "3.10.0",
276+
"sliderTransition": "3.11.0",
277+
"sliderTransitionDuration": "3.11.0"
276278
}
277279
},
278280
"VTabs": {
279281
"props": {
280-
"spaced": "3.10.0"
282+
"spaced": "3.10.0",
283+
"sliderTransition": "3.11.0",
284+
"sliderTransitionDuration": "3.11.0"
281285
},
282286
"slots": {
283287
"next": "3.11.0",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<template>
2+
<v-defaults-provider :defaults="{ VTab: { ripple: false } }">
3+
<v-card class="pa-3">
4+
<h5>slider-transition: fade, duration 900ms</h5>
5+
<v-tabs slider-transition="fade" slider-transition-duration="900" fixed-tabs>
6+
<v-tab>Tab 1</v-tab>
7+
<v-tab>Tab 2</v-tab>
8+
<v-tab>Tab 3</v-tab>
9+
</v-tabs>
10+
</v-card>
11+
<v-card class="pa-3 mt-3">
12+
<h5>slider-transition: grow</h5>
13+
<v-tabs slider-transition="grow" fixed-tabs>
14+
<v-tab>Tab 1</v-tab>
15+
<v-tab>Tab 2</v-tab>
16+
<v-tab>Tab 3</v-tab>
17+
</v-tabs>
18+
</v-card>
19+
</v-defaults-provider>
20+
</template>

packages/docs/src/pages/en/components/tabs.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ Using **stacked** increases the `v-tabs` height to 72px to allow for both icons
8787

8888
<ExamplesExample file="v-tabs/prop-stacked" />
8989

90+
#### Slider Transition
91+
92+
With **slider-transition** you can change default animation of the slider so it better fits with the app design.
93+
94+
<ExamplesExample file="v-tabs/prop-slider-transition" />
95+
9096
#### Pagination
9197

9298
If the tab items overflow their container, pagination controls will appear on desktop. For mobile devices, arrows will only display with the **show-arrows** prop.

packages/vuetify/src/components/VTabs/VTab.tsx

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export const makeVTabProps = propsFactory({
2121
fixed: Boolean,
2222

2323
sliderColor: String,
24+
sliderTransition: String as PropType<'shift' | 'grow' | 'fade'>,
25+
sliderTransitionDuration: [String, Number],
2426
hideSlider: Boolean,
2527

2628
direction: {
@@ -55,6 +57,49 @@ export const VTab = genericComponent<VBtnSlots>()({
5557
const isHorizontal = computed(() => props.direction === 'horizontal')
5658
const isSelected = computed(() => rootEl.value?.group?.isSelected.value ?? false)
5759

60+
function fade (nextEl: HTMLElement, prevEl: HTMLElement) {
61+
return { opacity: [0, 1] }
62+
}
63+
64+
function grow (nextEl: HTMLElement, prevEl: HTMLElement) {
65+
return props.direction === 'vertical'
66+
? { transform: ['scaleY(0)', 'scaleY(1)'] }
67+
: { transform: ['scaleX(0)', 'scaleX(1)'] }
68+
}
69+
70+
function shift (nextEl: HTMLElement, prevEl: HTMLElement) {
71+
const prevBox = prevEl.getBoundingClientRect()
72+
const nextBox = nextEl.getBoundingClientRect()
73+
74+
const xy = isHorizontal.value ? 'x' : 'y'
75+
const XY = isHorizontal.value ? 'X' : 'Y'
76+
const rightBottom = isHorizontal.value ? 'right' : 'bottom'
77+
const widthHeight = isHorizontal.value ? 'width' : 'height'
78+
79+
const prevPos = prevBox[xy]
80+
const nextPos = nextBox[xy]
81+
const delta = prevPos > nextPos
82+
? prevBox[rightBottom] - nextBox[rightBottom]
83+
: prevBox[xy] - nextBox[xy]
84+
const origin =
85+
Math.sign(delta) > 0 ? (isHorizontal.value ? 'right' : 'bottom')
86+
: Math.sign(delta) < 0 ? (isHorizontal.value ? 'left' : 'top')
87+
: 'center'
88+
const size = Math.abs(delta) + (Math.sign(delta) < 0 ? prevBox[widthHeight] : nextBox[widthHeight])
89+
const scale = size / Math.max(prevBox[widthHeight], nextBox[widthHeight]) || 0
90+
const initialScale = prevBox[widthHeight] / nextBox[widthHeight] || 0
91+
const sigma = 1.5
92+
93+
return {
94+
transform: [
95+
`translate${XY}(${delta}px) scale${XY}(${initialScale})`,
96+
`translate${XY}(${delta / sigma}px) scale${XY}(${(scale - 1) / sigma + 1})`,
97+
'none',
98+
],
99+
transformOrigin: Array(3).fill(origin),
100+
}
101+
}
102+
58103
function updateSlider ({ value }: { value: boolean }) {
59104
if (value) {
60105
const prevEl: HTMLElement | undefined = rootEl.value?.$el.parentElement?.querySelector('.v-tab--selected .v-tab__slider')
@@ -64,38 +109,15 @@ export const VTab = genericComponent<VBtnSlots>()({
64109

65110
const color = getComputedStyle(prevEl).color
66111

67-
const prevBox = prevEl.getBoundingClientRect()
68-
const nextBox = nextEl.getBoundingClientRect()
69-
70-
const xy = isHorizontal.value ? 'x' : 'y'
71-
const XY = isHorizontal.value ? 'X' : 'Y'
72-
const rightBottom = isHorizontal.value ? 'right' : 'bottom'
73-
const widthHeight = isHorizontal.value ? 'width' : 'height'
74-
75-
const prevPos = prevBox[xy]
76-
const nextPos = nextBox[xy]
77-
const delta = prevPos > nextPos
78-
? prevBox[rightBottom] - nextBox[rightBottom]
79-
: prevBox[xy] - nextBox[xy]
80-
const origin =
81-
Math.sign(delta) > 0 ? (isHorizontal.value ? 'right' : 'bottom')
82-
: Math.sign(delta) < 0 ? (isHorizontal.value ? 'left' : 'top')
83-
: 'center'
84-
const size = Math.abs(delta) + (Math.sign(delta) < 0 ? prevBox[widthHeight] : nextBox[widthHeight])
85-
const scale = size / Math.max(prevBox[widthHeight], nextBox[widthHeight]) || 0
86-
const initialScale = prevBox[widthHeight] / nextBox[widthHeight] || 0
87-
88-
const sigma = 1.5
112+
const keyframes = { fade, grow, shift }[props.sliderTransition ?? 'shift'] ?? shift
113+
const duration = Number(props.sliderTransitionDuration) ||
114+
({ fade: 400, grow: 350, shift: 225 }[props.sliderTransition ?? 'shift'] ?? 225)
115+
89116
animate(nextEl, {
90117
backgroundColor: [color, 'currentcolor'],
91-
transform: [
92-
`translate${XY}(${delta}px) scale${XY}(${initialScale})`,
93-
`translate${XY}(${delta / sigma}px) scale${XY}(${(scale - 1) / sigma + 1})`,
94-
'none',
95-
],
96-
transformOrigin: Array(3).fill(origin),
118+
...keyframes(nextEl, prevEl),
97119
}, {
98-
duration: 225,
120+
duration,
99121
easing: standardEasing,
100122
})
101123
}

packages/vuetify/src/components/VTabs/VTabs.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export const makeVTabsProps = propsFactory({
7373
hideSlider: Boolean,
7474
sliderColor: String,
7575

76-
...pick(makeVTabProps(), ['spaced']),
76+
...pick(makeVTabProps(), ['spaced', 'sliderTransition', 'sliderTransitionDuration']),
7777
...makeVSlideGroupProps({
7878
mandatory: 'force' as const,
7979
selectedClass: 'v-tab-item--selected',
@@ -105,12 +105,14 @@ export const VTabs = genericComponent<new <T = TabItem>(
105105

106106
provideDefaults({
107107
VTab: {
108-
color: toRef(() => props.color),
109-
direction: toRef(() => props.direction),
110-
stacked: toRef(() => props.stacked),
111-
fixed: toRef(() => props.fixedTabs),
112-
sliderColor: toRef(() => props.sliderColor),
113-
hideSlider: toRef(() => props.hideSlider),
108+
color: toRef(props, 'color'),
109+
direction: toRef(props, 'direction'),
110+
stacked: toRef(props, 'stacked'),
111+
fixed: toRef(props, 'fixedTabs'),
112+
sliderColor: toRef(props, 'sliderColor'),
113+
sliderTransition: toRef(props, 'sliderTransition'),
114+
sliderTransitionDuration: toRef(props, 'sliderTransitionDuration'),
115+
hideSlider: toRef(props, 'hideSlider'),
114116
},
115117
})
116118

0 commit comments

Comments
 (0)