Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a3c1fb9
use forwardedAs prop in components
francinelucca Sep 26, 2025
cf445d8
type fix
francinelucca Sep 26, 2025
3872af0
Merge branch 'main' into chore/fix-forward-as-prop
francinelucca Sep 26, 2025
3f72067
Merge branch 'main' into chore/fix-forward-as-prop
francinelucca Sep 29, 2025
7ffaa55
Merge branch 'main' of github.com:primer/react into chore/fix-forward…
francinelucca Sep 29, 2025
1aae62b
fix ts error
francinelucca Sep 29, 2025
2cfa66a
prop type fix
francinelucca Sep 29, 2025
0eebd23
Merge branch 'main' into chore/fix-forward-as-prop
francinelucca Sep 29, 2025
3e993d9
path of least resistance
francinelucca Sep 29, 2025
b52988f
Merge branch 'chore/fix-forward-as-prop' of github.com:primer/react i…
francinelucca Sep 29, 2025
a5ee699
wrong proptpye
francinelucca Sep 29, 2025
b4ed960
add forwardedAs to link
francinelucca Sep 29, 2025
30ffc4e
Add browser tests for polymorphic `as` prop behavior in styled-react …
liuliu-dev Sep 29, 2025
89bffe4
Merge branch 'main' into chore/fix-forward-as-prop
francinelucca Sep 29, 2025
c5eeed3
fix reference
francinelucca Sep 29, 2025
5778836
Merge branch 'chore/fix-forward-as-prop' of github.com:primer/react i…
francinelucca Sep 29, 2025
5fb9a41
test
francinelucca Sep 29, 2025
bbc7f1e
Merge branch 'main' into chore/fix-forward-as-prop
francinelucca Sep 29, 2025
3a439a0
fix import
francinelucca Sep 29, 2025
d42cdb0
Merge branch 'chore/fix-forward-as-prop' of github.com:primer/react i…
francinelucca Sep 29, 2025
a88a5bf
fix types...again
francinelucca Sep 30, 2025
c6b6073
Merge branch 'main' into chore/fix-forward-as-prop
francinelucca Sep 30, 2025
9c17ca5
fix types...again
francinelucca Sep 30, 2025
4db0db3
fix types...again
francinelucca Sep 30, 2025
af45d58
fix types
francinelucca Sep 30, 2025
eda8fd2
fix types
francinelucca Sep 30, 2025
51c281f
remove unused var
francinelucca Sep 30, 2025
d3fbd96
eslint fix
francinelucca Sep 30, 2025
1527829
Merge branch 'main' of github.com:primer/react into chore/fix-forward…
francinelucca Sep 30, 2025
e0b79ec
remove assertion
francinelucca Sep 30, 2025
11bc90a
type fix
francinelucca Sep 30, 2025
7ac11a7
as extends a
liuliu-dev Sep 30, 2025
6cbe54b
conflict
liuliu-dev Sep 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/afraid-eyes-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/styled-react": patch
---

chore: use forwardedAs prop in styled-react
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('@primer/react/deprecated', () => {

test('TabNav.Link supports `sx` prop', () => {
render(<TabNav.Link data-testid="component" sx={{background: 'red'}} as={Button} />)
expect(screen.getByTestId('component')).toHaveAttribute('role', 'tab')
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
expect(window.getComputedStyle(screen.getByRole('tab')).backgroundColor).toBe('rgb(255, 0, 0)')
expect(screen.getByRole('tab').tagName).toBe('BUTTON')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ describe('@primer/react/experimental', () => {
})

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

Expand Down
79 changes: 58 additions & 21 deletions packages/styled-react/src/__tests__/primer-react.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ import {

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

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

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

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

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

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

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

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

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

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

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

test('IconButton supports `sx` prop', () => {
render(<IconButton aria-label="test" data-testid="component" sx={{background: 'red'}} icon={() => <svg />} />)
render(
<IconButton
as="button"
aria-label="test"
data-testid="component"
sx={{background: 'red'}}
icon={() => <svg />}
/>,
)
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')

// Test that IconButton renders the icon component (SVG) in its children
const iconButton = screen.getByTestId('component')
const svgElement = iconButton.querySelector('svg')
expect(svgElement).toBeInTheDocument()
expect(iconButton.children.length).toBeGreaterThan(0)
})

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

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

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

test('NavList supports `sx` prop', () => {
Expand Down Expand Up @@ -286,19 +306,23 @@ describe('@primer/react', () => {
render(
<ThemeProvider>
<Overlay
as="div"
data-testid="component"
sx={{background: 'red'}}
onClickOutside={() => {}}
onEscape={() => {}}
returnFocusRef={ref}
role="dialog"
/>
</ThemeProvider>,
)
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
expect(screen.getByTestId('component')).toHaveAttribute('role', 'dialog')
})

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

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

test('PageLayout.Content supports `sx` prop', () => {
const {container} = render(<PageLayout.Content data-testid="component" sx={{background: 'red'}} />)
expect(window.getComputedStyle(container.firstElementChild!).backgroundColor).toBe('rgb(255, 0, 0)')
const {container} = render(
<PageLayout.Content as="section" data-testid="component" sx={{background: 'red'}} aria-labelledby="normal" />,
)

const outerElement = container.firstElementChild! as HTMLElement
expect(window.getComputedStyle(outerElement).backgroundColor).toBe('rgb(255, 0, 0)')
expect(outerElement).toHaveAttribute('aria-labelledby', 'normal')
})

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

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

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

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

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

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

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

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

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

test('UnderlineNav supports `sx` prop', () => {
render(
<UnderlineNav aria-label="navigation" data-testid="component" sx={{background: 'red'}}>
<UnderlineNav as="nav" aria-label="navigation" data-testid="component" sx={{background: 'red'}} variant="inset">
<UnderlineNav.Item>test</UnderlineNav.Item>
</UnderlineNav>,
)
expect(window.getComputedStyle(screen.getByLabelText('navigation')).backgroundColor).toBe('rgb(255, 0, 0)')
expect(screen.getByLabelText('navigation')).toHaveAttribute('data-variant', 'inset')
})

test('UnderlineNav.Item supports `sx` prop', () => {
render(
<UnderlineNav.Item data-testid="component" sx={{background: 'red'}}>
<UnderlineNav.Item as="a" data-testid="component" sx={{background: 'red'}} icon={<svg />}>
test
</UnderlineNav.Item>,
)
expect(window.getComputedStyle(screen.getByTestId('component')).backgroundColor).toBe('rgb(255, 0, 0)')
const svgElement = screen.getByTestId('component').querySelector('svg')
expect(svgElement).toBeInTheDocument()
})
})
6 changes: 3 additions & 3 deletions packages/styled-react/src/components/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ const StyledBreadcrumbsItem: ForwardRefComponent<'a', BreadcrumbsItemProps> = st
${sx}
`

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

const Breadcrumbs: ForwardRefComponent<'nav', BreadcrumbsProps> & {Item: typeof BreadcrumbsItem} = Object.assign(
BreadcrumbsImpl,
Expand Down
13 changes: 11 additions & 2 deletions packages/styled-react/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type HeaderItemProps as PrimerHeaderItemProps,
type HeaderLinkProps as PrimerHeaderLinkProps,
Header as PrimerHeader,
type HeaderLinkProps,
} from '@primer/react'
import {forwardRef} from 'react'
import {Box} from './Box'
Expand All @@ -11,20 +12,28 @@ import type {SxProp} from '../sx'

type HeaderProps = PrimerHeaderProps & SxProp

const HeaderImpl = forwardRef(function Header(props, ref) {
const StyledHeader = forwardRef(function Header(props, ref) {
return <Box as={PrimerHeader} ref={ref} {...props} />
}) as ForwardRefComponent<'header', HeaderProps>

const HeaderImpl = forwardRef(({as, ...props}: HeaderProps, ref) => (
<StyledHeader {...props} {...(as ? {forwardedAs: as} : {})} ref={ref} />
)) as ForwardRefComponent<'header', HeaderProps>

type HeaderItemProps = PrimerHeaderItemProps & SxProp

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

const HeaderLink = forwardRef<HTMLAnchorElement, PrimerHeaderLinkProps>(function HeaderLink(props, ref) {
const StyledHeaderLink = forwardRef<HTMLAnchorElement, PrimerHeaderLinkProps>(function HeaderLink(props, ref) {
return <Box as={PrimerHeader.Link} ref={ref} {...props} />
})

const HeaderLink = forwardRef<HTMLAnchorElement, HeaderLinkProps>(({as, ...props}, ref) => (
<StyledHeaderLink {...props} {...(as ? {forwardedAs: as} : {})} ref={ref} />
))

const Header = Object.assign(HeaderImpl, {
Item: HeaderItem,
Link: HeaderLink,
Expand Down
8 changes: 6 additions & 2 deletions packages/styled-react/src/components/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import {type SxProp} from '../sx'
import {forwardRef} from 'react'
import type {ForwardRefComponent} from '../polymorphic'

type LabelProps = PrimerLabelProps & SxProp
type LabelProps = PrimerLabelProps & SxProp & {as?: React.ElementType}

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

const Label = forwardRef<HTMLElement, LabelProps>(({as, ...props}, ref) => {
return <StyledLabel {...props} {...(as ? {forwardedAs: as} : {})} ref={ref} />
}) as ForwardRefComponent<'span', LabelProps>

export {Label, type LabelProps}
11 changes: 9 additions & 2 deletions packages/styled-react/src/components/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import {Link as PrimerLink, type LinkProps as PrimerLinkProps} from '@primer/react'
import styled from 'styled-components'
import {sx, type SxProp} from '../sx'
import type {ForwardRefComponent} from '../polymorphic'
import {forwardRef} from 'react'

type LinkProps = PrimerLinkProps & SxProp

const Link = styled(PrimerLink).withConfig<LinkProps>({
const StyledLink = styled(PrimerLink).withConfig<LinkProps>({
shouldForwardProp: prop => prop !== 'sx',
})`
${sx}
`
` as ForwardRefComponent<'a', LinkProps>

const Link = forwardRef<HTMLAnchorElement, LinkProps>(({as, ...props}, ref) => {
return <StyledLink {...props} {...(as ? {forwardedAs: as} : {})} ref={ref} />
}) as ForwardRefComponent<'a', LinkProps>

export {Link, type LinkProps}
17 changes: 13 additions & 4 deletions packages/styled-react/src/components/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ import {sx, type SxProp} from '../sx'
import type {ForwardRefComponent} from '../polymorphic'
import {Box} from './Box'
import type {PropsWithChildren} from 'react'
import React from 'react'

type PageHeaderProps = PrimerPageHeaderProps & SxProp

const PageHeaderImpl: ForwardRefComponent<'div', PageHeaderProps> = styled(
const StyledPageHeader: ForwardRefComponent<'div', PageHeaderProps> = styled(
PrimerPageHeader,
).withConfig<PageHeaderProps>({
shouldForwardProp: prop => prop !== 'sx',
})`
${sx}
`

const PageHeaderImpl = React.forwardRef<HTMLDivElement, PageHeaderProps>(({as, ...props}, ref) => (
<StyledPageHeader {...props} {...(as ? {forwardedAs: as} : {})} ref={ref} />
)) as ForwardRefComponent<'div', PageHeaderProps>

type PageHeaderActionsProps = PrimerPageHeaderActionsProps & SxProp

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

function PageHeaderTitle({sx, ...rest}: PageHeaderTitleProps) {
function StyledPageHeaderTitle({sx, ...rest}: PageHeaderTitleProps) {
const style: CSSCustomProperties = {}
if (sx) {
// @ts-ignore sx can have color attribute
Expand All @@ -65,6 +70,10 @@ function PageHeaderTitle({sx, ...rest}: PageHeaderTitleProps) {
return <Box {...rest} as={PrimerPageHeader.Title} style={style} sx={sx} />
}

const PageHeaderTitle = ({as, ...props}: PageHeaderTitleProps) => (
<StyledPageHeaderTitle {...props} {...(as ? {forwardedAs: as} : {})} />
)

type PageHeaderTitleAreaProps = PropsWithChildren<PrimerPageHeaderTitleAreaProps> & SxProp

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

type PageHeaderComponent = ForwardRefComponent<'div', PageHeaderProps> & {
type PageHeaderComponentType = ForwardRefComponent<'div', PageHeaderProps> & {
Actions: typeof PageHeaderActions
ContextArea: typeof PrimerPageHeader.ContextArea
ParentLink: typeof PrimerPageHeader.ParentLink
Expand All @@ -91,7 +100,7 @@ type PageHeaderComponent = ForwardRefComponent<'div', PageHeaderProps> & {
TrailingAction: typeof PrimerPageHeader.TrailingAction
}

const PageHeader: PageHeaderComponent = Object.assign(PageHeaderImpl, {
const PageHeader: PageHeaderComponentType = Object.assign(PageHeaderImpl, {
Actions: PageHeaderActions,
ContextArea: PrimerPageHeader.ContextArea,
ParentLink: PrimerPageHeader.ParentLink,
Expand Down
Loading
Loading