Skip to content

Commit

Permalink
refactor(FormControl): update FormControl to use CSS Modules behind f…
Browse files Browse the repository at this point in the history
…lag (#5438)

* 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

* add missing sx prop

---------

Co-authored-by: Katie Langerman <[email protected]>
Co-authored-by: Hussam Ghazzi <[email protected]>
  • Loading branch information
3 people authored Dec 19, 2024
1 parent 83386f4 commit 4776d13
Show file tree
Hide file tree
Showing 12 changed files with 404 additions and 140 deletions.
5 changes: 5 additions & 0 deletions .changeset/gentle-stingrays-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

Update FormControl to use CSS Modules behind feature flag
57 changes: 57 additions & 0 deletions packages/react/src/FormControl/FormControl.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.ControlHorizontalLayout {
display: flex;

&:where([data-has-leading-visual]) {
align-items: center;
}
}

.ControlVerticalLayout {
display: flex;
flex-direction: column;
align-items: flex-start;

& > *:not(label) + * {
margin-top: var(--base-size-4);
}

&[data-has-label] > * + * {
margin-top: var(--base-size-4);
}
}

.ControlChoiceInputs > input {
margin-right: 0;
margin-left: 0;
}

.LabelContainer {
> * {
/* stylelint-disable-next-line primer/spacing */
padding-left: var(--stack-gap-condensed);
}

> label {
font-weight: var(--base-text-weight-normal);
}
}

.LeadingVisual {
margin-left: var(--base-size-8);
color: var(--fgColor-muted);

&:where([data-disabled]) {
color: var(--control-fgColor-disabled);
}

> * {
min-width: var(--text-body-size-large);
min-height: var(--text-body-size-large);
fill: currentColor;
}

> *:where([data-has-caption]) {
min-width: var(--base-size-24);
min-height: var(--base-size-24);
}
}
186 changes: 134 additions & 52 deletions packages/react/src/FormControl/FormControl.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {clsx} from 'clsx'
import React, {useContext} from 'react'
import Autocomplete from '../Autocomplete'
import Box from '../Box'
import Checkbox from '../Checkbox'
import Radio from '../Radio'
import Select from '../Select/Select'
Expand All @@ -10,7 +10,6 @@ import TextInputWithTokens from '../TextInputWithTokens'
import Textarea from '../Textarea'
import {CheckboxOrRadioGroupContext} from '../internal/components/CheckboxOrRadioGroup'
import ValidationAnimationContainer from '../internal/components/ValidationAnimationContainer'
import {get} from '../constants'
import {useSlots} from '../hooks/useSlots'
import type {SxProp} from '../sx'
import {useId} from '../hooks/useId'
Expand All @@ -20,6 +19,12 @@ import FormControlLeadingVisual from './FormControlLeadingVisual'
import FormControlValidation from './_FormControlValidation'
import {FormControlContextProvider} from './_FormControlContext'
import {warning} from '../utils/warning'
import styled from 'styled-components'
import sx from '../sx'
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
import {cssModulesFlag} from './feature-flags'
import {useFeatureFlag} from '../FeatureFlags'
import classes from './FormControl.module.css'

export type FormControlProps = {
children?: React.ReactNode
Expand All @@ -45,6 +50,7 @@ export type FormControlProps = {

const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
({children, disabled: disabledProp, layout = 'vertical', id: idProp, required, sx, className}, ref) => {
const enabled = useFeatureFlag(cssModulesFlag)
const [slots, childrenWithoutSlots] = useSlots(children, {
caption: FormControlCaption,
label: FormControlLabel,
Expand Down Expand Up @@ -127,69 +133,62 @@ const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
}}
>
{isChoiceInput || layout === 'horizontal' ? (
<Box
<StyledHorizontalLayout
ref={ref}
display="flex"
alignItems={slots.leadingVisual ? 'center' : undefined}
data-has-leading-visual={slots.leadingVisual ? '' : undefined}
sx={sx}
className={className}
className={clsx(className, {
[classes.ControlHorizontalLayout]: enabled,
})}
>
<Box sx={{'> input': {marginLeft: 0, marginRight: 0}}}>
{React.isValidElement(InputComponent) &&
React.cloneElement(
InputComponent as React.ReactElement<{
id: string
disabled: boolean
required: boolean
['aria-describedby']: string
}>,
{
id,
disabled,
// allow checkboxes to be required
required: required && !isRadioInput,
['aria-describedby']: captionId as string,
},
)}
<StyledChoiceInputs className={classes.ControlChoiceInputs}>
{React.isValidElement(InputComponent)
? React.cloneElement(
InputComponent as React.ReactElement<{
id: string
disabled: boolean
required: boolean
['aria-describedby']: string
}>,
{
id,
disabled,
// allow checkboxes to be required
required: required && !isRadioInput,
['aria-describedby']: captionId as string,
},
)
: null}
{childrenWithoutSlots.filter(
child =>
React.isValidElement(child) &&
![Checkbox, Radio].some(inputComponent => child.type === inputComponent),
)}
</Box>
{slots.leadingVisual && (
<Box
color={disabled ? 'fg.muted' : 'fg.default'}
sx={{
'> *': {
minWidth: slots.caption ? get('fontSizes.4') : get('fontSizes.2'),
minHeight: slots.caption ? get('fontSizes.4') : get('fontSizes.2'),
fill: 'currentColor',
},
}}
ml={2}
</StyledChoiceInputs>
{slots.leadingVisual ? (
<StyledLeadingVisual
className={clsx({
[classes.LeadingVisual]: enabled,
})}
data-disabled={disabled ? '' : undefined}
data-has-caption={slots.caption ? '' : undefined}
>
{slots.leadingVisual}
</Box>
)}
<Box
sx={{
'> *': {paddingLeft: 'var(--stack-gap-condensed)'},
'> label': {fontWeight: 'var(--base-text-weight-normal)'},
}}
>
</StyledLeadingVisual>
) : null}
<StyledLabelContainer className={classes.LabelContainer}>
{slots.label}
{slots.caption}
</Box>
</Box>
</StyledLabelContainer>
</StyledHorizontalLayout>
) : (
<Box
<StyledVerticalLayout
ref={ref}
display="flex"
flexDirection="column"
alignItems="flex-start"
sx={{...(isLabelHidden ? {'> *:not(label) + *': {marginTop: 1}} : {'> * + *': {marginTop: 1}}), ...sx}}
className={className}
data-has-label={!isLabelHidden ? '' : undefined}
sx={sx}
className={clsx(className, {
[classes.ControlVerticalLayout]: enabled,
})}
>
{slots.label}
{React.isValidElement(InputComponent) &&
Expand All @@ -215,13 +214,96 @@ const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
<ValidationAnimationContainer show>{slots.validation}</ValidationAnimationContainer>
) : null}
{slots.caption}
</Box>
</StyledVerticalLayout>
)}
</FormControlContextProvider>
)
},
)

const StyledHorizontalLayout = toggleStyledComponent(
cssModulesFlag,
'div',
styled.div`
display: flex;
&:where([data-has-leading-visual]) {
align-items: center;
}
${sx}
`,
)

const StyledChoiceInputs = toggleStyledComponent(
cssModulesFlag,
'div',
styled.div`
> input {
margin-left: 0;
margin-right: 0;
}
`,
)

const StyledLabelContainer = toggleStyledComponent(
cssModulesFlag,
'div',
styled.div`
> * {
padding-left: var(--stack-gap-condensed);
}
> label {
font-weight: var(--base-text-weight-normal);
}
`,
)

const StyledVerticalLayout = toggleStyledComponent(
cssModulesFlag,
'div',
styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
& > *:not(label) + * {
margin-top: var(--base-size-4);
}
&:where([data-has-label]) > * + * {
margin-top: var(--base-size-4);
}
${sx}
`,
)

const StyledLeadingVisual = toggleStyledComponent(
cssModulesFlag,
'div',
styled.div`
color: var(--fgColor-default);
margin-left: var(--base-size-8);
&:where([data-disabled]) {
color: var(--fgColor-muted);
}
> * {
fill: currentColor;
min-width: var(--text-body-size-large);
min-height: var(--text-body-size-large);
}
> *:where([data-has-caption]) {
min-width: var(--base-size-24);
min-height: var(--base-size-24);
}
`,
)

export default Object.assign(FormControl, {
Caption: FormControlCaption,
Label: FormControlLabel,
Expand Down
9 changes: 9 additions & 0 deletions packages/react/src/FormControl/FormControlCaption.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.Caption {
display: block;
font-size: var(--text-body-size-small);
color: var(--fgColor-muted);

&:where([data-control-disabled]) {
color: var(--control-fgColor-disabled);
}
}
50 changes: 33 additions & 17 deletions packages/react/src/FormControl/FormControlCaption.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import {clsx} from 'clsx'
import React from 'react'
import type {SxProp} from '../sx'
import {useFormControlContext} from './_FormControlContext'
import Text from '../Text'
import styled from 'styled-components'
import {get} from '../constants'
import {cssModulesFlag} from './feature-flags'
import {useFeatureFlag} from '../FeatureFlags'
import Text from '../Text'
import sx from '../sx'

const StyledCaption = styled(Text)`
color: var(--fgColor-muted);
display: block;
font-size: ${get('fontSizes.0')};
&:where([data-control-disabled]) {
color: var(--control-fgColor-disabled);
}
${sx}
`
import type {SxProp} from '../sx'
import classes from './FormControlCaption.module.css'
import {useFormControlContext} from './_FormControlContext'
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'

type FormControlCaptionProps = React.PropsWithChildren<
{
Expand All @@ -25,12 +17,36 @@ type FormControlCaptionProps = React.PropsWithChildren<
>

function FormControlCaption({id, children, sx}: FormControlCaptionProps) {
const enabled = useFeatureFlag(cssModulesFlag)
const {captionId, disabled} = useFormControlContext()
return (
<StyledCaption id={id ?? captionId} data-control-disabled={disabled ? '' : undefined} sx={sx}>
<StyledCaption
id={id ?? captionId}
className={clsx({
[classes.Caption]: enabled,
})}
data-control-disabled={disabled ? '' : undefined}
sx={sx}
>
{children}
</StyledCaption>
)
}

const StyledCaption = toggleStyledComponent(
cssModulesFlag,
Text,
styled(Text)`
color: var(--fgColor-muted);
display: block;
font-size: var(--text-body-size-small);
&:where([data-control-disabled]) {
color: var(--control-fgColor-disabled);
}
${sx}
`,
)

export {FormControlCaption}
Loading

0 comments on commit 4776d13

Please sign in to comment.