Skip to content

Commit 204ded0

Browse files
refactor(FormControl): update to CSS Modules behind feature flag (#5370)
* refactor: move form control test files into folder * refactor(FormControl): update FormControl to use CSS Modules behind flag * refactor(InputLabel): update InputLabel to use CSS Modules behind feature flag * refactor(FormControl): update Caption to use CSS Modules behind feature flag * refactor(InputValidation): refactor InputValidation to CSS Modules behind feature flag * chore: add changeset * Apply suggestions from code review Co-authored-by: Katie Langerman <[email protected]> * chore: update stylelint violations * fix: update selector for leading visual --------- Co-authored-by: Katie Langerman <[email protected]>
1 parent 2c79798 commit 204ded0

12 files changed

+403
-140
lines changed

.changeset/gentle-stingrays-search.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+
Update FormControl to use CSS Modules behind feature flag
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
.ControlHorizontalLayout {
2+
display: flex;
3+
4+
&:where([data-has-leading-visual]) {
5+
align-items: center;
6+
}
7+
}
8+
9+
.ControlVerticalLayout {
10+
display: flex;
11+
flex-direction: column;
12+
align-items: flex-start;
13+
14+
& > *:not(label) + * {
15+
margin-top: var(--base-size-4);
16+
}
17+
18+
&[data-has-label] > * + * {
19+
margin-top: var(--base-size-4);
20+
}
21+
}
22+
23+
.ControlChoiceInputs > input {
24+
margin-right: 0;
25+
margin-left: 0;
26+
}
27+
28+
.LabelContainer {
29+
> * {
30+
/* stylelint-disable-next-line primer/spacing */
31+
padding-left: var(--stack-gap-condensed);
32+
}
33+
34+
> label {
35+
font-weight: var(--base-text-weight-normal);
36+
}
37+
}
38+
39+
.LeadingVisual {
40+
margin-left: var(--base-size-8);
41+
color: var(--fgColor-muted);
42+
43+
&:where([data-disabled]) {
44+
color: var(--control-fgColor-disabled);
45+
}
46+
47+
> * {
48+
min-width: var(--text-body-size-large);
49+
min-height: var(--text-body-size-large);
50+
fill: currentColor;
51+
}
52+
53+
> *:where([data-has-caption]) {
54+
min-width: var(--base-size-24);
55+
min-height: var(--base-size-24);
56+
}
57+
}

packages/react/src/FormControl/FormControl.tsx

Lines changed: 133 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import {clsx} from 'clsx'
12
import React, {useContext} from 'react'
23
import Autocomplete from '../Autocomplete'
3-
import Box from '../Box'
44
import Checkbox from '../Checkbox'
55
import Radio from '../Radio'
66
import Select from '../Select/Select'
@@ -10,7 +10,6 @@ import TextInputWithTokens from '../TextInputWithTokens'
1010
import Textarea from '../Textarea'
1111
import {CheckboxOrRadioGroupContext} from '../internal/components/CheckboxOrRadioGroup'
1212
import ValidationAnimationContainer from '../internal/components/ValidationAnimationContainer'
13-
import {get} from '../constants'
1413
import {useSlots} from '../hooks/useSlots'
1514
import type {SxProp} from '../sx'
1615
import {useId} from '../hooks/useId'
@@ -20,6 +19,12 @@ import FormControlLeadingVisual from './FormControlLeadingVisual'
2019
import FormControlValidation from './_FormControlValidation'
2120
import {FormControlContextProvider} from './_FormControlContext'
2221
import {warning} from '../utils/warning'
22+
import styled from 'styled-components'
23+
import sx from '../sx'
24+
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
25+
import {cssModulesFlag} from './feature-flags'
26+
import {useFeatureFlag} from '../FeatureFlags'
27+
import classes from './FormControl.module.css'
2328

2429
export type FormControlProps = {
2530
children?: React.ReactNode
@@ -45,6 +50,7 @@ export type FormControlProps = {
4550

4651
const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
4752
({children, disabled: disabledProp, layout = 'vertical', id: idProp, required, sx, className}, ref) => {
53+
const enabled = useFeatureFlag(cssModulesFlag)
4854
const [slots, childrenWithoutSlots] = useSlots(children, {
4955
caption: FormControlCaption,
5056
label: FormControlLabel,
@@ -127,69 +133,61 @@ const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
127133
}}
128134
>
129135
{isChoiceInput || layout === 'horizontal' ? (
130-
<Box
136+
<StyledHorizontalLayout
131137
ref={ref}
132-
display="flex"
133-
alignItems={slots.leadingVisual ? 'center' : undefined}
138+
data-has-leading-visual={slots.leadingVisual ? '' : undefined}
134139
sx={sx}
135-
className={className}
140+
className={clsx(className, {
141+
[classes.ControlHorizontalLayout]: enabled,
142+
})}
136143
>
137-
<Box sx={{'> input': {marginLeft: 0, marginRight: 0}}}>
138-
{React.isValidElement(InputComponent) &&
139-
React.cloneElement(
140-
InputComponent as React.ReactElement<{
141-
id: string
142-
disabled: boolean
143-
required: boolean
144-
['aria-describedby']: string
145-
}>,
146-
{
147-
id,
148-
disabled,
149-
// allow checkboxes to be required
150-
required: required && !isRadioInput,
151-
['aria-describedby']: captionId as string,
152-
},
153-
)}
144+
<StyledChoiceInputs className={classes.ControlChoiceInputs}>
145+
{React.isValidElement(InputComponent)
146+
? React.cloneElement(
147+
InputComponent as React.ReactElement<{
148+
id: string
149+
disabled: boolean
150+
required: boolean
151+
['aria-describedby']: string
152+
}>,
153+
{
154+
id,
155+
disabled,
156+
// allow checkboxes to be required
157+
required: required && !isRadioInput,
158+
['aria-describedby']: captionId as string,
159+
},
160+
)
161+
: null}
154162
{childrenWithoutSlots.filter(
155163
child =>
156164
React.isValidElement(child) &&
157165
![Checkbox, Radio].some(inputComponent => child.type === inputComponent),
158166
)}
159-
</Box>
160-
{slots.leadingVisual && (
161-
<Box
162-
color={disabled ? 'fg.muted' : 'fg.default'}
163-
sx={{
164-
'> *': {
165-
minWidth: slots.caption ? get('fontSizes.4') : get('fontSizes.2'),
166-
minHeight: slots.caption ? get('fontSizes.4') : get('fontSizes.2'),
167-
fill: 'currentColor',
168-
},
169-
}}
170-
ml={2}
167+
</StyledChoiceInputs>
168+
{slots.leadingVisual ? (
169+
<StyledLeadingVisual
170+
className={clsx({
171+
[classes.LeadingVisual]: enabled,
172+
})}
173+
data-disabled={disabled ? '' : undefined}
174+
data-has-caption={slots.caption ? '' : undefined}
171175
>
172176
{slots.leadingVisual}
173-
</Box>
174-
)}
175-
<Box
176-
sx={{
177-
'> *': {paddingLeft: 'var(--stack-gap-condensed)'},
178-
'> label': {fontWeight: 'var(--base-text-weight-normal)'},
179-
}}
180-
>
177+
</StyledLeadingVisual>
178+
) : null}
179+
<StyledLabelContainer className={classes.LabelContainer}>
181180
{slots.label}
182181
{slots.caption}
183-
</Box>
184-
</Box>
182+
</StyledLabelContainer>
183+
</StyledHorizontalLayout>
185184
) : (
186-
<Box
185+
<StyledVerticalLayout
187186
ref={ref}
188-
display="flex"
189-
flexDirection="column"
190-
alignItems="flex-start"
191-
sx={{...(isLabelHidden ? {'> *:not(label) + *': {marginTop: 1}} : {'> * + *': {marginTop: 1}}), ...sx}}
192-
className={className}
187+
data-has-label={!isLabelHidden ? '' : undefined}
188+
className={clsx(className, {
189+
[classes.ControlVerticalLayout]: enabled,
190+
})}
193191
>
194192
{slots.label}
195193
{React.isValidElement(InputComponent) &&
@@ -215,13 +213,96 @@ const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
215213
<ValidationAnimationContainer show>{slots.validation}</ValidationAnimationContainer>
216214
) : null}
217215
{slots.caption}
218-
</Box>
216+
</StyledVerticalLayout>
219217
)}
220218
</FormControlContextProvider>
221219
)
222220
},
223221
)
224222

223+
const StyledHorizontalLayout = toggleStyledComponent(
224+
cssModulesFlag,
225+
'div',
226+
styled.div`
227+
display: flex;
228+
229+
&:where([data-has-leading-visual]) {
230+
align-items: center;
231+
}
232+
233+
${sx}
234+
`,
235+
)
236+
237+
const StyledChoiceInputs = toggleStyledComponent(
238+
cssModulesFlag,
239+
'div',
240+
styled.div`
241+
> input {
242+
margin-left: 0;
243+
margin-right: 0;
244+
}
245+
`,
246+
)
247+
248+
const StyledLabelContainer = toggleStyledComponent(
249+
cssModulesFlag,
250+
'div',
251+
styled.div`
252+
> * {
253+
padding-left: var(--stack-gap-condensed);
254+
}
255+
256+
> label {
257+
font-weight: var(--base-text-weight-normal);
258+
}
259+
`,
260+
)
261+
262+
const StyledVerticalLayout = toggleStyledComponent(
263+
cssModulesFlag,
264+
'div',
265+
styled.div`
266+
display: flex;
267+
flex-direction: column;
268+
align-items: flex-start;
269+
270+
& > *:not(label) + * {
271+
margin-top: var(--base-size-4);
272+
}
273+
274+
&:where([data-has-label]) > * + * {
275+
margin-top: var(--base-size-4);
276+
}
277+
278+
${sx}
279+
`,
280+
)
281+
282+
const StyledLeadingVisual = toggleStyledComponent(
283+
cssModulesFlag,
284+
'div',
285+
styled.div`
286+
color: var(--fgColor-default);
287+
margin-left: var(--base-size-8);
288+
289+
&:where([data-disabled]) {
290+
color: var(--fgColor-muted);
291+
}
292+
293+
> * {
294+
fill: currentColor;
295+
min-width: var(--text-body-size-large);
296+
min-height: var(--text-body-size-large);
297+
}
298+
299+
> *:where([data-has-caption]) {
300+
min-width: var(--base-size-24);
301+
min-height: var(--base-size-24);
302+
}
303+
`,
304+
)
305+
225306
export default Object.assign(FormControl, {
226307
Caption: FormControlCaption,
227308
Label: FormControlLabel,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.Caption {
2+
display: block;
3+
font-size: var(--text-body-size-small);
4+
color: var(--fgColor-muted);
5+
6+
&:where([data-control-disabled]) {
7+
color: var(--control-fgColor-disabled);
8+
}
9+
}
Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1+
import {clsx} from 'clsx'
12
import React from 'react'
2-
import type {SxProp} from '../sx'
3-
import {useFormControlContext} from './_FormControlContext'
4-
import Text from '../Text'
53
import styled from 'styled-components'
6-
import {get} from '../constants'
4+
import {cssModulesFlag} from './feature-flags'
5+
import {useFeatureFlag} from '../FeatureFlags'
6+
import Text from '../Text'
77
import sx from '../sx'
8-
9-
const StyledCaption = styled(Text)`
10-
color: var(--fgColor-muted);
11-
display: block;
12-
font-size: ${get('fontSizes.0')};
13-
14-
&:where([data-control-disabled]) {
15-
color: var(--control-fgColor-disabled);
16-
}
17-
18-
${sx}
19-
`
8+
import type {SxProp} from '../sx'
9+
import classes from './FormControlCaption.module.css'
10+
import {useFormControlContext} from './_FormControlContext'
11+
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
2012

2113
type FormControlCaptionProps = React.PropsWithChildren<
2214
{
@@ -25,12 +17,36 @@ type FormControlCaptionProps = React.PropsWithChildren<
2517
>
2618

2719
function FormControlCaption({id, children, sx}: FormControlCaptionProps) {
20+
const enabled = useFeatureFlag(cssModulesFlag)
2821
const {captionId, disabled} = useFormControlContext()
2922
return (
30-
<StyledCaption id={id ?? captionId} data-control-disabled={disabled ? '' : undefined} sx={sx}>
23+
<StyledCaption
24+
id={id ?? captionId}
25+
className={clsx({
26+
[classes.Caption]: enabled,
27+
})}
28+
data-control-disabled={disabled ? '' : undefined}
29+
sx={sx}
30+
>
3131
{children}
3232
</StyledCaption>
3333
)
3434
}
3535

36+
const StyledCaption = toggleStyledComponent(
37+
cssModulesFlag,
38+
Text,
39+
styled(Text)`
40+
color: var(--fgColor-muted);
41+
display: block;
42+
font-size: var(--text-body-size-small);
43+
44+
&:where([data-control-disabled]) {
45+
color: var(--control-fgColor-disabled);
46+
}
47+
48+
${sx}
49+
`,
50+
)
51+
3652
export {FormControlCaption}

0 commit comments

Comments
 (0)