Skip to content

Commit a84a149

Browse files
Button: Make button visuals overridable by sx (#3683)
* Button: Make button visuals overridable by sx * add changeset * update comments * test(vrt): update snapshots * add tests * update bits * same color name as storybook * test(vrt): update snapshots * override icon styles with css custom var * stage the export * update snapshots --------- Co-authored-by: broccolinisoup <[email protected]>
1 parent f4648b1 commit a84a149

17 files changed

+61
-48
lines changed

.changeset/purple-panthers-accept.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@primer/react': patch
3+
---
4+
5+
Button: Allow leadingIcon, trailingIcon, trailingAction to be overridable with sx
6+
7+
<!-- Changed components: Button -->
Loading
Loading

src/Button/Button.dev.stories.tsx

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import {SearchIcon, TriangleDownIcon, EyeIcon} from '@primer/octicons-react'
1+
import {SearchIcon, TriangleDownIcon, EyeIcon, IssueClosedIcon} from '@primer/octicons-react'
22
import React from 'react'
33
import {Button, IconButton} from '.'
4+
import {default as Text} from '../Text'
45

56
export default {
67
title: 'Components/Button/DevOnly',
@@ -67,8 +68,11 @@ export const TestSxProp = () => {
6768
>
6869
Red
6970
</Button>
70-
<Button leadingIcon={SearchIcon} variant="invisible" sx={{color: 'firebrick'}}>
71-
Red
71+
<Button variant="invisible" sx={{color: 'firebrick'}}>
72+
Invariant color overridden
73+
</Button>
74+
<Button leadingIcon={IssueClosedIcon} sx={{color: 'done.fg'}}>
75+
<Text sx={{color: 'fg.default'}}>Close issue</Text>
7276
</Button>
7377
<Button
7478
size="small"

src/Button/Button.tsx

+10-6
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import {ButtonProps} from './types'
33
import {ButtonBase} from './ButtonBase'
44
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
55
import {defaultSxProp} from '../utils/defaultSxProp'
6-
import {BetterSystemStyleObject} from '../sx'
6+
import {BetterSystemStyleObject, CSSCustomProperties} from '../sx'
77

88
const ButtonComponent = forwardRef(({children, sx: sxProp = defaultSxProp, ...props}, forwardedRef): JSX.Element => {
99
let sxStyles = sxProp
10+
const style: CSSCustomProperties = {}
1011
const leadingVisual = props.leadingVisual ?? props.leadingIcon
1112
const trailingVisual = props.trailingVisual ?? props.trailingIcon
1213

@@ -15,10 +16,14 @@ const ButtonComponent = forwardRef(({children, sx: sxProp = defaultSxProp, ...pr
1516

1617
if (sxProp !== null && Object.keys(sxProp).length > 0) {
1718
sxStyles = generateCustomSxProp({block, size, leadingVisual, trailingVisual, trailingAction}, sxProp)
19+
20+
// @ts-ignore sxProp can have color attribute
21+
const {color} = sxProp
22+
if (color) style['--button-color'] = color
1823
}
1924

2025
return (
21-
<ButtonBase ref={forwardedRef} as="button" sx={sxStyles} type="button" {...props}>
26+
<ButtonBase ref={forwardedRef} as="button" sx={sxStyles} style={style} type="button" {...props}>
2227
{children}
2328
</ButtonBase>
2429
)
@@ -71,11 +76,10 @@ export function generateCustomSxProp(
7176
// Possible data attributes: data-size, data-block, data-no-visuals
7277
const size = props.size && props.size !== 'medium' ? `[data-size="${props.size}"]` : '' // medium is a default size therefore it doesn't have a data attribute that used for styling
7378
const block = props.block ? `[data-block="block"]` : ''
74-
const noVisuals =
75-
props.leadingVisual || props.trailingVisual || props.trailingAction ? '' : '[data-no-visuals="true"]'
79+
const noVisuals = props.leadingVisual || props.trailingVisual || props.trailingAction ? '' : '[data-no-visuals]'
7680

77-
// this is custom selector. We need to make sure we add the data attributes to the base css class (& -> &[data-attributename="value"]])
78-
const cssSelector = `&${size}${block}${noVisuals}` // &[data-size="small"][data-block="block"][data-no-visuals="true"]
81+
// this is a custom selector. We need to make sure we add the data attributes to the base css class (& -> &[data-attributename="value"]])
82+
const cssSelector = `&${size}${block}${noVisuals}` // &[data-size="small"][data-block="block"][data-no-visuals]
7983

8084
const customSxProp: {
8185
[key: string]: BetterSystemStyleObject

src/Button/__tests__/__snapshots__/Button.test.tsx.snap

+6-5
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ exports[`Button renders consistently 1`] = `
212212
.c0 [data-component="leadingVisual"],
213213
.c0 [data-component="trailingVisual"],
214214
.c0 [data-component="trailingAction"] {
215-
color: #656d76;
215+
color: var(--button-color,#656d76);
216216
}
217217
218218
@media (forced-colors:active) {
@@ -225,6 +225,7 @@ exports[`Button renders consistently 1`] = `
225225
className="c0"
226226
data-block={null}
227227
data-no-visuals={true}
228+
style={{}}
228229
type="button"
229230
>
230231
<span
@@ -445,7 +446,7 @@ exports[`Button respects block prop 1`] = `
445446
.c0 [data-component="leadingVisual"],
446447
.c0 [data-component="trailingVisual"],
447448
.c0 [data-component="trailingAction"] {
448-
color: fg.muted;
449+
color: var(--button-color,undefined);
449450
}
450451
451452
@media (forced-colors:active) {
@@ -684,7 +685,7 @@ exports[`Button respects the alignContent prop 1`] = `
684685
.c0 [data-component="leadingVisual"],
685686
.c0 [data-component="trailingVisual"],
686687
.c0 [data-component="trailingAction"] {
687-
color: fg.muted;
688+
color: var(--button-color,undefined);
688689
}
689690
690691
@media (forced-colors:active) {
@@ -922,7 +923,7 @@ exports[`Button respects the large size prop 1`] = `
922923
.c0 [data-component="leadingVisual"],
923924
.c0 [data-component="trailingVisual"],
924925
.c0 [data-component="trailingAction"] {
925-
color: fg.muted;
926+
color: var(--button-color,undefined);
926927
}
927928
928929
@media (forced-colors:active) {
@@ -1161,7 +1162,7 @@ exports[`Button respects the small size prop 1`] = `
11611162
.c0 [data-component="leadingVisual"],
11621163
.c0 [data-component="trailingVisual"],
11631164
.c0 [data-component="trailingAction"] {
1164-
color: fg.muted;
1165+
color: var(--button-color,undefined);
11651166
}
11661167
11671168
@media (forced-colors:active) {

src/Button/styles.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme
2626
borderColor: 'btn.activeBorder',
2727
},
2828
'[data-component="leadingVisual"], [data-component="trailingVisual"], [data-component="trailingAction"]': {
29-
color: 'fg.muted',
29+
color: `var(--button-color, ${theme?.colors.fg.muted})`,
3030
},
3131
},
3232
primary: {

src/__tests__/__snapshots__/ActionMenu.test.tsx.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ exports[`ActionMenu renders consistently 1`] = `
226226
.c1 [data-component="leadingVisual"],
227227
.c1 [data-component="trailingVisual"],
228228
.c1 [data-component="trailingAction"] {
229-
color: #656d76;
229+
color: var(--button-color,#656d76);
230230
}
231231
232232
@media (forced-colors:active) {
@@ -251,6 +251,7 @@ exports[`ActionMenu renders consistently 1`] = `
251251
id="react-aria-1"
252252
onClick={[Function]}
253253
onKeyDown={[Function]}
254+
style={{}}
254255
tabIndex={0}
255256
type="button"
256257
>

src/__tests__/__snapshots__/TextInput.test.tsx.snap

+27-31
Original file line numberDiff line numberDiff line change
@@ -1868,15 +1868,15 @@ exports[`TextInput renders trailingAction icon button 1`] = `
18681868
color: inherit;
18691869
}
18701870
1871-
.c4[data-size="small"][data-no-visuals="true"] {
1871+
.c4[data-size="small"][data-no-visuals] {
18721872
padding-top: 2px;
18731873
padding-right: 4px;
18741874
padding-bottom: 2px;
18751875
padding-left: 4px;
18761876
position: relative;
18771877
}
18781878
1879-
.c4[data-size="small"][data-no-visuals="true"][data-component="IconButton"] {
1879+
.c4[data-size="small"][data-no-visuals][data-component="IconButton"] {
18801880
width: var(--inner-action-size);
18811881
height: var(--inner-action-size);
18821882
}
@@ -2202,7 +2202,7 @@ exports[`TextInput renders trailingAction icon button 1`] = `
22022202
}
22032203
22042204
@media (pointer:coarse) {
2205-
.c4[data-size="small"][data-no-visuals="true"]:after {
2205+
.c4[data-size="small"][data-no-visuals]:after {
22062206
content: "";
22072207
position: absolute;
22082208
left: 0;
@@ -2505,6 +2505,16 @@ exports[`TextInput renders trailingAction text button 1`] = `
25052505
25062506
.c3[data-no-visuals] {
25072507
color: #0969da;
2508+
padding-top: 2px;
2509+
padding-right: 4px;
2510+
padding-bottom: 2px;
2511+
padding-left: 4px;
2512+
position: relative;
2513+
}
2514+
2515+
.c3[data-no-visuals][data-component="IconButton"] {
2516+
width: var(--inner-action-size);
2517+
height: var(--inner-action-size);
25082518
}
25092519
25102520
.c3:has([data-component="ButtonCounter"]) {
@@ -2519,19 +2529,6 @@ exports[`TextInput renders trailingAction text button 1`] = `
25192529
color: inherit;
25202530
}
25212531
2522-
.c3[data-no-visuals="true"] {
2523-
padding-top: 2px;
2524-
padding-right: 4px;
2525-
padding-bottom: 2px;
2526-
padding-left: 4px;
2527-
position: relative;
2528-
}
2529-
2530-
.c3[data-no-visuals="true"][data-component="IconButton"] {
2531-
width: var(--inner-action-size);
2532-
height: var(--inner-action-size);
2533-
}
2534-
25352532
.c0 {
25362533
font-size: 14px;
25372534
line-height: 20px;
@@ -2628,7 +2625,7 @@ exports[`TextInput renders trailingAction text button 1`] = `
26282625
}
26292626
26302627
@media (pointer:coarse) {
2631-
.c3[data-no-visuals="true"]:after {
2628+
.c3[data-no-visuals]:after {
26322629
content: "";
26332630
position: absolute;
26342631
left: 0;
@@ -2669,6 +2666,7 @@ exports[`TextInput renders trailingAction text button 1`] = `
26692666
data-block={null}
26702667
data-no-visuals={true}
26712668
onClick={[MockFunction]}
2669+
style={{}}
26722670
type="button"
26732671
>
26742672
<span
@@ -2910,6 +2908,16 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = `
29102908
29112909
.c4[data-no-visuals] {
29122910
color: #0969da;
2911+
padding-top: 2px;
2912+
padding-right: 4px;
2913+
padding-bottom: 2px;
2914+
padding-left: 4px;
2915+
position: relative;
2916+
}
2917+
2918+
.c4[data-no-visuals][data-component="IconButton"] {
2919+
width: var(--inner-action-size);
2920+
height: var(--inner-action-size);
29132921
}
29142922
29152923
.c4:has([data-component="ButtonCounter"]) {
@@ -2924,19 +2932,6 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = `
29242932
color: inherit;
29252933
}
29262934
2927-
.c4[data-no-visuals="true"] {
2928-
padding-top: 2px;
2929-
padding-right: 4px;
2930-
padding-bottom: 2px;
2931-
padding-left: 4px;
2932-
position: relative;
2933-
}
2934-
2935-
.c4[data-no-visuals="true"][data-component="IconButton"] {
2936-
width: var(--inner-action-size);
2937-
height: var(--inner-action-size);
2938-
}
2939-
29402935
.c0 {
29412936
font-size: 14px;
29422937
line-height: 20px;
@@ -3259,7 +3254,7 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = `
32593254
}
32603255
32613256
@media (pointer:coarse) {
3262-
.c4[data-no-visuals="true"]:after {
3257+
.c4[data-no-visuals]:after {
32633258
content: "";
32643259
position: absolute;
32653260
left: 0;
@@ -3305,6 +3300,7 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = `
33053300
data-block={null}
33063301
data-no-visuals={true}
33073302
onClick={[MockFunction]}
3303+
style={{}}
33083304
type="button"
33093305
>
33103306
<span

src/sx.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type BetterCssProperties = {
1414
}
1515

1616
// Support CSS custom properties in the `sx` prop
17-
type CSSCustomProperties = {
17+
export type CSSCustomProperties = {
1818
[key: `--${string}`]: string | number
1919
}
2020

0 commit comments

Comments
 (0)