Skip to content

Commit

Permalink
feat: improve RadioButton and RadioButtonGroup types (#16648)
Browse files Browse the repository at this point in the history
Changes:
- Export prop interfaces for `RadioButton` and `RadioButtonGroup`.
- Narrow `onChange` argument type for `RadioButtonGroupProps`.
- Reference `RadioButtonProps` for `RadioButtonGroup` types where
  we are expecting a `RadioButton` value.
- Add missing types for component `refs` and event handlers in
  `RadioButton` and `RadioButtonGroup`.
- Simplify `getRadioButtons()` with a type assertion on `children`
  argument and refactoring to use an early return statement.
  • Loading branch information
cuppajoey authored Jun 12, 2024
1 parent f715af5 commit e582548
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 98 deletions.
149 changes: 77 additions & 72 deletions packages/react/src/components/RadioButton/RadioButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ export interface RadioButtonProps
* the underlying `<input>` changes
*/
onChange?: (
value: string | number,
name: string | undefined,
value: RadioButtonProps['value'],
name: RadioButtonProps['name'],
event: React.ChangeEvent<HTMLInputElement>
) => void;

Expand All @@ -98,80 +98,85 @@ export interface RadioButtonProps
required?: boolean;
}

const RadioButton = React.forwardRef((props: RadioButtonProps, ref) => {
const {
className,
disabled,
hideLabel,
id,
labelPosition = 'right',
labelText = '',
name,
onChange = () => {},
value = '',
slug,
required,
...rest
} = props;

const prefix = usePrefix();
const uid = useId('radio-button');
const uniqueId = id || uid;

function handleOnChange(event) {
onChange(value, name, event);
}

const innerLabelClasses = classNames(`${prefix}--radio-button__label-text`, {
[`${prefix}--visually-hidden`]: hideLabel,
});

const wrapperClasses = classNames(
className,
`${prefix}--radio-button-wrapper`,
{
[`${prefix}--radio-button-wrapper--label-${labelPosition}`]:
labelPosition !== 'right',
[`${prefix}--radio-button-wrapper--slug`]: slug,
const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
(props, ref) => {
const {
className,
disabled,
hideLabel,
id,
labelPosition = 'right',
labelText = '',
name,
onChange = () => {},
value = '',
slug,
required,
...rest
} = props;

const prefix = usePrefix();
const uid = useId('radio-button');
const uniqueId = id || uid;

function handleOnChange(event: React.ChangeEvent<HTMLInputElement>) {
onChange(value, name, event);
}
);

const inputRef = useRef<HTMLInputElement>(null);
const innerLabelClasses = classNames(
`${prefix}--radio-button__label-text`,
{
[`${prefix}--visually-hidden`]: hideLabel,
}
);

const wrapperClasses = classNames(
className,
`${prefix}--radio-button-wrapper`,
{
[`${prefix}--radio-button-wrapper--label-${labelPosition}`]:
labelPosition !== 'right',
[`${prefix}--radio-button-wrapper--slug`]: slug,
}
);

const inputRef = useRef<HTMLInputElement>(null);

let normalizedSlug: React.ReactElement | undefined;
if (slug && React.isValidElement(slug)) {
const size = slug.props?.['kind'] === 'inline' ? 'md' : 'mini';
normalizedSlug = React.cloneElement(slug as React.ReactElement<any>, {
size,
});
}

let normalizedSlug;
if (slug && React.isValidElement(slug)) {
const size = slug.props?.['kind'] === 'inline' ? 'md' : 'mini';
normalizedSlug = React.cloneElement(slug as React.ReactElement<any>, {
size,
});
return (
<div className={wrapperClasses}>
<input
{...rest}
type="radio"
className={`${prefix}--radio-button`}
onChange={handleOnChange}
id={uniqueId}
ref={mergeRefs(inputRef, ref)}
disabled={disabled}
value={value}
name={name}
required={required}
/>
<label htmlFor={uniqueId} className={`${prefix}--radio-button__label`}>
<span className={`${prefix}--radio-button__appearance`} />
{labelText && (
<Text className={innerLabelClasses}>
{labelText}
{normalizedSlug}
</Text>
)}
</label>
</div>
);
}

return (
<div className={wrapperClasses}>
<input
{...rest}
type="radio"
className={`${prefix}--radio-button`}
onChange={handleOnChange}
id={uniqueId}
ref={mergeRefs(inputRef, ref)}
disabled={disabled}
value={value}
name={name}
required={required}
/>
<label htmlFor={uniqueId} className={`${prefix}--radio-button__label`}>
<span className={`${prefix}--radio-button__appearance`} />
{labelText && (
<Text className={innerLabelClasses}>
{labelText}
{normalizedSlug}
</Text>
)}
</label>
</div>
);
});
);

RadioButton.displayName = 'RadioButton';

Expand Down
4 changes: 3 additions & 1 deletion packages/react/src/components/RadioButton/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/

import RadioButton from './RadioButton';
import RadioButton, { RadioButtonProps } from './RadioButton';

export default RadioButton;
export { RadioButton };

export type { RadioButtonProps };
59 changes: 35 additions & 24 deletions packages/react/src/components/RadioButtonGroup/RadioButtonGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React, {
useState,
} from 'react';
import classNames from 'classnames';
import type { RadioButtonProps } from '../RadioButton';
import { Legend } from '../Text';
import { usePrefix } from '../../internal/usePrefix';
import { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
Expand Down Expand Up @@ -44,7 +45,7 @@ export interface RadioButtonGroupProps
/**
* Specify the `<RadioButton>` to be selected by default
*/
defaultSelected?: string | number;
defaultSelected?: RadioButtonProps['value'];

/**
* Specify whether the group is disabled
Expand Down Expand Up @@ -87,10 +88,11 @@ export interface RadioButtonGroupProps
* the group changes
*/
onChange?: (
selection: React.ReactNode,
name: string,
selection: RadioButtonProps['value'],
name: RadioButtonGroupProps['name'],
event: React.ChangeEvent<HTMLInputElement>
) => void;

/**
* Provide where radio buttons should be placed
*/
Expand Down Expand Up @@ -119,7 +121,8 @@ export interface RadioButtonGroupProps
/**
* Specify the value that is currently selected in the group
*/
valueSelected?: string | number;
valueSelected?: RadioButtonProps['value'];

/**
* `true` to specify if input selection in group is required.
*/
Expand Down Expand Up @@ -166,30 +169,38 @@ const RadioButtonGroup = React.forwardRef(
}

function getRadioButtons() {
const mappedChildren = React.Children.map(children, (radioButton) => {
const { value } = (radioButton as ReactElement)?.props ?? undefined;

const newProps = {
name: name,
key: value,
value: value,
onChange: handleOnChange,
checked: value === selected,
required: required,
};

if (!selected && (radioButton as ReactElement)?.props.checked) {
newProps.checked = true;
const mappedChildren = React.Children.map(
children as ReactElement<RadioButtonProps>,
(radioButton) => {
if (!radioButton) {
return;
}

const newProps = {
name: name,
key: radioButton.props.value,
value: radioButton.props.value,
onChange: handleOnChange,
checked: radioButton.props.value === selected,
required: required,
};

if (!selected && radioButton.props.checked) {
newProps.checked = true;
}

return React.cloneElement(radioButton, newProps);
}
if (radioButton) {
return React.cloneElement(radioButton as ReactElement, newProps);
}
});
);

return mappedChildren;
}

function handleOnChange(newSelection, value, evt) {
function handleOnChange(
newSelection: RadioButtonProps['value'],
value: RadioButtonProps['name'],
evt: React.ChangeEvent<HTMLInputElement>
) {
if (!readOnly) {
if (newSelection !== selected) {
setSelected(newSelection);
Expand Down Expand Up @@ -230,7 +241,7 @@ const RadioButtonGroup = React.forwardRef(
const divRef = useRef<HTMLDivElement>(null);

// Slug is always size `mini`
let normalizedSlug;
let normalizedSlug: ReactElement | undefined;
if (slug && slug['type']?.displayName === 'Slug') {
normalizedSlug = React.cloneElement(slug as React.ReactElement<any>, {
size: 'mini',
Expand Down
4 changes: 3 additions & 1 deletion packages/react/src/components/RadioButtonGroup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/

import RadioButtonGroup from './RadioButtonGroup';
import RadioButtonGroup, { RadioButtonGroupProps } from './RadioButtonGroup';

export default RadioButtonGroup;
export { RadioButtonGroup };

export type { RadioButtonGroupProps };

0 comments on commit e582548

Please sign in to comment.