Skip to content

Commit cf30e8a

Browse files
authored
Merge pull request #251 from devtron-labs/feat/tab-group-component
feat: TabGroup component
2 parents 9375c5b + 2309409 commit cf30e8a

File tree

11 files changed

+378
-5
lines changed

11 files changed

+378
-5
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "0.2.27",
3+
"version": "0.2.28",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",

src/Shared/Components/CICDHistory/LogStageAccordion.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const LogStageAccordion = ({
4242
<button
4343
className={`flexbox dc__transparent dc__content-space py-6 px-8 br-4 dc__align-items-center dc__select-text logs-renderer__stage-accordion ${
4444
isOpen ? 'logs-renderer__stage-accordion--open-stage' : ''
45-
} dc__position-sticky dc__zi-1 ${fullScreenView ? 'dc__top-44' : 'dc__top-82'}`}
45+
} dc__position-sticky dc__zi-1 ${fullScreenView ? 'dc__top-44' : 'dc__top-80'}`}
4646
type="button"
4747
role="tab"
4848
onClick={handleAccordionToggle}

src/Shared/Components/CICDHistory/LogsRenderer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ export const LogsRenderer = ({
408408
}}
409409
>
410410
<div
411-
className={`flexbox-col pb-7 dc__position-sticky dc__zi-2 ${fullScreenView ? 'dc__top-0' : 'dc__top-38'}`}
411+
className={`flexbox-col pb-7 dc__position-sticky dc__zi-2 ${fullScreenView ? 'dc__top-0' : 'dc__top-36'}`}
412412
style={{
413413
backgroundColor: '#0C1021',
414414
}}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Link, NavLink } from 'react-router-dom'
2+
3+
import { ReactComponent as ICErrorExclamation } from '@Icons/ic-error-exclamation.svg'
4+
import { ReactComponent as ICWarning } from '@Icons/ic-warning.svg'
5+
import { ComponentSizeType } from '@Shared/constants'
6+
7+
import { TabGroupProps, TabProps } from './TabGroup.types'
8+
import './TabGroup.scss'
9+
import { getClassNameBySizeMap, getIconColorClassMap } from './TabGroup.utils'
10+
11+
const Tab = ({
12+
label,
13+
props,
14+
tabType,
15+
active,
16+
icon: Icon,
17+
iconType = 'fill',
18+
size,
19+
badge = null,
20+
alignActiveBorderWithContainer,
21+
hideTopPadding,
22+
showIndicator,
23+
showError,
24+
showWarning,
25+
disabled,
26+
}: TabProps & Pick<TabGroupProps, 'size' | 'alignActiveBorderWithContainer' | 'hideTopPadding'>) => {
27+
const { tabClassName, iconClassName, badgeClassName } = getClassNameBySizeMap({
28+
hideTopPadding,
29+
alignActiveBorderWithContainer,
30+
})[size]
31+
32+
const getTabComponent = () => {
33+
const content = (
34+
<>
35+
{showError && <ICErrorExclamation className={`${iconClassName}`} />}
36+
{!showError && showWarning && <ICWarning className={`${iconClassName} warning-icon-y7`} />}
37+
{!showError && !showWarning && Icon && (
38+
<Icon className={`${iconClassName} ${getIconColorClassMap({ active })[iconType] || ''}`} />
39+
)}
40+
{label}
41+
{badge !== null && (
42+
<div className={`tab-group__tab__badge bcn-1 cn-7 fw-6 flex px-4 ${badgeClassName}`}>{badge}</div>
43+
)}
44+
{showIndicator && <span className="tab-group__tab__indicator bcr-5 mt-4 dc__align-self-start" />}
45+
</>
46+
)
47+
48+
switch (tabType) {
49+
case 'link':
50+
return (
51+
<Link
52+
className={`${tabClassName} dc__no-decor flexbox dc__align-items-center dc__gap-6 ${disabled ? 'cursor-not-allowed' : ''}`}
53+
aria-disabled={disabled}
54+
{...props}
55+
>
56+
{content}
57+
</Link>
58+
)
59+
case 'navLink':
60+
return (
61+
<NavLink
62+
className={`${tabClassName} dc__no-decor tab-group__tab__nav-link flexbox dc__align-items-center dc__gap-6 ${disabled ? 'cursor-not-allowed' : ''}`}
63+
aria-disabled={disabled}
64+
{...props}
65+
>
66+
{content}
67+
</NavLink>
68+
)
69+
default:
70+
return (
71+
<button
72+
type="button"
73+
className={`dc__unset-button-styles ${tabClassName} flexbox dc__align-items-center dc__gap-6 ${disabled ? 'cursor-not-allowed' : ''}`}
74+
disabled={disabled}
75+
{...props}
76+
>
77+
{content}
78+
</button>
79+
)
80+
}
81+
}
82+
83+
return (
84+
<li
85+
className={`tab-group__tab cursor lh-20 ${active ? 'tab-group__tab--active cb-5 fw-6' : 'cn-9 fw-4'} ${alignActiveBorderWithContainer ? 'tab-group__tab--align-active-border' : ''}`}
86+
>
87+
{getTabComponent()}
88+
</li>
89+
)
90+
}
91+
92+
export const TabGroup = ({
93+
tabs = [],
94+
size = ComponentSizeType.large,
95+
rightComponent,
96+
alignActiveBorderWithContainer,
97+
hideTopPadding,
98+
}: TabGroupProps) => (
99+
<div className="flexbox dc__align-items-center dc__content-space">
100+
<ul
101+
role="tablist"
102+
className={`tab-group flexbox dc__align-items-center p-0 m-0 ${size === ComponentSizeType.large ? 'dc__gap-16' : 'dc__gap-12'}`}
103+
>
104+
{tabs.map(({ id, ...resProps }) => (
105+
<Tab
106+
key={id}
107+
id={id}
108+
size={size}
109+
alignActiveBorderWithContainer={alignActiveBorderWithContainer}
110+
hideTopPadding={hideTopPadding}
111+
{...resProps}
112+
/>
113+
))}
114+
</ul>
115+
{rightComponent || null}
116+
</div>
117+
)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
.tab-group {
2+
list-style: none;
3+
4+
&__tab {
5+
$parent-selector: &;
6+
position: relative;
7+
8+
&::after {
9+
content: '';
10+
position: absolute;
11+
bottom: 0;
12+
left: 0;
13+
width: 100%;
14+
height: 2px;
15+
background-color: transparent;
16+
border-top-left-radius: 2px;
17+
border-top-right-radius: 2px;
18+
}
19+
20+
&--align-active-border::after {
21+
bottom: -1px;
22+
}
23+
24+
&:hover {
25+
color: var(--B500);
26+
27+
#{$parent-selector}__icon {
28+
&--stroke {
29+
stroke: var(--B500);
30+
31+
path {
32+
stroke: var(--B500);
33+
}
34+
}
35+
36+
&--fill {
37+
fill: var(--B500);
38+
39+
path {
40+
fill: var(--B500);
41+
}
42+
}
43+
}
44+
}
45+
46+
&--active::after {
47+
background-color: var(--B500);
48+
}
49+
50+
&__badge {
51+
border-radius: 10px;
52+
min-width: 20px;
53+
54+
&--medium {
55+
min-width: 18px;
56+
}
57+
}
58+
59+
&__indicator {
60+
width: 6px;
61+
height: 6px;
62+
border-radius: 100%;
63+
}
64+
65+
&__nav-link {
66+
&.active {
67+
color: var(--B500);
68+
font-weight: 600;
69+
70+
> #{$parent-selector}__icon--fill {
71+
fill: var(--B500);
72+
73+
path {
74+
fill: var(--B500);
75+
}
76+
}
77+
78+
> #{$parent-selector}__icon--stroke {
79+
stroke: var(--B500);
80+
81+
path {
82+
stroke: var(--B500);
83+
}
84+
}
85+
}
86+
87+
&:not(.active) {
88+
color: var(--N900);
89+
}
90+
91+
&:hover {
92+
color: var(--B500);
93+
}
94+
}
95+
96+
&:has(.active) {
97+
&::after {
98+
background-color: var(--B500);
99+
}
100+
}
101+
}
102+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { LinkProps, NavLinkProps } from 'react-router-dom'
2+
3+
import { ComponentSizeType } from '@Shared/constants'
4+
import { DataAttributes } from '@Shared/types'
5+
6+
type TabComponentProps<TabTypeProps> = TabTypeProps & DataAttributes
7+
8+
type ConditionalTabType =
9+
| {
10+
/**
11+
* Type of the tab, either `button`, `link` or `navLink`.
12+
*/
13+
tabType: 'button'
14+
/**
15+
* Props passed to button component.
16+
*/
17+
props?: TabComponentProps<Omit<React.ComponentProps<'button'>, 'className' | 'style'>>
18+
/**
19+
* Indicates if the tab is currently active.
20+
*/
21+
active?: boolean
22+
}
23+
| {
24+
/**
25+
* Type of the tab, either `button`, `link` or `navLink`.
26+
*/
27+
tabType: 'navLink'
28+
/**
29+
* Props passed to nav link component.
30+
*/
31+
props: TabComponentProps<Omit<NavLinkProps, 'className' | 'style' | 'activeClassName'>>
32+
/**
33+
* Active state is determined by matching the URL.
34+
*/
35+
active?: false
36+
}
37+
| {
38+
/**
39+
* Type of the tab, either `button`, `link` or `navLink`.
40+
*/
41+
tabType: 'link'
42+
/**
43+
* Props passed to link component.
44+
*/
45+
props: TabComponentProps<Omit<LinkProps, 'className' | 'style'>>
46+
/**
47+
* Indicates if the tab is currently active.
48+
*/
49+
active?: boolean
50+
}
51+
52+
export type TabProps = {
53+
/**
54+
* Unique identifier for the tab.
55+
*/
56+
id: string | number
57+
/**
58+
* Text label for the tab.
59+
*/
60+
label: string
61+
/**
62+
* Icon component to be displayed in the tab.
63+
* This should be a functional component that renders an SVG.
64+
*/
65+
icon?: React.FunctionComponent<React.SVGProps<SVGSVGElement>>
66+
/**
67+
* Type of the icon, determining whether it uses a stroke or fill style.
68+
*/
69+
iconType?: 'stroke' | 'fill'
70+
/**
71+
* Badge number to be displayed on the tab, typically for notifications.
72+
*/
73+
badge?: number
74+
/**
75+
* Indicates if an indicator should be shown on the tab.
76+
*/
77+
showIndicator?: boolean
78+
/**
79+
* Indicates if a warning state should be displayed on the tab.
80+
* @note error state will take precedence over warning state.
81+
*/
82+
showWarning?: boolean
83+
/**
84+
* Indicates if an error state should be displayed on the tab.
85+
* @note error state will take precedence over warning state.
86+
*/
87+
showError?: boolean
88+
/**
89+
* Disables the tab, preventing interaction and indicating an inactive state.
90+
*/
91+
disabled?: boolean
92+
} & ConditionalTabType
93+
94+
export interface TabGroupProps {
95+
/**
96+
* Array of tabs to be rendered.
97+
*/
98+
tabs: TabProps[]
99+
/**
100+
* Size of the tabs.
101+
* @default ComponentSizeType.large
102+
*/
103+
size?: ComponentSizeType.large | ComponentSizeType.medium
104+
/**
105+
* Optional component to be rendered on the right side of the tab list.
106+
*/
107+
rightComponent?: React.ReactElement
108+
/**
109+
* Set to `true` to align the active tab's border with the bottom border of the parent container.
110+
* @default false
111+
*/
112+
alignActiveBorderWithContainer?: boolean
113+
/**
114+
* Determines if the top padding of the tab group should be hidden.
115+
* @default false
116+
*/
117+
hideTopPadding?: boolean
118+
}

0 commit comments

Comments
 (0)