Skip to content

Commit 03c0c59

Browse files
chore: use forwardedAs prop in styled-react (#6910)
Co-authored-by: LiuLiu <[email protected]>
1 parent a67ad8a commit 03c0c59

File tree

12 files changed

+123
-53
lines changed

12 files changed

+123
-53
lines changed

.changeset/afraid-eyes-serve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/styled-react": patch
3+
---
4+
5+
chore: use forwardedAs prop in styled-react

packages/styled-react/src/__tests__/primer-react-deprecated.browser.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('@primer/react/deprecated', () => {
2626

2727
test('TabNav.Link supports `sx` prop', () => {
2828
render(<TabNav.Link data-testid="component" sx={{background: 'red'}} as={Button} />)
29+
expect(screen.getByTestId('component')).toHaveAttribute('role', 'tab')
2930
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
3031
expect(window.getComputedStyle(screen.getByRole('tab')).backgroundColor).toBe('rgb(255, 0, 0)')
3132
expect(screen.getByRole('tab').tagName).toBe('BUTTON')

packages/styled-react/src/__tests__/primer-react-experimental.browser.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ describe('@primer/react/experimental', () => {
99
})
1010

1111
test('PageHeader supports `sx` prop', () => {
12-
const {container} = render(<PageHeader data-testid="component" sx={{background: 'red'}} />)
12+
const {container} = render(<PageHeader as="div" data-testid="component" sx={{background: 'red'}} role="article" />)
13+
expect(container.firstElementChild!).toHaveAttribute('role', 'article')
1314
expect(window.getComputedStyle(container.firstElementChild!).backgroundColor).toBe('rgb(255, 0, 0)')
1415
})
1516

packages/styled-react/src/__tests__/primer-react.browser.test.tsx

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ import {
4747

4848
describe('@primer/react', () => {
4949
test('ActionList supports `sx` prop', () => {
50-
render(<ActionList data-testid="component" sx={{background: 'red'}} />)
50+
render(<ActionList as="div" data-testid="component" sx={{background: 'red'}} variant="inset" />)
5151
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
52+
expect(screen.getByTestId('component')).toHaveAttribute('data-variant', 'inset')
5253
})
5354

5455
test('ActionMenu.Button supports `sx` prop', () => {
@@ -109,7 +110,7 @@ describe('@primer/react', () => {
109110
})
110111

111112
test('Box supports `sx` prop', () => {
112-
render(<Box data-testid="component" sx={{background: 'red'}} />)
113+
render(<Box as="div" data-testid="component" sx={{background: 'red'}} />)
113114
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
114115
})
115116

@@ -119,14 +120,15 @@ describe('@primer/react', () => {
119120
})
120121

121122
test('Breadcrumbs.Item supports `sx` prop', () => {
122-
render(<Breadcrumbs.Item data-testid="component" sx={{background: 'red'}} href="#" />)
123+
render(<Breadcrumbs.Item as="li" data-testid="component" sx={{background: 'red'}} selected />)
123124
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
124-
expect(window.getComputedStyle(screen.getByRole('link')).backgroundColor).toBe('rgb(255, 0, 0)')
125+
expect(screen.getByTestId('component').className.includes('selected')).toBe(true)
125126
})
126127

127128
test('Button supports `sx` prop', () => {
128-
render(<Button data-testid="component" sx={{background: 'red'}} />)
129+
render(<Button as="button" data-testid="component" sx={{background: 'red'}} size="medium" />)
129130
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
131+
expect(screen.getByTestId('component')).toHaveAttribute('data-size', 'medium')
130132
})
131133

132134
test('Checkbox supports `sx` prop', () => {
@@ -184,8 +186,9 @@ describe('@primer/react', () => {
184186
})
185187

186188
test('Flash supports `sx` prop', () => {
187-
render(<Flash data-testid="component" sx={{background: 'red'}} />)
189+
render(<Flash as="div" data-testid="component" sx={{background: 'red'}} variant="success" />)
188190
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
191+
expect(screen.getByTestId('component')).toHaveAttribute('variant', 'success')
189192
})
190193

191194
test('FormControl supports `sx` prop', () => {
@@ -198,7 +201,7 @@ describe('@primer/react', () => {
198201
})
199202

200203
test('Header supports `sx` prop', () => {
201-
render(<Header data-testid="component" sx={{background: 'red'}} />)
204+
render(<Header as="header" data-testid="component" sx={{background: 'red'}} />)
202205
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
203206
})
204207

@@ -208,23 +211,40 @@ describe('@primer/react', () => {
208211
})
209212

210213
test('IconButton supports `sx` prop', () => {
211-
render(<IconButton aria-label="test" data-testid="component" sx={{background: 'red'}} icon={() => <svg />} />)
214+
render(
215+
<IconButton
216+
as="button"
217+
aria-label="test"
218+
data-testid="component"
219+
sx={{background: 'red'}}
220+
icon={() => <svg />}
221+
/>,
222+
)
212223
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
224+
225+
// Test that IconButton renders the icon component (SVG) in its children
226+
const iconButton = screen.getByTestId('component')
227+
const svgElement = iconButton.querySelector('svg')
228+
expect(svgElement).toBeInTheDocument()
229+
expect(iconButton.children.length).toBeGreaterThan(0)
213230
})
214231

215232
test('Label supports `sx` prop', () => {
216-
render(<Label data-testid="component" sx={{background: 'red'}} />)
233+
render(<Label as="span" data-testid="component" sx={{background: 'red'}} size="large" />)
217234
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
235+
expect(screen.getByTestId('component')).toHaveAttribute('data-size', 'large')
218236
})
219237

220238
test('Link supports `sx` prop', () => {
221-
render(<Link data-testid="component" sx={{background: 'red'}} />)
239+
render(<Link as="a" data-testid="component" sx={{background: 'red'}} inline />)
240+
expect(screen.getByTestId('component')).toHaveAttribute('data-inline', 'true')
222241
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
223242
})
224243

225244
test('LinkButton supports `sx` prop', () => {
226-
render(<LinkButton data-testid="component" sx={{background: 'red'}} />)
245+
render(<LinkButton as="a" data-testid="component" sx={{background: 'red'}} icon={<svg />} />)
227246
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
247+
expect(screen.getByTestId('component')).toHaveAttribute('icon')
228248
})
229249

230250
test('NavList supports `sx` prop', () => {
@@ -286,19 +306,23 @@ describe('@primer/react', () => {
286306
render(
287307
<ThemeProvider>
288308
<Overlay
309+
as="div"
289310
data-testid="component"
290311
sx={{background: 'red'}}
291312
onClickOutside={() => {}}
292313
onEscape={() => {}}
293314
returnFocusRef={ref}
315+
role="dialog"
294316
/>
295317
</ThemeProvider>,
296318
)
297319
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
320+
expect(screen.getByTestId('component')).toHaveAttribute('role', 'dialog')
298321
})
299322

300323
test('PageHeader supports `sx` prop', () => {
301-
const {container} = render(<PageHeader data-testid="component" sx={{background: 'red'}} />)
324+
const {container} = render(<PageHeader as="div" data-testid="component" sx={{background: 'red'}} role="article" />)
325+
expect(container.firstElementChild!).toHaveAttribute('role', 'article')
302326
expect(window.getComputedStyle(container.firstElementChild!).backgroundColor).toBe('rgb(255, 0, 0)')
303327
})
304328

@@ -323,8 +347,13 @@ describe('@primer/react', () => {
323347
})
324348

325349
test('PageLayout.Content supports `sx` prop', () => {
326-
const {container} = render(<PageLayout.Content data-testid="component" sx={{background: 'red'}} />)
327-
expect(window.getComputedStyle(container.firstElementChild!).backgroundColor).toBe('rgb(255, 0, 0)')
350+
const {container} = render(
351+
<PageLayout.Content as="section" data-testid="component" sx={{background: 'red'}} aria-labelledby="normal" />,
352+
)
353+
354+
const outerElement = container.firstElementChild! as HTMLElement
355+
expect(window.getComputedStyle(outerElement).backgroundColor).toBe('rgb(255, 0, 0)')
356+
expect(outerElement).toHaveAttribute('aria-labelledby', 'normal')
328357
})
329358

330359
test('PageLayout.Pane supports `sx` prop', () => {
@@ -386,8 +415,9 @@ describe('@primer/react', () => {
386415
})
387416

388417
test.skip('Select supports `sx` prop', () => {
389-
render(<Select data-testid="component" sx={{background: 'red'}} />)
418+
render(<Select as="select" data-testid="component" sx={{background: 'red'}} required />)
390419
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
420+
expect(screen.getByTestId('component')).toHaveAttribute('required')
391421
})
392422

393423
test('Spinner supports `sx` prop', () => {
@@ -411,13 +441,15 @@ describe('@primer/react', () => {
411441
})
412442

413443
test('Text supports `sx` prop', () => {
414-
render(<Text data-testid="component" sx={{background: 'red'}} />)
444+
render(<Text as="span" data-testid="component" sx={{background: 'red'}} size="small" />)
415445
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
446+
expect(screen.getByTestId('component')).toHaveAttribute('data-size', 'small')
416447
})
417448

418449
test('TextInput supports `sx` prop', () => {
419-
const {container} = render(<TextInput sx={{background: 'red'}} />)
450+
const {container} = render(<TextInput as="input" sx={{background: 'red'}} loading />)
420451
expect(window.getComputedStyle(container.firstElementChild!).backgroundColor).toBe('rgb(255, 0, 0)')
452+
expect(container.firstElementChild).toHaveAttribute('data-trailing-visual', 'true')
421453
})
422454

423455
test('TextInput.Action supports `sx` prop', () => {
@@ -456,8 +488,9 @@ describe('@primer/react', () => {
456488
})
457489

458490
test('Token supports `sx` prop', () => {
459-
render(<Token data-testid="component" sx={{background: 'red'}} text="test" />)
491+
render(<Token as="button" data-testid="component" sx={{background: 'red'}} text="test" />)
460492
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
493+
expect(screen.getByTestId('component')).toHaveTextContent('test')
461494
})
462495

463496
test.todo('Tooltip supports `sx` prop', () => {
@@ -470,25 +503,29 @@ describe('@primer/react', () => {
470503
})
471504

472505
test('Truncate supports `sx` prop', () => {
473-
render(<Truncate data-testid="component" sx={{background: 'red'}} title="test" />)
506+
render(<Truncate as="div" data-testid="component" sx={{background: 'red'}} title="test" />)
474507
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
508+
expect(screen.getByTestId('component')).toHaveAttribute('title', 'test')
475509
})
476510

477511
test('UnderlineNav supports `sx` prop', () => {
478512
render(
479-
<UnderlineNav aria-label="navigation" data-testid="component" sx={{background: 'red'}}>
513+
<UnderlineNav as="nav" aria-label="navigation" data-testid="component" sx={{background: 'red'}} variant="inset">
480514
<UnderlineNav.Item>test</UnderlineNav.Item>
481515
</UnderlineNav>,
482516
)
483517
expect(window.getComputedStyle(screen.getByLabelText('navigation')).backgroundColor).toBe('rgb(255, 0, 0)')
518+
expect(screen.getByLabelText('navigation')).toHaveAttribute('data-variant', 'inset')
484519
})
485520

486521
test('UnderlineNav.Item supports `sx` prop', () => {
487522
render(
488-
<UnderlineNav.Item data-testid="component" sx={{background: 'red'}}>
523+
<UnderlineNav.Item as="a" data-testid="component" sx={{background: 'red'}} icon={<svg />}>
489524
test
490525
</UnderlineNav.Item>,
491526
)
492527
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
528+
const svgElement = screen.getByTestId('component').querySelector('svg')
529+
expect(svgElement).toBeInTheDocument()
493530
})
494531
})

packages/styled-react/src/components/Breadcrumbs.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ const StyledBreadcrumbsItem: ForwardRefComponent<'a', BreadcrumbsItemProps> = st
2525
${sx}
2626
`
2727

28-
const BreadcrumbsItem = ({as, ...props}: BreadcrumbsItemProps) => (
29-
<StyledBreadcrumbsItem {...props} {...(as ? {forwardedAs: as} : {})} />
30-
)
28+
function BreadcrumbsItem<As extends React.ElementType = 'a'>({as, ...props}: BreadcrumbsItemProps<As>) {
29+
return <StyledBreadcrumbsItem {...props} {...(as ? {forwardedAs: as} : {})} />
30+
}
3131

3232
const Breadcrumbs: ForwardRefComponent<'nav', BreadcrumbsProps> & {Item: typeof BreadcrumbsItem} = Object.assign(
3333
BreadcrumbsImpl,

packages/styled-react/src/components/Header.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type HeaderItemProps as PrimerHeaderItemProps,
44
type HeaderLinkProps as PrimerHeaderLinkProps,
55
Header as PrimerHeader,
6+
type HeaderLinkProps,
67
} from '@primer/react'
78
import {forwardRef} from 'react'
89
import {Box} from './Box'
@@ -11,20 +12,28 @@ import type {SxProp} from '../sx'
1112

1213
type HeaderProps = PrimerHeaderProps & SxProp
1314

14-
const HeaderImpl = forwardRef(function Header(props, ref) {
15+
const StyledHeader = forwardRef(function Header(props, ref) {
1516
return <Box as={PrimerHeader} ref={ref} {...props} />
1617
}) as ForwardRefComponent<'header', HeaderProps>
1718

19+
const HeaderImpl = forwardRef(({as, ...props}: HeaderProps, ref) => (
20+
<StyledHeader {...props} {...(as ? {forwardedAs: as} : {})} ref={ref} />
21+
)) as ForwardRefComponent<'header', HeaderProps>
22+
1823
type HeaderItemProps = PrimerHeaderItemProps & SxProp
1924

2025
const HeaderItem = forwardRef<HTMLDivElement, HeaderItemProps>(function HeaderItem(props, ref) {
2126
return <Box as={PrimerHeader.Item} ref={ref} {...props} />
2227
})
2328

24-
const HeaderLink = forwardRef<HTMLAnchorElement, PrimerHeaderLinkProps>(function HeaderLink(props, ref) {
29+
const StyledHeaderLink = forwardRef<HTMLAnchorElement, PrimerHeaderLinkProps>(function HeaderLink(props, ref) {
2530
return <Box as={PrimerHeader.Link} ref={ref} {...props} />
2631
})
2732

33+
const HeaderLink = forwardRef<HTMLAnchorElement, HeaderLinkProps>(({as, ...props}, ref) => (
34+
<StyledHeaderLink {...props} {...(as ? {forwardedAs: as} : {})} ref={ref} />
35+
))
36+
2837
const Header = Object.assign(HeaderImpl, {
2938
Item: HeaderItem,
3039
Link: HeaderLink,

packages/styled-react/src/components/Label.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import {type SxProp} from '../sx'
33
import {forwardRef} from 'react'
44
import type {ForwardRefComponent} from '../polymorphic'
55

6-
type LabelProps = PrimerLabelProps & SxProp
6+
type LabelProps = PrimerLabelProps & SxProp & {as?: React.ElementType}
77

8-
const Label = forwardRef(function Label(props, ref) {
8+
const StyledLabel = forwardRef(function Label(props, ref) {
99
return <Box as={PrimerLabel} ref={ref} {...props} />
1010
}) as ForwardRefComponent<'span', LabelProps>
1111

12+
const Label = forwardRef<HTMLElement, LabelProps>(({as, ...props}, ref) => {
13+
return <StyledLabel {...props} {...(as ? {forwardedAs: as} : {})} ref={ref} />
14+
}) as ForwardRefComponent<'span', LabelProps>
15+
1216
export {Label, type LabelProps}
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import {Link as PrimerLink, type LinkProps as PrimerLinkProps} from '@primer/react'
22
import styled from 'styled-components'
33
import {sx, type SxProp} from '../sx'
4+
import type {ForwardRefComponent} from '../polymorphic'
5+
import {forwardRef} from 'react'
46

57
type LinkProps = PrimerLinkProps & SxProp
68

7-
const Link = styled(PrimerLink).withConfig<LinkProps>({
9+
const StyledLink = styled(PrimerLink).withConfig<LinkProps>({
810
shouldForwardProp: prop => prop !== 'sx',
911
})`
1012
${sx}
11-
`
13+
` as ForwardRefComponent<'a', LinkProps>
14+
15+
const Link = forwardRef<HTMLAnchorElement, LinkProps>(({as, ...props}, ref) => {
16+
return <StyledLink {...props} {...(as ? {forwardedAs: as} : {})} ref={ref} />
17+
}) as ForwardRefComponent<'a', LinkProps>
18+
1219
export {Link, type LinkProps}

packages/styled-react/src/components/PageHeader.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,22 @@ import {sx, type SxProp} from '../sx'
1010
import type {ForwardRefComponent} from '../polymorphic'
1111
import {Box} from './Box'
1212
import type {PropsWithChildren} from 'react'
13+
import React from 'react'
1314

1415
type PageHeaderProps = PrimerPageHeaderProps & SxProp
1516

16-
const PageHeaderImpl: ForwardRefComponent<'div', PageHeaderProps> = styled(
17+
const StyledPageHeader: ForwardRefComponent<'div', PageHeaderProps> = styled(
1718
PrimerPageHeader,
1819
).withConfig<PageHeaderProps>({
1920
shouldForwardProp: prop => prop !== 'sx',
2021
})`
2122
${sx}
2223
`
2324

25+
const PageHeaderImpl = React.forwardRef<HTMLDivElement, PageHeaderProps>(({as, ...props}, ref) => (
26+
<StyledPageHeader {...props} {...(as ? {forwardedAs: as} : {})} ref={ref} />
27+
)) as ForwardRefComponent<'div', PageHeaderProps>
28+
2429
type PageHeaderActionsProps = PrimerPageHeaderActionsProps & SxProp
2530

2631
function PageHeaderActions({sx, ...rest}: PageHeaderActionsProps) {
@@ -43,7 +48,7 @@ type CSSCustomProperties = {
4348
[key: `--${string}`]: string | number
4449
}
4550

46-
function PageHeaderTitle({sx, ...rest}: PageHeaderTitleProps) {
51+
function StyledPageHeaderTitle({sx, ...rest}: PageHeaderTitleProps) {
4752
const style: CSSCustomProperties = {}
4853
if (sx) {
4954
// @ts-ignore sx can have color attribute
@@ -65,6 +70,10 @@ function PageHeaderTitle({sx, ...rest}: PageHeaderTitleProps) {
6570
return <Box {...rest} as={PrimerPageHeader.Title} style={style} sx={sx} />
6671
}
6772

73+
const PageHeaderTitle = ({as, ...props}: PageHeaderTitleProps) => (
74+
<StyledPageHeaderTitle {...props} {...(as ? {forwardedAs: as} : {})} />
75+
)
76+
6877
type PageHeaderTitleAreaProps = PropsWithChildren<PrimerPageHeaderTitleAreaProps> & SxProp
6978

7079
const PageHeaderTitleArea: ForwardRefComponent<'div', PageHeaderTitleAreaProps> = styled(
@@ -75,7 +84,7 @@ const PageHeaderTitleArea: ForwardRefComponent<'div', PageHeaderTitleAreaProps>
7584
${sx}
7685
`
7786

78-
type PageHeaderComponent = ForwardRefComponent<'div', PageHeaderProps> & {
87+
type PageHeaderComponentType = ForwardRefComponent<'div', PageHeaderProps> & {
7988
Actions: typeof PageHeaderActions
8089
ContextArea: typeof PrimerPageHeader.ContextArea
8190
ParentLink: typeof PrimerPageHeader.ParentLink
@@ -91,7 +100,7 @@ type PageHeaderComponent = ForwardRefComponent<'div', PageHeaderProps> & {
91100
TrailingAction: typeof PrimerPageHeader.TrailingAction
92101
}
93102

94-
const PageHeader: PageHeaderComponent = Object.assign(PageHeaderImpl, {
103+
const PageHeader: PageHeaderComponentType = Object.assign(PageHeaderImpl, {
95104
Actions: PageHeaderActions,
96105
ContextArea: PrimerPageHeader.ContextArea,
97106
ParentLink: PrimerPageHeader.ParentLink,

0 commit comments

Comments
 (0)