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
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}
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

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

Use boolean literal true instead of {true} for boolean props. This can be simplified to just aria-disabled.

Copilot uses AI. Check for mistakes.
icon={EyeIcon}
className="testCustomClassnameColor"
>
Preview
</SegmentedControl.IconButton>
<SegmentedControl.IconButton
aria-disabled={true}
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

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

Use boolean literal true instead of {true} for boolean props. These can be simplified to just aria-disabled.

Copilot uses AI. Check for mistakes.
onClick={handleOnClick}
aria-label={'Raw'}
icon={FileCodeIcon}
className="testCustomClassnameColor"
>
Raw
</SegmentedControl.IconButton>
<SegmentedControl.IconButton
aria-disabled={true}
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

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

Use boolean literal true instead of {true} for boolean props. These can be simplified to just aria-disabled.

Copilot uses AI. Check for mistakes.
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 = () => (
<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 @@ -5,7 +5,6 @@ import {describe, expect, it, vi} from 'vitest'
import BaseStyles from '../BaseStyles'
import theme from '../theme'
import ThemeProvider from '../ThemeProvider'
import {FeatureFlags} from '../FeatureFlags'
import {SegmentedControl} from '../SegmentedControl'

const segmentData = [
Expand Down Expand Up @@ -144,19 +143,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 @@ -167,41 +160,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 @@ -163,13 +163,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
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 @@ -16,6 +16,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. */
'aria-disabled'?: boolean
/** Optional counter to display on the right side of the button */
count?: number | string
} & ButtonHTMLAttributes<HTMLButtonElement | HTMLLIElement>
Expand All @@ -25,14 +29,22 @@ const SegmentedControlButton: React.FC<React.PropsWithChildren<SegmentedControlB
leadingIcon: LeadingIcon,
selected,
className,
disabled,
'aria-disabled': ariaDisabled,
// 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 @@ -20,6 +19,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: React.FC<React.PropsWithChildren<SegmentedControlIconButtonProps>> = ({
Expand All @@ -29,48 +32,31 @@ export const SegmentedControlIconButton: React.FC<React.PropsWithChildren<Segmen
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>
)
}

export default SegmentedControlIconButton
Loading