Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions e2e/components/SegmentedControl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ const stories = [
title: 'Dev: With Css',
id: 'components-segmentedcontrol-dev--with-css',
},
{
title: 'With Disabled Buttons',
id: 'components-segmentedcontrol-examples--with-disabled-buttons',
},
] as const

test.describe('SegmentedControl', () => {
Expand Down
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
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
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
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} aria-disabled={true}>
Blame
</SegmentedControl.Button>
</SegmentedControl>
)
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,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 @@ -313,7 +324,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
22 changes: 17 additions & 5 deletions packages/react/src/SegmentedControl/SegmentedControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,25 @@ 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' ||
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' ||
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: 12 additions & 2 deletions packages/react/src/SegmentedControl/SegmentedControlButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type SegmentedControlButtonProps = {
/** @deprecated Use `leadingVisual` instead. The leading icon comes before item label */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leadingIcon?: React.FunctionComponent<React.PropsWithChildren<IconProps>> | React.ReactElement<any>
/** Applies `aria-disabled` to the button. This will disable certain functionality, such as `onClick` events. */
disabled?: boolean
/** Optional counter to display on the right side of the button */
count?: number | string
} & ButtonHTMLAttributes<HTMLButtonElement | HTMLLIElement>
Expand All @@ -31,17 +33,25 @@ const SegmentedControlButton: FCWithSlotMarker<React.PropsWithChildren<Segmented
leadingIcon,
selected,
className,
disabled,
// 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
...props
}) => {
const {'aria-disabled': ariaDisabled, ...rest} = props
// Use leadingVisual if provided, otherwise fall back to leadingIcon for backwards compatibility
const LeadingVisual = leadingVisual ?? leadingIcon

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')}>
{LeadingVisual && (
<div className={classes.LeadingIcon}>{isElement(LeadingVisual) ? LeadingVisual : <LeadingVisual />}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default {
icon: FileCodeIcon,
selected: false,
defaultSelected: false,
disabled: false,
},
argTypes: {
icon: {
Expand All @@ -26,6 +27,9 @@ export default {
defaultSelected: {
type: 'boolean',
},
disabled: {
type: 'boolean',
},
},
decorators: [
Story => {
Expand Down
Loading
Loading