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/witty-chicken-leave.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@primer/react': major
---

Remove sx prop support from the SegmentedControl component.
8 changes: 0 additions & 8 deletions e2e/components/SegmentedControl.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,14 +60,6 @@ const stories = [
title: 'Dev: With Css',
id: 'components-segmentedcontrol-dev--with-css',
},
{
title: 'Dev: With Sx',
id: 'components-segmentedcontrol-dev--with-sx',
},
{
title: 'Dev: With Sx And Css',
id: 'components-segmentedcontrol-dev--with-sx-and-css',
},
] as const

test.describe('SegmentedControl', () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,47 +31,3 @@ export const WithCss = () => (
</SegmentedControl.Button>
</SegmentedControl>
)

export const WithSx = () => (
<SegmentedControl aria-label="File view" sx={{fontFamily: 'monospace'}}>
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingIcon={EyeIcon} sx={{color: 'success.fg'}}>
Preview
</SegmentedControl.Button>
<SegmentedControl.Button aria-label={'Raw'} leadingIcon={FileCodeIcon} sx={{color: 'success.fg'}}>
Raw
</SegmentedControl.Button>
<SegmentedControl.Button aria-label={'Blame'} leadingIcon={PeopleIcon} sx={{color: 'success.fg'}}>
Blame
</SegmentedControl.Button>
</SegmentedControl>
)

export const WithSxAndCss = () => (
<SegmentedControl aria-label="File view" sx={{fontFamily: 'monospace'}} className="testCustomClassnameMono">
<SegmentedControl.Button
defaultSelected
aria-label={'Preview'}
leadingIcon={EyeIcon}
sx={{color: 'success.fg'}}
className="testCustomClassnameColor"
>
Preview
</SegmentedControl.Button>
<SegmentedControl.Button
aria-label={'Raw'}
leadingIcon={FileCodeIcon}
sx={{color: 'success.fg'}}
className="testCustomClassnameColor"
>
Raw
</SegmentedControl.Button>
<SegmentedControl.Button
aria-label={'Blame'}
leadingIcon={PeopleIcon}
sx={{color: 'success.fg'}}
className="testCustomClassnameColor"
>
Blame
</SegmentedControl.Button>
</SegmentedControl>
)
21 changes: 3 additions & 18 deletions packages/react/src/SegmentedControl/SegmentedControl.docs.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -78,12 +78,7 @@
"type": "'small' | 'medium'",
"description": "The size of the buttons",
"defaultValue": ""
},
{
"name": "sx",
"type": "SystemStyleObject",
"deprecated": true
},
},
{
"name": "ref",
"type": "React.RefObject<HTMLDivElement>"
Expand All@@ -110,12 +105,7 @@
"type": "boolean",
"defaultValue": "",
"description": "Whether the segment is selected. This is used for uncontrolled SegmentedControls to pick one SegmentedControlButton that is selected on the initial render."
},
{
"name": "sx",
"type": "SystemStyleObject",
"deprecated": true
},
},
{
"name": "ref",
"type": "React.RefObject<HTMLButtonElement>"
Expand DownExpand Up@@ -156,12 +146,7 @@
"type": "boolean",
"defaultValue": "",
"description": "Whether the segment is selected. This is used for uncontrolled SegmentedControls to pick one SegmentedControlButton that is selected on the initial render."
},
{
"name": "sx",
"type": "SystemStyleObject",
"deprecated": true
},
},
{
"name": "ref",
"type": "React.RefObject<HTMLButtonElement>"
Expand Down
28 changes: 7 additions & 21 deletions packages/react/src/SegmentedControl/SegmentedControl.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,18 +6,14 @@ import SegmentedControlIconButton from './SegmentedControlIconButton'
import {ActionList} from '../ActionList'
import {ActionMenu} from '../ActionMenu'
import {useTheme} from '../ThemeProvider'
import type {SxProp} from '../sx'
import type {ResponsiveValue} from '../hooks/useResponsiveValue'
import {useResponsiveValue} from '../hooks/useResponsiveValue'
import type {WidthOnlyViewportRangeKeys} from '../utils/types/ViewportRangeKeys'
import {isElement} from 'react-is'

import classes from './SegmentedControl.module.css'

import {clsx} from 'clsx'
import {BoxWithFallback} from '../internal/components/BoxWithFallback'

type SegmentedControlProps = {
export type SegmentedControlProps = {
'aria-label'?: string
'aria-labelledby'?: string
'aria-describedby'?: string
Expand All@@ -30,7 +26,7 @@ type SegmentedControlProps = {
/** Configure alternative ways to render the control when it gets rendered in tight spaces */
variant?: 'default' | Partial<Record<WidthOnlyViewportRangeKeys, 'hideLabels' | 'dropdown' | 'default'>>
className?: string
} & SxProp
}

const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
'aria-label': ariaLabel,
Expand All@@ -39,7 +35,6 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
fullWidth,
onChange,
size,
sx: sxProp,
variant = 'default',
className,
...rest
Expand DownExpand Up@@ -153,9 +148,7 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
</>
) : (
// Render a segmented control
<BoxWithFallback
as="ul"
sx={sxProp}
<ul
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
ref={segmentedControlContainerRef}
Expand DownExpand Up@@ -186,7 +179,6 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
index === selectedIndex || index === selectedIndex - 1 ? 'transparent' : theme?.colors.border.default,
...child.props.style,
},
sx: child.props.sx,
}

// Render the 'hideLabels' variant of the SegmentedControlButton
Expand All@@ -201,7 +193,6 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
children: childPropsChildren,
...restChildProps
} = child.props
const {sx: sharedSxProp, ...restSharedChildProps} = sharedChildProps
if (!leadingIcon) {
// eslint-disable-next-line no-console
console.warn('A `leadingIcon` prop is required when hiding visible labels')
Expand All@@ -210,14 +201,9 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
<SegmentedControlIconButton
aria-label={childAriaLabel || childPropsChildren}
icon={leadingIcon}
sx={
{
...sharedSxProp,
// setting width here avoids having to pass `isFullWidth` directly to child components
width: !isFullWidth ? '32px' : '100%', // TODO: use primitive `control.medium.size` when it is available instead of '32px'
} as React.CSSProperties
}
{...restSharedChildProps}
// Width is now handled by CSS: 32px default, 100% when data-full-width is set on parent
className={classes.IconButton}
Comment on lines +204 to +205

CopilotAISep 9, 2025

Copy link

Choose a reason for hiding this comment

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

The comment mentions that width is handled by CSS using classes.IconButton, but this class is not defined in the visible changes and may not actually handle the width styling that was previously managed by the sx prop. This could result in incorrect styling behavior.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Reusing the IconButton className in SegmentedControl.module.css

{...sharedChildProps}
{...restChildProps}
/>
)
Expand All@@ -227,7 +213,7 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
// Render the children as-is and add the shared child props
return React.cloneElement(child, sharedChildProps)
})}
</BoxWithFallback>
</ul>
)
}

Expand Down
20 changes: 5 additions & 15 deletions packages/react/src/SegmentedControl/SegmentedControlButton.tsx
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
import type {ButtonHTMLAttributes} from 'react'
import type React from 'react'
import type {IconProps} from '@primer/octicons-react'
import type {SxProp} from '../sx'
import {isElement} from 'react-is'

import classes from './SegmentedControl.module.css'
import {clsx} from 'clsx'
import {BoxWithFallback} from '../internal/components/BoxWithFallback'

export type SegmentedControlButtonProps = {
/** The visible label rendered in the button */
Expand All@@ -17,28 +15,20 @@ export type SegmentedControlButtonProps = {
defaultSelected?: boolean
/** The leading icon comes before item label */
leadingIcon?: React.FunctionComponent<React.PropsWithChildren<IconProps>> | React.ReactElement
} & SxProp &
ButtonHTMLAttributes<HTMLButtonElement | HTMLLIElement>
} & ButtonHTMLAttributes<HTMLButtonElement | HTMLLIElement>

const SegmentedControlButton: React.FC<React.PropsWithChildren<SegmentedControlButtonProps>> = ({
children,
leadingIcon: LeadingIcon,
selected,
sx: sxProp,
className,
// Note: this value is read in the `SegmentedControl` component to determine which button is selected but we do not need to apply it to an underlying element
defaultSelected: _defaultSelected,
...rest
}) => {
return (
<BoxWithFallback as="li" sx={sxProp} className={clsx(classes.Item)} data-selected={selected ? '' : undefined}>
<BoxWithFallback
as="button"
aria-current={selected}
className={clsx(classes.Button, className)}
type="button"
{...rest}
>
<li className={clsx(classes.Item)} data-selected={selected ? '' : undefined}>
<button aria-current={selected} className={clsx(classes.Button, className)} type="button" {...rest}>
<span className={clsx(classes.Content, 'segmentedControl-content')}>
{LeadingIcon && (
<div className={classes.LeadingIcon}>{isElement(LeadingIcon) ? LeadingIcon : <LeadingIcon />}</div>
Expand All@@ -47,8 +37,8 @@ const SegmentedControlButton: React.FC<React.PropsWithChildren<SegmentedControlB
{children}
</div>
</span>
</BoxWithFallback>
</BoxWithFallback>
</button>
</li>
)
}

Expand Down
36 changes: 11 additions & 25 deletions packages/react/src/SegmentedControl/SegmentedControlIconButton.tsx
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
import type {ButtonHTMLAttributes} from 'react'
import type React from 'react'
import type {IconProps} from '@primer/octicons-react'
import type {SxProp} from '../sx'
import {isElement} from 'react-is'
import {useFeatureFlag} from '../FeatureFlags'
import type {TooltipDirection} from '../TooltipV2'
import classes from './SegmentedControl.module.css'
import {clsx} from 'clsx'
import {Tooltip} from '../TooltipV2'
import {BoxWithFallback} from '../internal/components/BoxWithFallback'

export type SegmentedControlIconButtonProps = {
'aria-label': string
Expand All@@ -22,14 +20,12 @@ export type SegmentedControlIconButtonProps = {
description?: string
/** The direction for the tooltip.*/
tooltipDirection?: TooltipDirection
} & SxProp &
ButtonHTMLAttributes<HTMLButtonElement | HTMLLIElement>
} & ButtonHTMLAttributes<HTMLButtonElement | HTMLLIElement>

export const SegmentedControlIconButton: React.FC<React.PropsWithChildren<SegmentedControlIconButtonProps>> = ({
'aria-label': ariaLabel,
icon: Icon,
selected,
sx: sxProp,
className,
description,
tooltipDirection,
Expand All@@ -38,19 +34,14 @@ export const SegmentedControlIconButton: React.FC<React.PropsWithChildren<Segmen
const tooltipFlagEnabled = useFeatureFlag('primer_react_segmented_control_tooltip')
if (tooltipFlagEnabled) {
return (
<BoxWithFallback
as="li"
sx={sxProp}
className={clsx(classes.Item, className)}
data-selected={selected || undefined}
>
<li className={clsx(classes.Item, className)} data-selected={selected || undefined}>
<Tooltip
type={description ? undefined : 'label'}
text={description ? description : ariaLabel}
direction={tooltipDirection}
>
<BoxWithFallback
as="button"
<button
type="button"
aria-current={selected}
// If description is provided, we will use the tooltip to describe the button, so we need to keep the aria-label to label the button.
aria-label={description ? ariaLabel : undefined}
Expand All@@ -60,29 +51,24 @@ export const SegmentedControlIconButton: React.FC<React.PropsWithChildren<Segmen
<span className={clsx(classes.Content, 'segmentedControl-content')}>
{isElement(Icon) ? Icon : <Icon />}
</span>
</BoxWithFallback>
</button>
</Tooltip>
</BoxWithFallback>
</li>
)
} else {
// This can be removed when primer_react_segmented_control_tooltip feature flag is GA-ed.
return (
<BoxWithFallback
as="li"
sx={sxProp}
className={clsx(classes.Item, className)}
data-selected={selected || undefined}
>
<BoxWithFallback
as="button"
<li className={clsx(classes.Item, className)} data-selected={selected || undefined}>
<button
type="button"
aria-label={ariaLabel}
aria-current={selected}
className={clsx(classes.Button, classes.IconButton)}
{...rest}
>
<span className={clsx(classes.Content, 'segmentedControl-content')}>{isElement(Icon) ? Icon : <Icon />}</span>
</BoxWithFallback>
</BoxWithFallback>
</button>
</li>
)
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/SegmentedControl/index.ts
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
export * from './SegmentedControl'

export type {SegmentedControlProps} from './SegmentedControl'
export type {SegmentedControlButtonProps} from './SegmentedControlButton'
export type {SegmentedControlIconButtonProps} from './SegmentedControlIconButton'
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,9 @@ exports[`@primer/react > should not update exports without a semver change 1`] =
"type RelativeTimeProps",
"ResponsiveValue",
"SegmentedControl",
"type SegmentedControlButtonProps",
"type SegmentedControlIconButtonProps",
"type SegmentedControlProps",
"Select",
"SelectPanel",
"type SelectPanelGroupedListProps",
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,6 +134,11 @@ export {default as RadioGroup} from './RadioGroup'
export type {RelativeTimeProps} from './RelativeTime'
export {default as RelativeTime} from './RelativeTime'
export {SegmentedControl} from './SegmentedControl'
export type {
SegmentedControlProps,
SegmentedControlButtonProps,
SegmentedControlIconButtonProps,
} from './SegmentedControl'
// Curently there is a duplicate Select component at the root of the dir, so need to be explicit about exporting from the src/Select dir
export {default as Select} from './Select'
export type {SelectProps} from './Select'
Expand Down
Loading
Loading