Skip to content

Commit b319b29

Browse files
authored
Fixes onRemove console error coming from token components (#2087)
* prevents onRemove prop from being passed from token components though to the HTML element * adds changeset
1 parent 0530103 commit b319b29

File tree

5 files changed

+1734
-1899
lines changed

5 files changed

+1734
-1899
lines changed

.changeset/grumpy-crabs-compete.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': patch
3+
---
4+
5+
Prevents the `onRemove` prop from being passed through to the HTML element from Token, AvatarToken, and IssueToken.

src/Token/Token.tsx

Lines changed: 50 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, {forwardRef, MouseEventHandler} from 'react'
2-
import styled, {css} from 'styled-components'
3-
import {get} from '../constants'
4-
import sx, {SxProp} from '../sx'
2+
import {Box} from '..'
3+
import {merge, SxProp} from '../sx'
54
import TokenBase, {defaultTokenSize, isTokenInteractive, TokenBaseProps} from './TokenBase'
65
import RemoveTokenButton from './_RemoveTokenButton'
76
import TokenTextContainer from './_TokenTextContainer'
@@ -16,53 +15,33 @@ export interface TokenProps extends TokenBaseProps {
1615

1716
const tokenBorderWidthPx = 1
1817

19-
const DefaultTokenStyled = styled(TokenBase)<TokenProps & {isTokenInteractive: boolean} & SxProp>`
20-
background-color: ${get('colors.neutral.subtle')};
21-
border-color: ${props => (props.isSelected ? get('colors.fg.default') : get('colors.border.subtle'))};
22-
border-style: solid;
23-
border-width: ${tokenBorderWidthPx}px;
24-
color: ${props => (props.isSelected ? get('colors.fg.default') : get('colors.fg.muted'))};
25-
max-width: 100%;
26-
padding-right: ${props => (!props.hideRemoveButton ? 0 : undefined)};
27-
position: relative;
28-
${sx}
29-
30-
${props => {
31-
if (props.isTokenInteractive) {
32-
return css`
33-
&:hover {
34-
background-color: ${get('colors.neutral.muted')};
35-
box-shadow: ${get('colors.shadow.medium')};
36-
color: ${get('colors.fg.default')};
37-
}
38-
`
39-
}
40-
}}
41-
`
42-
43-
const LeadingVisualContainer = styled('span')<Pick<TokenBaseProps, 'size'>>`
44-
flex-shrink: 0;
45-
line-height: 0;
46-
47-
${props => {
48-
switch (props.size) {
49-
case 'large':
50-
case 'extralarge':
51-
case 'xlarge':
52-
return css`
53-
margin-right: ${get('space.2')};
54-
`
55-
default:
56-
return css`
57-
margin-right: ${get('space.1')};
58-
`
59-
}
60-
}}
61-
`
18+
const LeadingVisualContainer: React.FC<Pick<TokenBaseProps, 'size'>> = ({children, size}) => (
19+
<Box
20+
sx={{
21+
flexShrink: 0,
22+
lineHeight: 0,
23+
marginRight: size && ['large', 'extralarge', 'xlarge'].includes(size) ? 2 : 1
24+
}}
25+
>
26+
{children}
27+
</Box>
28+
)
6229

6330
const Token = forwardRef<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement, TokenProps & SxProp>(
6431
(props, forwardedRef) => {
65-
const {as, onRemove, id, leadingVisual: LeadingVisual, text, size, hideRemoveButton, href, onClick, ...rest} = props
32+
const {
33+
as,
34+
onRemove,
35+
id,
36+
leadingVisual: LeadingVisual,
37+
text,
38+
size,
39+
hideRemoveButton,
40+
href,
41+
onClick,
42+
sx: sxProp = {},
43+
...rest
44+
} = props
6645
const hasMultipleActionTargets = isTokenInteractive(props) && Boolean(onRemove) && !hideRemoveButton
6746
const onRemoveClick: MouseEventHandler = e => {
6847
e.stopPropagation()
@@ -73,15 +52,35 @@ const Token = forwardRef<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement
7352
href,
7453
onClick
7554
}
55+
const sx = merge(
56+
{
57+
backgroundColor: 'neutral.subtle',
58+
borderColor: props.isSelected ? 'fg.default' : 'border.subtle',
59+
borderStyle: 'solid',
60+
borderWidth: `${tokenBorderWidthPx}px`,
61+
color: props.isSelected ? 'fg.default' : 'fg.muted',
62+
maxWidth: '100%',
63+
paddingRight: !(hideRemoveButton || !onRemove) ? 0 : undefined,
64+
...(isTokenInteractive(props)
65+
? {
66+
'&:hover': {
67+
backgroundColor: 'neutral.muted',
68+
boxShadow: 'shadow.medium',
69+
color: 'fg.default'
70+
}
71+
}
72+
: {})
73+
},
74+
sxProp as SxProp
75+
)
7676

7777
return (
78-
<DefaultTokenStyled
78+
<TokenBase
7979
onRemove={onRemove}
80-
hideRemoveButton={hideRemoveButton || !onRemove}
8180
id={id?.toString()}
8281
text={text}
8382
size={size}
84-
isTokenInteractive={isTokenInteractive(props)}
83+
sx={sx}
8584
{...(!hasMultipleActionTargets ? interactiveTokenProps : {})}
8685
{...rest}
8786
ref={forwardedRef}
@@ -109,7 +108,7 @@ const Token = forwardRef<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement
109108
}
110109
/>
111110
) : null}
112-
</DefaultTokenStyled>
111+
</TokenBase>
113112
)
114113
}
115114
)

src/Token/TokenBase.tsx

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {KeyboardEvent} from 'react'
1+
import React, {KeyboardEvent} from 'react'
22
import styled from 'styled-components'
33
import {variant} from 'styled-system'
44
import {get} from '../constants'
@@ -50,7 +50,14 @@ export interface TokenBaseProps
5050
size?: TokenSizeKeys
5151
}
5252

53-
export const isTokenInteractive = ({as = 'span', onClick, onFocus, tabIndex = -1}: TokenBaseProps) =>
53+
type TokenElements = HTMLSpanElement | HTMLButtonElement | HTMLAnchorElement
54+
55+
export const isTokenInteractive = ({
56+
as = 'span',
57+
onClick,
58+
onFocus,
59+
tabIndex = -1
60+
}: Pick<TokenBaseProps, 'as' | 'onClick' | 'onFocus' | 'tabIndex'>) =>
5461
Boolean(onFocus || onClick || tabIndex > -1 || ['a', 'button'].includes(as))
5562

5663
const xlargeVariantStyles = {
@@ -112,28 +119,40 @@ const variants = variant<
112119
}
113120
})
114121

115-
const TokenBase = styled.span.attrs<TokenBaseProps>(({text, onRemove, onKeyDown}) => ({
116-
onKeyDown: (event: KeyboardEvent<HTMLSpanElement | HTMLButtonElement | HTMLAnchorElement>) => {
117-
onKeyDown && onKeyDown(event)
118-
119-
if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) {
120-
onRemove()
121-
}
122-
},
123-
'aria-label': onRemove ? `${text}, press backspace or delete to remove` : undefined
124-
}))<TokenBaseProps & SxProp>`
122+
const StyledTokenBase = styled.span<SxProp>`
125123
align-items: center;
126124
border-radius: 999px;
127125
cursor: ${props => (isTokenInteractive(props) ? 'pointer' : 'auto')};
128126
display: inline-flex;
129127
font-weight: ${get('fontWeights.bold')};
130128
font-family: inherit;
131129
text-decoration: none;
130+
position: relative;
132131
white-space: nowrap;
133132
${variants}
134133
${sx}
135134
`
136135

136+
const TokenBase = React.forwardRef<TokenElements, TokenBaseProps & SxProp>(
137+
({text, onRemove, onKeyDown, id, ...rest}, forwardedRef) => {
138+
return (
139+
<StyledTokenBase
140+
onKeyDown={(event: KeyboardEvent<TokenElements>) => {
141+
onKeyDown && onKeyDown(event)
142+
143+
if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) {
144+
onRemove()
145+
}
146+
}}
147+
aria-label={onRemove ? `${text}, press backspace or delete to remove` : undefined}
148+
id={id?.toString()}
149+
{...rest}
150+
ref={forwardedRef}
151+
/>
152+
)
153+
}
154+
)
155+
137156
TokenBase.defaultProps = {
138157
as: 'span',
139158
size: defaultTokenSize

0 commit comments

Comments
 (0)