Skip to content

Commit 88c0ca6

Browse files
authored
Merge pull request #290 from devtron-labs/feat/icon-button
feat: add icon button
2 parents bdb65ca + 0053432 commit 88c0ca6

File tree

7 files changed

+184
-37
lines changed

7 files changed

+184
-37
lines changed

package-lock.json

Lines changed: 6 additions & 5 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.30",
3+
"version": "0.2.32",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",

src/Shared/Components/Button/Button.component.tsx

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { ButtonHTMLAttributes, PropsWithChildren } from 'react'
22
import { Link, LinkProps } from 'react-router-dom'
33
import { Progressing } from '@Common/Progressing'
44
import { Tooltip } from '@Common/Tooltip'
5+
import { TooltipProps } from '@Common/Tooltip/types'
56
import { ComponentSizeType } from '@Shared/constants'
67
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'
8+
import { getButtonDerivedClass, getButtonIconClassName, getButtonLoaderSize } from './utils'
99
import './button.scss'
1010

1111
const ButtonElement = ({
@@ -27,9 +27,11 @@ const ButtonElement = ({
2727
| 'tooltipProps'
2828
| 'dataTestId'
2929
| 'isLoading'
30+
| 'ariaLabel'
3031
> & {
3132
className: string
3233
'data-testid': ButtonProps['dataTestId']
34+
'aria-label': ButtonProps['ariaLabel']
3335
}
3436
>) => {
3537
if (component === ButtonComponentType.link) {
@@ -113,6 +115,11 @@ const ButtonElement = ({
113115
* ```tsx
114116
* <Button component={ButtonComponentType.link} linkProps={{ to: '#' }} />
115117
* ```
118+
*
119+
* @example Icon button
120+
* ```tsx
121+
* <Button icon={<ICCube />} ariaLabel="Label" />
122+
* ```
116123
*/
117124
const Button = ({
118125
dataTestId,
@@ -126,24 +133,57 @@ const Button = ({
126133
isLoading = false,
127134
showTooltip = false,
128135
tooltipProps = {},
136+
icon = null,
137+
ariaLabel = null,
129138
...props
130139
}: ButtonProps) => {
131140
const isDisabled = disabled || isLoading
132-
const iconClass = `dc__no-shrink flex dc__fill-available-space ${BUTTON_SIZE_TO_ICON_CLASS_NAME_MAP[size]}`
141+
const iconClass = `dc__no-shrink flex dc__fill-available-space ${getButtonIconClassName({
142+
size,
143+
icon,
144+
})}`
145+
146+
const getTooltipProps = (): TooltipProps => {
147+
if (!showTooltip && icon && ariaLabel) {
148+
return {
149+
alwaysShowTippyOnHover: true,
150+
content: ariaLabel,
151+
}
152+
}
153+
154+
return {
155+
alwaysShowTippyOnHover: showTooltip && !!tooltipProps?.content,
156+
...tooltipProps,
157+
}
158+
}
133159

134160
return (
135-
<Tooltip {...tooltipProps} alwaysShowTippyOnHover={showTooltip && !!tooltipProps?.content}>
161+
<Tooltip {...getTooltipProps()}>
136162
<div>
137163
<ButtonElement
138164
{...props}
139165
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' : ''}`}
166+
className={`br-4 flex cursor dc__tab-focus dc__position-rel dc__capitalize ${getButtonDerivedClass({ size, variant, style, isLoading, icon })} ${isDisabled ? 'dc__disabled' : ''}`}
141167
data-testid={dataTestId}
168+
aria-label={ariaLabel}
142169
>
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]} />}
170+
{icon ? (
171+
<span className={iconClass}>{icon}</span>
172+
) : (
173+
<>
174+
{startIcon && <span className={iconClass}>{startIcon}</span>}
175+
<span className="dc__mxw-150 dc__align-left dc__truncate">{text}</span>
176+
{endIcon && <span className={iconClass}>{endIcon}</span>}
177+
</>
178+
)}
179+
{isLoading && (
180+
<Progressing
181+
size={getButtonLoaderSize({
182+
size,
183+
icon,
184+
})}
185+
/>
186+
)}
147187
</ButtonElement>
148188
</div>
149189
</Tooltip>

src/Shared/Components/Button/button.scss

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
@mixin svg-styles($color) {
2+
svg *[stroke^="#"] {
3+
stroke: $color;
4+
}
5+
6+
svg *[fill^="#"] {
7+
fill: $color;
8+
}
9+
}
10+
111
@mixin button-variant-styles($background, $text-color, $border-color) {
212
background: $background;
313
color: $text-color;
@@ -7,11 +17,7 @@
717
color: $text-color;
818
}
919

10-
// Only stroke icons are supposed to be used with button
11-
svg,
12-
svg * {
13-
stroke: $text-color;
14-
}
20+
@include svg-styles($text-color);
1521

1622
// Custom state for loader
1723
// Added using css to ensure using the respective text-color
@@ -27,7 +33,7 @@
2733
}
2834
}
2935

30-
@mixin pseudo-states($hover-bg-color, $active-bg-color, $hover-border-color: null) {
36+
@mixin pseudo-states($hover-bg-color, $active-bg-color, $hover-border-color: null, $color: null) {
3137
$parent-selector: &;
3238

3339
&:hover:not([disabled]) {
@@ -36,10 +42,22 @@
3642
@if ($hover-border-color) {
3743
border-color: $hover-border-color;
3844
}
45+
46+
@if ($color) {
47+
color: $color;
48+
49+
@include svg-styles($color);
50+
}
3951
}
4052

4153
&:active:not([disabled]) {
4254
background: $active-bg-color;
55+
56+
@if ($color) {
57+
color: $color;
58+
59+
@include svg-styles($color);
60+
}
4361
}
4462
}
4563

@@ -65,6 +83,11 @@
6583
@include pseudo-states(var(--R600), var(--R700));
6684
}
6785

86+
&--negative-grey {
87+
@include button-variant-styles(var(--N600), $text-color, $border-color);
88+
@include pseudo-states(var(--R600), var(--R700), null, $text-color);
89+
}
90+
6891
&--positive {
6992
@include button-variant-styles(var(--G500), $text-color, $border-color);
7093
@include pseudo-states(var(--G600), var(--G700));
@@ -92,7 +115,12 @@
92115

93116
&--negative {
94117
@include button-variant-styles($background, var(--R600), $border-color);
95-
@include pseudo-states(var(--R100), var(--R200), var(--R600));
118+
@include pseudo-states(var(--R100), var(--R200), var(--R600), var(--R600));
119+
}
120+
121+
&--negative-grey {
122+
@include button-variant-styles($background, var(--N700), $border-color);
123+
@include pseudo-states(var(--R100), var(--R200), var(--R600), var(--R600));
96124
}
97125

98126
&--positive {
@@ -125,6 +153,10 @@
125153
@include button-variant-styles($background, var(--R600), $border-color);
126154
}
127155

156+
&--negative-grey {
157+
@include button-variant-styles($background, var(--N700), $border-color);
158+
}
159+
128160
&--positive {
129161
@include button-variant-styles($background, var(--G600), $border-color);
130162
}
@@ -148,6 +180,10 @@
148180
@include pseudo-states(var(--R100), var(--R200));
149181
}
150182

183+
&--negative-grey {
184+
@include pseudo-states(var(--R100), var(--R200), null, var(--R600));
185+
}
186+
151187
&--positive {
152188
@include pseudo-states(var(--G100), var(--G200));
153189
}
@@ -166,6 +202,7 @@
166202

167203
&--default,
168204
&--negative,
205+
&--negative-grey,
169206
&--positive,
170207
&--warning,
171208
&--neutral {

src/Shared/Components/Button/constants.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ export const BUTTON_SIZE_TO_CLASS_NAME_MAP: Record<ButtonProps['size'], string>
1010
[ComponentSizeType.xl]: 'px-15 py-9 fs-14 lh-20 fw-6 dc__gap-12',
1111
} as const
1212

13+
export const ICON_BUTTON_SIZE_TO_CLASS_NAME_MAP: Record<ButtonProps['size'], string> = {
14+
[ComponentSizeType.xs]: 'p-3',
15+
[ComponentSizeType.small]: 'p-5',
16+
[ComponentSizeType.medium]: 'p-7',
17+
[ComponentSizeType.large]: 'p-7',
18+
[ComponentSizeType.xl]: 'p-7',
19+
} as const
20+
1321
export const BUTTON_SIZE_TO_ICON_CLASS_NAME_MAP: Record<ButtonProps['size'], string> = {
1422
[ComponentSizeType.xs]: 'icon-dim-12',
1523
[ComponentSizeType.small]: 'icon-dim-12',
@@ -18,10 +26,26 @@ export const BUTTON_SIZE_TO_ICON_CLASS_NAME_MAP: Record<ButtonProps['size'], str
1826
[ComponentSizeType.xl]: 'icon-dim-20',
1927
} as const
2028

29+
export const ICON_BUTTON_SIZE_TO_ICON_CLASS_NAME_MAP: Record<ButtonProps['size'], string> = {
30+
[ComponentSizeType.xs]: 'icon-dim-16',
31+
[ComponentSizeType.small]: 'icon-dim-16',
32+
[ComponentSizeType.medium]: 'icon-dim-16',
33+
[ComponentSizeType.large]: 'icon-dim-20',
34+
[ComponentSizeType.xl]: 'icon-dim-24',
35+
} as const
36+
2137
export const BUTTON_SIZE_TO_LOADER_SIZE_MAP: Record<ButtonProps['size'], ProgressingProps['size']> = {
2238
[ComponentSizeType.xs]: 12,
2339
[ComponentSizeType.small]: 12,
2440
[ComponentSizeType.medium]: 16,
2541
[ComponentSizeType.large]: 16,
2642
[ComponentSizeType.xl]: 20,
2743
} as const
44+
45+
export const ICON_BUTTON_SIZE_TO_LOADER_SIZE_MAP: Record<ButtonProps['size'], ProgressingProps['size']> = {
46+
[ComponentSizeType.xs]: 16,
47+
[ComponentSizeType.small]: 16,
48+
[ComponentSizeType.medium]: 16,
49+
[ComponentSizeType.large]: 20,
50+
[ComponentSizeType.xl]: 24,
51+
} as const

src/Shared/Components/Button/types.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export enum ButtonVariantType {
1414
export enum ButtonStyleType {
1515
default = 'default',
1616
negative = 'negative',
17+
negativeGrey = 'negative-grey',
1718
positive = 'positive',
1819
warning = 'warning',
1920
neutral = 'neutral',
@@ -70,18 +71,6 @@ export type ButtonProps = (
7071
* @default ButtonStyleType.default
7172
*/
7273
style?: ButtonStyleType
73-
/**
74-
* Text to be displayed in the button
75-
*/
76-
text: string
77-
/**
78-
* If provided, icon to be displayed at the start of the button
79-
*/
80-
startIcon?: ReactElement
81-
/**
82-
* If provided, icon to be displayed at the end of the button
83-
*/
84-
endIcon?: ReactElement
8574
/**
8675
* If true, the loading state is shown for the button with disabled
8776
*/
@@ -111,4 +100,36 @@ export type ButtonProps = (
111100
showTooltip?: never
112101
tooltipProps?: never
113102
}
103+
) &
104+
(
105+
| {
106+
icon?: never
107+
ariaLabel?: never
108+
/**
109+
* Text to be displayed in the button
110+
*/
111+
text: string
112+
/**
113+
* If provided, icon to be displayed at the start of the button
114+
*/
115+
startIcon?: ReactElement
116+
/**
117+
* If provided, icon to be displayed at the end of the button
118+
*/
119+
endIcon?: ReactElement
120+
}
121+
| {
122+
/**
123+
* If provided, icon button is rendered
124+
*/
125+
icon: ReactElement
126+
/**
127+
* Label for the icon button for accessibility.
128+
* Shown on hover in tooltip if tippy is not provided explicitly
129+
*/
130+
ariaLabel: string
131+
text?: never
132+
startIcon?: never
133+
endIcon?: never
134+
}
114135
)

src/Shared/Components/Button/utils.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,34 @@
1-
import { BUTTON_SIZE_TO_CLASS_NAME_MAP } from './constants'
1+
import {
2+
BUTTON_SIZE_TO_CLASS_NAME_MAP,
3+
BUTTON_SIZE_TO_ICON_CLASS_NAME_MAP,
4+
BUTTON_SIZE_TO_LOADER_SIZE_MAP,
5+
ICON_BUTTON_SIZE_TO_CLASS_NAME_MAP,
6+
ICON_BUTTON_SIZE_TO_ICON_CLASS_NAME_MAP,
7+
ICON_BUTTON_SIZE_TO_LOADER_SIZE_MAP,
8+
} from './constants'
29
import { ButtonProps } from './types'
310

11+
export const getButtonIconClassName = ({ size, icon }: Pick<ButtonProps, 'size' | 'icon'>) => {
12+
if (icon) {
13+
return ICON_BUTTON_SIZE_TO_ICON_CLASS_NAME_MAP[size]
14+
}
15+
16+
return BUTTON_SIZE_TO_ICON_CLASS_NAME_MAP[size]
17+
}
18+
19+
export const getButtonLoaderSize = ({ size, icon }: Pick<ButtonProps, 'size' | 'icon'>) => {
20+
if (icon) {
21+
return ICON_BUTTON_SIZE_TO_LOADER_SIZE_MAP[size]
22+
}
23+
24+
return BUTTON_SIZE_TO_LOADER_SIZE_MAP[size]
25+
}
26+
427
export const getButtonDerivedClass = ({
528
size,
629
variant,
730
style,
831
isLoading,
9-
}: Pick<ButtonProps, 'variant' | 'size' | 'style' | 'isLoading'>) =>
10-
`button button__${variant}--${style} ${BUTTON_SIZE_TO_CLASS_NAME_MAP[size]} ${isLoading ? 'button--loading' : ''}`
32+
icon,
33+
}: Pick<ButtonProps, 'variant' | 'size' | 'style' | 'isLoading' | 'icon'>) =>
34+
`button button__${variant}--${style} ${icon ? ICON_BUTTON_SIZE_TO_CLASS_NAME_MAP[size] : `${BUTTON_SIZE_TO_CLASS_NAME_MAP[size]} dc__mnw-100`} ${isLoading ? 'button--loading' : ''}`

0 commit comments

Comments
 (0)