I recently found myself wanting to extend the WordPress block editor’s RadioControl component. Unfortunately this component is not designed to be extended with additional functionality, so I needed a different solution. The rest of this post covers how to create a custom RadioControl component for the WordPress block editor.
Get the Core Code
We’ll start with the core code for the RadioControl block, which lives in /wp-includes/js/dist/components.js and looks like this:
function RadioControl({
label,
className,
selected,
help,
onChange,
options = [],
...props
}) {
const instanceId = Object(external_wp_compose_["useInstanceId"])(RadioControl);
const id = `inspector-radio-control-${instanceId}`;
const onChangeValue = event => onChange(event.target.value);
return !Object(external_lodash_["isEmpty"])(options) && Object(external_wp_element_["createElement"])(base_control, {
label: label,
id: id,
help: help,
className: classnames_default()(className, 'components-radio-control')
}, options.map((option, index) => Object(external_wp_element_["createElement"])("div", {
key: `${id}-${index}`,
className: "components-radio-control__option"
}, Object(external_wp_element_["createElement"])("input", Object(esm_extends["a" /* default */])({
id: `${id}-${index}`,
className: "components-radio-control__input",
type: "radio",
name: id,
value: option.value,
onChange: onChangeValue,
checked: option.value === selected,
"aria-describedby": !!help ? `${id}__help` : undefined
}, props)), Object(external_wp_element_["createElement"])("label", {
htmlFor: `${id}-${index}`
}, option.label))));
}
Initial Refactoring
In order to make this work for us as our own custom component we’ll need to make a few modifications, which will also make the code easier to read:
import PropTypes from 'prop-types';
import { useInstanceId } from '@wordpress/compose';
import { BaseControl } from '@wordpress/components';
import classnames from 'classnames';
export default function NwdCustomRadioControl({
label,
hideLabelFromVision,
className,
selected,
help,
onChange,
options = [],
...props
}) {
const instanceId = useInstanceId(NwdCustomRadioControl);
const id = `inspector-radio-control-nwd-${instanceId}`;
const onChangeValue = event => onChange(event.target.value);
if (! options?.length) {
return null;
}
return (
<BaseControl
label={ label }
id={ id }
help={ help }
className={ classnames(className, 'components-radio-control') }
>
{ options.map((option, index) => (
<div
key={ `${ id }-${ index }` }
className="components-radio-control__option"
>
<input
id={ `${ id }-${ index }` }
className="components-radio-control__input"
type="radio"
name={ `${ id }-${ index }` }
value={ option.value }
onChange={ onChangeValue }
checked={ option.value === selected }
aria-describedby={
! help ? `${ id }__help` : undefined
}
/>
<label
className="components-radio-control__label"
htmlFor={ `${ id }-${ index }` }
>
{ option.label }
</label>
</div>
))}
)
}
NwdCustomRadioControl.propTypes = {
label: PropTypes.string,
hideLabelFromVision: PropTypes.bool,
help: PropTypes.string,
selected: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
options: PropTypes.array,
className: PropTypes.string,
}
NwdCustomRadioControl.defaultProps = {
label: '',
hideLabelFromVision: true,
help: '',
options: [],
className: '',
};
Customization
The refactored code above should work just as the default RadioControl does. From there we can add our own custom functionality. In my case I needed the ability to toggle a “disabled” attribute on specific radio options, depending on the state of a different element on the page. Adding the “disabled” attribute can be done like this:
<input
id={ `${ id }-${ index }` }
className="components-radio-control__input"
type="radio"
name={ `${ id }-${ index }` }
value={ option.value }
onChange={ onChangeValue }
checked={ option.value === selected }
aria-describedby={
! help ? `${ id }__help` : undefined
}
disabled={ option.disabled }
/>
If we want the corresponding label to appear differently when the radio option is disabled, we can add a class and target it with CSS:
<label
className={
option.disabled ?
'components-radio-control__label disabled' :
'components-radio-control__label'
}
htmlFor={ `${ id }-${ index }` }
>
{ option.label }
</label>
I’m not showing it here, but in my case I used opacity: 0.5;
on the label for a disabled radio option.
With both of those changes in place, our complete component looks like:
import PropTypes from 'prop-types';
import { useInstanceId } from '@wordpress/compose';
import { BaseControl } from '@wordpress/components';
import classnames from 'classnames';
export default function NwdCustomRadioControl({
label,
hideLabelFromVision,
className,
selected,
help,
onChange,
options = [],
...props
}) {
const instanceId = useInstanceId(NwdCustomRadioControl);
const id = `inspector-radio-control-nwd-${instanceId}`;
const onChangeValue = event => onChange(event.target.value);
if (! options?.length) {
return null;
}
return (
<BaseControl
label={ label }
id={ id }
help={ help }
className={ classnames(className, 'components-radio-control') }
>
{ options.map((option, index) => (
<div
key={ `${ id }-${ index }` }
className="components-radio-control__option"
>
<input
id={ `${ id }-${ index }` }
className="components-radio-control__input"
type="radio"
name={ `${ id }-${ index }` }
value={ option.value }
onChange={ onChangeValue }
checked={ option.value === selected }
aria-describedby={
! help ? `${ id }__help` : undefined
}
disabled={ option.disabled }
/>
<label
className={
option.disabled ?
'components-radio-control__label disabled' :
'components-radio-control__label'
}
htmlFor={ `${ id }-${ index }` }
>
{ option.label }
</label>
</div>
))}
)
}
NwdCustomRadioControl.propTypes = {
label: PropTypes.string,
hideLabelFromVision: PropTypes.bool,
help: PropTypes.string,
selected: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
options: PropTypes.array,
className: PropTypes.string,
}
NwdCustomRadioControl.defaultProps = {
label: '',
hideLabelFromVision: true,
help: '',
options: [],
className: '',
};
Using the above code will allow you to achieve something like the following image, where we see “Option Four” is disabled.