Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
27b3a27
Ensure that when aria-disabled is set, the selected tab doesn't change
khiga8 Aug 7, 2025
94c4b25
Add `disabled` prop to `SegmentedControl.Button`
TylerJDev Aug 11, 2025
46f8beb
Re-introduce the reverted "Clean up the feature flag for `primer_reac…
khiga8 Jul 29, 2025
57f988d
Merge branch 'main' into kh-segmented-control
khiga8 Aug 29, 2025
c2aad6e
Update strong-mangos-rest.md
khiga8 Aug 29, 2025
af99208
Update packages/react/src/SegmentedControl/SegmentedControl.examples.…
khiga8 Aug 29, 2025
307289d
Update .changeset/strong-mangos-rest.md
khiga8 Aug 29, 2025
088b051
Update SegmentedControl.dev.stories.tsx
khiga8 Sep 4, 2025
528e05a
Aria-disabled and disabled (#6725)
khiga8 Sep 4, 2025
3b6eefe
Merge branch 'main' into kh-segmented-control
TylerJDev Sep 12, 2025
9acdcc4
Add styles
TylerJDev Sep 12, 2025
d99fc85
Run format
TylerJDev Sep 15, 2025
3984849
Remove `:hover` and `:active` styles when disabled
TylerJDev Sep 15, 2025
3b95781
Move `:not`
TylerJDev Sep 15, 2025
9ff45d6
Fix lint
TylerJDev Sep 15, 2025
af884b0
Merge branch 'main' into kh-segmented-control
TylerJDev Sep 15, 2025
c31274e
Merge branch 'main' into kh-segmented-control
TylerJDev Sep 29, 2025
154deb0
Merge branch 'main' into kh-segmented-control
hectahertz Oct 10, 2025
6eeecc9
Merge branch 'main' into kh-segmented-control
TylerJDev Oct 21, 2025
fe20f4f
Lint fix
TylerJDev Oct 21, 2025
30ecae0
Changes from feedback
TylerJDev Dec 1, 2025
9f7a7f5
Merge branch 'main' into kh-segmented-control
TylerJDev Dec 1, 2025
b27cd1c
Add to VRT
TylerJDev Dec 1, 2025
9d52fb7
test(vrt): update snapshots
TylerJDev Dec 1, 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
6 changes: 6 additions & 0 deletions .changeset/strong-mangos-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@primer/react": minor
---

Remove the feature flag for `primer_react_segmented_control_tooltip` and GA tooltip by default behavior.
- Ensure that when `disabled` is applied, the tooltip is still triggered.
1 change: 0 additions & 1 deletion packages/react/src/FeatureFlags/DefaultFeatureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export const DefaultFeatureFlags = FeatureFlagScope.create({
primer_react_action_list_item_as_button: false,
primer_react_breadcrumbs_overflow_menu: false,
primer_react_overlay_overflow: false,
primer_react_segmented_control_tooltip: false,
primer_react_select_panel_fullscreen_on_narrow: false,
primer_react_select_panel_order_selected_at_top: false,
primer_react_select_panel_remove_active_descendant: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,82 @@ export default {
parameters: {controls: {exclude: excludedControlKeys}},
} as Meta<typeof SegmentedControl>

export const WithAriaDisabled = () => {
const handleOnClick = () => {
alert('Button clicked!')
}

return (
<SegmentedControl aria-label="File view" className="testCustomClassnameMono">
<SegmentedControl.IconButton
onClick={handleOnClick}
aria-label={'Preview'}
aria-disabled={true}
icon={EyeIcon}
className="testCustomClassnameColor"
>
Preview
</SegmentedControl.IconButton>
<SegmentedControl.IconButton
aria-disabled={true}
onClick={handleOnClick}
aria-label={'Raw'}
icon={FileCodeIcon}
className="testCustomClassnameColor"
>
Raw
</SegmentedControl.IconButton>
<SegmentedControl.IconButton
aria-disabled={true}
onClick={handleOnClick}
aria-label={'Blame'}
icon={PeopleIcon}
className="testCustomClassnameColor"
>
Blame
</SegmentedControl.IconButton>
</SegmentedControl>
)
}

export const WithDisabled = () => {
const handleOnClick = () => {
alert('Button clicked!')
}

return (
<SegmentedControl aria-label="File view" className="testCustomClassnameMono">
<SegmentedControl.IconButton
onClick={handleOnClick}
aria-label={'Preview'}
disabled={true}
icon={EyeIcon}
className="testCustomClassnameColor"
>
Preview
</SegmentedControl.IconButton>
<SegmentedControl.IconButton
disabled={true}
onClick={handleOnClick}
aria-label={'Raw'}
icon={FileCodeIcon}
className="testCustomClassnameColor"
>
Raw
</SegmentedControl.IconButton>
<SegmentedControl.IconButton
disabled={true}
onClick={handleOnClick}
aria-label={'Blame'}
icon={PeopleIcon}
className="testCustomClassnameColor"
>
Blame
</SegmentedControl.IconButton>
</SegmentedControl>
)
}

export const WithCss = () => (
<SegmentedControl aria-label="File view" className="testCustomClassnameMono">
<SegmentedControl.Button
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type {Meta} from '@storybook/react-vite'
import {SegmentedControl} from '.'
import {EyeIcon, FileCodeIcon, PeopleIcon} from '@primer/octicons-react'

export default {
title: 'Components/SegmentedControl/Examples',
component: SegmentedControl,
} as Meta<typeof SegmentedControl>

export const WithDisabledButtons = () => (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

does this need VRT?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah! I'll add one.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Should be added now!

<SegmentedControl aria-label="File view">
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingIcon={EyeIcon} disabled>
Preview
</SegmentedControl.Button>
<SegmentedControl.Button aria-label={'Raw'} leadingIcon={FileCodeIcon}>
Raw
</SegmentedControl.Button>
<SegmentedControl.Button aria-label={'Blame'} leadingIcon={PeopleIcon} disabled>
Blame
</SegmentedControl.Button>
</SegmentedControl>
)
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@
width: 0;
}

&[aria-disabled='true']:not([aria-current='true']) {
cursor: not-allowed;
color: var(--fgColor-disabled);
background-color: transparent;

& svg {
fill: var(--fgColor-disabled);
color: var(--fgColor-disabled);
}
}

@media (pointer: coarse) {
&::before {
position: absolute;
Expand Down Expand Up @@ -183,7 +194,7 @@
}
}

.Button:not([aria-current='true']) {
.Button:not([aria-current='true'], [aria-disabled='true']) {
&:hover .Content {
background-color: var(--controlTrack-bgColor-hover);
}
Expand Down
52 changes: 12 additions & 40 deletions packages/react/src/SegmentedControl/SegmentedControl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {EyeIcon, FileCodeIcon, PeopleIcon} from '@primer/octicons-react'
import userEvent from '@testing-library/user-event'
import {describe, expect, it, vi} from 'vitest'
import BaseStyles from '../BaseStyles'
import {FeatureFlags} from '../FeatureFlags'
import {SegmentedControl} from '../SegmentedControl'

const segmentData = [
Expand Down Expand Up @@ -142,19 +141,13 @@ describe('SegmentedControl', () => {
}
})

it('renders icon button with tooltip as label when feature flag is enabled', () => {
it('renders icon button with tooltip as label', () => {
const {getByRole, getByText} = render(
<FeatureFlags
flags={{
primer_react_segmented_control_tooltip: true,
}}
>
<SegmentedControl aria-label="File view">
{segmentData.map(({label, icon}) => (
<SegmentedControl.IconButton icon={icon} aria-label={label} key={label} />
))}
</SegmentedControl>
</FeatureFlags>,
<SegmentedControl aria-label="File view">
{segmentData.map(({label, icon}) => (
<SegmentedControl.IconButton icon={icon} aria-label={label} key={label} />
))}
</SegmentedControl>,
)

for (const datum of segmentData) {
Expand All @@ -165,41 +158,20 @@ describe('SegmentedControl', () => {
}
})

it('renders icon button with tooltip description when feature flag is enabled', () => {
it('renders icon button with tooltip description', () => {
const {getByRole, getByText} = render(
<FeatureFlags
flags={{
primer_react_segmented_control_tooltip: true,
}}
>
<SegmentedControl aria-label="File view">
{segmentData.map(({label, icon, description}) => (
<SegmentedControl.IconButton icon={icon} aria-label={label} description={description} key={label} />
))}
</SegmentedControl>
</FeatureFlags>,
)

for (const datum of segmentData) {
const labelledButton = getByRole('button', {name: datum.label})
const tooltipElement = getByText(datum.description)
expect(labelledButton).toHaveAttribute('aria-describedby', tooltipElement.id)
expect(labelledButton).toHaveAccessibleName(datum.label)
expect(labelledButton).toHaveAttribute('aria-label', datum.label)
}
})

it('renders icon button with aria-label and no tooltip', () => {
const {getByRole} = render(
<SegmentedControl aria-label="File view">
{segmentData.map(({label, icon}) => (
<SegmentedControl.IconButton icon={icon} aria-label={label} key={label} />
{segmentData.map(({label, icon, description}) => (
<SegmentedControl.IconButton icon={icon} aria-label={label} description={description} key={label} />
))}
</SegmentedControl>,
)

for (const datum of segmentData) {
const labelledButton = getByRole('button', {name: datum.label})
const tooltipElement = getByText(datum.description)
expect(labelledButton).toHaveAttribute('aria-describedby', tooltipElement.id)
expect(labelledButton).toHaveAccessibleName(datum.label)
expect(labelledButton).toHaveAttribute('aria-label', datum.label)
}
})
Expand Down
16 changes: 11 additions & 5 deletions packages/react/src/SegmentedControl/SegmentedControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,19 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
const sharedChildProps = {
onClick: onChange
? (event: React.MouseEvent<HTMLButtonElement>) => {
onChange(index)
isUncontrolled && setSelectedIndexInternalState(index)
child.props.onClick && child.props.onClick(event)
const isDisabled = child.props.disabled === true || child.props['aria-disabled'] === true

Copilot AI Oct 21, 2025

Copy link

Choose a reason for hiding this comment

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

The disabled check is duplicated on lines 170 and 178. Consider extracting this logic into a reusable function or variable before the conditional branches to avoid repetition.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Imo, I don't think this matters too much 🤔

if (!isDisabled) {
onChange(index)
isUncontrolled && setSelectedIndexInternalState(index)
child.props.onClick && child.props.onClick(event)
}
}
: (event: React.MouseEvent<HTMLButtonElement>) => {
child.props.onClick && child.props.onClick(event)
isUncontrolled && setSelectedIndexInternalState(index)
const isDisabled = child.props.disabled === true || child.props['aria-disabled'] === true
if (!isDisabled) {
child.props.onClick && child.props.onClick(event)
isUncontrolled && setSelectedIndexInternalState(index)
}
},
selected: index === selectedIndex,
style: {
Expand Down
14 changes: 13 additions & 1 deletion packages/react/src/SegmentedControl/SegmentedControlButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export type SegmentedControlButtonProps = {
defaultSelected?: boolean
/** The leading icon comes before item label */
leadingIcon?: React.FunctionComponent<React.PropsWithChildren<IconProps>> | React.ReactElement
/** Applies `aria-disabled` to the button. This will disable certain functionality, such as `onClick` events. */
disabled?: boolean
/** Applies `aria-disabled` to the button. This will disable certain functionality, such as `onClick` events. */
Comment thread
TylerJDev marked this conversation as resolved.
Outdated
'aria-disabled'?: boolean
/** Optional counter to display on the right side of the button */
count?: number | string
} & ButtonHTMLAttributes<HTMLButtonElement | HTMLLIElement>
Expand All @@ -26,14 +30,22 @@ const SegmentedControlButton: FCWithSlotMarker<React.PropsWithChildren<Segmented
leadingIcon: LeadingIcon,
selected,
className,
disabled,
'aria-disabled': ariaDisabled,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

im confused on what the difference between these two props is.
Also, does the *.docs.json need to be updated?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is to be backwards compatible from what I recall. Ideally we'll remove one or the other once we reduce usage in dotcom.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should we deprecate one now then?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I went ahead and removed aria-disabled, since it wasn't used anywhere.

// 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,
count,
...rest
}) => {
return (
<li className={clsx(classes.Item)} data-selected={selected ? '' : undefined}>
<button aria-current={selected} className={clsx(classes.Button, className)} type="button" {...rest}>
<button
aria-current={selected}
aria-disabled={disabled || ariaDisabled || undefined}
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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export default {
icon: FileCodeIcon,
selected: false,
defaultSelected: false,
disabled: false,
'aria-disabled': false,
},
argTypes: {
icon: {
Expand All @@ -26,6 +28,12 @@ export default {
defaultSelected: {
type: 'boolean',
},
disabled: {
type: 'boolean',
},
'aria-disabled': {
type: 'boolean',
},
},
decorators: [
Story => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type {ButtonHTMLAttributes} from 'react'
import type React from 'react'
import type {IconProps} from '@primer/octicons-react'
import {isElement} from 'react-is'
import {useFeatureFlag} from '../FeatureFlags'
import type {TooltipDirection} from '../TooltipV2'
import classes from './SegmentedControl.module.css'
import {clsx} from 'clsx'
Expand All @@ -21,6 +20,10 @@ export type SegmentedControlIconButtonProps = {
description?: string
/** The direction for the tooltip.*/
tooltipDirection?: TooltipDirection
/** Whether the button is disabled. */
disabled?: boolean
/** Whether the button is aria-disabled. */
'aria-disabled'?: boolean
} & ButtonHTMLAttributes<HTMLButtonElement | HTMLLIElement>

export const SegmentedControlIconButton: FCWithSlotMarker<React.PropsWithChildren<SegmentedControlIconButtonProps>> = ({
Expand All @@ -30,48 +33,31 @@ export const SegmentedControlIconButton: FCWithSlotMarker<React.PropsWithChildre
className,
description,
tooltipDirection,
disabled,
'aria-disabled': ariaDisabled,
...rest
}) => {
const tooltipFlagEnabled = useFeatureFlag('primer_react_segmented_control_tooltip')
if (tooltipFlagEnabled) {
return (
<li className={clsx(classes.Item, className)} data-selected={selected || undefined}>
<Tooltip
type={description ? undefined : 'label'}
text={description ? description : ariaLabel}
direction={tooltipDirection}
>
<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}
className={clsx(classes.Button, classes.IconButton)}
{...rest}
>
<span className={clsx(classes.Content, 'segmentedControl-content')}>
{isElement(Icon) ? Icon : <Icon />}
</span>
</button>
</Tooltip>
</li>
)
} else {
// This can be removed when primer_react_segmented_control_tooltip feature flag is GA-ed.
return (
<li className={clsx(classes.Item, className)} data-selected={selected || undefined}>
return (
<li className={clsx(classes.Item, className)} data-selected={selected || undefined}>
<Tooltip
type={description ? undefined : 'label'}
text={description ? description : ariaLabel}
direction={tooltipDirection}
>
<button
type="button"
aria-label={ariaLabel}
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}
aria-disabled={disabled || ariaDisabled || undefined}
className={clsx(classes.Button, classes.IconButton)}
{...rest}
>
<span className={clsx(classes.Content, 'segmentedControl-content')}>{isElement(Icon) ? Icon : <Icon />}</span>
</button>
</li>
)
}
</Tooltip>
</li>
)
}

SegmentedControlIconButton.__SLOT__ = Symbol('SegmentedControl.IconButton')
Expand Down
Loading