Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Radio][Joy] Integrate with form control #34277

Merged
merged 7 commits into from Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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