Skip to content

Commit 9375c5b

Browse files
authored
Merge pull request #285 from devtron-labs/feat/button
feat: add generic button component
2 parents 59831b9 + 03c3f2e commit 9375c5b

File tree

10 files changed

+512
-3
lines changed

10 files changed

+512
-3
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { ButtonHTMLAttributes, PropsWithChildren } from 'react'
2+
import { Link, LinkProps } from 'react-router-dom'
3+
import { Progressing } from '@Common/Progressing'
4+
import { Tooltip } from '@Common/Tooltip'
5+
import { ComponentSizeType } from '@Shared/constants'
6+
import { ButtonComponentType, ButtonProps, ButtonStyleType, ButtonVariantType } from './types'
7+
import { BUTTON_SIZE_TO_ICON_CLASS_NAME_MAP, BUTTON_SIZE_TO_LOADER_SIZE_MAP } from './constants'
8+
import { getButtonDerivedClass } from './utils'
9+
import './button.scss'
10+
11+
const ButtonElement = ({
12+
component = ButtonComponentType.button,
13+
linkProps,
14+
buttonProps,
15+
onClick,
16+
...props
17+
}: PropsWithChildren<
18+
Omit<
19+
ButtonProps,
20+
| 'text'
21+
| 'variant'
22+
| 'size'
23+
| 'style'
24+
| 'startIcon'
25+
| 'endIcon'
26+
| 'showTooltip'
27+
| 'tooltipProps'
28+
| 'dataTestId'
29+
| 'isLoading'
30+
> & {
31+
className: string
32+
'data-testid': ButtonProps['dataTestId']
33+
}
34+
>) => {
35+
if (component === ButtonComponentType.link) {
36+
return (
37+
<Link
38+
{...linkProps}
39+
{...props}
40+
// Added the specific class to ensure that the link override is applied
41+
className={`${props.className} button__link ${props.disabled ? 'dc__disable-click' : ''}`}
42+
onClick={onClick as LinkProps['onClick']}
43+
/>
44+
)
45+
}
46+
47+
return (
48+
<button
49+
{...buttonProps}
50+
{...props}
51+
// eslint-disable-next-line react/button-has-type
52+
type={buttonProps?.type || 'button'}
53+
onClick={onClick as ButtonHTMLAttributes<HTMLButtonElement>['onClick']}
54+
/>
55+
)
56+
}
57+
58+
/**
59+
* Generic component for Button.
60+
* Should be used in combination of variant, size and style.
61+
*
62+
* @example Default button
63+
* ```tsx
64+
* <Button text="Hello World" />
65+
* ```
66+
*
67+
* @example Custom variant
68+
* ```tsx
69+
* <Button text="Hello World" variant={ButtonVariantType.secondary} />
70+
* ```
71+
*
72+
* @example Custom size
73+
* ```tsx
74+
* <Button text="Hello World" size={ComponentSizeType.medium} />
75+
* ```
76+
*
77+
* @example Custom style
78+
* ```tsx
79+
* <Button text="Hello World" style={ButtonStyleType.positive} />
80+
* ```
81+
*
82+
* @example Disabled state
83+
* ```tsx
84+
* <Button text="Hello World" disabled />
85+
* ```
86+
*
87+
* @example Loading state
88+
* ```tsx
89+
* <Button text="Hello World" isLoading />
90+
* ```
91+
*
92+
* @example With start icon
93+
* ```tsx
94+
* <Button text="Hello World" startIcon={<ICCube />} />
95+
* ```
96+
*
97+
* @example With end icon
98+
* ```tsx
99+
* <Button text="Hello World" endIcon={<ICCube />} />
100+
* ```
101+
*
102+
* @example With tippy
103+
* ```tsx
104+
* <Button text="Hello World" showTippy tippyContent="Tippy content" />
105+
* ```
106+
*
107+
* @example With onClick
108+
* ```tsx
109+
* <Button text="Hello World" onClick={noop} />
110+
* ```
111+
*
112+
* @example Link component
113+
* ```tsx
114+
* <Button component={ButtonComponentType.link} linkProps={{ to: '#' }} />
115+
* ```
116+
*/
117+
const Button = ({
118+
dataTestId,
119+
text,
120+
variant = ButtonVariantType.primary,
121+
size = ComponentSizeType.large,
122+
style = ButtonStyleType.default,
123+
startIcon = null,
124+
endIcon = null,
125+
disabled = false,
126+
isLoading = false,
127+
showTooltip = false,
128+
tooltipProps = {},
129+
...props
130+
}: ButtonProps) => {
131+
const isDisabled = disabled || isLoading
132+
const iconClass = `dc__no-shrink flex dc__fill-available-space ${BUTTON_SIZE_TO_ICON_CLASS_NAME_MAP[size]}`
133+
134+
return (
135+
<Tooltip {...tooltipProps} alwaysShowTippyOnHover={showTooltip && !!tooltipProps?.content}>
136+
<div>
137+
<ButtonElement
138+
{...props}
139+
disabled={isDisabled}
140+
className={`br-4 flex cursor dc__mnw-100 dc__tab-focus dc__position-rel dc__capitalize ${getButtonDerivedClass({ size, variant, style, isLoading })} ${isDisabled ? 'dc__disabled' : ''}`}
141+
data-testid={dataTestId}
142+
>
143+
{startIcon && <span className={iconClass}>{startIcon}</span>}
144+
<span className="dc__mxw-150 dc__align-left dc__truncate">{text}</span>
145+
{endIcon && <span className={iconClass}>{endIcon}</span>}
146+
{isLoading && <Progressing size={BUTTON_SIZE_TO_LOADER_SIZE_MAP[size]} />}
147+
</ButtonElement>
148+
</div>
149+
</Tooltip>
150+
)
151+
}
152+
153+
export default Button
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
@mixin button-variant-styles($background, $text-color, $border-color) {
2+
background: $background;
3+
color: $text-color;
4+
border: 1px solid $border-color;
5+
6+
&.button__link {
7+
color: $text-color;
8+
}
9+
10+
// Only stroke icons are supposed to be used with button
11+
svg,
12+
svg * {
13+
stroke: $text-color;
14+
}
15+
16+
// Custom state for loader
17+
// Added using css to ensure using the respective text-color
18+
.loader {
19+
visibility: visible;
20+
position: absolute;
21+
22+
svg,
23+
svg * {
24+
fill: $text-color;
25+
stroke: none;
26+
}
27+
}
28+
}
29+
30+
@mixin pseudo-states($hover-bg-color, $active-bg-color, $hover-border-color: null) {
31+
$parent-selector: &;
32+
33+
&:hover:not([disabled]) {
34+
background: $hover-bg-color;
35+
36+
@if ($hover-border-color) {
37+
border-color: $hover-border-color;
38+
}
39+
}
40+
41+
&:active:not([disabled]) {
42+
background: $active-bg-color;
43+
}
44+
}
45+
46+
.button {
47+
// Reset the default styles
48+
background: none;
49+
color: inherit;
50+
border: none;
51+
outline: inherit;
52+
text-decoration: none;
53+
54+
&__primary {
55+
$border-color: transparent;
56+
$text-color: var(--N0);
57+
58+
&--default {
59+
@include button-variant-styles(var(--B500), $text-color, $border-color);
60+
@include pseudo-states(var(--B600), var(--B700));
61+
}
62+
63+
&--negative {
64+
@include button-variant-styles(var(--R500), $text-color, $border-color);
65+
@include pseudo-states(var(--R600), var(--R700));
66+
}
67+
68+
&--positive {
69+
@include button-variant-styles(var(--G500), $text-color, $border-color);
70+
@include pseudo-states(var(--G600), var(--G700));
71+
}
72+
73+
&--warning {
74+
@include button-variant-styles(var(--Y500), var(--N900), $border-color);
75+
@include pseudo-states(var(--Y600), var(--Y700));
76+
}
77+
78+
&--neutral {
79+
@include button-variant-styles(var(--N600), $text-color, $border-color);
80+
@include pseudo-states(var(--N700), var(--N800));
81+
}
82+
}
83+
84+
&__secondary {
85+
$background: var(--N0);
86+
$border-color: var(--N200);
87+
88+
&--default {
89+
@include button-variant-styles($background, var(--B500), $border-color);
90+
@include pseudo-states(var(--B100), var(--B200), var(--B500));
91+
}
92+
93+
&--negative {
94+
@include button-variant-styles($background, var(--R600), $border-color);
95+
@include pseudo-states(var(--R100), var(--R200), var(--R600));
96+
}
97+
98+
&--positive {
99+
@include button-variant-styles($background, var(--G600), $border-color);
100+
@include pseudo-states(var(--G100), var(--G200), var(--G600));
101+
}
102+
103+
&--warning {
104+
@include button-variant-styles($background, var(--Y700), $border-color);
105+
@include pseudo-states(var(--Y100), var(--Y200), var(--Y700));
106+
}
107+
108+
&--neutral {
109+
@include button-variant-styles($background, var(--N700), $border-color);
110+
@include pseudo-states(var(--N100), var(--N200), var(--N700));
111+
}
112+
}
113+
114+
// Base styling is same for border-less and text button
115+
&__border-less,
116+
&__text {
117+
$background: transparent;
118+
$border-color: transparent;
119+
120+
&--default {
121+
@include button-variant-styles($background, var(--B500), $border-color);
122+
}
123+
124+
&--negative {
125+
@include button-variant-styles($background, var(--R600), $border-color);
126+
}
127+
128+
&--positive {
129+
@include button-variant-styles($background, var(--G600), $border-color);
130+
}
131+
132+
&--warning {
133+
@include button-variant-styles($background, var(--Y700), $border-color);
134+
}
135+
136+
&--neutral {
137+
@include button-variant-styles($background, var(--N700), $border-color);
138+
}
139+
}
140+
141+
// Pseudo states for border-less button
142+
&__border-less {
143+
&--default {
144+
@include pseudo-states(var(--B100), var(--B200));
145+
}
146+
147+
&--negative {
148+
@include pseudo-states(var(--R100), var(--R200));
149+
}
150+
151+
&--positive {
152+
@include pseudo-states(var(--G100), var(--G200));
153+
}
154+
155+
&--warning {
156+
@include pseudo-states(var(--Y100), var(--Y200));
157+
}
158+
159+
&--neutral {
160+
@include pseudo-states(var(--N100), var(--N200));
161+
}
162+
}
163+
164+
// Overrides for text button
165+
&__text {
166+
167+
&--default,
168+
&--negative,
169+
&--positive,
170+
&--warning,
171+
&--neutral {
172+
padding: 0;
173+
min-width: 0;
174+
border: none;
175+
176+
&:hover {
177+
text-decoration: underline !important;
178+
}
179+
}
180+
}
181+
182+
// Hide the visibility for the button child elements when loading is true
183+
&--loading {
184+
// Override the opacity from dc__disabled
185+
opacity: 1 !important;
186+
187+
&>* {
188+
visibility: hidden;
189+
}
190+
}
191+
192+
&__link {
193+
&:hover {
194+
text-decoration: none;
195+
}
196+
}
197+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ComponentSizeType } from '@Shared/constants'
2+
import { ProgressingProps } from '@Common/Types'
3+
import { ButtonProps } from './types'
4+
5+
export const BUTTON_SIZE_TO_CLASS_NAME_MAP: Record<ButtonProps['size'], string> = {
6+
[ComponentSizeType.xs]: 'px-9 py-1 fs-12 lh-20 fw-6 dc__gap-6',
7+
[ComponentSizeType.small]: 'px-9 py-3 fs-12 lh-20 fw-6 dc__gap-6',
8+
[ComponentSizeType.medium]: 'px-11 py-5 fs-13 lh-20 fw-6 dc__gap-8',
9+
[ComponentSizeType.large]: 'px-13 py-7 fs-13 lh-20 fw-6 dc__gap-10',
10+
[ComponentSizeType.xl]: 'px-15 py-9 fs-14 lh-20 fw-6 dc__gap-12',
11+
} as const
12+
13+
export const BUTTON_SIZE_TO_ICON_CLASS_NAME_MAP: Record<ButtonProps['size'], string> = {
14+
[ComponentSizeType.xs]: 'icon-dim-12',
15+
[ComponentSizeType.small]: 'icon-dim-12',
16+
[ComponentSizeType.medium]: 'icon-dim-16',
17+
[ComponentSizeType.large]: 'icon-dim-16',
18+
[ComponentSizeType.xl]: 'icon-dim-20',
19+
} as const
20+
21+
export const BUTTON_SIZE_TO_LOADER_SIZE_MAP: Record<ButtonProps['size'], ProgressingProps['size']> = {
22+
[ComponentSizeType.xs]: 12,
23+
[ComponentSizeType.small]: 12,
24+
[ComponentSizeType.medium]: 16,
25+
[ComponentSizeType.large]: 16,
26+
[ComponentSizeType.xl]: 20,
27+
} as const

src/Shared/Components/Button/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as Button } from './Button.component'
2+
export * from './types'

0 commit comments

Comments
 (0)