Skip to content

Commit 98335a8

Browse files
committed
feat: add animation in tab group tabs
1 parent 5efac09 commit 98335a8

3 files changed

Lines changed: 76 additions & 39 deletions

File tree

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

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

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

1920
import { Tooltip } from '@Common/Tooltip'
2021
import { ComponentSizeType } from '@Shared/constants'
2122

22-
import { getTabBadge, getTabDescription, getTabIcon, getTabIndicator } from './TabGroup.helpers'
23+
import { getPathnameToMatch, getTabBadge, getTabDescription, getTabIcon, getTabIndicator } from './TabGroup.helpers'
2324
import { TabGroupProps, TabProps } from './TabGroup.types'
2425
import { getClassNameBySizeMap, tabGroupClassMap } from './TabGroup.utils'
2526

2627
import './TabGroup.scss'
2728

29+
const MotionLayoutUnderline = ({
30+
layoutId,
31+
alignActiveBorderWithContainer,
32+
}: {
33+
layoutId: string
34+
alignActiveBorderWithContainer: boolean
35+
}) => (
36+
<motion.div
37+
layout="position"
38+
layoutId={layoutId}
39+
className="bcb-5"
40+
style={{ height: 2, ...(alignActiveBorderWithContainer ? { bottom: -1 } : {}) }}
41+
/>
42+
)
43+
2844
const Tab = ({
2945
label,
3046
props,
@@ -42,7 +58,13 @@ const Tab = ({
4258
description,
4359
shouldWrapTooltip,
4460
tooltipProps,
45-
}: TabProps & Pick<TabGroupProps, 'size' | 'alignActiveBorderWithContainer' | 'hideTopPadding'>) => {
61+
uniqueGroupId,
62+
}: TabProps &
63+
Pick<TabGroupProps, 'size' | 'alignActiveBorderWithContainer' | 'hideTopPadding'> & { uniqueGroupId: string }) => {
64+
const { path } = useRouteMatch()
65+
const pathToMatch = tabType === 'navLink' || tabType === 'link' ? getPathnameToMatch(props.to, path) : ''
66+
const match = useRouteMatch(pathToMatch)
67+
4668
const { tabClassName, iconClassName, badgeClassName } = getClassNameBySizeMap({
4769
hideTopPadding,
4870
alignActiveBorderWithContainer,
@@ -119,11 +141,19 @@ const Tab = ({
119141
}
120142
}
121143

144+
const isTabActive = tabType === 'button' ? active : !!match
145+
122146
const renderTabContainer = () => (
123147
<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'}`}
148+
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'}`}
125149
>
126150
{getTabComponent()}
151+
{isTabActive && (
152+
<MotionLayoutUnderline
153+
layoutId={uniqueGroupId}
154+
alignActiveBorderWithContainer={alignActiveBorderWithContainer}
155+
/>
156+
)}
127157
</li>
128158
)
129159

@@ -140,20 +170,26 @@ export const TabGroup = ({
140170
rightComponent,
141171
alignActiveBorderWithContainer,
142172
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-
)
173+
}: TabGroupProps) => {
174+
// Unique layoutId for motion.div to handle multiple tab groups on same page
175+
const uniqueGroupId = tabs.map((tab) => tab.label).join('-')
176+
177+
return (
178+
<div className="flexbox dc__align-items-center dc__content-space">
179+
<ul role="tablist" className={`tab-group flexbox dc__align-items-center p-0 m-0 ${tabGroupClassMap[size]}`}>
180+
{tabs.map(({ id, ...resProps }) => (
181+
<Tab
182+
key={id}
183+
id={id}
184+
size={size}
185+
alignActiveBorderWithContainer={alignActiveBorderWithContainer}
186+
hideTopPadding={hideTopPadding}
187+
uniqueGroupId={uniqueGroupId}
188+
{...resProps}
189+
/>
190+
))}
191+
</ul>
192+
{rightComponent || null}
193+
</div>
194+
)
195+
}

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

Lines changed: 19 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,20 @@ 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+
// Handling both absolute and relative paths
75+
if (typeof to === 'string') {
76+
return to.startsWith('/') ? to : `${replaceTrailingSlash(currentPathname)}/${to}`
77+
}
78+
if (typeof to === 'function') {
79+
return ''
80+
}
81+
if (to && typeof to === 'object' && 'pathname' in to) {
82+
const pathname = to.pathname || ''
83+
return pathname.startsWith('/') ? pathname : `${replaceTrailingSlash(currentPathname)}/${pathname}`
84+
}
85+
return ''
86+
}

src/Shared/Components/TabGroup/TabGroup.scss

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,6 @@
4545
border-top-right-radius: 2px;
4646
}
4747

48-
&--align-active-border::after {
49-
bottom: -1px;
50-
}
51-
5248
&:hover:not(.tab-group__tab--block):not(.dc__disabled) {
5349
color: var(--B500);
5450
@include svg-styles(var(--B500));
@@ -58,14 +54,6 @@
5854
}
5955
}
6056

61-
&--active {
62-
@include svg-styles(var(--B500));
63-
64-
&::after {
65-
background-color: var(--B500);
66-
}
67-
}
68-
6957
&__badge {
7058
border-radius: 10px;
7159
min-width: 20px;
@@ -104,11 +92,5 @@
10492
color: var(--B500);
10593
}
10694
}
107-
108-
&:has(.active) {
109-
&::after {
110-
background-color: var(--B500);
111-
}
112-
}
11395
}
11496
}

0 commit comments

Comments
 (0)