Skip to content

Commit

Permalink
[Radio][Joy] Integrate with form control (#34277)
Browse files Browse the repository at this point in the history
  • Loading branch information
siriwatknp committed Sep 12, 2022
1 parent f015784 commit a2cd245
Show file tree
Hide file tree
Showing 13 changed files with 212 additions and 22 deletions.
3 changes: 2 additions & 1 deletion docs/data/joy/components/radio/ExampleAlignmentButtons.js
Expand Up @@ -7,7 +7,7 @@ import FormatAlignJustifyIcon from '@mui/icons-material/FormatAlignJustify';
import FormatAlignLeftIcon from '@mui/icons-material/FormatAlignLeft';
import FormatAlignRightIcon from '@mui/icons-material/FormatAlignRight';

export default function RadioButtonsGroup() {
export default function ExampleAlignmentButtons() {
const [alignment, setAlignment] = React.useState('left');
return (
<RadioGroup
Expand All @@ -20,6 +20,7 @@ export default function RadioButtonsGroup() {
>
{['left', 'center', 'right', 'justify'].map((item) => (
<Box
key={item}
sx={(theme) => ({
position: 'relative',
display: 'flex',
Expand Down
3 changes: 2 additions & 1 deletion docs/data/joy/components/radio/ExampleSegmentedControls.js
Expand Up @@ -4,7 +4,7 @@ import Radio from '@mui/joy/Radio';
import RadioGroup from '@mui/joy/RadioGroup';
import Typography from '@mui/joy/Typography';

export default function RadioButtonsGroup() {
export default function ExampleSegmentedControls() {
const [justify, setJustify] = React.useState('flex-start');
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
Expand All @@ -28,6 +28,7 @@ export default function RadioButtonsGroup() {
>
{['flex-start', 'center', 'flex-end'].map((item) => (
<Radio
key={item}
color="neutral"
value={item}
disableIcon
Expand Down
53 changes: 53 additions & 0 deletions docs/data/joy/components/radio/ExampleTiers.js
@@ -0,0 +1,53 @@
import * as React from 'react';
import FormControl from '@mui/joy/FormControl';
import FormLabel from '@mui/joy/FormLabel';
import FormHelperText from '@mui/joy/FormHelperText';
import RadioGroup from '@mui/joy/RadioGroup';
import Radio from '@mui/joy/Radio';
import Sheet from '@mui/joy/Sheet';

export default function ExampleTiers() {
return (
<Sheet
variant="outlined"
sx={{
boxShadow: 'sm',
borderRadius: 'sm',
p: 1,
}}
>
<RadioGroup
name="tiers"
sx={{ gap: 1, '& > div': { p: 1, flexDirection: 'row', gap: 2 } }}
>
<FormControl>
<Radio overlay value="small" />
<div>
<FormLabel>Small</FormLabel>
<FormHelperText>
For light background jobs like sending email
</FormHelperText>
</div>
</FormControl>
<FormControl>
<Radio overlay value="medium" />
<div>
<FormLabel>Medium</FormLabel>
<FormHelperText>
For tasks like image resizing, exporting PDFs, etc.
</FormHelperText>
</div>
</FormControl>
<FormControl>
<Radio overlay value="large" />
<div>
<FormLabel>Large</FormLabel>
<FormHelperText>
For intensive tasks like video encoding, etc.
</FormHelperText>
</div>
</FormControl>
</RadioGroup>
</Sheet>
);
}
17 changes: 17 additions & 0 deletions docs/data/joy/components/radio/RadioButtonControl.js
@@ -0,0 +1,17 @@
import * as React from 'react';
import FormControl from '@mui/joy/FormControl';
import FormLabel from '@mui/joy/FormLabel';
import FormHelperText from '@mui/joy/FormHelperText';
import Radio from '@mui/joy/Radio';

export default function RadioButtonControl() {
return (
<FormControl sx={{ p: 2, flexDirection: 'row', gap: 2 }}>
<Radio overlay defaultChecked />
<div>
<FormLabel>Selection title</FormLabel>
<FormHelperText>One line description maximum lorem ipsum </FormHelperText>
</div>
</FormControl>
);
}
6 changes: 6 additions & 0 deletions docs/data/joy/components/radio/RadioButtonLabel.js
@@ -0,0 +1,6 @@
import * as React from 'react';
import Radio from '@mui/joy/Radio';

export default function RadioButtonLabel() {
return <Radio label="Text" defaultChecked />;
}
16 changes: 16 additions & 0 deletions docs/data/joy/components/radio/radio.md
Expand Up @@ -46,6 +46,16 @@ The `Radio` component supports every Joy UI global variant and it comes with `ou

{{"demo": "RadioButtons.js"}}

### Label

Use `label` prop to label the radio buttons.

{{"demo": "RadioButtonLabel.js"}}

For complex layout, compose a radio button with `FormControl`, `FormLabel`, and `FormHelperText` (optional).

{{"demo": "RadioButtonControl.js"}}

### Position

To swap the label and radio position, use the CSS property `flex-direction: row-reverse`.
Expand Down Expand Up @@ -125,6 +135,12 @@ Visit the [WAI-ARIA documentation](https://www.w3.org/WAI/ARIA/apg/patterns/radi

{{"demo": "ExampleSegmentedControls.js"}}

### Tiers

A clone of an [inspiration](https://dribbble.com/shots/11239824-Radio-button-groups) that demonstrate the composition of the components.

{{"demo": "ExampleTiers.js", "bg": true}}

### Alignment buttons

Provide an icon as a label to the `Radio` to make the radio buttons concise. You need to provide `aria-label` to the input slot for users who rely on screen readers.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/modules/components/JoyUsageDemo.tsx
Expand Up @@ -267,13 +267,13 @@ export default function JoyUsageDemo<T extends { [k: string]: any } = {}>({
if (knob === 'switch') {
return (
<FormControl
key={propName}
size="sm"
orientation="horizontal"
sx={{ justifyContent: 'space-between' }}
>
<FormLabel sx={{ textTransform: 'capitalize' }}>{propName}</FormLabel>
<Switch
key={propName}
checked={Boolean(resolvedValue)}
onChange={(event) =>
setProps((latestProps) => ({
Expand Down
2 changes: 1 addition & 1 deletion packages/mui-joy/src/Checkbox/Checkbox.tsx
Expand Up @@ -226,7 +226,7 @@ const Checkbox = React.forwardRef(function Checkbox(inProps, ref) {
}, [registerEffect]);
}

const id = useId(idOverride);
const id = useId(idOverride ?? formControl?.htmlFor);

const useCheckboxProps = {
checked: checkedProp,
Expand Down
73 changes: 71 additions & 2 deletions packages/mui-joy/src/FormControl/FormControl.test.tsx
Expand Up @@ -11,6 +11,7 @@ import Input, { inputClasses } from '@mui/joy/Input';
import Select, { selectClasses } from '@mui/joy/Select';
import Textarea, { textareaClasses } from '@mui/joy/Textarea';
import RadioGroup from '@mui/joy/RadioGroup';
import Radio, { radioClasses } from '@mui/joy/Radio';
import Switch, { switchClasses } from '@mui/joy/Switch';

describe('<FormControl />', () => {
Expand Down Expand Up @@ -184,10 +185,11 @@ describe('<FormControl />', () => {
});

describe('Checkbox', () => {
it('should linked the helper text', () => {
it('should linked the label and helper text', () => {
const { getByLabelText, getByText } = render(
<FormControl>
<Checkbox label="label" />
<FormLabel>label</FormLabel>
<Checkbox />
<FormHelperText>helper text</FormHelperText>
</FormControl>,
);
Expand Down Expand Up @@ -246,6 +248,73 @@ describe('<FormControl />', () => {
expect(getByRole('radiogroup')).to.have.attribute('aria-labelledby', label.id);
expect(getByRole('radiogroup')).to.have.attribute('aria-describedby', helperText.id);
});

it('works with radio buttons', () => {
const { getByLabelText, getByRole, getByText } = render(
<FormControl>
<FormLabel>label</FormLabel>
<RadioGroup>
<Radio />
</RadioGroup>
<FormHelperText>helper text</FormHelperText>
</FormControl>,
);

const label = getByText('label');
const helperText = getByText('helper text');

expect(getByRole('radio')).toBeVisible();
expect(getByLabelText('label')).to.have.attribute('role', 'radiogroup');
expect(getByRole('radiogroup')).to.have.attribute('aria-labelledby', label.id);
expect(getByRole('radiogroup')).to.have.attribute('aria-describedby', helperText.id);
});
});

describe('Radio', () => {
it('should linked the label and helper text', () => {
const { getByLabelText, getByText } = render(
<FormControl>
<FormLabel>label</FormLabel>
<Radio />
<FormHelperText>helper text</FormHelperText>
</FormControl>,
);

const helperText = getByText('helper text');

expect(getByLabelText('label')).to.have.attribute('aria-describedby', helperText.id);
});

it('should inherit color prop from FormControl', () => {
const { getByTestId } = render(
<FormControl color="success">
<Radio data-testid="radio" />
</FormControl>,
);

expect(getByTestId('radio')).to.have.class(radioClasses.colorSuccess);
});

it('should inherit error prop from FormControl', () => {
const { getByTestId } = render(
<FormControl error>
<Radio data-testid="radio" />
</FormControl>,
);

expect(getByTestId('radio')).to.have.class(radioClasses.colorDanger);
});

it('should inherit disabled from FormControl', () => {
const { getByLabelText, getByTestId } = render(
<FormControl disabled>
<Radio label="label" data-testid="radio" />
</FormControl>,
);

expect(getByTestId('radio')).to.have.class(radioClasses.disabled);
expect(getByLabelText('label')).to.have.attribute('disabled');
});
});

describe('Switch', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/mui-joy/src/FormControl/FormControl.tsx
Expand Up @@ -31,7 +31,7 @@ export const FormControlRoot = styled('div', {
overridesResolver: (props, styles) => styles.root,
})<{ ownerState: FormControlOwnerState }>(({ theme, ownerState }) => ({
'--FormLabel-margin':
ownerState.orientation === 'horizontal' ? '0 0.375rem 0 0' : '0 0 0.375rem 0',
ownerState.orientation === 'horizontal' ? '0 0.375rem 0 0' : '0 0 0.25rem 0',
'--FormHelperText-margin': '0.375rem 0 0 0',
'--FormLabel-asterisk-color': theme.vars.palette.danger[500],
'--FormHelperText-color': theme.vars.palette[ownerState.color!]?.[500],
Expand Down
4 changes: 4 additions & 0 deletions packages/mui-joy/src/FormHelperText/FormHelperText.tsx
Expand Up @@ -8,6 +8,7 @@ import { styled, useThemeProps } from '../styles';
import { FormHelperTextProps, FormHelperTextTypeMap } from './FormHelperTextProps';
import { getFormHelperTextUtilityClass } from './formHelperTextClasses';
import FormControlContext from '../FormControl/FormControlContext';
import formLabelClasses from '../FormLabel/formLabelClasses';

const useUtilityClasses = () => {
const slots = {
Expand All @@ -29,6 +30,9 @@ const FormHelperTextRoot = styled('p', {
lineHeight: theme.vars.lineHeight.sm,
color: `var(--FormHelperText-color, ${theme.vars.palette.text.secondary})`,
margin: 'var(--FormHelperText-margin, 0px)',
[`.${formLabelClasses.root} + &`]: {
'--FormHelperText-margin': '0px', // remove the margin if the helper text is next to the form label.
},
}));

const FormHelperText = React.forwardRef(function FormHelperText(inProps, ref) {
Expand Down
31 changes: 26 additions & 5 deletions packages/mui-joy/src/Radio/Radio.tsx
Expand Up @@ -10,6 +10,7 @@ import radioClasses, { getRadioUtilityClass } from './radioClasses';
import { RadioOwnerState, RadioTypeMap } from './RadioProps';
import RadioGroupContext from '../RadioGroup/RadioGroupContext';
import { TypographyContext } from '../Typography/Typography';
import FormControlContext from '../FormControl/FormControlContext';

const useUtilityClasses = (ownerState: RadioOwnerState) => {
const { checked, disabled, disableIcon, focusVisible, color, variant, size } = ownerState;
Expand Down Expand Up @@ -243,11 +244,30 @@ const Radio = React.forwardRef(function Radio(inProps, ref) {
value,
...other
} = props;
const id = useId(idOverride);

const formControl = React.useContext(FormControlContext);

if (process.env.NODE_ENV !== 'production') {
const registerEffect = formControl?.registerEffect;
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
if (registerEffect) {
return registerEffect();
}

return undefined;
}, [registerEffect]);
}

const id = useId(idOverride ?? formControl?.htmlFor);
const radioGroup = React.useContext(RadioGroupContext);
const activeColor = color || 'primary';
const inactiveColor = color || 'neutral';
const size = inProps.size || radioGroup?.size || sizeProp;
const activeColor = formControl?.error
? 'danger'
: inProps.color ?? formControl?.color ?? color ?? 'primary';
const inactiveColor = formControl?.error
? 'danger'
: inProps.color ?? formControl?.color ?? color ?? 'neutral';
const size = inProps.size || formControl?.size || radioGroup?.size || sizeProp;
const name = inProps.name || radioGroup?.name || nameProp;
const disableIcon = inProps.disableIcon || radioGroup?.disableIcon || disableIconProp;
const overlay = inProps.overlay || radioGroup?.overlay || overlayProp;
Expand All @@ -259,7 +279,7 @@ const Radio = React.forwardRef(function Radio(inProps, ref) {
const useRadioProps = {
checked: radioChecked,
defaultChecked,
disabled: disabledProp,
disabled: disabledProp ?? formControl?.disabled,
onBlur,
onChange,
onFocus,
Expand Down Expand Up @@ -326,6 +346,7 @@ const Radio = React.forwardRef(function Radio(inProps, ref) {
id,
name,
value: String(value),
'aria-describedby': formControl?.['aria-describedby'],
},
ownerState,
});
Expand Down
22 changes: 12 additions & 10 deletions packages/mui-joy/src/RadioGroup/RadioGroup.tsx
Expand Up @@ -140,16 +140,18 @@ const RadioGroup = React.forwardRef(function RadioGroup(inProps, ref) {
aria-describedby={formControl?.['aria-describedby']}
{...other}
>
{React.Children.map(children, (child, index) =>
React.isValidElement(child)
? React.cloneElement(child, {
// to let Radio knows when to apply margin(Inline|Block)Start
...(index === 0 && { 'data-first-child': '' }),
...(index === React.Children.count(children) - 1 && { 'data-last-child': '' }),
'data-parent': 'RadioGroup',
} as Record<string, string>)
: child,
)}
<FormControlContext.Provider value={undefined}>
{React.Children.map(children, (child, index) =>
React.isValidElement(child)
? React.cloneElement(child, {
// to let Radio knows when to apply margin(Inline|Block)Start
...(index === 0 && { 'data-first-child': '' }),
...(index === React.Children.count(children) - 1 && { 'data-last-child': '' }),
'data-parent': 'RadioGroup',
} as Record<string, string>)
: child,
)}
</FormControlContext.Provider>
</RadioGroupRoot>
</RadioGroupContext.Provider>
);
Expand Down

0 comments on commit a2cd245

Please sign in to comment.