Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/chilly-lemons-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": major
---

Remove sx from UnderlinePanels
6 changes: 3 additions & 3 deletions packages/react/src/UnderlineNav/UnderlineNav.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {MutableRefObject, RefObject} from 'react'
import type {RefObject} from 'react'
import React, {useRef, forwardRef, useCallback, useState, useEffect} from 'react'
import {UnderlineNavContext} from './UnderlineNavContext'
import type {ResizeObserverEntry} from '../hooks/useResizeObserver'
Expand Down Expand Up @@ -144,7 +144,7 @@ export const UnderlineNav = forwardRef(
forwardedRef,
) => {
const backupRef = useRef<HTMLElement>(null)
const navRef = (forwardedRef ?? backupRef) as MutableRefObject<HTMLElement>
const navRef = (forwardedRef ?? backupRef) as RefObject<HTMLElement>
const listRef = useRef<HTMLUListElement>(null)
const moreMenuRef = useRef<HTMLLIElement>(null)
const moreMenuBtnRef = useRef<HTMLButtonElement>(null)
Expand Down Expand Up @@ -205,7 +205,7 @@ export const UnderlineNav = forwardRef(
const widthToFitIntoList = getItemsWidth(prospectiveListItem.props.children)
// Check if there is any empty space on the right side of the list
const availableSpace =
navRef.current.getBoundingClientRect().width - (listRef.current?.getBoundingClientRect().width ?? 0)
(navRef.current?.getBoundingClientRect().width ?? 0) - (listRef.current?.getBoundingClientRect().width ?? 0)

// Calculate how many items need to be pulled in to the menu to make room for the selected menu item
// I.e. if we need to pull 2 items in (index 0 and index 1), breakpoint (index) will return 1.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,14 @@ import React, {
import {TabContainerElement} from '@github/tab-container-element'
import type {IconProps} from '@primer/octicons-react'
import {createComponent} from '../../utils/create-component'
import {
UnderlineItemList,
UnderlineWrapper,
UnderlineItem,
type UnderlineItemProps,
} from '../../internal/components/UnderlineTabbedInterface'
import {type BoxProps} from '../../Box'
import {UnderlineItemList, UnderlineWrapper, UnderlineItem} from '../../internal/components/UnderlineTabbedInterface'
import {useId} from '../../hooks'
import {invariant} from '../../utils/invariant'
import {type SxProp} from '../../sx'
import {useResizeObserver, type ResizeObserverEntry} from '../../hooks/useResizeObserver'
import useIsomorphicLayoutEffect from '../../utils/useIsomorphicLayoutEffect'
import classes from './UnderlinePanels.module.css'
import {clsx} from 'clsx'
import {BoxWithFallback} from '../../internal/components/BoxWithFallback'

export type UnderlinePanelsProps = {
/**
Expand Down Expand Up @@ -74,7 +67,7 @@ export type TabProps = PropsWithChildren<{
}> &
SxProp

export type PanelProps = Omit<BoxProps, 'as'>
export type PanelProps = React.HTMLAttributes<HTMLDivElement>

const TabContainerComponent = createComponent(TabContainerElement, 'tab-container')

Expand Down Expand Up @@ -104,23 +97,44 @@ const UnderlinePanels: FC<UnderlinePanelsProps> = ({
let panelIndex = 0

const childrenWithProps = Children.map(children, child => {
if (isValidElement<UnderlineItemProps>(child) && child.type === Tab) {
if (
isValidElement(child) &&
(typeof child.type === 'function' || typeof child.type === 'object') &&
'displayName' in child.type &&
child.type.displayName === 'UnderlinePanels.Tab'
) {
return cloneElement(child, {id: `${parentId}-tab-${tabIndex++}`, loadingCounters, iconsVisible})
}

if (isValidElement<PanelProps>(child) && child.type === Panel) {
return cloneElement(child, {'aria-labelledby': `${parentId}-tab-${panelIndex++}`})
if (
isValidElement(child) &&
(typeof child.type === 'function' || typeof child.type === 'object') &&
'displayName' in child.type &&
child.type.displayName === 'UnderlinePanels.Panel'
) {
const childPanel = child as React.ReactElement<PanelProps>
return cloneElement(childPanel, {'aria-labelledby': `${parentId}-tab-${panelIndex++}`})
}
return child
})

const newTabs = Children.toArray(childrenWithProps).filter(child => {
return isValidElement(child) && child.type === Tab
return (
isValidElement(child) &&
(typeof child.type === 'function' || typeof child.type === 'object') &&
'displayName' in child.type &&
child.type.displayName === 'UnderlinePanels.Tab'
)
})

const newTabPanels = Children.toArray(childrenWithProps).filter(
child => isValidElement(child) && child.type === Panel,
)
const newTabPanels = Children.toArray(childrenWithProps).filter(child => {
return (
isValidElement(child) &&
(typeof child.type === 'function' || typeof child.type === 'object') &&
'displayName' in child.type &&
child.type.displayName === 'UnderlinePanels.Panel'
)
})

setTabs(newTabs)
setTabPanels(newTabPanels)
Expand Down Expand Up @@ -221,8 +235,12 @@ const Tab: FC<TabProps> = ({'aria-selected': ariaSelected, onSelect, ...props})

Tab.displayName = 'UnderlinePanels.Tab'

const Panel: FC<PanelProps> = props => {
return <BoxWithFallback as="div" role="tabpanel" {...props} />
const Panel: FC<PanelProps> = ({children, ...rest}) => {
return (
<div role="tabpanel" {...rest}>
{children}
</div>
)
}

Panel.displayName = 'UnderlinePanels.Panel'
Expand Down
100 changes: 44 additions & 56 deletions packages/react/src/internal/components/UnderlineTabbedInterface.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
// Used for UnderlineNav and UnderlinePanels components

import type React from 'react'
import {forwardRef, type FC, type PropsWithChildren} from 'react'
import React from 'react'
import {type ForwardedRef, forwardRef, type FC, type PropsWithChildren, type ElementType} from 'react'
import {isElement} from 'react-is'
import type {IconProps} from '@primer/octicons-react'
import CounterLabel from '../../CounterLabel'
import {type SxProp} from '../../sx'
import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../../utils/polymorphic'

import classes from './UnderlineTabbedInterface.module.css'
import {clsx} from 'clsx'
import {BoxWithFallback} from './BoxWithFallback'

// The gap between the list items. It is a constant because the gap is used to calculate the possible number of items that can fit in the container.
export const GAP = 8

type UnderlineWrapperProps = {
type UnderlineWrapperProps<As extends React.ElementType> = {
slot?: string
as?: React.ElementType
as?: As
className?: string
ref?: React.Ref<unknown>
} & SxProp
ref?: React.Ref<HTMLElement>
}

export const UnderlineWrapper = forwardRef(
({children, className, ...rest}: PropsWithChildren<UnderlineWrapperProps>, forwardedRef) => {
return (
<BoxWithFallback className={clsx(classes.UnderlineWrapper, className)} ref={forwardedRef} {...rest}>
{children}
</BoxWithFallback>
)
},
)
export const UnderlineWrapper = forwardRef((props, ref) => {
const {children, className, as: Component = 'nav', ...rest} = props
return (
<Component
className={clsx(classes.UnderlineWrapper, className)}
ref={ref as ForwardedRef<HTMLDivElement>}
{...rest}
>
{children}
</Component>
)
}) as PolymorphicForwardRefComponent<ElementType, UnderlineWrapperProps<ElementType>>

export const UnderlineItemList = forwardRef(({children, ...rest}: PropsWithChildren, forwardedRef) => {
return (
Expand All @@ -44,51 +45,38 @@ export const LoadingCounter = () => {
return <span className={classes.LoadingCounter} />
}

export type UnderlineItemProps = {
as?: React.ElementType | 'a' | 'button'
export type UnderlineItemProps<As extends React.ElementType> = {
as?: As | 'a' | 'button'
className?: string
iconsVisible?: boolean
loadingCounters?: boolean
counter?: number | string
icon?: FC<IconProps> | React.ReactElement
id?: string
ref?: React.Ref<unknown>
} & SxProp
} & React.ComponentPropsWithoutRef<As extends 'a' ? 'a' : As extends 'button' ? 'button' : As>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😵‍💫


export const UnderlineItem = forwardRef(
(
{
as = 'a',
children,
counter,
icon: Icon,
iconsVisible,
loadingCounters,
className,
...rest
}: PropsWithChildren<UnderlineItemProps>,
forwardedRef,
) => {
return (
<BoxWithFallback ref={forwardedRef} as={as} className={clsx(classes.UnderlineItem, className)} {...rest}>
{iconsVisible && Icon && <span data-component="icon">{isElement(Icon) ? Icon : <Icon />}</span>}
{children && (
<span data-component="text" data-content={children}>
{children}
export const UnderlineItem = React.forwardRef((props, ref) => {
const {as: Component = 'a', children, counter, icon: Icon, iconsVisible, loadingCounters, className, ...rest} = props
Comment on lines +59 to +60
Copy link
Preview

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The forwardRef should include proper TypeScript generics and parameter names for better type inference and debugging. Consider using the same pattern as UnderlineWrapper with explicit generic parameters.

See below for a potential fix:

export const UnderlineItem = React.forwardRef<
  HTMLElement,
  UnderlineItemProps<ElementType>
>(
  (props, ref) => {
    const {as: Component = 'a', children, counter, icon: Icon, iconsVisible, loadingCounters, className, ...rest} = props
    return (
      <Component {...rest} ref={ref} className={clsx(classes.UnderlineItem, className)}>
        {iconsVisible && Icon && <span data-component="icon">{isElement(Icon) ? Icon : <Icon />}</span>}
        {children && (
          <span data-component="text" data-content={children}>
            {children}
          </span>
        )}
        {counter !== undefined ? (
          loadingCounters ? (
            <span data-component="counter">
              <LoadingCounter />
            </span>
          ) : (
            <span data-component="counter">
              <CounterLabel>{counter}</CounterLabel>
            </span>
          )
        ) : null}
      </Component>
    )
  }
)

Copilot uses AI. Check for mistakes.

return (
<Component {...rest} ref={ref} className={clsx(classes.UnderlineItem, className)}>
{iconsVisible && Icon && <span data-component="icon">{isElement(Icon) ? Icon : <Icon />}</span>}
{children && (
<span data-component="text" data-content={children}>
{children}
</span>
)}
{counter !== undefined ? (
loadingCounters ? (
<span data-component="counter">
<LoadingCounter />
</span>
)}
{counter !== undefined ? (
loadingCounters ? (
<span data-component="counter">
<LoadingCounter />
</span>
) : (
<span data-component="counter">
<CounterLabel>{counter}</CounterLabel>
</span>
)
) : null}
</BoxWithFallback>
)
},
) as PolymorphicForwardRefComponent<'a', UnderlineItemProps>
) : (
<span data-component="counter">
<CounterLabel>{counter}</CounterLabel>
</span>
)
) : null}
</Component>
)
}) as PolymorphicForwardRefComponent<ElementType, UnderlineItemProps<ElementType>>
Copy link
Preview

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type assertion uses ElementType as both the constraint and the argument, which may not provide the intended polymorphic behavior. Consider using a more specific constraint like keyof JSX.IntrinsicElements | React.ComponentType<any> to ensure proper component props are inferred.

Suggested change
}) as PolymorphicForwardRefComponent<ElementType, UnderlineItemProps<ElementType>>
}) as PolymorphicForwardRefComponent<keyof JSX.IntrinsicElements | React.ComponentType<any>, UnderlineItemProps<any>>

Copilot uses AI. Check for mistakes.

10 changes: 6 additions & 4 deletions packages/styled-react/src/components/TabNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ const StyledTabNav = styled(PrimerTabNav).withConfig({
${sx}
`

// @ts-ignore forwardedAs is valid here but I don't know how to fix the typescript error
const TabNavImpl = ({as, ...props}: TabNavProps) => <StyledTabNav forwardedAs={as} {...props} />
const TabNavImpl = ({as, ...props}: TabNavProps) => {
return <StyledTabNav {...props} {...(as ? {forwardedAs: as as React.ElementType} : {})} />
}

const StyledTabNavLink: ForwardRefComponent<'a', TabNavLinkProps> = styled(PrimerTabNav.Link).withConfig({
shouldForwardProp: prop => (prop as keyof TabNavLinkProps) !== 'sx',
})<TabNavLinkProps>`
${sx}
`

// @ts-ignore forwardedAs is valid here but I don't know how to fix the typescript error
const TabNavLink = ({as, ...props}: TabNavLinkProps) => <StyledTabNavLink forwardedAs={as} {...props} />
const TabNavLink = ({as, ...props}: TabNavLinkProps) => (
<StyledTabNavLink {...props} {...(as ? {forwardedAs: as} : {})} />
)

const TabNav = Object.assign(TabNavImpl, {
Link: TabNavLink,
Expand Down
59 changes: 59 additions & 0 deletions packages/styled-react/src/components/UnderlinePanels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
UnderlinePanels as PrimerUnderlinePanels,
type UnderlinePanelsProps as PrimerUnderlinePanelsProps,
type UnderlinePanelsPanelProps as PrimerUnderlinePanelsPanelProps,
type UnderlinePanelsTabProps as PrimerUnderlinePanelsTabProps,
} from '@primer/react/experimental'
import styled from 'styled-components'
import {sx, type SxProp} from '../sx'

type UnderlinePanelsProps = PrimerUnderlinePanelsProps & SxProp

const StyledUnderlinePanels = styled(PrimerUnderlinePanels).withConfig<UnderlinePanelsProps>({
shouldForwardProp: prop => prop !== 'sx',
})`
${sx}
`

// @ts-ignore forwardedAs is valid here but I don't know how to fix the typescript error
const UnderlinePanelsImpl = ({as, ...props}: UnderlinePanelsProps) => (
<StyledUnderlinePanels forwardedAs={as} {...props} />
)

UnderlinePanelsImpl.displayName = 'UnderlinePanels'

type UnderlinePanelsPanelProps = PrimerUnderlinePanelsPanelProps & SxProp

const StyledUnderlinePanelsPanel = styled(PrimerUnderlinePanels.Panel).withConfig<UnderlinePanelsPanelProps>({
shouldForwardProp: prop => prop !== 'sx',
})`
${sx}
`

// @ts-ignore forwardedAs is valid here but I don't know how to fix the typescript error
const UnderlinePanelsPanel = ({as, ...props}: UnderlinePanelsPanelProps) => {
return <StyledUnderlinePanelsPanel forwardedAs={as} {...props} />
}

UnderlinePanelsPanel.displayName = 'UnderlinePanels.Panel'

type UnderlinePanelsTabProps = PrimerUnderlinePanelsTabProps & SxProp

const StyledUnderlinePanelsTab = styled(PrimerUnderlinePanels.Tab).withConfig<UnderlinePanelsTabProps>({
shouldForwardProp: prop => prop !== 'sx',
})`
${sx}
`
// @ts-ignore forwardedAs is valid here but I don't know how to fix the typescript error
const UnderlinePanelsTab = ({as, ...props}: UnderlinePanelsTabProps) => (
<StyledUnderlinePanelsTab forwardedAs={as} {...props} />
)

UnderlinePanelsTab.displayName = 'UnderlinePanels.Tab'

const UnderlinePanels = Object.assign(UnderlinePanelsImpl, {
Tab: UnderlinePanelsTab,
Panel: UnderlinePanelsPanel,
})

export {UnderlinePanels, type UnderlinePanelsProps, type UnderlinePanelsTabProps, type UnderlinePanelsPanelProps}
10 changes: 9 additions & 1 deletion packages/styled-react/src/experimental.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
export {Dialog, type DialogProps} from './components/Dialog'

export {
PageHeader,
type PageHeaderProps,
type PageHeaderActionsProps,
type PageHeaderTitleProps,
} from './components/PageHeader'
export {Table, Tooltip, UnderlinePanels} from '@primer/react/experimental'

export {
UnderlinePanels,
type UnderlinePanelsProps,
type UnderlinePanelsTabProps,
type UnderlinePanelsPanelProps,
} from './components/UnderlinePanels'
export {Table, Tooltip} from '@primer/react/experimental'
Loading