diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.js b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.js index 389318108e4..8af44251879 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.js +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.js @@ -1,20 +1,18 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import get from 'lodash/get'; import FormControl from '@material-ui/core/FormControl'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; import FormHelperText from '@material-ui/core/FormHelperText'; import InputLabel from '@material-ui/core/InputLabel'; import RadioGroup from '@material-ui/core/RadioGroup'; -import Radio from '@material-ui/core/Radio'; -import { withStyles, createStyles } from '@material-ui/core/styles'; -import compose from 'recompose/compose'; -import { addField, FieldTitle, useTranslate } from 'ra-core'; +import { makeStyles } from '@material-ui/core/styles'; +import get from 'lodash/get'; +import { useInput, FieldTitle } from 'ra-core'; import sanitizeRestProps from './sanitizeRestProps'; import InputHelperText from './InputHelperText'; +import RadioButtonGroupInputItem from './RadioButtonGroupInputItem'; -const styles = createStyles({ +const useStyles = makeStyles({ label: { position: 'relative', }, @@ -78,70 +76,41 @@ const styles = createStyles({ * The object passed as `options` props is passed to the material-ui component */ export const RadioButtonGroupInput = ({ - classes, - className, - label, - resource, - source, - input, - isRequired, choices, - options, - meta, helperText, + label, + onBlur, + onChange, + onFocus, + options, optionText, optionValue, + resource, + source, translateChoice, + validate, ...rest }) => { - const translate = useTranslate(); + const classes = useStyles({}); - const handleChange = useCallback( - (event, value) => { - input.onChange(value); - }, - [input] - ); - - const renderRadioButton = useCallback( - choice => { - const choiceName = React.isValidElement(optionText) // eslint-disable-line no-nested-ternary - ? React.cloneElement(optionText, { record: choice }) - : typeof optionText === 'function' - ? optionText(choice) - : get(choice, optionText); - - const nodeId = `${source}_${get(choice, optionValue)}`; - - return ( - } - label={ - translateChoice - ? translate(choiceName, { _: choiceName }) - : choiceName - } - /> - ); - }, - [optionText, optionValue, source, translate, translateChoice] - ); - - if (typeof meta === 'undefined') { - throw new Error( - "The RadioButtonGroupInput component wasn't called within a redux-form . Did you decorate it and forget to add the addField prop to your component? See https://marmelab.com/react-admin/Inputs.html#writing-your-own-input-component for details." - ); - } - - const { touched, error } = meta; + const { + id, + input, + isRequired, + meta: { error, touched }, + } = useInput({ + onBlur, + onChange, + onFocus, + resource, + source, + validate, + ...rest, + }); return ( @@ -154,13 +123,17 @@ export const RadioButtonGroupInput = ({ /> - - {choices.map(renderRadioButton)} + + {choices.map(choice => ( + + ))} {helperText || (touched && error) ? ( @@ -177,10 +150,6 @@ export const RadioButtonGroupInput = ({ RadioButtonGroupInput.propTypes = { choices: PropTypes.arrayOf(PropTypes.object), - classes: PropTypes.object, - className: PropTypes.string, - input: PropTypes.object, - isRequired: PropTypes.bool, label: PropTypes.string, options: PropTypes.object, optionText: PropTypes.oneOfType([ @@ -191,13 +160,10 @@ RadioButtonGroupInput.propTypes = { optionValue: PropTypes.string.isRequired, resource: PropTypes.string, source: PropTypes.string, - translate: PropTypes.func.isRequired, translateChoice: PropTypes.bool.isRequired, - meta: PropTypes.object, }; RadioButtonGroupInput.defaultProps = { - classes: {}, choices: [], options: {}, optionText: 'name', @@ -205,7 +171,4 @@ RadioButtonGroupInput.defaultProps = { translateChoice: true, }; -export default compose( - addField, - withStyles(styles) -)(RadioButtonGroupInput); +export default RadioButtonGroupInput; diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.js b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.js index 6b5c21a3e66..17551b6ab22 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.js +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.js @@ -1,123 +1,157 @@ import React from 'react'; import expect from 'expect'; -import { render, cleanup } from '@testing-library/react'; - -import { RadioButtonGroupInput } from './RadioButtonGroupInput'; +import { render, cleanup, fireEvent } from '@testing-library/react'; +import { Form } from 'react-final-form'; import { TranslationContext } from 'ra-core'; +import RadioButtonGroupInput from './RadioButtonGroupInput'; + describe('', () => { const defaultProps = { - resource: 'bar', - source: 'foo', - meta: {}, - input: {}, - translate: x => x, + resource: 'creditcards', + source: 'type', + choices: [ + { id: 'visa', name: 'VISA' }, + { id: 'mastercard', name: 'Mastercard' }, + ], }; afterEach(cleanup); it('should render choices as radio inputs', () => { const { getByLabelText, queryByText } = render( - ( + + )} /> ); - expect(queryByText('hello')).not.toBeNull(); - const input1 = getByLabelText('Male'); + expect(queryByText('Credit card')).not.toBeNull(); + const input1 = getByLabelText('VISA'); expect(input1.type).toBe('radio'); - expect(input1.name).toBe('foo'); + expect(input1.name).toBe('type'); expect(input1.checked).toBeFalsy(); - const input2 = getByLabelText('Female'); + const input2 = getByLabelText('Mastercard'); expect(input2.type).toBe('radio'); - expect(input2.name).toBe('foo'); + expect(input2.name).toBe('type'); expect(input2.checked).toBeFalsy(); }); it('should use the input parameter value as the initial input value', () => { const { getByLabelText } = render( - } /> ); - expect(getByLabelText('Male').checked).toBeFalsy(); - expect(getByLabelText('Female').checked).toBeTruthy(); + expect(getByLabelText('VISA').checked).toBeFalsy(); + expect(getByLabelText('Mastercard').checked).toBeTruthy(); }); it('should use optionValue as value identifier', () => { const { getByLabelText } = render( - ( + + )} /> ); - expect(getByLabelText('Male').value).toBe('M'); + expect(getByLabelText('Mastercard').value).toBe('mc'); }); it('should use optionValue including "." as value identifier', () => { const { getByLabelText } = render( - ( + + )} /> ); - expect(getByLabelText('Male').value).toBe('M'); + expect(getByLabelText('Mastercard').value).toBe('mc'); }); it('should use optionText with a string value as text identifier', () => { const { queryByText } = render( - ( + + )} /> ); - expect(queryByText('Male')).not.toBeNull(); + expect(queryByText('Mastercard')).not.toBeNull(); }); it('should use optionText with a string value including "." as text identifier', () => { const { queryByText } = render( - ( + + )} /> ); - expect(queryByText('Male')).not.toBeNull(); + expect(queryByText('Mastercard')).not.toBeNull(); }); it('should use optionText with a function value as text identifier', () => { const { queryByText } = render( - choice.foobar} - choices={[{ id: 'M', foobar: 'Male' }]} +
( + choice.longname} + choices={[{ id: 'mc', longname: 'Mastercard' }]} + /> + )} /> ); - expect(queryByText('Male')).not.toBeNull(); + expect(queryByText('Mastercard')).not.toBeNull(); }); it('should use optionText with an element value as text identifier', () => { - const Foobar = ({ record }) => {record.foobar}; + const Foobar = ({ record }) => {record.longname}; const { queryByText } = render( - } - choices={[{ id: 'M', foobar: 'Male' }]} + ( + } + choices={[{ id: 'mc', longname: 'Mastercard' }]} + /> + )} /> ); - expect(queryByText('Male')).not.toBeNull(); + expect(queryByText('Mastercard')).not.toBeNull(); }); it('should translate the choices by default', () => { @@ -127,13 +161,18 @@ describe('', () => { translate: x => `**${x}**`, }} > - ( + + )} /> ); - expect(queryByText('**Male**')).not.toBeNull(); + expect(queryByText('**Mastercard**')).not.toBeNull(); }); it('should not translate the choices if translateChoice is false', () => { @@ -143,22 +182,32 @@ describe('', () => { translate: x => `**${x}**`, }} > - ( + + )} /> ); - expect(queryByText('**Male**')).toBeNull(); - expect(queryByText('Male')).not.toBeNull(); + expect(queryByText('**Mastercard**')).toBeNull(); + expect(queryByText('Mastercard')).not.toBeNull(); }); it('should displayed helperText if prop is present in meta', () => { const { queryByText } = render( - ( + + )} /> ); expect(queryByText('Can I help you?')).not.toBeNull(); @@ -166,37 +215,74 @@ describe('', () => { describe('error message', () => { it('should not be displayed if field is pristine', () => { + // This validator always returns an error + const validate = () => 'ra.validation.error'; + const { queryByText } = render( - ( + + )} /> ); - expect(queryByText('Required field.')).toBeNull(); + expect(queryByText('ra.validation.required')).toBeNull(); }); it('should be displayed if field has been touched and is invalid', () => { - const { getByText } = render( - 'ra.validation.error'; + + const { getByLabelText, getByText } = render( + ( + + )} /> ); - expect(getByText('Required field.')).toBeDefined(); + + const input = getByLabelText('Mastercard'); + fireEvent.click(input); + expect(input.checked).toBe(true); + + fireEvent.blur(input); + + expect(getByText('ra.validation.error')).toBeDefined(); }); it('should be displayed even with an helper Text', () => { - const { getByText, queryByText } = render( - 'ra.validation.error'; + + const { getByLabelText, getByText, queryByText } = render( + ( + + )} /> ); - expect(getByText('Required field.')).toBeDefined(); + const input = getByLabelText('Mastercard'); + fireEvent.click(input); + expect(input.checked).toBe(true); + + fireEvent.blur(input); + + expect(getByText('ra.validation.error')).toBeDefined(); expect(queryByText('Can I help you?')).toBeNull(); }); }); diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInputItem.js b/packages/ra-ui-materialui/src/input/RadioButtonGroupInputItem.js new file mode 100644 index 00000000000..439bde7a7df --- /dev/null +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInputItem.js @@ -0,0 +1,38 @@ +import React, { isValidElement, cloneElement } from 'react'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Radio from '@material-ui/core/Radio'; +import get from 'lodash/get'; +import { useTranslate } from 'ra-core'; + +const RadioButtonGroupInputItem = ({ + choice, + optionText, + optionValue, + source, + translateChoice, +}) => { + const translate = useTranslate(); + + const choiceName = isValidElement(optionText) // eslint-disable-line no-nested-ternary + ? cloneElement(optionText, { record: choice }) + : typeof optionText === 'function' + ? optionText(choice) + : get(choice, optionText); + + const nodeId = `${source}_${get(choice, optionValue)}`; + + return ( + } + label={ + translateChoice + ? translate(choiceName, { _: choiceName }) + : choiceName + } + /> + ); +}; + +export default RadioButtonGroupInputItem;