Skip to content

Commit 05db651

Browse files
authoredDec 4, 2024··
feat(Token): Migrate to CSS modules behind feature flag Pt 2 (#5271)
* update token and token base to css modules * add changeset and update snapshot * fix css * properly merge in style prop * key sx and style prop correctly off of feature flag * update snapshot * update avatartoken * update IssueLabelToken to css modules * fix remove btn logic * fix render logic * update snapshots * add missing title attribute * remove unneeded prop * small refactor * refactor * remove :where to add specificity to a css rule * remove accidental check in * remove unneeded css * re-add CSS
1 parent 39df71e commit 05db651

9 files changed

+310
-32
lines changed
 

‎.changeset/slow-spoons-peel.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/react": minor
3+
---
4+
5+
Update `Token`, `IssueLabelToken`, `AvatarToken` components to use CSS Modules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
:root {
2+
--spacing: calc(var(--base-size-4) * 2);
3+
}
4+
5+
.AvatarContainer {
6+
display: block;
7+
}
8+
9+
.Avatar {
10+
width: 100%;
11+
height: 100%;
12+
}
13+
14+
.Token {
15+
padding-left: var(--base-size-4) !important;
16+
}
17+
18+
.AvatarContainer:where([data-size='small']) {
19+
width: calc(16px - var(--spacing));
20+
height: calc(16px - var(--spacing));
21+
}
22+
23+
.AvatarContainer:where([data-size='medium']) {
24+
width: calc(20px - var(--spacing));
25+
height: calc(20px - var(--spacing));
26+
}
27+
28+
.AvatarContainer:where([data-size='large']) {
29+
width: calc(24px - var(--spacing));
30+
height: calc(24px - var(--spacing));
31+
}
32+
33+
.AvatarContainer:where([data-size='xlarge']) {
34+
width: calc(32px - var(--spacing));
35+
height: calc(32px - var(--spacing));
36+
}

‎packages/react/src/Token/AvatarToken.tsx

+37-9
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,51 @@ import {defaultTokenSize, tokenSizes} from './TokenBase'
66
import Token from './Token'
77
import Avatar from '../Avatar'
88
import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
9+
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
10+
import {useFeatureFlag} from '../FeatureFlags'
11+
import classes from './AvatarToken.module.css'
12+
import {clsx} from 'clsx'
913

1014
// TODO: update props to only accept 'large' and 'xlarge' on the next breaking change
1115
export interface AvatarTokenProps extends TokenBaseProps {
1216
avatarSrc: string
1317
}
1418

15-
const AvatarContainer = styled.span<{avatarSize: TokenSizeKeys}>`
16-
// 'space.1' is used because to match space from the left of the token to the left of the avatar
17-
// '* 2' is done to account for the top and bottom
18-
--spacing: calc(${get('space.1')} * 2);
19+
const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team'
1920

20-
display: block;
21-
height: ${props => `calc(${tokenSizes[props.avatarSize]} - var(--spacing))`};
22-
width: ${props => `calc(${tokenSizes[props.avatarSize]} - var(--spacing))`};
23-
`
21+
const AvatarContainer = toggleStyledComponent(
22+
CSS_MODULES_FEATURE_FLAG,
23+
'span',
24+
styled.span<{avatarSize: TokenSizeKeys}>`
25+
// 'space.1' is used because to match space from the left of the token to the left of the avatar
26+
// '* 2' is done to account for the top and bottom
27+
--spacing: calc(${get('space.1')} * 2);
28+
29+
display: block;
30+
height: ${props => `calc(${tokenSizes[props.avatarSize]} - var(--spacing))`};
31+
width: ${props => `calc(${tokenSizes[props.avatarSize]} - var(--spacing))`};
32+
`,
33+
)
34+
35+
const AvatarToken = forwardRef(({avatarSrc, id, size = defaultTokenSize, className, ...rest}, forwardedRef) => {
36+
const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)
37+
if (enabled) {
38+
return (
39+
<Token
40+
leadingVisual={() => (
41+
<AvatarContainer avatarSize={size} className={classes.AvatarContainer} data-size={size}>
42+
<Avatar src={avatarSrc} size={parseInt(tokenSizes[size], 10)} className={classes.Avatar} />
43+
</AvatarContainer>
44+
)}
45+
size={size}
46+
id={id?.toString()}
47+
className={clsx(classes.Token, className)}
48+
{...rest}
49+
ref={forwardedRef}
50+
/>
51+
)
52+
}
2453

25-
const AvatarToken = forwardRef(({avatarSrc, id, size = defaultTokenSize, ...rest}, forwardedRef) => {
2654
return (
2755
<Token
2856
leadingVisual={() => (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.IssueLabel:where([data-has-remove-button='true']) {
2+
padding-right: 0;
3+
}
4+
5+
.RemoveButton:where([data-has-multiple-action-targets='true']) {
6+
position: relative;
7+
z-index: 1;
8+
}

‎packages/react/src/Token/IssueLabelToken.tsx

+39
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import {parseToHsla, parseToRgba} from 'color2k'
88
import {useTheme} from '../ThemeProvider'
99
import TokenTextContainer from './_TokenTextContainer'
1010
import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
11+
import classes from './IssueLabelToken.module.css'
12+
import {useFeatureFlag} from '../FeatureFlags'
13+
import {clsx} from 'clsx'
1114

1215
export interface IssueLabelTokenProps extends TokenBaseProps {
1316
/**
@@ -16,6 +19,7 @@ export interface IssueLabelTokenProps extends TokenBaseProps {
1619
fillColor?: string
1720
}
1821

22+
const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team'
1923
const tokenBorderWidthPx = 1
2024

2125
const lightModeStyles = {
@@ -43,6 +47,8 @@ const darkModeStyles = {
4347
}
4448

4549
const IssueLabelToken = forwardRef((props, forwardedRef) => {
50+
const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)
51+
4652
const {
4753
as,
4854
fillColor = '#999',
@@ -54,13 +60,15 @@ const IssueLabelToken = forwardRef((props, forwardedRef) => {
5460
hideRemoveButton,
5561
href,
5662
onClick,
63+
className,
5764
...rest
5865
} = props
5966
const interactiveTokenProps = {
6067
as,
6168
href,
6269
onClick,
6370
}
71+
6472
const {resolvedColorScheme} = useTheme()
6573
const hasMultipleActionTargets = isTokenInteractive(props) && Boolean(onRemove) && !hideRemoveButton
6674
const onRemoveClick: MouseEventHandler = e => {
@@ -133,6 +141,37 @@ const IssueLabelToken = forwardRef((props, forwardedRef) => {
133141
}
134142
}, [fillColor, resolvedColorScheme, hideRemoveButton, onRemove, isSelected, props])
135143

144+
if (enabled) {
145+
return (
146+
<TokenBase
147+
onRemove={onRemove}
148+
id={id?.toString()}
149+
isSelected={isSelected}
150+
className={clsx(classes.IssueLabel, className)}
151+
text={text}
152+
size={size}
153+
style={labelStyles}
154+
data-has-remove-button={!hideRemoveButton && !!onRemove}
155+
{...(!hasMultipleActionTargets ? interactiveTokenProps : {})}
156+
{...rest}
157+
ref={forwardedRef}
158+
>
159+
<TokenTextContainer {...(hasMultipleActionTargets ? interactiveTokenProps : {})}>{text}</TokenTextContainer>
160+
{!hideRemoveButton && onRemove ? (
161+
<RemoveTokenButton
162+
borderOffset={tokenBorderWidthPx}
163+
onClick={onRemoveClick}
164+
size={size}
165+
aria-hidden={hasMultipleActionTargets ? 'true' : 'false'}
166+
isParentInteractive={isTokenInteractive(props)}
167+
data-has-multiple-action-targets={hasMultipleActionTargets}
168+
className={classes.RemoveButton}
169+
/>
170+
) : null}
171+
</TokenBase>
172+
)
173+
}
174+
136175
return (
137176
<TokenBase
138177
onRemove={onRemove}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.Token {
2+
max-width: 100%;
3+
color: var(--fgColor-muted);
4+
background-color: var(--bgColor-neutral-muted);
5+
border-color: var(--borderColor-muted);
6+
border-style: solid;
7+
}
8+
9+
.Token:where([data-interactive='true']):hover {
10+
color: var(--fgColor-default);
11+
background-color: var(--bgColor-neutral-muted);
12+
box-shadow: var(--shadow-resting-medium);
13+
}
14+
15+
.Token:where([data-is-selected='true']) {
16+
color: var(--fgColor-default);
17+
}
18+
19+
.Token[data-is-remove-btn='true'] {
20+
padding-right: 0;
21+
}

‎packages/react/src/Token/Token.tsx

+54-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import type {MouseEventHandler} from 'react'
22
import React, {forwardRef} from 'react'
33
import Box from '../Box'
4-
import type {BetterSystemStyleObject, SxProp} from '../sx'
5-
import {merge} from '../sx'
4+
import {merge, type BetterSystemStyleObject, type SxProp} from '../sx'
65
import {defaultSxProp} from '../utils/defaultSxProp'
76
import type {TokenBaseProps} from './TokenBase'
87
import TokenBase, {defaultTokenSize, isTokenInteractive} from './TokenBase'
98
import RemoveTokenButton from './_RemoveTokenButton'
109
import TokenTextContainer from './_TokenTextContainer'
1110
import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
1211
import VisuallyHidden from '../_VisuallyHidden'
12+
import {useFeatureFlag} from '../FeatureFlags'
13+
14+
import classes from './Token.module.css'
15+
import {clsx} from 'clsx'
1316

1417
// Omitting onResize and onResizeCapture because seems like React 18 types includes these menthod in the expansion but React 17 doesn't.
1518
// TODO: This is a temporary solution until we figure out why these methods are causing type errors.
@@ -20,6 +23,8 @@ export interface TokenProps extends TokenBaseProps, SxProp {
2023
leadingVisual?: React.ElementType
2124
}
2225

26+
const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team'
27+
2328
const tokenBorderWidthPx = 1
2429

2530
const LeadingVisualContainer: React.FC<React.PropsWithChildren<Pick<TokenBaseProps, 'size'>>> = ({children, size}) => (
@@ -35,6 +40,8 @@ const LeadingVisualContainer: React.FC<React.PropsWithChildren<Pick<TokenBasePro
3540
)
3641

3742
const Token = forwardRef((props, forwardedRef) => {
43+
const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)
44+
3845
const {
3946
as,
4047
onRemove,
@@ -46,6 +53,8 @@ const Token = forwardRef((props, forwardedRef) => {
4653
href,
4754
onClick,
4855
sx: sxProp = defaultSxProp,
56+
className,
57+
style,
4958
...rest
5059
} = props
5160
const hasMultipleActionTargets = isTokenInteractive(props) && Boolean(onRemove) && !hideRemoveButton
@@ -58,7 +67,8 @@ const Token = forwardRef((props, forwardedRef) => {
5867
href,
5968
onClick,
6069
}
61-
const sx = merge<BetterSystemStyleObject>(
70+
71+
const mergedSx = merge<BetterSystemStyleObject>(
6272
{
6373
backgroundColor: 'neutral.subtle',
6474
borderColor: props.isSelected ? 'fg.default' : 'border.subtle',
@@ -80,16 +90,56 @@ const Token = forwardRef((props, forwardedRef) => {
8090
sxProp,
8191
)
8292

93+
if (enabled) {
94+
return (
95+
<TokenBase
96+
onRemove={onRemove}
97+
id={id?.toString()}
98+
className={clsx(className, classes.Token)}
99+
text={text}
100+
size={size}
101+
sx={sxProp}
102+
data-is-selected={props.isSelected}
103+
data-is-remove-btn={!(hideRemoveButton || !onRemove)}
104+
{...(!hasMultipleActionTargets ? interactiveTokenProps : {})}
105+
{...rest}
106+
ref={forwardedRef}
107+
style={{borderWidth: `${tokenBorderWidthPx}px`, ...style}}
108+
>
109+
{LeadingVisual ? (
110+
<LeadingVisualContainer size={size}>
111+
<LeadingVisual />
112+
</LeadingVisualContainer>
113+
) : null}
114+
<TokenTextContainer {...(hasMultipleActionTargets ? interactiveTokenProps : {})}>
115+
{text}
116+
{onRemove && <VisuallyHidden> (press backspace or delete to remove)</VisuallyHidden>}
117+
</TokenTextContainer>
118+
119+
{!hideRemoveButton && onRemove ? (
120+
<RemoveTokenButton
121+
borderOffset={tokenBorderWidthPx}
122+
onClick={onRemoveClick}
123+
size={size}
124+
isParentInteractive={isTokenInteractive(props)}
125+
aria-hidden={hasMultipleActionTargets ? 'true' : 'false'}
126+
/>
127+
) : null}
128+
</TokenBase>
129+
)
130+
}
131+
83132
return (
84133
<TokenBase
85134
onRemove={onRemove}
86135
id={id?.toString()}
87136
text={text}
88137
size={size}
89-
sx={sx}
138+
sx={mergedSx}
90139
{...(!hasMultipleActionTargets ? interactiveTokenProps : {})}
91140
{...rest}
92141
ref={forwardedRef}
142+
style={style}
93143
>
94144
{LeadingVisual ? (
95145
<LeadingVisualContainer size={size}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
.TokenBase {
2+
position: relative;
3+
display: inline-flex;
4+
font-family: inherit;
5+
font-weight: var(--base-text-weight-semibold);
6+
text-decoration: none;
7+
white-space: nowrap;
8+
border-radius: var(--borderRadius-full);
9+
align-items: center;
10+
}
11+
12+
.TokenBase:where([data-cursor-is-interactive='true']) {
13+
cursor: pointer;
14+
}
15+
16+
.TokenBase:where([data-cursor-is-interactive='false']) {
17+
cursor: auto;
18+
}
19+
20+
.TokenBase:where([data-size='small']) {
21+
width: auto;
22+
height: 16px;
23+
padding-right: var(--base-size-4);
24+
padding-left: var(--base-size-4);
25+
font-size: var(--text-body-size-small);
26+
/* stylelint-disable-next-line primer/typography */
27+
line-height: 16px;
28+
}
29+
30+
.TokenBase:where([data-size='medium']) {
31+
width: auto;
32+
height: 20px;
33+
padding-right: var(--base-size-8);
34+
padding-left: var(--base-size-8);
35+
font-size: var(--text-body-size-small);
36+
/* stylelint-disable-next-line primer/typography */
37+
line-height: 20px;
38+
}
39+
40+
.TokenBase[data-size='large'] {
41+
width: auto;
42+
height: 24px;
43+
padding-right: var(--base-size-8);
44+
padding-left: var(--base-size-8);
45+
font-size: var(--text-body-size-small);
46+
/* stylelint-disable-next-line primer/typography */
47+
line-height: 24px;
48+
}
49+
50+
.TokenBase[data-size='xlarge'] {
51+
width: auto;
52+
height: 32px;
53+
padding-top: 0;
54+
padding-right: var(--base-size-16);
55+
padding-bottom: 0;
56+
padding-left: var(--base-size-16);
57+
font-size: var(--text-body-size-medium);
58+
/* stylelint-disable-next-line primer/typography */
59+
line-height: 32px;
60+
}

‎packages/react/src/Token/TokenBase.tsx

+50-19
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ import type {ComponentProps, KeyboardEvent} from 'react'
22
import React from 'react'
33
import styled from 'styled-components'
44
import {variant} from 'styled-system'
5+
import {clsx} from 'clsx'
56
import {get} from '../constants'
67
import type {SxProp} from '../sx'
78
import sx from '../sx'
89
import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
10+
import {useFeatureFlag} from '../FeatureFlags'
11+
import classes from './TokenBase.module.css'
12+
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
913

1014
export type TokenSizeKeys = 'small' | 'medium' | 'large' | 'xlarge'
1115

@@ -112,26 +116,54 @@ const variants = variant<
112116
},
113117
})
114118

115-
const StyledTokenBase = styled.span<
116-
{
117-
size?: TokenSizeKeys
118-
} & SxProp
119-
>`
120-
align-items: center;
121-
border-radius: 999px;
122-
cursor: ${props => (isTokenInteractive(props) ? 'pointer' : 'auto')};
123-
display: inline-flex;
124-
font-weight: ${get('fontWeights.bold')};
125-
font-family: inherit;
126-
text-decoration: none;
127-
position: relative;
128-
white-space: nowrap;
129-
${variants}
130-
${sx}
131-
`
119+
const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team'
120+
121+
const StyledTokenBase = toggleStyledComponent(
122+
CSS_MODULES_FEATURE_FLAG,
123+
'span',
124+
styled.span<
125+
{
126+
size?: TokenSizeKeys
127+
} & SxProp
128+
>`
129+
align-items: center;
130+
border-radius: 999px;
131+
cursor: ${props => (isTokenInteractive(props) ? 'pointer' : 'auto')};
132+
display: inline-flex;
133+
font-weight: ${get('fontWeights.bold')};
134+
font-family: inherit;
135+
text-decoration: none;
136+
position: relative;
137+
white-space: nowrap;
138+
${variants}
139+
${sx}
140+
`,
141+
)
132142

133143
const TokenBase = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | HTMLSpanElement | undefined, TokenBaseProps>(
134-
({onRemove, onKeyDown, id, size = defaultTokenSize, ...rest}, forwardedRef) => {
144+
({onRemove, onKeyDown, id, className, size = defaultTokenSize, ...rest}, forwardedRef) => {
145+
const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)
146+
147+
if (enabled) {
148+
return (
149+
<StyledTokenBase
150+
onKeyDown={(event: KeyboardEvent<HTMLSpanElement & HTMLAnchorElement & HTMLButtonElement>) => {
151+
onKeyDown && onKeyDown(event)
152+
153+
if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) {
154+
onRemove()
155+
}
156+
}}
157+
className={clsx(classes.TokenBase, className)}
158+
data-cursor-is-interactive={isTokenInteractive(rest)}
159+
data-size={size}
160+
id={id?.toString()}
161+
{...rest}
162+
ref={forwardedRef}
163+
/>
164+
)
165+
}
166+
135167
return (
136168
<StyledTokenBase
137169
onKeyDown={(event: KeyboardEvent<HTMLSpanElement & HTMLAnchorElement & HTMLButtonElement>) => {
@@ -144,7 +176,6 @@ const TokenBase = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | HTMLS
144176
id={id?.toString()}
145177
size={size}
146178
{...rest}
147-
// @ts-expect-error TokenBase wants Anchor, Button, and Span refs
148179
ref={forwardedRef}
149180
/>
150181
)

0 commit comments

Comments
 (0)
Please sign in to comment.