import { useTheme } from '@ankhorage/surface'; import React from 'react'; import { Platform, Pressable, StyleSheet, View } from 'react-native'; import type { RadioGroupProps } from '../../../../../types/radio'; import { Icon } from '../../../../icon/public'; import { Text } from '../../../../typography/public'; import { handleRadioOptionKeyDown, type RadioOptionKeyboardTarget, } from '../../utils/handleRadioOptionKeyDown'; import { resolveRadioOptionColors } from '../../utils/resolveRadioOptionColors'; /*** Render a selectable icon radio option with one accessible radio target and a trailing check. */ export function RadioOptionControl(props: RadioOptionControlProps) { const { theme } = useTheme(); const { option, checked, disabled, readOnly, interactionPolicy, onSelect } = props; const colors = resolveRadioOptionColors(theme, props); const [focused, setFocused] = React.useState(false); const keyboardProps = Platform.OS === 'web' && !readOnly && !disabled && interactionPolicy !== 'passive' ? { onKeyDown: (event: React.KeyboardEvent) => handleRadioOptionKeyDown(event, onSelect), } : {}; const controlStyle = { flex: 1, width: '100%' as const, minHeight: props.size === 'l' ? 84 : 44, borderWidth: checked || focused ? 2 : 1, borderColor: focused ? theme.semantics.border.focus : checked ? colors.accent : colors.border, borderRadius: theme.radii.l, backgroundColor: colors.surface, }; return ( setFocused(true)} onBlur={() => setFocused(false)} testID={option.testID} style={controlStyle} > {(state) => } ); } /*** Arrange the icon, label and passive indicator within the native radio press target. */ function RadioOptionControlBody( props: RadioOptionControlProps & { pressed: boolean }, ) { const { theme } = useTheme(); const colors = resolveRadioOptionColors(theme, props); const contentStyle = { padding: props.size === 's' ? theme.spacing.s : props.size === 'm' ? 12 : theme.spacing.m, gap: props.layout === 'vertical' || !props.option.iconSource ? theme.spacing.s : theme.spacing.m, ...(!props.option.iconSource ? { flexDirection: 'row-reverse' as const } : {}), opacity: props.disabled ? 0.5 : props.pressed ? 0.7 : 1, }; return ( {props.option.iconSource || props.checked ? ( ) : null} ); } interface RadioOptionControlProps extends Pick< RadioGroupProps, 'color' | 'size' | 'interactionPolicy' > { option: RadioGroupProps['options'][number]; checked: boolean; disabled: boolean; readOnly: boolean; invalid: boolean; layout: 'horizontal' | 'vertical'; onSelect: () => void; } /*** Keep decoration passive while allowing labels to wrap at narrow widths and large text sizes. */ function RadioOptionControlContent({ option, layout, size, color, invalid, disabled, }: RadioOptionControlProps) { const { theme } = useTheme(); const colors = resolveRadioOptionColors(theme, { color, invalid, disabled }); return ( <> {option.iconSource ? ( ) : null} {option.label} {option.description ? ( {option.description} ) : null} ); } /*** Draw a passive check using themed geometry without requiring an application media asset. */ function RadioOptionControlIndicator({ checked, accent, onAccent, }: { checked: boolean; accent: string; onAccent: string; }) { const indicatorStyle = { backgroundColor: checked ? accent : 'transparent' }; return ( {checked ? : null} ); } const styles = StyleSheet.create({ content: { gap: 16, flex: 1, minWidth: 0 }, horizontal: { flexDirection: 'row', alignItems: 'center' }, vertical: { flexDirection: 'column', alignItems: 'center', justifyContent: 'center', paddingTop: 24, }, label: { flex: 1, minWidth: 0, gap: 4 }, centerLabel: { alignSelf: 'stretch', gap: 4 }, corner: { position: 'absolute', right: 8, top: 8 }, indicator: { width: 24, height: 24, borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, check: { width: 7, height: 12, borderRightWidth: 2.5, borderBottomWidth: 2.5, transform: [{ rotate: '45deg' }, { translateY: -2 }], }, });