From 98335a84f7649ee2151a996a5684a1bef21ef374 Mon Sep 17 00:00:00 2001 From: Arun Jain Date: Fri, 25 Apr 2025 14:09:11 +0530 Subject: [PATCH 1/4] feat: add animation in tab group tabs --- .../TabGroup/TabGroup.component.tsx | 78 ++++++++++++++----- .../Components/TabGroup/TabGroup.helpers.tsx | 19 +++++ src/Shared/Components/TabGroup/TabGroup.scss | 18 ----- 3 files changed, 76 insertions(+), 39 deletions(-) diff --git a/src/Shared/Components/TabGroup/TabGroup.component.tsx b/src/Shared/Components/TabGroup/TabGroup.component.tsx index 801a4d806..fd49c9b06 100644 --- a/src/Shared/Components/TabGroup/TabGroup.component.tsx +++ b/src/Shared/Components/TabGroup/TabGroup.component.tsx @@ -14,17 +14,33 @@ * limitations under the License. */ -import { Link, NavLink } from 'react-router-dom' +import { Link, NavLink, useRouteMatch } from 'react-router-dom' +import { motion } from 'framer-motion' import { Tooltip } from '@Common/Tooltip' import { ComponentSizeType } from '@Shared/constants' -import { getTabBadge, getTabDescription, getTabIcon, getTabIndicator } from './TabGroup.helpers' +import { getPathnameToMatch, getTabBadge, getTabDescription, getTabIcon, getTabIndicator } from './TabGroup.helpers' import { TabGroupProps, TabProps } from './TabGroup.types' import { getClassNameBySizeMap, tabGroupClassMap } from './TabGroup.utils' import './TabGroup.scss' +const MotionLayoutUnderline = ({ + layoutId, + alignActiveBorderWithContainer, +}: { + layoutId: string + alignActiveBorderWithContainer: boolean +}) => ( + +) + const Tab = ({ label, props, @@ -42,7 +58,13 @@ const Tab = ({ description, shouldWrapTooltip, tooltipProps, -}: TabProps & Pick) => { + uniqueGroupId, +}: TabProps & + Pick & { uniqueGroupId: string }) => { + const { path } = useRouteMatch() + const pathToMatch = tabType === 'navLink' || tabType === 'link' ? getPathnameToMatch(props.to, path) : '' + const match = useRouteMatch(pathToMatch) + const { tabClassName, iconClassName, badgeClassName } = getClassNameBySizeMap({ hideTopPadding, alignActiveBorderWithContainer, @@ -119,11 +141,19 @@ const Tab = ({ } } + const isTabActive = tabType === 'button' ? active : !!match + const renderTabContainer = () => (
  • {getTabComponent()} + {isTabActive && ( + + )}
  • ) @@ -140,20 +170,26 @@ export const TabGroup = ({ rightComponent, alignActiveBorderWithContainer, hideTopPadding, -}: TabGroupProps) => ( -
    -
      - {tabs.map(({ id, ...resProps }) => ( - - ))} -
    - {rightComponent || null} -
    -) +}: TabGroupProps) => { + // Unique layoutId for motion.div to handle multiple tab groups on same page + const uniqueGroupId = tabs.map((tab) => tab.label).join('-') + + return ( +
    +
      + {tabs.map(({ id, ...resProps }) => ( + + ))} +
    + {rightComponent || null} +
    + ) +} diff --git a/src/Shared/Components/TabGroup/TabGroup.helpers.tsx b/src/Shared/Components/TabGroup/TabGroup.helpers.tsx index 3d515b910..3b50db019 100644 --- a/src/Shared/Components/TabGroup/TabGroup.helpers.tsx +++ b/src/Shared/Components/TabGroup/TabGroup.helpers.tsx @@ -14,6 +14,8 @@ * limitations under the License. */ +import { LinkProps, NavLinkProps } from 'react-router-dom' + import { ReactComponent as ICErrorExclamation } from '@Icons/ic-error-exclamation.svg' import { ReactComponent as ICWarning } from '@Icons/ic-warning.svg' @@ -65,3 +67,20 @@ export const getTabDescription = (description: TabProps['description']) => : description} ) + +const replaceTrailingSlash = (pathname: string) => pathname.replace(/\/+$/, '') + +export const getPathnameToMatch = (to: NavLinkProps['to'] | LinkProps['to'], currentPathname: string): string => { + // Handling both absolute and relative paths + if (typeof to === 'string') { + return to.startsWith('/') ? to : `${replaceTrailingSlash(currentPathname)}/${to}` + } + if (typeof to === 'function') { + return '' + } + if (to && typeof to === 'object' && 'pathname' in to) { + const pathname = to.pathname || '' + return pathname.startsWith('/') ? pathname : `${replaceTrailingSlash(currentPathname)}/${pathname}` + } + return '' +} diff --git a/src/Shared/Components/TabGroup/TabGroup.scss b/src/Shared/Components/TabGroup/TabGroup.scss index c5a352898..909ddd12b 100644 --- a/src/Shared/Components/TabGroup/TabGroup.scss +++ b/src/Shared/Components/TabGroup/TabGroup.scss @@ -45,10 +45,6 @@ border-top-right-radius: 2px; } - &--align-active-border::after { - bottom: -1px; - } - &:hover:not(.tab-group__tab--block):not(.dc__disabled) { color: var(--B500); @include svg-styles(var(--B500)); @@ -58,14 +54,6 @@ } } - &--active { - @include svg-styles(var(--B500)); - - &::after { - background-color: var(--B500); - } - } - &__badge { border-radius: 10px; min-width: 20px; @@ -104,11 +92,5 @@ color: var(--B500); } } - - &:has(.active) { - &::after { - background-color: var(--B500); - } - } } } From 3f1ebc01c53407d68efaec327328ab3c3ff0a547 Mon Sep 17 00:00:00 2001 From: Arun Jain Date: Fri, 25 Apr 2025 15:14:16 +0530 Subject: [PATCH 2/4] feat: add type for unique tab group id prop --- src/Shared/Components/TabGroup/TabGroup.component.tsx | 5 +++-- src/Shared/Components/TabGroup/TabGroup.types.ts | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Shared/Components/TabGroup/TabGroup.component.tsx b/src/Shared/Components/TabGroup/TabGroup.component.tsx index fd49c9b06..8073567cb 100644 --- a/src/Shared/Components/TabGroup/TabGroup.component.tsx +++ b/src/Shared/Components/TabGroup/TabGroup.component.tsx @@ -21,7 +21,7 @@ import { Tooltip } from '@Common/Tooltip' import { ComponentSizeType } from '@Shared/constants' import { getPathnameToMatch, getTabBadge, getTabDescription, getTabIcon, getTabIndicator } from './TabGroup.helpers' -import { TabGroupProps, TabProps } from './TabGroup.types' +import { AdditionalTabProps, TabGroupProps, TabProps } from './TabGroup.types' import { getClassNameBySizeMap, tabGroupClassMap } from './TabGroup.utils' import './TabGroup.scss' @@ -60,7 +60,8 @@ const Tab = ({ tooltipProps, uniqueGroupId, }: TabProps & - Pick & { uniqueGroupId: string }) => { + Pick & + AdditionalTabProps) => { const { path } = useRouteMatch() const pathToMatch = tabType === 'navLink' || tabType === 'link' ? getPathnameToMatch(props.to, path) : '' const match = useRouteMatch(pathToMatch) diff --git a/src/Shared/Components/TabGroup/TabGroup.types.ts b/src/Shared/Components/TabGroup/TabGroup.types.ts index 0ddb160f9..a1c81b693 100644 --- a/src/Shared/Components/TabGroup/TabGroup.types.ts +++ b/src/Shared/Components/TabGroup/TabGroup.types.ts @@ -163,3 +163,7 @@ export interface TabGroupProps { */ hideTopPadding?: boolean } + +export type AdditionalTabProps = { + uniqueGroupId: string +} From 1999c44ecfbc9dc53f18ae3b23ce03eab7660955 Mon Sep 17 00:00:00 2001 From: Arun Jain Date: Fri, 25 Apr 2025 16:10:23 +0530 Subject: [PATCH 3/4] chore: remove extra css --- src/Shared/Components/TabGroup/TabGroup.component.tsx | 4 ++-- src/Shared/Components/TabGroup/TabGroup.scss | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Shared/Components/TabGroup/TabGroup.component.tsx b/src/Shared/Components/TabGroup/TabGroup.component.tsx index 8073567cb..0d7ee6d33 100644 --- a/src/Shared/Components/TabGroup/TabGroup.component.tsx +++ b/src/Shared/Components/TabGroup/TabGroup.component.tsx @@ -36,8 +36,8 @@ const MotionLayoutUnderline = ({ ) diff --git a/src/Shared/Components/TabGroup/TabGroup.scss b/src/Shared/Components/TabGroup/TabGroup.scss index 909ddd12b..89ed04960 100644 --- a/src/Shared/Components/TabGroup/TabGroup.scss +++ b/src/Shared/Components/TabGroup/TabGroup.scss @@ -33,14 +33,8 @@ @include svg-styles(var(--N700)); - &::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 100%; + .underline { height: 2px; - background-color: transparent; border-top-left-radius: 2px; border-top-right-radius: 2px; } From aac6961f2a71ede2d0fe7f6e9dcbc1ba9701b5e0 Mon Sep 17 00:00:00 2001 From: Arun Jain Date: Mon, 28 Apr 2025 14:09:41 +0530 Subject: [PATCH 4/4] feat: remove prop for align active border with container, use default true --- .../TabGroup/TabGroup.component.tsx | 41 ++++++------------- .../Components/TabGroup/TabGroup.helpers.tsx | 12 ++---- src/Shared/Components/TabGroup/TabGroup.scss | 4 ++ .../Components/TabGroup/TabGroup.types.ts | 5 --- .../Components/TabGroup/TabGroup.utils.ts | 10 ++--- 5 files changed, 24 insertions(+), 48 deletions(-) diff --git a/src/Shared/Components/TabGroup/TabGroup.component.tsx b/src/Shared/Components/TabGroup/TabGroup.component.tsx index 0d7ee6d33..d7bcba5f8 100644 --- a/src/Shared/Components/TabGroup/TabGroup.component.tsx +++ b/src/Shared/Components/TabGroup/TabGroup.component.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { useMemo } from 'react' import { Link, NavLink, useRouteMatch } from 'react-router-dom' import { motion } from 'framer-motion' @@ -26,19 +27,8 @@ import { getClassNameBySizeMap, tabGroupClassMap } from './TabGroup.utils' import './TabGroup.scss' -const MotionLayoutUnderline = ({ - layoutId, - alignActiveBorderWithContainer, -}: { - layoutId: string - alignActiveBorderWithContainer: boolean -}) => ( - +const MotionLayoutUnderline = ({ layoutId }: { layoutId: string }) => ( + ) const Tab = ({ @@ -49,7 +39,6 @@ const Tab = ({ icon, size, badge = null, - alignActiveBorderWithContainer, hideTopPadding, showIndicator, showError, @@ -59,16 +48,18 @@ const Tab = ({ shouldWrapTooltip, tooltipProps, uniqueGroupId, -}: TabProps & - Pick & - AdditionalTabProps) => { +}: TabProps & Pick & AdditionalTabProps) => { const { path } = useRouteMatch() const pathToMatch = tabType === 'navLink' || tabType === 'link' ? getPathnameToMatch(props.to, path) : '' + + // using match to define if tab is active as useRouteMatch return an object if path is matched otherwise return null/undefined const match = useRouteMatch(pathToMatch) + const isTabActive = tabType === 'button' ? active : !!match + const { tabClassName, iconClassName, badgeClassName } = getClassNameBySizeMap({ hideTopPadding, - alignActiveBorderWithContainer, + isTabActive, })[size] const onClickHandler = ( @@ -142,19 +133,12 @@ const Tab = ({ } } - const isTabActive = tabType === 'button' ? active : !!match - const renderTabContainer = () => (
  • {getTabComponent()} - {isTabActive && ( - - )} + {isTabActive && }
  • ) @@ -169,11 +153,11 @@ export const TabGroup = ({ tabs = [], size = ComponentSizeType.large, rightComponent, - alignActiveBorderWithContainer, hideTopPadding, }: TabGroupProps) => { // Unique layoutId for motion.div to handle multiple tab groups on same page - const uniqueGroupId = tabs.map((tab) => tab.label).join('-') + // Using tab labels so that id remains same on re mount as well + const uniqueGroupId = useMemo(() => tabs.map((tab) => tab.label).join('-'), []) return (
    @@ -183,7 +167,6 @@ export const TabGroup = ({ key={id} id={id} size={size} - alignActiveBorderWithContainer={alignActiveBorderWithContainer} hideTopPadding={hideTopPadding} uniqueGroupId={uniqueGroupId} {...resProps} diff --git a/src/Shared/Components/TabGroup/TabGroup.helpers.tsx b/src/Shared/Components/TabGroup/TabGroup.helpers.tsx index 3b50db019..9aec1c1da 100644 --- a/src/Shared/Components/TabGroup/TabGroup.helpers.tsx +++ b/src/Shared/Components/TabGroup/TabGroup.helpers.tsx @@ -71,15 +71,9 @@ export const getTabDescription = (description: TabProps['description']) => const replaceTrailingSlash = (pathname: string) => pathname.replace(/\/+$/, '') export const getPathnameToMatch = (to: NavLinkProps['to'] | LinkProps['to'], currentPathname: string): string => { - // Handling both absolute and relative paths - if (typeof to === 'string') { - return to.startsWith('/') ? to : `${replaceTrailingSlash(currentPathname)}/${to}` - } - if (typeof to === 'function') { - return '' - } - if (to && typeof to === 'object' && 'pathname' in to) { - const pathname = to.pathname || '' + if (typeof to === 'string' || (to && typeof to === 'object' && 'pathname' in to)) { + const pathname = typeof to === 'string' ? to : to.pathname || '' + // handling absolute and relative paths return pathname.startsWith('/') ? pathname : `${replaceTrailingSlash(currentPathname)}/${pathname}` } return '' diff --git a/src/Shared/Components/TabGroup/TabGroup.scss b/src/Shared/Components/TabGroup/TabGroup.scss index 89ed04960..2ddaaf6b9 100644 --- a/src/Shared/Components/TabGroup/TabGroup.scss +++ b/src/Shared/Components/TabGroup/TabGroup.scss @@ -39,6 +39,10 @@ border-top-right-radius: 2px; } + &--active { + @include svg-styles(var(--B500)); + } + &:hover:not(.tab-group__tab--block):not(.dc__disabled) { color: var(--B500); @include svg-styles(var(--B500)); diff --git a/src/Shared/Components/TabGroup/TabGroup.types.ts b/src/Shared/Components/TabGroup/TabGroup.types.ts index a1c81b693..022b83254 100644 --- a/src/Shared/Components/TabGroup/TabGroup.types.ts +++ b/src/Shared/Components/TabGroup/TabGroup.types.ts @@ -152,11 +152,6 @@ export interface TabGroupProps { * Optional component to be rendered on the right side of the tab list. */ rightComponent?: React.ReactElement - /** - * Set to `true` to align the active tab's border with the bottom border of the parent container. - * @default false - */ - alignActiveBorderWithContainer?: boolean /** * Determines if the top padding of the tab group should be hidden. * @default false diff --git a/src/Shared/Components/TabGroup/TabGroup.utils.ts b/src/Shared/Components/TabGroup/TabGroup.utils.ts index e2f73bb93..5bc8aeb6f 100644 --- a/src/Shared/Components/TabGroup/TabGroup.utils.ts +++ b/src/Shared/Components/TabGroup/TabGroup.utils.ts @@ -21,8 +21,8 @@ import { TabGroupProps } from './TabGroup.types' export const getClassNameBySizeMap = ({ hideTopPadding, - alignActiveBorderWithContainer, -}: Pick): Record< + isTabActive, +}: Pick & { isTabActive: boolean }): Record< TabGroupProps['size'], { tabClassName: string @@ -31,17 +31,17 @@ export const getClassNameBySizeMap = ({ } > => ({ [ComponentSizeType.medium]: { - tabClassName: `fs-12 ${!hideTopPadding ? 'pt-6' : ''} ${alignActiveBorderWithContainer ? 'pb-5' : 'pb-6'}`, + tabClassName: `fs-12 ${!hideTopPadding ? 'pt-6' : ''} ${isTabActive ? 'pb-3' : 'pb-5'}`, iconClassName: 'icon-dim-14', badgeClassName: 'fs-11 lh-18 tab-group__tab__badge--medium', }, [ComponentSizeType.large]: { - tabClassName: `fs-13 ${!hideTopPadding ? 'pt-8' : ''} ${alignActiveBorderWithContainer ? 'pb-7' : 'pb-8'}`, + tabClassName: `fs-13 ${!hideTopPadding ? 'pt-8' : ''} ${isTabActive ? 'pb-5' : 'pb-7'}`, iconClassName: 'icon-dim-16', badgeClassName: 'fs-12 lh-20', }, [ComponentSizeType.xl]: { - tabClassName: `min-w-200 fs-13 ${!hideTopPadding ? 'pt-10' : ''} ${alignActiveBorderWithContainer ? 'pb-9' : 'pb-10'}`, + tabClassName: `min-w-200 fs-13 ${!hideTopPadding ? 'pt-10' : ''} ${isTabActive ? 'pb-7' : 'pb-9'}`, iconClassName: 'icon-dim-16', badgeClassName: 'fs-12 lh-20', },