Skip to content

Commit 8829c4c

Browse files
authored
Merge pull request #713 from devtron-labs/feat/tab-group-animations
feat: add animation in tab group tabs
2 parents fb512c6 + ef85e86 commit 8829c4c

File tree

5 files changed

+70
-58
lines changed

5 files changed

+70
-58
lines changed

src/Shared/Components/TabGroup/TabGroup.component.tsx

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,23 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Link, NavLink } from 'react-router-dom'
17+
import { useMemo } from 'react'
18+
import { Link, NavLink, useRouteMatch } from 'react-router-dom'
19+
import { motion } from 'framer-motion'
1820

1921
import { Tooltip } from '@Common/Tooltip'
2022
import { ComponentSizeType } from '@Shared/constants'
2123

22-
import { getTabBadge, getTabDescription, getTabIcon, getTabIndicator } from './TabGroup.helpers'
23-
import { TabGroupProps, TabProps } from './TabGroup.types'
24+
import { getPathnameToMatch, getTabBadge, getTabDescription, getTabIcon, getTabIndicator } from './TabGroup.helpers'
25+
import { AdditionalTabProps, TabGroupProps, TabProps } from './TabGroup.types'
2426
import { getClassNameBySizeMap, tabGroupClassMap } from './TabGroup.utils'
2527

2628
import './TabGroup.scss'
2729

30+
const MotionLayoutUnderline = ({ layoutId }: { layoutId: string }) => (
31+
<motion.div layout="position" layoutId={layoutId} className="underline bcb-5" />
32+
)
33+
2834
const Tab = ({
2935
label,
3036
props,
@@ -33,7 +39,6 @@ const Tab = ({
3339
icon,
3440
size,
3541
badge = null,
36-
alignActiveBorderWithContainer,
3742
hideTopPadding,
3843
showIndicator,
3944
showError,
@@ -42,10 +47,19 @@ const Tab = ({
4247
description,
4348
shouldWrapTooltip,
4449
tooltipProps,
45-
}: TabProps & Pick<TabGroupProps, 'size' | 'alignActiveBorderWithContainer' | 'hideTopPadding'>) => {
50+
uniqueGroupId,
51+
}: TabProps & Pick<TabGroupProps, 'size' | 'hideTopPadding'> & AdditionalTabProps) => {
52+
const { path } = useRouteMatch()
53+
const pathToMatch = tabType === 'navLink' || tabType === 'link' ? getPathnameToMatch(props.to, path) : ''
54+
55+
// using match to define if tab is active as useRouteMatch return an object if path is matched otherwise return null/undefined
56+
const match = useRouteMatch(pathToMatch)
57+
58+
const isTabActive = tabType === 'button' ? active : !!match
59+
4660
const { tabClassName, iconClassName, badgeClassName } = getClassNameBySizeMap({
4761
hideTopPadding,
48-
alignActiveBorderWithContainer,
62+
isTabActive,
4963
})[size]
5064

5165
const onClickHandler = (
@@ -121,9 +135,10 @@ const Tab = ({
121135

122136
const renderTabContainer = () => (
123137
<li
124-
className={`tab-group__tab lh-20 ${active ? 'tab-group__tab--active cb-5 fw-6' : 'cn-9 fw-4'} ${alignActiveBorderWithContainer ? 'tab-group__tab--align-active-border' : ''} ${tabType === 'block' ? 'tab-group__tab--block' : ''} ${disabled ? 'dc__disabled' : 'cursor'}`}
138+
className={`tab-group__tab lh-20 ${active ? 'cb-5 fw-6' : 'cn-9 fw-4'} ${tabType === 'block' ? 'tab-group__tab--block' : ''} ${disabled ? 'dc__disabled' : 'cursor'}`}
125139
>
126140
{getTabComponent()}
141+
{isTabActive && <MotionLayoutUnderline layoutId={uniqueGroupId} />}
127142
</li>
128143
)
129144

@@ -138,22 +153,27 @@ export const TabGroup = ({
138153
tabs = [],
139154
size = ComponentSizeType.large,
140155
rightComponent,
141-
alignActiveBorderWithContainer,
142156
hideTopPadding,
143-
}: TabGroupProps) => (
144-
<div className="flexbox dc__align-items-center dc__content-space">
145-
<ul role="tablist" className={`tab-group flexbox dc__align-items-center p-0 m-0 ${tabGroupClassMap[size]}`}>
146-
{tabs.map(({ id, ...resProps }) => (
147-
<Tab
148-
key={id}
149-
id={id}
150-
size={size}
151-
alignActiveBorderWithContainer={alignActiveBorderWithContainer}
152-
hideTopPadding={hideTopPadding}
153-
{...resProps}
154-
/>
155-
))}
156-
</ul>
157-
{rightComponent || null}
158-
</div>
159-
)
157+
}: TabGroupProps) => {
158+
// Unique layoutId for motion.div to handle multiple tab groups on same page
159+
// Using tab labels so that id remains same on re mount as well
160+
const uniqueGroupId = useMemo(() => tabs.map((tab) => tab.label).join('-'), [])
161+
162+
return (
163+
<div className="flexbox dc__align-items-center dc__content-space">
164+
<ul role="tablist" className={`tab-group flexbox dc__align-items-center p-0 m-0 ${tabGroupClassMap[size]}`}>
165+
{tabs.map(({ id, ...resProps }) => (
166+
<Tab
167+
key={id}
168+
id={id}
169+
size={size}
170+
hideTopPadding={hideTopPadding}
171+
uniqueGroupId={uniqueGroupId}
172+
{...resProps}
173+
/>
174+
))}
175+
</ul>
176+
{rightComponent || null}
177+
</div>
178+
)
179+
}

src/Shared/Components/TabGroup/TabGroup.helpers.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { LinkProps, NavLinkProps } from 'react-router-dom'
18+
1719
import { ReactComponent as ICErrorExclamation } from '@Icons/ic-error-exclamation.svg'
1820
import { ReactComponent as ICWarning } from '@Icons/ic-warning.svg'
1921

@@ -65,3 +67,14 @@ export const getTabDescription = (description: TabProps['description']) =>
6567
: description}
6668
</ul>
6769
)
70+
71+
const replaceTrailingSlash = (pathname: string) => pathname.replace(/\/+$/, '')
72+
73+
export const getPathnameToMatch = (to: NavLinkProps['to'] | LinkProps['to'], currentPathname: string): string => {
74+
if (typeof to === 'string' || (to && typeof to === 'object' && 'pathname' in to)) {
75+
const pathname = typeof to === 'string' ? to : to.pathname || ''
76+
// handling absolute and relative paths
77+
return pathname.startsWith('/') ? pathname : `${replaceTrailingSlash(currentPathname)}/${pathname}`
78+
}
79+
return ''
80+
}

src/Shared/Components/TabGroup/TabGroup.scss

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,14 @@
3333

3434
@include svg-styles(var(--N700));
3535

36-
&::after {
37-
content: '';
38-
position: absolute;
39-
bottom: 0;
40-
left: 0;
41-
width: 100%;
36+
.underline {
4237
height: 2px;
43-
background-color: transparent;
4438
border-top-left-radius: 2px;
4539
border-top-right-radius: 2px;
4640
}
4741

48-
&--align-active-border::after {
49-
bottom: -1px;
42+
&--active {
43+
@include svg-styles(var(--B500));
5044
}
5145

5246
&:hover:not(.tab-group__tab--block):not(.dc__disabled) {
@@ -58,14 +52,6 @@
5852
}
5953
}
6054

61-
&--active {
62-
@include svg-styles(var(--B500));
63-
64-
&::after {
65-
background-color: var(--B500);
66-
}
67-
}
68-
6955
&__badge {
7056
border-radius: 10px;
7157
min-width: 20px;
@@ -104,11 +90,5 @@
10490
color: var(--B500);
10591
}
10692
}
107-
108-
&:has(.active) {
109-
&::after {
110-
background-color: var(--B500);
111-
}
112-
}
11393
}
11494
}

src/Shared/Components/TabGroup/TabGroup.types.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,13 @@ export interface TabGroupProps {
152152
* Optional component to be rendered on the right side of the tab list.
153153
*/
154154
rightComponent?: React.ReactElement
155-
/**
156-
* Set to `true` to align the active tab's border with the bottom border of the parent container.
157-
* @default false
158-
*/
159-
alignActiveBorderWithContainer?: boolean
160155
/**
161156
* Determines if the top padding of the tab group should be hidden.
162157
* @default false
163158
*/
164159
hideTopPadding?: boolean
165160
}
161+
162+
export type AdditionalTabProps = {
163+
uniqueGroupId: string
164+
}

src/Shared/Components/TabGroup/TabGroup.utils.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import { TabGroupProps } from './TabGroup.types'
2121

2222
export const getClassNameBySizeMap = ({
2323
hideTopPadding,
24-
alignActiveBorderWithContainer,
25-
}: Pick<TabGroupProps, 'hideTopPadding' | 'alignActiveBorderWithContainer'>): Record<
24+
isTabActive,
25+
}: Pick<TabGroupProps, 'hideTopPadding'> & { isTabActive: boolean }): Record<
2626
TabGroupProps['size'],
2727
{
2828
tabClassName: string
@@ -31,17 +31,17 @@ export const getClassNameBySizeMap = ({
3131
}
3232
> => ({
3333
[ComponentSizeType.medium]: {
34-
tabClassName: `fs-12 ${!hideTopPadding ? 'pt-6' : ''} ${alignActiveBorderWithContainer ? 'pb-5' : 'pb-6'}`,
34+
tabClassName: `fs-12 ${!hideTopPadding ? 'pt-6' : ''} ${isTabActive ? 'pb-3' : 'pb-5'}`,
3535
iconClassName: 'icon-dim-14',
3636
badgeClassName: 'fs-11 lh-18 tab-group__tab__badge--medium',
3737
},
3838
[ComponentSizeType.large]: {
39-
tabClassName: `fs-13 ${!hideTopPadding ? 'pt-8' : ''} ${alignActiveBorderWithContainer ? 'pb-7' : 'pb-8'}`,
39+
tabClassName: `fs-13 ${!hideTopPadding ? 'pt-8' : ''} ${isTabActive ? 'pb-5' : 'pb-7'}`,
4040
iconClassName: 'icon-dim-16',
4141
badgeClassName: 'fs-12 lh-20',
4242
},
4343
[ComponentSizeType.xl]: {
44-
tabClassName: `min-w-200 fs-13 ${!hideTopPadding ? 'pt-10' : ''} ${alignActiveBorderWithContainer ? 'pb-9' : 'pb-10'}`,
44+
tabClassName: `min-w-200 fs-13 ${!hideTopPadding ? 'pt-10' : ''} ${isTabActive ? 'pb-7' : 'pb-9'}`,
4545
iconClassName: 'icon-dim-16',
4646
badgeClassName: 'fs-12 lh-20',
4747
},

0 commit comments

Comments
 (0)