-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: improve RadioButton
and RadioButtonGroup
types
#16648
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
||
|
@@ -98,80 +98,85 @@ export interface RadioButtonProps | |
required?: boolean; | ||
} | ||
|
||
const RadioButton = React.forwardRef((props: RadioButtonProps, ref) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Most of the following diff below is due to prettier's formatting adjusting the indentation. I recommend viewing with ⚙️ whitespace changes hidden. |
||
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>) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a bonus, I added some missing types while working in these files. |
||
onChange(value, name, event); | ||
} | ||
); | ||
|
||
const inputRef = useRef<HTMLInputElement>(null); | ||
const innerLabelClasses = classNames( | ||
`${prefix}--radio-button__label-text`, | ||
{ | ||
[`${prefix}--visually-hidden`]: hideLabel, | ||
} | ||
); | ||
Comment on lines
+126
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was an automatic prettier adjustment. |
||
|
||
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'; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 `<RadioButton>` 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
name: string, | ||
selection: RadioButtonProps['value'], | ||
name: RadioButtonGroupProps['name'], | ||
event: React.ChangeEvent<HTMLInputElement> | ||
) => 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<RadioButtonProps>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This way, we no longer need to keep asserting the type of |
||
(radioButton) => { | ||
if (!radioButton) { | ||
return; | ||
} | ||
Comment on lines
+175
to
+177
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The null-check from 🔴 184 was moved up here as an early 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); | ||
|
@@ -230,7 +241,7 @@ const RadioButtonGroup = React.forwardRef( | |
const divRef = useRef<HTMLDivElement>(null); | ||
|
||
// Slug is always size `mini` | ||
let normalizedSlug; | ||
let normalizedSlug: ReactElement | undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be done across all components for Type Safety and Clarity |
||
if (slug && slug['type']?.displayName === 'Slug') { | ||
normalizedSlug = React.cloneElement(slug as React.ReactElement<any>, { | ||
size: 'mini', | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can directly reference other types within the same interface when the expected value is the same (e.g.,
props.name
andprops.value
are what we're passing to theonChange
handler).Referencing types this way provides more context and reduces the work later, should
RadioButton
types change in the future.This is also how it was originally typed (See DefinitelyTyped)