Skip to content

Commit 4c4c4cd

Browse files
authored
Deprecate leadingIcon, add leadingVisual for SegmentedControl.Button (#7033)
1 parent 8b2632b commit 4c4c4cd

11 files changed

+101
-47
lines changed

.changeset/itchy-guests-see.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
Deprecate `leadingIcon` in favor of `leadingVisual` for `SegmentedControl.Button`.

packages/react/src/SegmentedControl/SegmentedControl.dev.stories.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ export const WithCss = () => (
1818
<SegmentedControl.Button
1919
defaultSelected
2020
aria-label={'Preview'}
21-
leadingIcon={EyeIcon}
21+
leadingVisual={EyeIcon}
2222
className="testCustomClassnameColor"
2323
>
2424
Preview
2525
</SegmentedControl.Button>
26-
<SegmentedControl.Button aria-label={'Raw'} leadingIcon={FileCodeIcon} className="testCustomClassnameColor">
26+
<SegmentedControl.Button aria-label={'Raw'} leadingVisual={FileCodeIcon} className="testCustomClassnameColor">
2727
Raw
2828
</SegmentedControl.Button>
29-
<SegmentedControl.Button aria-label={'Blame'} leadingIcon={PeopleIcon} className="testCustomClassnameColor">
29+
<SegmentedControl.Button aria-label={'Blame'} leadingVisual={PeopleIcon} className="testCustomClassnameColor">
3030
Blame
3131
</SegmentedControl.Button>
3232
</SegmentedControl>

packages/react/src/SegmentedControl/SegmentedControl.docs.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,18 @@
8888
{
8989
"name": "SegmentedControl.Button",
9090
"props": [
91+
{
92+
"name": "leadingVisual",
93+
"type": "Component",
94+
"defaultValue": "",
95+
"description": "The leading visual comes before item label"
96+
},
9197
{
9298
"name": "leadingIcon",
99+
"deprecated": true,
93100
"type": "Component",
94101
"defaultValue": "",
95-
"description": "The leading icon comes before item label"
102+
"description": "Deprecated: use `leadingVisual` instead. The leading icon comes before item label."
96103
},
97104
{
98105
"name": "selected",
@@ -172,4 +179,4 @@
172179
]
173180
}
174181
]
175-
}
182+
}

packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ export default {
1212

1313
export const WithIcons = () => (
1414
<SegmentedControl aria-label="File view">
15-
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingIcon={EyeIcon}>
15+
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingVisual={EyeIcon}>
1616
Preview
1717
</SegmentedControl.Button>
18-
<SegmentedControl.Button aria-label={'Raw'} leadingIcon={FileCodeIcon}>
18+
<SegmentedControl.Button aria-label={'Raw'} leadingVisual={FileCodeIcon}>
1919
Raw
2020
</SegmentedControl.Button>
21-
<SegmentedControl.Button aria-label={'Blame'} leadingIcon={PeopleIcon}>
21+
<SegmentedControl.Button aria-label={'Blame'} leadingVisual={PeopleIcon}>
2222
Blame
2323
</SegmentedControl.Button>
2424
</SegmentedControl>
@@ -54,13 +54,13 @@ export const Controlled = () => {
5454

5555
export const VariantNarrowHideLabels = () => (
5656
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels', regular: 'default', wide: 'default'}}>
57-
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingIcon={EyeIcon}>
57+
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingVisual={EyeIcon}>
5858
Preview
5959
</SegmentedControl.Button>
60-
<SegmentedControl.Button aria-label={'Raw'} leadingIcon={FileCodeIcon}>
60+
<SegmentedControl.Button aria-label={'Raw'} leadingVisual={FileCodeIcon}>
6161
Raw
6262
</SegmentedControl.Button>
63-
<SegmentedControl.Button aria-label={'Blame'} leadingIcon={PeopleIcon}>
63+
<SegmentedControl.Button aria-label={'Blame'} leadingVisual={PeopleIcon}>
6464
Blame
6565
</SegmentedControl.Button>
6666
</SegmentedControl>
@@ -69,13 +69,13 @@ VariantNarrowHideLabels.storyName = '[variant: narrow] Hide labels'
6969

7070
export const VariantNarrowActionMenu = () => (
7171
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown', regular: 'default', wide: 'default'}}>
72-
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingIcon={EyeIcon}>
72+
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingVisual={EyeIcon}>
7373
Preview
7474
</SegmentedControl.Button>
75-
<SegmentedControl.Button aria-label={'Raw'} leadingIcon={FileCodeIcon}>
75+
<SegmentedControl.Button aria-label={'Raw'} leadingVisual={FileCodeIcon}>
7676
Raw
7777
</SegmentedControl.Button>
78-
<SegmentedControl.Button aria-label={'Blame'} leadingIcon={PeopleIcon}>
78+
<SegmentedControl.Button aria-label={'Blame'} leadingVisual={PeopleIcon}>
7979
Blame
8080
</SegmentedControl.Button>
8181
</SegmentedControl>
@@ -84,13 +84,13 @@ VariantNarrowActionMenu.storyName = '[variant: narrow] Action menu'
8484

8585
export const FullwidthNarrow = () => (
8686
<SegmentedControl aria-label="File view" fullWidth={{narrow: true, regular: false, wide: false}}>
87-
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingIcon={EyeIcon}>
87+
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingVisual={EyeIcon}>
8888
Preview
8989
</SegmentedControl.Button>
90-
<SegmentedControl.Button aria-label={'Raw'} leadingIcon={FileCodeIcon}>
90+
<SegmentedControl.Button aria-label={'Raw'} leadingVisual={FileCodeIcon}>
9191
Raw
9292
</SegmentedControl.Button>
93-
<SegmentedControl.Button aria-label={'Blame'} leadingIcon={PeopleIcon}>
93+
<SegmentedControl.Button aria-label={'Blame'} leadingVisual={PeopleIcon}>
9494
Blame
9595
</SegmentedControl.Button>
9696
</SegmentedControl>
@@ -99,13 +99,13 @@ FullwidthNarrow.storyName = '[fullWidth: narrow]'
9999

100100
export const FullwidthRegular = () => (
101101
<SegmentedControl aria-label="File view" fullWidth={{narrow: false, regular: true, wide: false}}>
102-
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingIcon={EyeIcon}>
102+
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingVisual={EyeIcon}>
103103
Preview
104104
</SegmentedControl.Button>
105-
<SegmentedControl.Button aria-label={'Raw'} leadingIcon={FileCodeIcon}>
105+
<SegmentedControl.Button aria-label={'Raw'} leadingVisual={FileCodeIcon}>
106106
Raw
107107
</SegmentedControl.Button>
108-
<SegmentedControl.Button aria-label={'Blame'} leadingIcon={PeopleIcon}>
108+
<SegmentedControl.Button aria-label={'Blame'} leadingVisual={PeopleIcon}>
109109
Blame
110110
</SegmentedControl.Button>
111111
</SegmentedControl>
@@ -114,13 +114,13 @@ FullwidthRegular.storyName = '[fullWidth: regular]'
114114

115115
export const FullwidthAll = () => (
116116
<SegmentedControl aria-label="File view" fullWidth>
117-
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingIcon={EyeIcon}>
117+
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingVisual={EyeIcon}>
118118
Preview
119119
</SegmentedControl.Button>
120-
<SegmentedControl.Button aria-label={'Raw'} leadingIcon={FileCodeIcon}>
120+
<SegmentedControl.Button aria-label={'Raw'} leadingVisual={FileCodeIcon}>
121121
Raw
122122
</SegmentedControl.Button>
123-
<SegmentedControl.Button aria-label={'Blame'} leadingIcon={PeopleIcon}>
123+
<SegmentedControl.Button aria-label={'Blame'} leadingVisual={PeopleIcon}>
124124
Blame
125125
</SegmentedControl.Button>
126126
</SegmentedControl>

packages/react/src/SegmentedControl/SegmentedControl.figma.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ figma.connect(
3131
},
3232
example: ({selected, label, leadingIcon}) => (
3333
// @ts-expect-error: leadingIcon is optional
34-
<SegmentedControl.Button selected={selected} leadingIcon={leadingIcon.fn}>
34+
<SegmentedControl.Button selected={selected} leadingVisual={leadingIcon.fn}>
3535
{label}
3636
</SegmentedControl.Button>
3737
),

packages/react/src/SegmentedControl/SegmentedControl.stories.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,13 @@ export const Playground: StoryFn<Args> = args => (
111111
variant={parseVariantFromArgs(args)}
112112
size={args.size}
113113
>
114-
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingIcon={EyeIcon}>
114+
<SegmentedControl.Button defaultSelected aria-label={'Preview'} leadingVisual={EyeIcon}>
115115
Preview
116116
</SegmentedControl.Button>
117-
<SegmentedControl.Button aria-label={'Raw'} leadingIcon={FileCodeIcon}>
117+
<SegmentedControl.Button aria-label={'Raw'} leadingVisual={FileCodeIcon}>
118118
Raw
119119
</SegmentedControl.Button>
120-
<SegmentedControl.Button aria-label={'Blame'} leadingIcon={PeopleIcon}>
120+
<SegmentedControl.Button aria-label={'Blame'} leadingVisual={PeopleIcon}>
121121
Blame
122122
</SegmentedControl.Button>
123123
</SegmentedControl>

packages/react/src/SegmentedControl/SegmentedControl.test.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ describe('SegmentedControl', () => {
8383
const {getByLabelText} = render(
8484
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels'}}>
8585
{segmentData.map(({label, icon}, index) => (
86-
<SegmentedControl.Button leadingIcon={icon} selected={index === 1} key={label}>
86+
<SegmentedControl.Button leadingVisual={icon} selected={index === 1} key={label}>
8787
{label}
8888
</SegmentedControl.Button>
8989
))}
@@ -114,7 +114,7 @@ describe('SegmentedControl', () => {
114114
const {getByLabelText} = render(
115115
<SegmentedControl aria-label="File view">
116116
{segmentData.map(({label, icon}, index) => (
117-
<SegmentedControl.Button selected={index === 0} leadingIcon={icon} key={label}>
117+
<SegmentedControl.Button selected={index === 0} leadingVisual={icon} key={label}>
118118
{label}
119119
</SegmentedControl.Button>
120120
))}
@@ -313,7 +313,7 @@ describe('SegmentedControl', () => {
313313
expect(handleClick).toHaveBeenCalled()
314314
})
315315

316-
it('warns users if they try to use the hideLabels variant without a leadingIcon', () => {
316+
it('warns users if they try to use the hideLabels variant without a leadingVisual', () => {
317317
const spy = vi.spyOn(globalThis.console, 'warn').mockImplementation(() => {})
318318

319319
render(
@@ -330,6 +330,36 @@ describe('SegmentedControl', () => {
330330
spy.mockRestore()
331331
})
332332

333+
it('supports deprecated leadingIcon prop for backward compatibility', () => {
334+
const {getByText} = render(
335+
<SegmentedControl aria-label="File view">
336+
<SegmentedControl.Button leadingIcon={EyeIcon}>Preview</SegmentedControl.Button>
337+
<SegmentedControl.Button>Code</SegmentedControl.Button>
338+
</SegmentedControl>,
339+
)
340+
341+
const button = getByText('Preview').closest('button')
342+
expect(button).toBeDefined()
343+
// Verify the icon is rendered
344+
expect(button?.querySelector('svg')).toBeDefined()
345+
})
346+
347+
it('prioritizes leadingVisual over deprecated leadingIcon when both are provided', () => {
348+
const {getByRole} = render(
349+
<SegmentedControl aria-label="File view">
350+
<SegmentedControl.Button
351+
leadingVisual={() => <EyeIcon aria-label="EyeIcon" />}
352+
leadingIcon={() => <FileCodeIcon aria-label="FileCodeIcon" />}
353+
>
354+
Preview
355+
</SegmentedControl.Button>
356+
</SegmentedControl>,
357+
)
358+
359+
// Should find EyeIcon, not FileCodeIcon
360+
expect(getByRole('img', {name: 'EyeIcon'})).toBeInTheDocument()
361+
})
362+
333363
it('should warn the user if they neglect to specify a label for the segmented control', () => {
334364
const spy = vi.spyOn(globalThis.console, 'warn').mockImplementation(() => {})
335365

packages/react/src/SegmentedControl/SegmentedControl.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,17 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
6464
const getChildIcon = (childArg: React.ReactNode): React.ReactElement | null => {
6565
if (
6666
React.isValidElement<SegmentedControlButtonProps>(childArg) &&
67-
(childArg.type === Button || isSlot(childArg, Button)) &&
68-
childArg.props.leadingIcon
67+
(childArg.type === Button || isSlot(childArg, Button))
6968
) {
70-
if (isElement(childArg.props.leadingIcon)) {
71-
return childArg.props.leadingIcon
72-
} else {
73-
const LeadingIcon = childArg.props.leadingIcon
74-
return <LeadingIcon />
69+
// Use leadingVisual if provided, otherwise fall back to leadingIcon for backwards compatibility
70+
const leadingVisual = childArg.props.leadingVisual ?? childArg.props.leadingIcon
71+
if (leadingVisual) {
72+
if (isElement(leadingVisual)) {
73+
return leadingVisual
74+
} else {
75+
const LeadingVisual = leadingVisual
76+
return <LeadingVisual />
77+
}
7578
}
7679
}
7780

@@ -191,18 +194,21 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
191194
) {
192195
const {
193196
'aria-label': childAriaLabel,
197+
leadingVisual,
194198
leadingIcon,
195199
children: childPropsChildren,
196200
...restChildProps
197201
} = child.props
198-
if (!leadingIcon) {
202+
// Use leadingVisual if provided, otherwise fall back to leadingIcon
203+
const visual = leadingVisual ?? leadingIcon
204+
if (!visual) {
199205
// eslint-disable-next-line no-console
200-
console.warn('A `leadingIcon` prop is required when hiding visible labels')
206+
console.warn('A `leadingVisual` or `leadingIcon` prop is required when hiding visible labels')
201207
} else {
202208
return (
203209
<SegmentedControlIconButton
204210
aria-label={childAriaLabel || childPropsChildren}
205-
icon={leadingIcon}
211+
icon={visual}
206212
// Width is now handled by CSS: 32px default, 100% when data-full-width is set on parent
207213
className={classes.IconButton}
208214
{...sharedChildProps}

packages/react/src/SegmentedControl/SegmentedControl.types.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {SegmentedControl} from './SegmentedControl'
44
export function buttonWithLeadingIconElement() {
55
return (
66
<SegmentedControl>
7-
<SegmentedControl.Button leadingIcon={<LogoGithubIcon />}>Button</SegmentedControl.Button>
7+
<SegmentedControl.Button leadingVisual={<LogoGithubIcon />}>Button</SegmentedControl.Button>
88
</SegmentedControl>
99
)
1010
}

packages/react/src/SegmentedControl/SegmentedControlButton.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ export default {
2424
export const Playground: StoryFn<SegmentedControlButtonProps> = args => <SegmentedControlButton {...args} />
2525
Playground.args = {
2626
children: 'Option',
27-
leadingIcon: undefined,
27+
leadingVisual: undefined,
2828
selected: false,
2929
defaultSelected: false,
3030
}
3131
Playground.argTypes = {
3232
children: {
3333
type: 'string',
3434
},
35-
leadingIcon: {
35+
leadingVisual: {
3636
control: 'select',
3737
options: Object.keys(icons),
3838
mapping: icons,

0 commit comments

Comments
 (0)