import React, { Fragment } from 'react'; import classNames from 'classnames'; import { RadioGroup as CoreRadioGroup } from '@shoptet/ui-core-web'; import type { SafeExtract, SafeOmit } from '@shoptet/utils'; import type { Spacing } from '../../../tokens/types'; import { RadioGroupImageOption } from './RadioGroupImageOption'; import { RadioGroupOption } from './RadioGroupOption'; import type { RenderRadioGroupOptionProps } from './types'; export type SelectKey = string | number; export interface RadioGroupOwnProps { /** * The id of the radio group. */ id?: string; /** * Required for grouping radio buttons (sets the name attribute on each input). */ name: string; /** * Default selected key value of the input (uncontrolled). */ defaultValue?: V; /** * Selected key value (controlled). */ value?: V | null; /** * Function to handle option selection. * @param value - The selected value. * @param event - The change event. */ onChange?: (value: V | null, event: React.ChangeEvent) => void; /** * Disables the input. */ disabled?: boolean; /** * List of keys of the disabled options (controlled). */ disabledValues?: Array; /** * Function to get the value for an option. * @param option - The option to get the value for. */ getOptionValue(option: T): V; /** * Function to get the text label for an option. * @param option - The option to get the text label for. */ getOptionLabel(option: T): string; /** * Function to get the description for an option. * @param option - The option to get the description for. */ getOptionTooltip?(option: T): string | undefined; /** * Function to render a custom option. When provided, replaces the default RadioOption rendering. */ renderOption?(option: T, props: RenderRadioGroupOptionProps): React.ReactNode; /** * List of options to display (controlled). */ options: Array; /** * Whether the field is required (adds asterisk to the label and sets required attribute on each input). */ required?: boolean; /** * Layout engine used to arrange options. * @default 'flex' */ layout?: 'flex' | 'grid'; /** * Direction of options. Applies only when layout is 'flex'. * @default 'vertical' */ orientation?: 'horizontal' | 'vertical'; /** * Minimum column width in pixels. Applies only when layout is 'grid'. * The grid auto-fills as many columns of at least this width as fit the container, * so the number of columns adapts to available space without media queries. */ minColumnWidth?: number; /** * Gap between options. Sets both row and column gap to the same value. * Defaults: column gap 'xl', row gap 'sm'. */ gap?: SafeExtract; } interface InheritedHTMLDivProps extends SafeOmit< React.ComponentProps<'div'>, 'defaultValue' | 'onChange' | 'className' | 'style' > { /** * Validation state of the radio group. */ validity?: 'error' | 'warning'; } export interface RadioGroupProps extends RadioGroupOwnProps, InheritedHTMLDivProps {} export const RadioGroup = Object.assign( function RadioGroup({ name, value, defaultValue, onChange, disabled, disabledValues = [], getOptionValue, getOptionLabel, getOptionTooltip, options, renderOption, required, validity, layout = 'flex', orientation = 'vertical', minColumnWidth, gap, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabeledBy, 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, ...rest }: RadioGroupProps) { rest satisfies InheritedHTMLDivProps; return ( onChange?.(newValue as V | null, event)} disabled={disabled} required={required} invalid={validity === 'error'} a11yLabel={ariaLabel} a11yLabeledBy={ariaLabeledBy} a11yDescribedBy={ariaDescribedBy} a11yErrorMessage={ariaErrorMessage} className={classNames('radioGroup', { 'radioGroup--flex': layout === 'flex' && orientation === 'vertical', 'radioGroup--flexHorizontal': layout === 'flex' && orientation === 'horizontal', 'radioGroup--grid': layout === 'grid', 'radioGroup--gapSm': gap === 'sm', 'radioGroup--gapMd': gap === 'md', 'radioGroup--gapLg': gap === 'lg', 'radioGroup--gapXl': gap === 'xl', })} style={ minColumnWidth ? ({ '--radioGroup-min-column-width': `${minColumnWidth}px` } as React.CSSProperties) : undefined } > {({ state }) => options.map(option => { const optionValue = getOptionValue(option); const isOptionDisabled = disabled || disabledValues.includes(optionValue); const isChecked = state.value === optionValue; const renderProps: RenderRadioGroupOptionProps = { checked: isChecked, disabled: isOptionDisabled ?? false, value: optionValue, required, validity, onChange: (newValue, event) => state.onChange(newValue, event), textLabel: getOptionLabel(option), description: getOptionTooltip?.(option), }; if (renderOption) { return {renderOption(option, renderProps)}; } return ; }) } ); }, { displayName: 'RadioGroup', Option: RadioGroupOption, ImageOption: RadioGroupImageOption, } );