From e582548c949a56fba560f19a0c4cad46e63ce3a1 Mon Sep 17 00:00:00 2001 From: Joseph Schultz Date: Wed, 12 Jun 2024 11:42:48 -0500 Subject: [PATCH] feat: improve RadioButton and RadioButtonGroup types (#16648) 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. --- .../components/RadioButton/RadioButton.tsx | 149 +++++++++--------- .../react/src/components/RadioButton/index.ts | 4 +- .../RadioButtonGroup/RadioButtonGroup.tsx | 59 ++++--- .../src/components/RadioButtonGroup/index.ts | 4 +- 4 files changed, 118 insertions(+), 98 deletions(-) diff --git a/packages/react/src/components/RadioButton/RadioButton.tsx b/packages/react/src/components/RadioButton/RadioButton.tsx index 2c25a602be3c..12b18f8f3cf3 100644 --- a/packages/react/src/components/RadioButton/RadioButton.tsx +++ b/packages/react/src/components/RadioButton/RadioButton.tsx @@ -72,8 +72,8 @@ export interface RadioButtonProps * the underlying `` changes */ onChange?: ( - value: string | number, - name: string | undefined, + value: RadioButtonProps['value'], + name: RadioButtonProps['name'], event: React.ChangeEvent ) => void; @@ -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( + (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) { + onChange(value, name, event); } - ); - const inputRef = useRef(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(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, { + size, + }); + } - let normalizedSlug; - if (slug && React.isValidElement(slug)) { - const size = slug.props?.['kind'] === 'inline' ? 'md' : 'mini'; - normalizedSlug = React.cloneElement(slug as React.ReactElement, { - size, - }); + return ( +
+ + +
+ ); } - - return ( -
- - -
- ); -}); +); RadioButton.displayName = 'RadioButton'; diff --git a/packages/react/src/components/RadioButton/index.ts b/packages/react/src/components/RadioButton/index.ts index 7a945acbd50a..ac7ae6046761 100644 --- a/packages/react/src/components/RadioButton/index.ts +++ b/packages/react/src/components/RadioButton/index.ts @@ -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 }; diff --git a/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.tsx b/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.tsx index cf7054206de0..12028b795c5c 100644 --- a/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.tsx +++ b/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.tsx @@ -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'; @@ -44,7 +45,7 @@ export interface RadioButtonGroupProps /** * Specify the `` to be selected by default */ - defaultSelected?: string | number; + defaultSelected?: RadioButtonProps['value']; /** * Specify whether the group is disabled @@ -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 ) => void; + /** * Provide where radio buttons should be placed */ @@ -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. */ @@ -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, + (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 + ) { if (!readOnly) { if (newSelection !== selected) { setSelected(newSelection); @@ -230,7 +241,7 @@ const RadioButtonGroup = React.forwardRef( const divRef = useRef(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, { size: 'mini', diff --git a/packages/react/src/components/RadioButtonGroup/index.ts b/packages/react/src/components/RadioButtonGroup/index.ts index fbdadcbd2c57..85c816be1a29 100644 --- a/packages/react/src/components/RadioButtonGroup/index.ts +++ b/packages/react/src/components/RadioButtonGroup/index.ts @@ -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 };