import * as React from 'react' import { FieldWrapper } from '../shared/FieldWrapper' import type { SAILLabelPosition, SAILMarginSize } from '../../types/sail' type ChoiceLayout = "STACKED" | "COMPACT" type ChoiceStyle = "STANDARD" | "CARDS" type Spacing = "STANDARD" | "MORE" | "EVEN_MORE" type ChoicePosition = "START" | "END" /** * Displays a set of radio buttons for single-select input * Maps to SAIL a!radioButtonField() */ export interface RadioButtonFieldProps { /** Text to display as the field label */ label?: string /** Supplemental text about this field */ instructions?: string /** Determines if a value is required to submit the form */ required?: boolean /** Determines if the field should display as grayed out */ disabled?: boolean /** Array of options for the user to select */ choiceLabels: any[] /** Array of values associated with the corresponding choices */ choiceValues: any[] /** Value of choice to display as selected */ value?: any /** Validation errors to display below the field */ validations?: string[] /** Callback when the user changes the selection */ saveInto?: (value: any) => void /** Callback when the user changes the selection (React-style alias for saveInto) */ onChange?: (value: any) => void /** Validation group name (no spaces) */ validationGroup?: string /** Custom message when field is required and not provided */ requiredMessage?: string /** Determines where the label appears */ labelPosition?: SAILLabelPosition /** Determines the layout of choices */ choiceLayout?: ChoiceLayout /** Displays a help icon with tooltip text */ helpTooltip?: string /** Additional text for screen readers */ accessibilityText?: string /** Determines whether component is displayed */ showWhen?: boolean /** Determines how choices are displayed */ choiceStyle?: ChoiceStyle /** Determines space between options */ spacing?: Spacing /** Data source (record type) - not implemented in prototype */ data?: any /** Sort configurations - not implemented in prototype */ sort?: any[] /** Space added above component */ marginAbove?: SAILMarginSize /** Space added below component */ marginBelow?: SAILMarginSize /** Determines whether radio buttons appear on left or right */ choicePosition?: ChoicePosition /** Additional Tailwind classes for prototype-specific styling (not part of SAIL API) */ className?: string } export const RadioButtonField: React.FC = ({ label, instructions, required = false, disabled = false, choiceLabels, choiceValues, value, validations = [], saveInto, onChange, validationGroup: _validationGroup, requiredMessage, labelPosition = "ABOVE", choiceLayout = "STACKED", helpTooltip, accessibilityText, showWhen = true, choiceStyle = "STANDARD", spacing = "STANDARD", data: _data, sort: _sort, marginAbove = "NONE", marginBelow = "STANDARD", choicePosition, className }) => { // Visibility control if (!showWhen) return null const inputId = `radiobuttonfield-${Math.random().toString(36).substr(2, 9)}` // Auto default for choicePosition: START for STANDARD, END for CARDS const effectiveChoicePosition = choicePosition ?? (choiceStyle === "CARDS" ? "END" : "START") // Map SAIL spacing values to Tailwind classes const spacingMap: Record = { STANDARD: choiceLayout === "STACKED" ? 'gap-2' : 'gap-4', MORE: choiceLayout === "STACKED" ? 'gap-4' : 'gap-6', EVEN_MORE: choiceLayout === "STACKED" ? 'gap-6' : 'gap-8' } const radioLabelGapMap: Record = { STANDARD: 'gap-2', MORE: 'gap-4', EVEN_MORE: 'gap-6' } const handleChange = (choiceValue: any) => { const handler = onChange || saveInto if (!handler) return handler(choiceValue) } // Show validation errors const showValidations = validations.length > 0 // Show required message const showRequiredMessage = required && value === undefined && requiredMessage // Render radio buttons const radioButtonsElement = (
{choiceLabels.map((choiceLabel, index) => { const choiceValue = choiceValues[index] const isChecked = value === choiceValue const choiceId = `${inputId}-choice-${index}` // Base classes for each choice item container const itemContainerClasses = [ 'flex', 'items-center', radioLabelGapMap[spacing], effectiveChoicePosition === "END" && 'flex-row-reverse', choiceStyle === "CARDS" && `border rounded-sm p-4 hover:border-blue-500 transition-colors cursor-pointer ${isChecked ? 'border-blue-500' : 'border-gray-300'}`, disabled && 'opacity-50 cursor-not-allowed' ].filter(Boolean).join(' ') return (
{ // Allow clicking anywhere on card to select (except on the radio itself) if (choiceStyle === "CARDS" && !disabled) { const target = e.target as HTMLElement // Don't trigger if clicking directly on the input (it has its own handler) if (target.tagName !== 'INPUT') { handleChange(choiceValue) } } }} > handleChange(choiceValue)} className={[ 'appearance-none h-4 w-4 shrink-0 rounded-full border', isChecked ? 'border-blue-500 bg-blue-500' : 'border-gray-400 bg-white', 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500 focus-visible:ring-offset-1', 'transition-colors duration-150 ease-in-out', 'checked:bg-[url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTYgMTYiIGZpbGw9IndoaXRlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxjaXJjbGUgY3g9IjgiIGN5PSI4IiByPSI0Ii8+PC9zdmc+")] checked:bg-center checked:bg-no-repeat checked:bg-[length:10px_10px]', disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer' ].filter(Boolean).join(' ')} aria-invalid={showValidations} aria-errormessage={showValidations ? `${inputId}-error` : undefined} />
) })}
) // Footer content (validations and required message) const footerContent = ( <> {showValidations && ( )} {showRequiredMessage && (

{requiredMessage}

)} ) return ( {radioButtonsElement} ) }