// eslint-disable-next-line import/no-duplicates import * as React from 'react'; import { useRef, useMemo, // eslint-disable-next-line import/no-duplicates } from 'react'; import classNames from 'classnames'; import Text from '../../text/Text'; import generateRandomString from '../../../js/generateRandomString'; import useRadioContext from './useRadioContext'; import {useFirstPaint} from '../../utils/useFirstPaint'; export type RadioColorType = 'light' | 'dark'; type RadioLabelSizeType = 'medium' | 'small'; type StyleType = Partial< React.CSSProperties & { '--radioColor'?: string; '--radioRingInsideColor'?: string; '--radioHoverColor'?: string; '--radioInvalidColor'?: string; '--radioInvalidHoverColor'?: string; '--radioLabelColor'?: string; '--radioDescriptionColor'?: string; '--radioBorderWidth'?: string; '--radioRingColor'?: string; } >; export type RadioPropsType = { /** * Sets whether the radio is checked or unchecked. * @example */ checked?: boolean; /** * To be displayed to the right of the radio as a label. The label is clickable radio element. * @example Label */ children?: React.ReactNode; /** * Optional string. Additional classnames. */ className?: string | null | undefined; /** * Specify color variant of the radio that you want to use. * @example */ color?: RadioColorType | null | undefined; /** * To be displayed below radio and its label. The description is not clickable. You can either pass text or your own component with custom styling. * @example */ description?: React.ReactNode | string; /** * Sets whether the radio is disabled. * @example */ disabled?: boolean; /** * ID assigned to the radio input. If not provided, random id will be generated. * @example */ id?: string; /** * Sets whether the radio marked as invalid. * @example */ invalid?: boolean; /** * Sets label size. * @example * @default false */ labelSize?: RadioLabelSizeType; /** * The name of the radio input. * @example */ name?: string; /** * Function called with an object containing the react synthetic event, whenever the state of the radio changes. */ onChange?: (arg0: React.SyntheticEvent) => void; /** * Sets whether the radio input is marked as required. This doesn't affect radio style. * @example * @default false */ required?: boolean; /** * Style applied to the container. * @example */ style?: StyleType; /** * Value of the radio input. * @example */ value?: string | null | undefined; /** * ID of a custom label, that describes the radio input. * @example */ 'aria-labelledby'?: string; /** * ID of a custom text / section, that describes the radio input. * @example */ 'aria-describedby'?: string; } & Omit< React.AllHTMLAttributes, | 'checked' | 'children' | 'className' | 'color' | 'description' | 'disabled' | 'id' | 'invalid' | 'labelSize' | 'name' | 'onChange' | 'required' | 'style' | 'value' | 'undefined' | 'undefined' >; const Radio = ({ checked, color = 'dark', children, className, description, disabled, id, invalid, labelSize = 'medium', name, onChange, required = false, style, value, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, ...props }: RadioPropsType) => { const circleRef = React.useRef(); const {current: radioId} = useRef( id === undefined || id === '' ? generateRandomString() : id ); const radioGroupContext = useRadioContext(); const isWithinRadioGroup = Boolean( radioGroupContext && Object.keys(radioGroupContext).length ); const shouldAnimateRef = React.useRef(false); const isControlled = checked !== undefined || isWithinRadioGroup; let isChecked: boolean | undefined = undefined; useFirstPaint(() => { shouldAnimateRef.current = true; }); if (isControlled) { // Radio can either be directly set as checked, or be controlled by a RadioGroup isChecked = checked !== undefined ? checked : Boolean(radioGroupContext.selectedValue) && radioGroupContext.selectedValue === value; if (shouldAnimateRef.current && circleRef.current) { circleRef.current.classList.add('sg-radio__circle--with-animation'); } } const colorName = radioGroupContext.color || color; const isDisabled = disabled !== undefined ? disabled : radioGroupContext.disabled; const hasLabel = children !== undefined && children !== null; const isInputOnly = !hasLabel && !description; const descriptionId = useMemo(() => { if (ariaDescribedBy) return ariaDescribedBy; if (description) return `${radioId}-description`; return null; }, [radioId, ariaDescribedBy, description]); const radioClass = classNames('sg-radio', className, { [`sg-radio--${String(colorName)}`]: colorName, 'sg-radio--disabled': isDisabled, 'sg-radio--with-label': !!hasLabel, 'sg-radio--with-description': !!descriptionId, 'sg-radio--with-padding': !isInputOnly, }); const labelClass = classNames('sg-radio__label', { 'sg-radio__label--with-padding-bottom': description, [`sg-radio__label--${String(labelSize)}`]: labelSize, }); const circleClass = classNames('sg-radio__circle'); const labelId = ariaLabelledBy || `${radioId}-label`; const isInvalid = invalid !== undefined ? invalid : radioGroupContext.invalid; // @ts-ignore TS7006 const onInputChange = e => { if (isWithinRadioGroup) { radioGroupContext.setLastFocusedValue(value); radioGroupContext.setSelectedValue(e, value); } if (onChange) { onChange(e); } if (circleRef.current && shouldAnimateRef.current) { circleRef.current.classList.add('sg-radio__circle--with-animation'); } }; return (
{hasLabel && ( {children} )}
{description && ( {description} )}
); }; export default Radio;