Skip to content

Commit 59a6654

Browse files
authored
feat(SegmentedControl): convert to CSS modules behind feature flag (#5336)
1 parent 82bf850 commit 59a6654

File tree

6 files changed

+296
-30
lines changed

6 files changed

+296
-30
lines changed

.changeset/empty-crews-impress.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+
Convert SegmentedControl to use CSS modules behind feature flag
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
.SegmentedControl {
2+
display: inline-flex;
3+
4+
/* TODO: use primitive `control.{small|medium}.size` when it is available */
5+
height: 32px;
6+
padding: 0;
7+
margin: 0;
8+
font-size: var(--text-body-size-medium);
9+
background-color: var(--controlTrack-bgColor-rest);
10+
border: var(--borderWidth-thin) solid var(--controlTrack-borderColor-rest, transparent);
11+
border-radius: var(--borderRadius-medium);
12+
13+
&:where([data-full-width]) {
14+
display: flex;
15+
width: 100%;
16+
}
17+
18+
&:where([data-size='small']) {
19+
/* TODO: use primitive `control.{small|medium}.size` when it is available */
20+
height: 28px;
21+
font-size: var(--text-body-size-small);
22+
}
23+
}
24+
25+
.Item {
26+
position: relative;
27+
display: block;
28+
/* stylelint-disable-next-line primer/spacing */
29+
margin-top: -1px;
30+
/* stylelint-disable-next-line primer/spacing */
31+
margin-bottom: -1px;
32+
flex-grow: 1;
33+
34+
&:not(:last-child) {
35+
/* stylelint-disable-next-line primer/spacing */
36+
margin-right: 1px;
37+
38+
&::after {
39+
position: absolute;
40+
top: var(--base-size-8);
41+
right: calc(-1 * var(--base-size-2));
42+
bottom: var(--base-size-8);
43+
width: 1px;
44+
content: '';
45+
/* stylelint-disable-next-line primer/colors */
46+
background-color: var(--borderColor-default);
47+
}
48+
49+
&:has(+ [data-selected])::after,
50+
&:where([data-selected])::after {
51+
background-color: transparent;
52+
}
53+
}
54+
55+
&:focus-within:has(:focus-visible) {
56+
background-color: transparent;
57+
}
58+
59+
&:first-child {
60+
/* stylelint-disable-next-line primer/spacing */
61+
margin-left: -1px;
62+
}
63+
64+
&:last-child {
65+
/* stylelint-disable-next-line primer/spacing */
66+
margin-right: -1px;
67+
}
68+
}
69+
70+
.Button {
71+
/* TODO: use primitive `primer.control.medium.paddingInline.normal` when it is available */
72+
--segmented-control-button-inner-padding: 12px;
73+
--segmented-control-button-bg-inset: 4px;
74+
--segmented-control-outer-radius: var(--borderRadius-medium);
75+
76+
width: 100%;
77+
height: 100%;
78+
/* stylelint-disable-next-line primer/spacing */
79+
padding: var(--segmented-control-button-bg-inset);
80+
font-family: inherit;
81+
font-size: inherit;
82+
font-weight: var(--base-text-weight-normal);
83+
color: currentColor;
84+
cursor: pointer;
85+
background-color: transparent;
86+
border-color: transparent;
87+
border-width: 0;
88+
/* stylelint-disable-next-line primer/borders */
89+
border-radius: var(--segmented-control-outer-radius);
90+
91+
& svg {
92+
fill: var(--fgColor-muted);
93+
}
94+
95+
/* fallback :focus state */
96+
&:focus:not(:disabled) {
97+
outline: var(--base-size-2) solid var(--fgColor-accent);
98+
outline-offset: -1px;
99+
box-shadow: none;
100+
101+
/* remove fallback :focus if :focus-visible is supported */
102+
&:not(:focus-visible) {
103+
outline: solid 1px transparent;
104+
}
105+
}
106+
107+
/* default focus state */
108+
&:focus-visible:not(:disabled) {
109+
outline: var(--base-size-2) solid var(--fgColor-accent);
110+
outline-offset: -1px;
111+
box-shadow: none;
112+
}
113+
114+
/* stylelint-disable-next-line selector-max-specificity */
115+
&:focus:focus-visible:not(:last-child)::after {
116+
/* fixes an issue where the focus outline shows over the pseudo-element */
117+
width: 0;
118+
}
119+
120+
@media (pointer: coarse) {
121+
&::before {
122+
position: absolute;
123+
top: 50%;
124+
right: 0;
125+
left: 0;
126+
min-height: 44px;
127+
content: '';
128+
transform: translateY(-50%);
129+
}
130+
}
131+
}
132+
133+
.IconButton {
134+
/* TODO: use primitive `control.medium.size` when it is available instead of '32px' */
135+
width: 32px;
136+
137+
.SegmentedControl:where([data-full-width]) & {
138+
width: 100%;
139+
}
140+
}
141+
142+
.Content {
143+
display: flex;
144+
height: 100%;
145+
/* stylelint-disable-next-line primer/spacing */
146+
padding-right: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset));
147+
/* stylelint-disable-next-line primer/spacing */
148+
padding-left: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset));
149+
background-color: transparent;
150+
border-color: transparent;
151+
border-style: solid;
152+
border-width: var(--borderWidth-thin);
153+
154+
/*
155+
innerRadius = outerRadius - distance/2
156+
https://stackoverflow.com/questions/2932146/math-problem-determine-the-corner-radius-of-an-inner-border-based-on-outer-corn
157+
*/
158+
/* stylelint-disable-next-line primer/borders */
159+
border-radius: calc(var(--segmented-control-outer-radius) - var(--segmented-control-button-bg-inset) / 2);
160+
align-items: center;
161+
justify-content: center;
162+
}
163+
164+
.Button[aria-current='true'] {
165+
padding: 0;
166+
font-weight: var(--base-text-weight-semibold);
167+
168+
.Content {
169+
/* stylelint-disable-next-line primer/spacing */
170+
padding-right: var(--segmented-control-button-inner-padding);
171+
/* stylelint-disable-next-line primer/spacing */
172+
padding-left: var(--segmented-control-button-inner-padding);
173+
background-color: var(--controlKnob-bgColor-rest);
174+
border-color: var(--controlKnob-borderColor-rest);
175+
/* stylelint-disable-next-line primer/borders */
176+
border-radius: var(--segmented-control-outer-radius);
177+
}
178+
}
179+
180+
.Button:not([aria-current='true']) {
181+
&:hover .Content {
182+
background-color: var(--controlTrack-bgColor-hover);
183+
}
184+
185+
&:active .Content {
186+
background-color: var(--controlTrack-bgColor-active);
187+
}
188+
}
189+
190+
.Text::after {
191+
display: block;
192+
height: 0;
193+
overflow: hidden;
194+
font-weight: var(--base-text-weight-semibold);
195+
pointer-events: none;
196+
visibility: hidden;
197+
content: attr(data-text);
198+
user-select: none;
199+
}

packages/react/src/SegmentedControl/SegmentedControl.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,21 @@ import styled from 'styled-components'
1515
import {defaultSxProp} from '../utils/defaultSxProp'
1616
import {isElement} from 'react-is'
1717

18+
import classes from './SegmentedControl.module.css'
19+
20+
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
21+
import {useFeatureFlag} from '../FeatureFlags'
22+
import {clsx} from 'clsx'
23+
import {SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG} from './getSegmentedControlStyles'
24+
1825
// Needed because passing a ref to `Box` causes a type error
19-
const SegmentedControlList = styled.ul`
20-
${sx};
21-
`
26+
const SegmentedControlList = toggleStyledComponent(
27+
SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG,
28+
'ul',
29+
styled.ul`
30+
${sx};
31+
`,
32+
)
2233

2334
type SegmentedControlProps = {
2435
'aria-label'?: string
@@ -57,6 +68,7 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
5768
size,
5869
sx: sxProp = defaultSxProp,
5970
variant = 'default',
71+
className,
6072
...rest
6173
}) => {
6274
const segmentedControlContainerRef = useRef<HTMLUListElement>(null)
@@ -117,7 +129,9 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
117129

118130
return React.isValidElement<SegmentedControlIconButtonProps>(childArg) ? childArg.props['aria-label'] : null
119131
}
120-
const listSx = merge(getSegmentedControlStyles({isFullWidth, size}), sxProp as SxProp)
132+
133+
const enabled = useFeatureFlag(SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG)
134+
const listSx = enabled ? sxProp : merge(getSegmentedControlStyles({isFullWidth, size}), sxProp as SxProp)
121135

122136
if (!ariaLabel && !ariaLabelledby) {
123137
// eslint-disable-next-line no-console
@@ -174,6 +188,9 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
174188
aria-label={ariaLabel}
175189
aria-labelledby={ariaLabelledby}
176190
ref={segmentedControlContainerRef}
191+
className={clsx(enabled && classes.SegmentedControl, className)}
192+
data-full-width={isFullWidth || undefined}
193+
data-size={size}
177194
{...rest}
178195
>
179196
{React.Children.map(children, (child, index) => {

packages/react/src/SegmentedControl/SegmentedControlButton.tsx

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ import styled from 'styled-components'
55
import Box from '../Box'
66
import type {SxProp} from '../sx'
77
import sx, {merge} from '../sx'
8-
import {getSegmentedControlButtonStyles, getSegmentedControlListItemStyles} from './getSegmentedControlStyles'
8+
import {
9+
getSegmentedControlButtonStyles,
10+
getSegmentedControlListItemStyles,
11+
SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG,
12+
} from './getSegmentedControlStyles'
913
import {defaultSxProp} from '../utils/defaultSxProp'
1014
import {isElement} from 'react-is'
1115
import getGlobalFocusStyles from '../internal/utils/getGlobalFocusStyles'
16+
import {useFeatureFlag} from '../FeatureFlags'
17+
18+
import classes from './SegmentedControl.module.css'
19+
import {clsx} from 'clsx'
20+
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
1221

1322
export type SegmentedControlButtonProps = {
1423
/** The visible label rendered in the button */
@@ -22,31 +31,40 @@ export type SegmentedControlButtonProps = {
2231
} & SxProp &
2332
ButtonHTMLAttributes<HTMLButtonElement | HTMLLIElement>
2433

25-
const SegmentedControlButtonStyled = styled.button`
26-
${getGlobalFocusStyles('-1px')};
27-
${sx};
28-
`
34+
const SegmentedControlButtonStyled = toggleStyledComponent(
35+
SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG,
36+
'button',
37+
styled.button`
38+
${getGlobalFocusStyles('-1px')};
39+
${sx};
40+
`,
41+
)
2942

3043
const SegmentedControlButton: React.FC<React.PropsWithChildren<SegmentedControlButtonProps>> = ({
3144
children,
3245
leadingIcon: LeadingIcon,
3346
selected,
3447
sx: sxProp = defaultSxProp,
48+
className,
3549
...rest
3650
}) => {
37-
const mergedSx = merge(getSegmentedControlListItemStyles(), sxProp as SxProp)
51+
const enabled = useFeatureFlag(SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG)
52+
const mergedSx = enabled ? sxProp : merge(getSegmentedControlListItemStyles(), sxProp as SxProp)
3853

3954
return (
40-
<Box as="li" sx={mergedSx}>
55+
<Box as="li" sx={mergedSx} className={clsx(enabled && classes.Item)} data-selected={selected || undefined}>
4156
<SegmentedControlButtonStyled
4257
aria-current={selected}
43-
sx={getSegmentedControlButtonStyles({selected, children})}
58+
sx={enabled ? undefined : getSegmentedControlButtonStyles({selected, children})}
59+
className={clsx(enabled && classes.Button, className)}
4460
type="button"
4561
{...rest}
4662
>
47-
<span className="segmentedControl-content">
63+
<span className={clsx(enabled ? classes.Content : 'segmentedControl-content')}>
4864
{LeadingIcon && <Box mr={1}>{isElement(LeadingIcon) ? LeadingIcon : <LeadingIcon />}</Box>}
49-
<Box className="segmentedControl-text">{children}</Box>
65+
<Box className={clsx(enabled ? classes.Text : 'segmentedControl-text')} data-text={children}>
66+
{children}
67+
</Box>
5068
</span>
5169
</SegmentedControlButtonStyled>
5270
</Box>

0 commit comments

Comments
 (0)