import { forwardRef, ReactNode, useState } from 'react'; import classnames from 'classnames'; import Badge from './Badge'; import borderStyles from './Borders.css'; import Box from './Box'; import Flex from './Flex'; import focusStyles from './Focus.css'; import Label from './Label'; import layoutStyles from './Layout.css'; import { useRadioGroupContext } from './RadioGroup/Context'; import RadioButton from './RadioGroup/RadioButton'; import styles from './RadioGroupButton.css'; import FormHelperText from './sharedSubcomponents/FormHelperText'; import Text from './Text'; import useFocusVisible from './useFocusVisible'; import useExperimentalTheme from './utils/useExperimentalTheme'; import useTapScaleAnimation from './utils/useTapScaleAnimation'; type BadgeType = { text: string; type?: | 'info' | 'error' | 'warning' | 'success' | 'neutral' | 'recommendation' | 'darkWash' | 'lightWash'; }; type Props = { /** * An optional [Badge](https://gestalt.pinterest.systems/web/badge) component can be supplied to add a badge to each radio button. See the [badges example](https://gestalt.pinterest.systems/web/radiogroup#With-Badge) for more details. */ badge?: BadgeType; /** * Indicates if the input is checked. See the [state example](https://gestalt.pinterest.systems/web/radiogroup#States) for more details. */ checked?: boolean; /** * Indicates if the input is disabled. See the [state example](https://gestalt.pinterest.systems/web/radiogroup#States) for more details. */ disabled?: boolean; /** * A unique identifier for the input. */ id: string; /** * An optional [Image](https://gestalt.pinterest.systems/web/image) component can be supplied to add an image to each radio button. Spacing is already accounted for — simply specify the width and height. See the [images example](https://gestalt.pinterest.systems/web/radiogroup#With-Image) for more details. */ image?: ReactNode; /** * The displayed label for the input. */ label?: string; /** * The name given for all radio buttons in a single group. */ name?: string; /** * Callback triggered when the user interacts with the input. */ onChange: (arg1: { event: React.ChangeEvent; checked: boolean }) => void; /** * Ref forwarded to the underlying input element. See [ref example](https://gestalt.pinterest.systems/web/radiogroup#Using-ref) for more details. */ ref?: HTMLInputElement; // eslint-disable-line react/no-unused-prop-types, /** * sm: 16px, md: 24px */ size?: 'sm' | 'md'; /** * Optional description for the input, used to provide more detail about an option. See the [helperText example](https://gestalt.pinterest.systems/web/radiogroup#With-helperText) for more details. */ helperText?: string; /** * The value of the input. */ value: string; }; /** * Use [RadioGroup.RadioButtons](https://gestalt.pinterest.systems/web/radiogroup#RadioGroup.RadioButton) to present an option for selection to the user within a RadioGroup. They should not be used outside of RadioGroup or when the user can select more than one option from a list. * * ![RadioGroup light mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/RadioGroup.spec.ts-snapshots/RadioGroup-chromium-darwin.png) * ![RadioGroup dark mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/RadioGroup-dark.spec.ts-snapshots/RadioGroup-dark-chromium-darwin.png) * */ const RadioGroupButtonWithForwardRef = forwardRef( function RadioGroupButton( { checked = false, disabled = false, id, image, label, name, onChange, helperText, value, badge, size = 'md', }: Props, ref, ) { const theme = useExperimentalTheme(); const { parentName } = useRadioGroupContext(); const tapScaleAnimation = useTapScaleAnimation(); const { isFocusVisible } = useFocusVisible(); const [focused, setFocused] = useState(false); const [hovered, setHover] = useState(false); if (theme.MAIN) { return ( ); } const handleChange: (event: React.ChangeEvent) => void = (event) => onChange({ checked: event.target.checked, event }); const handleBlur: () => void = () => setFocused(false); const handleFocus: () => void = () => setFocused(true); const handleHover: (isHovered: boolean) => void = (isHovered: boolean) => setHover(isHovered); let borderColor = styles.Border; if (disabled && checked) { borderColor = styles.BorderDisabledChecked; } else if (!disabled && checked) { borderColor = styles.BorderSelected; } else if (!disabled && hovered && !theme.MAIN) { borderColor = styles.BorderHovered; } let borderWidth = styles.BorderUnchecked; if (disabled && !checked) { borderWidth = styles.BorderDisabled; } else if (checked && size === 'sm') { borderWidth = styles.BorderCheckedSm; } else if (checked && size === 'md') { borderWidth = styles.BorderCheckedMd; } const styleSize = size === 'sm' ? styles.sizeSm : styles.sizeMd; const bgStyle = disabled && !checked ? styles.BgDisabled : styles.BgEnabled; if (parentName !== 'RadioGroup') { throw new Error( `RadioGroup.RadioButton must be used within a [RadioGroup](https://gestalt.pinterest.systems/web/radiogroup).`, ); } const radioButtonStyles = theme.MAIN ? classnames( styleSize, layoutStyles.relative, borderStyles.circle, tapScaleAnimation.classes, { [styles.outerFocusedOutline]: !disabled && focused && isFocusVisible, [styles.innerBorderFocused]: checked && !disabled && focused && isFocusVisible, [styles.innerBorderDisabled]: disabled, [styles.innerBorderChecked]: checked && !disabled && !focused, [styles.innerBorderUnchecked]: !checked && !disabled && !focused, }, ) : classnames(bgStyle, borderColor, borderWidth, styleSize, styles.RadioButton, { [focusStyles.accessibilityOutlineFocus]: focused && isFocusVisible, }); return (
handleHover(true)} onMouseLeave={() => handleHover(false)} type="radio" value={value} />
{Boolean(image) && {image}} {label && ( )} {badge && ( )} {label && helperText ? ( ) : null}
); }, ); RadioGroupButtonWithForwardRef.displayName = 'RadioGroup.RadioButton'; export default RadioGroupButtonWithForwardRef;