import React, { type AriaAttributes, type ChangeEvent, Children, cloneElement, type FocusEvent, forwardRef, type HTMLAttributes, isValidElement, type MouseEvent, type ReactElement, type ReactNode, useState, } from 'react'; import cn from 'classnames'; import { getDataTestId } from '@alfalab/core-components-shared'; import { useDidUpdateEffect } from '@alfalab/hooks'; import commonStyles from './index.module.css'; export type Direction = 'horizontal' | 'vertical'; export type RadioGroupType = 'radio' | 'tag'; export interface BaseRadioGroupProps extends Omit< HTMLAttributes, 'onChange' | 'onBlur' | 'onFocus' | 'children' | 'className' >, AriaAttributes { /** * Заголовок группы */ label?: ReactNode; /** * Направление */ direction?: Direction; /** * Тип компонента */ type?: RadioGroupType; /** * Дополнительный класс */ className?: string; /** * Дополнительный класс для списка радио элементов */ radioListClassName?: string; /** * Отображение ошибки */ error?: ReactNode | boolean; /** * Текст подсказки снизу */ hint?: ReactNode; /** * Дочерние элементы. Ожидаются компоненты `Radio` или `Tag` */ children: ReactNode; /** * Обработчик изменения значения 'checked' одного из дочерних компонентов */ onChange?: ( event: ChangeEvent | MouseEvent, payload: { value: string; name?: string; }, ) => void; /** * Обработчик блюра. */ onBlur?: (event: FocusEvent) => void; /** * Обработчик фокуса. */ onFocus?: (event: FocusEvent) => void; /** * Управление возможностью изменения состояния 'checked' дочерних компонентов Radio | Tag */ disabled?: boolean; /** * Идентификатор для систем автоматизированного тестирования */ dataTestId?: string; /** * Атрибут name для всех дочерних компонентов */ name?: string; /** * Value выбранного дочернего элемента */ value?: string | null; /** * Основные стили компонента. */ styles: { [key: string]: string }; } export const BaseRadioGroup = forwardRef( ( { children, className, radioListClassName, direction = 'vertical', label, error, hint, onChange, onBlur, onFocus, type = 'radio', dataTestId, disabled = false, name, value, styles, ...restProps }, ref, ) => { const [stateValue, setStateValue] = useState(''); useDidUpdateEffect(() => { setStateValue(value); }, [value]); const isChecked = (childValue: string) => value !== null && (value || stateValue) === childValue; const handleChange = (event: ChangeEvent | MouseEvent, childValue: string) => { setStateValue(childValue); if (onChange) { onChange(event, { name, value: childValue }); } }; const renderRadio = (child: ReactElement) => { const { className: childClassName, value: childValue } = child.props; return cloneElement(child, { onChange: (event: ChangeEvent) => handleChange(event, childValue), disabled, ...child.props, checked: isChecked(childValue), name, className: cn(childClassName, commonStyles[`${direction}Radio`]), }); }; const renderTag = (child: ReactElement) => { const childValue = child.props.value; const checked = isChecked(childValue); const clone = cloneElement(child, { onClick: (event: MouseEvent) => handleChange(event, childValue), disabled, ...child.props, checked, name, tabIndex: -1, }); return ( ); }; const errorMessage = typeof error === 'boolean' ? '' : error; return (
{label ? ( {label} ) : null} {children ? (
{Children.map(children, (child) => { if (isValidElement(child)) { return type === 'radio' ? renderRadio(child) : renderTag(child); } return null; })}
) : null} {errorMessage && ( {errorMessage} )} {hint && !errorMessage && ( {hint} )}
); }, );