Skip to content

Commit 7363465

Browse files
authored
CSS customization for React Vanilla radio group
Adds customizable CSS classes to the React Vanilla radio group. Also refactors it to a functional component and includes test cases.
1 parent d5a9038 commit 7363465

File tree

3 files changed

+146
-71
lines changed

3 files changed

+146
-71
lines changed

packages/vanilla/src/controls/RadioGroup.tsx

Lines changed: 81 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -26,84 +26,95 @@ import React from 'react';
2626
import {
2727
computeLabel,
2828
ControlProps,
29-
ControlState,
3029
isDescriptionHidden,
3130
OwnPropsOfEnum
3231
} from '@jsonforms/core';
33-
import { Control } from '@jsonforms/react';
3432
import { VanillaRendererProps } from '../index';
33+
import { findStyleAsClassName } from '../reducers/styling';
34+
import { useStyles } from '../styles';
3535
import merge from 'lodash/merge';
36+
import { useMemo, useState } from 'react';
3637

37-
export class RadioGroup extends Control<
38-
ControlProps & VanillaRendererProps & OwnPropsOfEnum,
39-
ControlState
40-
> {
41-
render() {
42-
const {
43-
classNames,
44-
id,
45-
label,
46-
options,
47-
required,
48-
description,
49-
errors,
50-
data,
51-
uischema,
52-
visible,
53-
config,
54-
enabled
55-
} = this.props;
56-
const isValid = errors.length === 0;
57-
const divClassNames = `validation ${isValid ? classNames.description : 'validation_error'
58-
}`;
59-
const groupStyle: { [x: string]: any } = {
38+
export const RadioGroup = ({
39+
classNames,
40+
id,
41+
label,
42+
options,
43+
required,
44+
description,
45+
errors,
46+
data,
47+
uischema,
48+
visible,
49+
config,
50+
enabled,
51+
path,
52+
handleChange
53+
}: ControlProps & VanillaRendererProps & OwnPropsOfEnum) => {
54+
const contextStyles = useStyles();
55+
const [isFocused, setFocus] = useState(false);
56+
const radioControl = useMemo(() => findStyleAsClassName(contextStyles)('control.radio'), [contextStyles]);
57+
const radioOption = useMemo(() => findStyleAsClassName(contextStyles)('control.radio.option'), [contextStyles]);
58+
const radioInput = useMemo(() => findStyleAsClassName(contextStyles)('control.radio.input'), [contextStyles]);
59+
const radioLabel = useMemo(() => findStyleAsClassName(contextStyles)('control.radio.label'), [contextStyles]);
60+
const isValid = errors.length === 0;
61+
const divClassNames = `validation ${isValid ? classNames.description : 'validation_error' }`;
62+
const appliedUiSchemaOptions = merge({}, config, uischema.options);
63+
const showDescription = !isDescriptionHidden(
64+
visible,
65+
description,
66+
isFocused,
67+
appliedUiSchemaOptions.showUnfocusedDescription
68+
);
69+
const hasRadioClass = !radioControl || radioControl === 'radio';
70+
let groupStyle: { [x: string]: any } = {};
71+
if (hasRadioClass) {
72+
groupStyle = {
6073
display: 'flex',
6174
flexDirection: 'row'
6275
};
76+
}
77+
return (
78+
<div
79+
className={classNames.wrapper}
80+
hidden={!visible}
81+
onFocus={() => setFocus(true)}
82+
onBlur={() => setFocus(false)}
83+
>
84+
<label htmlFor={id} className={classNames.label}>
85+
{computeLabel(
86+
label,
87+
required,
88+
appliedUiSchemaOptions.hideRequiredAsterisk
89+
)}
90+
</label>
6391

64-
const appliedUiSchemaOptions = merge({}, config, uischema.options);
65-
const showDescription = !isDescriptionHidden(
66-
visible,
67-
description,
68-
this.state.isFocused,
69-
appliedUiSchemaOptions.showUnfocusedDescription
70-
);
71-
72-
return (
73-
<div
74-
className={classNames.wrapper}
75-
hidden={!visible}
76-
onFocus={this.onFocus}
77-
onBlur={this.onBlur}
78-
>
79-
<label htmlFor={id} className={classNames.label}>
80-
{computeLabel(
81-
label,
82-
required,
83-
appliedUiSchemaOptions.hideRequiredAsterisk
84-
)}
85-
</label>
86-
87-
<div style={groupStyle}>
88-
{options.map(option => (
89-
<div key={option.label}>
90-
<input
91-
type='radio'
92-
value={option.value}
93-
id={option.value}
94-
name={id}
95-
checked={data === option.value}
96-
onChange={ev => this.handleChange(ev.currentTarget.value)}
97-
disabled={!enabled}
98-
/>
99-
<label htmlFor={option.value}>{option.label}</label>
100-
</div>
101-
))}
102-
</div>
103-
<div className={divClassNames}>
104-
{!isValid ? errors : showDescription ? description : null}
105-
</div>
92+
<div className={radioControl} style={groupStyle}>
93+
{options.map(option => (
94+
<div key={option.label} className={radioOption}>
95+
<input
96+
type='radio'
97+
value={option.value}
98+
id={option.value}
99+
name={id}
100+
checked={data === option.value}
101+
onChange={ev => handleChange(path, ev.currentTarget.value)}
102+
disabled={!enabled}
103+
className={radioInput}
104+
/>
105+
<label
106+
htmlFor={option.value}
107+
className={radioLabel}
108+
>
109+
{option.label}
110+
</label>
111+
</div>
112+
))}
106113
</div>
107-
);
108-
}
109-
}
114+
<div className={divClassNames}>
115+
{!isValid ? errors : showDescription ? description : null}
116+
</div>
117+
</div>
118+
);
119+
};
120+

packages/vanilla/src/styles/styles.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,22 @@ export const vanillaStyles: StyleDef[] = [
5353
name: 'control.select',
5454
classNames: ['select']
5555
},
56+
{
57+
name: 'control.radio',
58+
classNames: ['radio']
59+
},
60+
{
61+
name: 'control.radio.option',
62+
classNames: ['radio-option']
63+
},
64+
{
65+
name: 'control.radio.input',
66+
classNames: ['radio-input']
67+
},
68+
{
69+
name: 'control.radio.label',
70+
classNames: ['radio-label']
71+
},
5672
{
5773
name: 'control.validation.error',
5874
classNames: ['validation_error']

packages/vanilla/test/renderers/RadioGroupControl.test.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import * as _ from 'lodash';
3333
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
3434
import Enzyme, { mount, ReactWrapper } from 'enzyme';
3535
import '../../src';
36-
import { RadioGroupControl, OneOfRadioGroupControl } from '../../src';
36+
import { vanillaStyles, RadioGroupControl, OneOfRadioGroupControl, JsonFormsStyleContext } from '../../src';
3737
import { initCore } from '../util';
3838

3939
Enzyme.configure({ adapter: new Adapter() });
@@ -99,6 +99,54 @@ describe('Radio group control', () => {
9999
expect((currentlyChecked.getDOMNode() as HTMLInputElement).value).toBe('D');
100100
});
101101

102+
test('render enum with CSS classes', () => {
103+
const renderers = [{ tester: rankWith(10, isEnumControl), renderer: RadioGroupControl }];
104+
const core = initCore(fixture.schema, fixture.uischema, fixture.data);
105+
wrapper = mount(
106+
<JsonFormsStateProvider initState={{ core, renderers }}>
107+
<RadioGroupControl schema={fixture.schema} uischema={fixture.uischema} />
108+
</JsonFormsStateProvider>
109+
);
110+
111+
const radioControl = wrapper.find('.radio');
112+
expect(radioControl).toHaveLength(1);
113+
expect(radioControl.prop('style')).toEqual({
114+
display: 'flex', flexDirection: 'row'
115+
});
116+
117+
const radioOptions = wrapper.find('.radio-option');
118+
expect(radioOptions).toHaveLength(4);
119+
120+
const radioInput = wrapper.find('.radio-input');
121+
expect(radioInput).toHaveLength(4);
122+
123+
const radioLabel = wrapper.find('.radio-label');
124+
expect(radioLabel).toHaveLength(4);
125+
});
126+
127+
test('do not render inline styles in radio if radio class is overwritten', () => {
128+
const renderers = [{ tester: rankWith(10, isEnumControl), renderer: RadioGroupControl }];
129+
const core = initCore(fixture.schema, fixture.uischema, fixture.data);
130+
const customStyles = vanillaStyles.map(style => {
131+
if (style.name !== 'control.radio') { return style; }
132+
133+
return {
134+
name: style.name,
135+
classNames: ['radio-custom-class']
136+
};
137+
});
138+
wrapper = mount(
139+
<JsonFormsStyleContext.Provider value={{ styles: customStyles }}>
140+
<JsonFormsStateProvider initState={{ core, renderers }}>
141+
<RadioGroupControl schema={fixture.schema} uischema={fixture.uischema} />
142+
</JsonFormsStateProvider>
143+
</JsonFormsStyleContext.Provider>
144+
);
145+
146+
const radioControl = wrapper.find('.radio-custom-class');
147+
expect(radioControl.prop('style')).toEqual({});
148+
});
149+
102150
test('render oneOf', () => {
103151
const renderers = [{ tester: rankWith(10, isOneOfEnumControl), renderer: OneOfRadioGroupControl }];
104152
const core = initCore(oneOfFixture.schema, oneOfFixture.uischema, oneOfFixture.data);

0 commit comments

Comments
 (0)