Skip to content

Commit 3138da3

Browse files
authored
[PAY-1720] Implements PlainButton (#3897)
1 parent b353f94 commit 3138da3

10 files changed

+439
-111
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/* ===Base Styles=== */
2+
.button {
3+
align-items: center;
4+
box-sizing: border-box;
5+
cursor: pointer;
6+
display: inline-flex;
7+
flex-shrink: 0;
8+
justify-content: center;
9+
overflow: hidden;
10+
position: relative;
11+
text-align: center;
12+
user-select: none;
13+
white-space: nowrap;
14+
}
15+
16+
.button:focus {
17+
outline: none !important;
18+
}
19+
20+
/* Only add hover styles on devices which support it */
21+
@media (hover: hover) {
22+
.button:not(.disabled):hover {
23+
transition: all var(--hover);
24+
transform: scale(1.04);
25+
}
26+
}
27+
28+
.button:not(.disabled):active {
29+
transition: all var(--press);
30+
transform: scale(0.98);
31+
}
32+
33+
.button.disabled {
34+
pointer-events: none;
35+
}
36+
37+
.icon path {
38+
fill: currentColor;
39+
}
40+
41+
.fullWidth {
42+
width: 100%;
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { forwardRef } from 'react'
2+
3+
import cn from 'classnames'
4+
5+
import { useMediaQueryListener } from 'hooks/useMediaQueryListener'
6+
7+
import baseStyles from './BaseButton.module.css'
8+
import { BaseButtonProps } from './types'
9+
10+
/**
11+
* Base component for Harmony buttons. Not intended to be used directly. Use
12+
* `HarmonyButton` or `HarmonyPlainButton`.
13+
*/
14+
export const BaseButton = forwardRef<HTMLButtonElement, BaseButtonProps>(
15+
function BaseButton(props, ref) {
16+
const {
17+
text,
18+
iconLeft: LeftIconComponent,
19+
iconRight: RightIconComponent,
20+
disabled,
21+
widthToHideText,
22+
minWidth,
23+
className,
24+
'aria-label': ariaLabelProp,
25+
fullWidth,
26+
styles,
27+
style,
28+
...other
29+
} = props
30+
const { isMatch: textIsHidden } = useMediaQueryListener(
31+
`(max-width: ${widthToHideText}px)`
32+
)
33+
34+
const isTextVisible = !!text && !textIsHidden
35+
36+
const getAriaLabel = () => {
37+
if (ariaLabelProp) return ariaLabelProp
38+
// Use the text prop as the aria-label if the text becomes hidden
39+
// and no aria-label was provided to keep the button accessible.
40+
else if (textIsHidden && typeof text === 'string') return text
41+
return undefined
42+
}
43+
44+
return (
45+
<button
46+
aria-label={getAriaLabel()}
47+
className={cn(
48+
baseStyles.button,
49+
styles.button,
50+
{
51+
[baseStyles.disabled]: disabled,
52+
[baseStyles.fullWidth]: fullWidth
53+
},
54+
className
55+
)}
56+
disabled={disabled}
57+
ref={ref}
58+
style={{
59+
minWidth: minWidth && isTextVisible ? `${minWidth}px` : 'unset',
60+
...style
61+
}}
62+
{...other}
63+
>
64+
{LeftIconComponent ? (
65+
<LeftIconComponent className={cn(baseStyles.icon, styles.icon)} />
66+
) : null}
67+
{isTextVisible ? (
68+
<span className={cn(baseStyles.text, styles.text)}>{text}</span>
69+
) : null}
70+
{RightIconComponent ? (
71+
<RightIconComponent className={cn(baseStyles.icon, styles.icon)} />
72+
) : null}
73+
</button>
74+
)
75+
}
76+
)

packages/stems/src/components/HarmonyButton/HarmonyButton.module.css

-40
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,9 @@
44
--text-color: var(--static-white);
55
--overlay-color: transparent;
66
--overlay-opacity: 0;
7-
align-items: center;
87
border: 1px solid var(--button-color);
98
border-radius: var(--unit-1);
10-
box-sizing: border-box;
119
color: var(--text-color);
12-
cursor: pointer;
13-
display: inline-flex;
14-
flex-shrink: 0;
15-
justify-content: center;
16-
overflow: hidden;
17-
position: relative;
18-
text-align: center;
19-
user-select: none;
20-
white-space: nowrap;
21-
}
22-
23-
.button:focus {
24-
outline: none !important;
25-
}
26-
27-
/* Only add hover styles on devices which support it */
28-
@media (hover: hover) {
29-
.button:not(.disabled):hover {
30-
transition: all var(--hover);
31-
transform: scale(1.04);
32-
}
33-
}
34-
35-
.button:not(.disabled):active {
36-
transition: all var(--press);
37-
transform: scale(0.98);
38-
}
39-
40-
.button.disabled {
41-
pointer-events: none;
4210
}
4311

4412
/* Overlay used for hover/press styling */
@@ -55,14 +23,6 @@
5523
pointer-events: none;
5624
}
5725

58-
.icon path {
59-
fill: currentColor;
60-
}
61-
62-
.fullWidth {
63-
width: 100%;
64-
}
65-
6626
/* === Sizes === */
6727

6828
/* Small */

packages/stems/src/components/HarmonyButton/HarmonyButton.stories.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const Template: Story<HarmonyButtonProps> = (args) => (
2727
display: 'flex',
2828
flexDirection: 'column',
2929
gap: '16px',
30+
justifyContent: 'center',
3031
alignItems: 'flex-start'
3132
}}
3233
>
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { CSSProperties, forwardRef } from 'react'
1+
import { forwardRef } from 'react'
22

33
import cn from 'classnames'
44

5-
import { useMediaQueryListener } from 'hooks/useMediaQueryListener'
65
import { CSSCustomProperties } from 'styles/types'
76
import { toCSSVariableName } from 'utils/styles'
87

8+
import { BaseButton } from './BaseButton'
99
import styles from './HarmonyButton.module.css'
1010
import {
1111
HarmonyButtonProps,
12-
HarmonyButtonType,
13-
HarmonyButtonSize
12+
HarmonyButtonSize,
13+
HarmonyButtonType
1414
} from './types'
1515

1616
const SIZE_STYLE_MAP: { [k in HarmonyButtonSize]: [string, string, string] } = {
@@ -48,69 +48,36 @@ export const HarmonyButton = forwardRef<HTMLButtonElement, HarmonyButtonProps>(
4848
function HarmonyButton(props, ref) {
4949
const {
5050
color,
51-
text,
5251
variant = HarmonyButtonType.PRIMARY,
5352
size = HarmonyButtonSize.DEFAULT,
54-
iconLeft: LeftIconComponent,
55-
iconRight: RightIconComponent,
5653
disabled,
57-
widthToHideText,
58-
minWidth,
59-
className,
60-
'aria-label': ariaLabelProp,
61-
fullWidth,
62-
...other
54+
...baseProps
6355
} = props
64-
const { isMatch: textIsHidden } = useMediaQueryListener(
65-
`(max-width: ${widthToHideText}px)`
66-
)
67-
68-
const isTextVisible = !!text && !textIsHidden
69-
70-
const getAriaLabel = () => {
71-
if (ariaLabelProp) return ariaLabelProp
72-
// Use the text prop as the aria-label if the text becomes hidden
73-
// and no aria-label was provided to keep the button accessible.
74-
else if (textIsHidden && typeof text === 'string') return text
75-
return undefined
76-
}
7756

7857
const style: CSSCustomProperties = {
79-
minWidth: minWidth && isTextVisible ? `${minWidth}px` : 'unset',
8058
'--button-color':
8159
!disabled && color ? `var(${toCSSVariableName(color)})` : undefined
8260
}
8361

8462
const [buttonSizeClass, iconSizeClass, textSizeClass] = SIZE_STYLE_MAP[size]
8563

8664
return (
87-
<button
88-
aria-label={getAriaLabel()}
89-
className={cn(
90-
styles.button,
91-
buttonSizeClass,
92-
TYPE_STYLE_MAP[variant],
93-
{
94-
[styles.disabled]: disabled,
95-
[styles.fullWidth]: fullWidth
96-
},
97-
className
98-
)}
99-
disabled={disabled}
65+
<BaseButton
10066
ref={ref}
101-
style={style as CSSProperties}
102-
{...other}
103-
>
104-
{LeftIconComponent ? (
105-
<LeftIconComponent className={cn(styles.icon, iconSizeClass)} />
106-
) : null}
107-
{isTextVisible ? (
108-
<span className={cn(styles.text, textSizeClass)}>{text}</span>
109-
) : null}
110-
{RightIconComponent ? (
111-
<RightIconComponent className={cn(styles.icon, iconSizeClass)} />
112-
) : null}
113-
</button>
67+
disabled={disabled}
68+
styles={{
69+
button: cn(
70+
styles.button,
71+
TYPE_STYLE_MAP[variant],
72+
{ [styles.disabled]: disabled },
73+
buttonSizeClass
74+
),
75+
icon: cn(styles.icon, iconSizeClass),
76+
text: cn(styles.text, textSizeClass)
77+
}}
78+
style={style}
79+
{...baseProps}
80+
/>
11481
)
11582
}
11683
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/* ===Base Styles=== */
2+
.button {
3+
--text-color: var(--text-default);
4+
background: transparent;
5+
border: none;
6+
color: var(--text-color);
7+
}
8+
9+
/* === Sizes === */
10+
11+
/* Default */
12+
.buttonDefault {
13+
gap: var(--unit-1);
14+
height: var(--unit-4);
15+
}
16+
17+
.iconDefault {
18+
width: var(--unit-4);
19+
height: var(--unit-4);
20+
}
21+
22+
.textDefault {
23+
font-size: var(--font-s);
24+
font-weight: var(--font-bold);
25+
line-height: var(--unit-4);
26+
}
27+
28+
/* Large */
29+
.buttonLarge {
30+
gap: var(--unit-2);
31+
height: var(--unit-5);
32+
}
33+
34+
.iconLarge {
35+
width: var(--unit-5);
36+
height: var(--unit-5);
37+
}
38+
39+
.textLarge {
40+
font-size: var(--font-l);
41+
font-weight: var(--font-bold);
42+
line-height: calc(4.5 * var(--unit-5));
43+
}
44+
45+
/* === Color Variants === */
46+
47+
/* Default */
48+
.default {
49+
--text-color: var(--text-default);
50+
}
51+
52+
.default:hover {
53+
--text-color: var(--secondary);
54+
}
55+
56+
.default:active {
57+
--text-color: var(--secondary-dark-2)
58+
}
59+
60+
/* Subdued */
61+
.subdued {
62+
--text-color: var(--text-subdued);
63+
}
64+
65+
.subdued:hover {
66+
--text-color: var(--secondary);
67+
}
68+
.subdued:active {
69+
--text-color: var(--secondary-dark-2);
70+
}
71+
72+
/* Inverted */
73+
.inverted {
74+
--text-color: var(--static-white);
75+
}
76+
.inverted:hover {
77+
opacity: 0.8;
78+
}
79+
.inverted:active {
80+
opacity: 0.5;
81+
}
82+
83+
/* Disabled states */
84+
.disabled {
85+
opacity: 0.2;
86+
}
87+
88+
.subdued.disabled {
89+
--text-color: var(--text-default);
90+
}

0 commit comments

Comments
 (0)